Merge branch 'develop' into AndroidAuto

This commit is contained in:
James Wells 2021-06-18 22:52:56 -04:00
commit 3853fce818
No known key found for this signature in database
GPG Key ID: DB1528F6EED16127
56 changed files with 640 additions and 1482 deletions

View File

@ -1,54 +0,0 @@
package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should be equal to`
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_6_0
import org.moire.ultrasonic.api.subsonic.interceptors.toHexBytes
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
/**
* Integration test for [SubsonicAPIClient.getStreamUrl] method.
*/
class GetStreamUrlTest {
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
val id = "boom"
private lateinit var client: SubsonicAPIClient
private lateinit var expectedUrl: String
@Before
fun setUp() {
val config = SubsonicClientConfiguration(
mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME,
PASSWORD,
V1_6_0,
CLIENT_ID
)
client = SubsonicAPIClient(config)
val baseExpectedUrl = mockWebServerRule.mockWebServer.url("").toString()
expectedUrl = "$baseExpectedUrl/rest/stream.view?id=$id&u=$USERNAME" +
"&c=$CLIENT_ID&f=json&v=${V1_6_0.restApiVersion}&p=enc:${PASSWORD.toHexBytes()}"
}
@Test
fun `Should return valid stream url`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val streamUrl = client.getStreamUrl(id)
streamUrl `should be equal to` expectedUrl
}
@Test
fun `Should still return stream url if connection failed`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(500))
val streamUrl = client.getStreamUrl(id)
streamUrl `should be equal to` expectedUrl
}
}

View File

@ -7,14 +7,14 @@ import org.amshove.kluent.`should not be`
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient.getAvatar] call.
* Integration test for [SubsonicAPIDefinition.getAvatar] call.
*/
class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
@Test
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
val response = client.getAvatar("some")
val response = client.api.getAvatar("some-id").execute().toStreamResponse()
with(response) {
stream `should be` null
@ -28,7 +28,7 @@ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
val httpErrorCode = 500
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
val response = client.getAvatar("some")
val response = client.api.getAvatar("some-id").execute().toStreamResponse()
with(response) {
stream `should be equal to` null
@ -44,7 +44,7 @@ class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
)
val response = client.stream("some")
val response = client.api.stream("some-id").execute().toStreamResponse()
with(response) {
responseHttpCode `should be equal to` 200

View File

@ -14,7 +14,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
val response = client.getCoverArt("some-id")
val response = client.api.getCoverArt("some-id").execute().toStreamResponse()
with(response) {
stream `should be` null
@ -28,7 +28,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
val httpErrorCode = 404
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
val response = client.getCoverArt("some-id")
val response = client.api.getCoverArt("some-id").execute().toStreamResponse()
with(response) {
stream `should be` null
@ -44,7 +44,7 @@ class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
)
val response = client.getCoverArt("some-id")
val response = client.api.getCoverArt("some-id").execute().toStreamResponse()
with(response) {
responseHttpCode `should be equal to` 200

View File

@ -14,7 +14,7 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
val response = client.stream("some-id")
val response = client.api.stream("some-id").execute().toStreamResponse()
with(response) {
stream `should be` null
@ -28,7 +28,7 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
val httpErrorCode = 404
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
val response = client.stream("some-id")
val response = client.api.stream("some-id").execute().toStreamResponse()
with(response) {
stream `should be` null
@ -38,13 +38,13 @@ class SubsonicApiStreamTest : SubsonicAPIClientTest() {
}
@Test
fun `Should return successfull call stream`() {
fun `Should return successful call stream`() {
mockWebServerRule.mockWebServer.enqueue(
MockResponse()
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json"))
)
val response = client.stream("some-id")
val response = client.api.stream("some-id").execute().toStreamResponse()
with(response) {
responseHttpCode `should be equal to` 200

View File

@ -45,7 +45,8 @@ import retrofit2.Call
@Suppress("TooManyFunctions")
internal class ApiVersionCheckWrapper(
val api: SubsonicAPIDefinition,
var currentApiVersion: SubsonicAPIVersions
var currentApiVersion: SubsonicAPIVersions,
var isRealProtocolVersion: Boolean = false
) : SubsonicAPIDefinition by api {
override fun getArtists(musicFolderId: String?): Call<GetArtistsResponse> {
checkVersion(V1_8_0)
@ -325,10 +326,15 @@ internal class ApiVersionCheckWrapper(
}
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
if (currentApiVersion < expectedVersion) throw ApiNotSupportedException(currentApiVersion)
// If it is true, it is probably the first call with this server
if (!isRealProtocolVersion) return
if (currentApiVersion < expectedVersion)
throw ApiNotSupportedException(currentApiVersion)
}
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
// If it is true, it is probably the first call with this server
if (!isRealProtocolVersion) return
if (param != null) {
checkVersion(expectedVersion)
}

View File

@ -0,0 +1,85 @@
package org.moire.ultrasonic.api.subsonic
import com.fasterxml.jackson.module.kotlin.readValue
import java.io.IOException
import okhttp3.ResponseBody
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import retrofit2.Response
/**
* Converts a Response to a StreamResponse
*/
fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
val response = this
return if (response.isSuccessful) {
val responseBody = response.body()
val contentType = responseBody?.contentType()
if (
contentType != null &&
contentType.type().equals("application", true) &&
contentType.subtype().equals("json", true)
) {
val error = SubsonicAPIClient.jacksonMapper.readValue<SubsonicResponse>(
responseBody.byteStream()
)
StreamResponse(apiError = error.error, responseHttpCode = response.code())
} else {
StreamResponse(
stream = responseBody?.byteStream(),
responseHttpCode = response.code()
)
}
} else {
StreamResponse(responseHttpCode = response.code())
}
}
/**
* This extension checks API call results for errors, API version, etc
* It creates Exceptions from the results returned by the Subsonic API
*/
@Suppress("ThrowsCount")
fun <T : SubsonicResponse> Response<out T>.throwOnFailure(): Response<out T> {
val response = this
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
return this as Response<T>
}
if (!response.isSuccessful) {
throw IOException("Server error, code: " + response.code())
} else if (
response.body()!!.status === SubsonicResponse.Status.ERROR &&
response.body()!!.error != null
) {
throw SubsonicRESTException(response.body()!!.error!!)
} else {
throw IOException("Failed to perform request: " + response.code())
}
}
/**
* This extension checks API call results for errors, API version, etc
* @return Boolean: True if everything was ok, false if an error was found
*/
fun Response<out SubsonicResponse>.falseOnFailure(): Boolean {
return (this.isSuccessful && this.body()!!.status === SubsonicResponse.Status.OK)
}
/**
* This call wraps Subsonic API calls so their results can be checked for errors, API version, etc
* It creates Exceptions from a StreamResponse
*/
fun StreamResponse.throwOnFailure(): StreamResponse {
val response = this
if (response.hasError() || response.stream == null) {
if (response.apiError != null) {
throw SubsonicRESTException(response.apiError)
} else {
throw IOException(
"Failed to make endpoint request, code: " + response.responseHttpCode
)
}
}
return this
}

View File

@ -3,7 +3,6 @@ package org.moire.ultrasonic.api.subsonic
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit.MILLISECONDS
@ -18,7 +17,6 @@ import org.moire.ultrasonic.api.subsonic.interceptors.ProxyPasswordInterceptor
import org.moire.ultrasonic.api.subsonic.interceptors.RangeHeaderInterceptor
import org.moire.ultrasonic.api.subsonic.interceptors.VersionInterceptor
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import retrofit2.Response
import retrofit2.Retrofit
@ -48,18 +46,23 @@ class SubsonicAPIClient(
config.enableLdapUserSupport
)
var onProtocolChange: (SubsonicAPIVersions) -> Unit = {}
/**
* Get currently used protocol version.
* The currently used protocol version.
* The setter also updates the interceptors and callback (if registered)
*/
var protocolVersion = config.minimalProtocolVersion
private set(value) {
field = value
proxyPasswordInterceptor.apiVersion = field
wrappedApi.currentApiVersion = field
wrappedApi.isRealProtocolVersion = true
versionInterceptor.protocolVersion = field
onProtocolChange(field)
}
private val okHttpClient = baseOkClient.newBuilder()
val okHttpClient: OkHttpClient = baseOkClient.newBuilder()
.readTimeout(READ_TIMEOUT, MILLISECONDS)
.apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() }
.addInterceptor { chain ->
@ -78,18 +81,19 @@ class SubsonicAPIClient(
.apply { if (config.debug) addLogging() }
.build()
private val jacksonMapper = ObjectMapper()
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
.registerModule(KotlinModule())
private val retrofit = Retrofit.Builder()
// Create the Retrofit instance, and register a special converter factory
// It will update our protocol version to the correct version, once we made a successful call
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("${config.baseUrl}/rest/")
.client(okHttpClient)
.addConverterFactory(
VersionAwareJacksonConverterFactory.create(
{ protocolVersion = it },
{
// Only trigger update on change, or if still using the default
if (protocolVersion != it || !config.isRealProtocolVersion) {
protocolVersion = it
}
},
jacksonMapper
)
)
@ -97,90 +101,12 @@ class SubsonicAPIClient(
private val wrappedApi = ApiVersionCheckWrapper(
retrofit.create(SubsonicAPIDefinition::class.java),
config.minimalProtocolVersion
config.minimalProtocolVersion,
config.isRealProtocolVersion
)
val api: SubsonicAPIDefinition get() = wrappedApi
/**
* TODO: Remove this in favour of handling the stream response inside RESTService
* Convenient method to get cover art from api using item [id] and optional maximum [size].
*
* It detects the response `Content-Type` and tries to parse subsonic error if there is one.
*
* Prefer this method over [SubsonicAPIDefinition.getCoverArt] as this handles error cases.
*/
fun getCoverArt(id: String, size: Long? = null): StreamResponse = handleStreamResponse {
api.getCoverArt(id, size).execute()
}
/**
* TODO: Remove this in favour of handling the stream response inside RESTService
* Convenient method to get media stream from api using item [id] and optional [maxBitrate].
*
* Optionally also you can provide [offset] that stream should start from.
*
* It detects the response `Content-Type` and tries to parse subsonic error if there is one.
*
* Prefer this method over [SubsonicAPIDefinition.stream] as this handles error cases.
*/
fun stream(id: String, maxBitrate: Int? = null, offset: Long? = null): StreamResponse =
handleStreamResponse {
api.stream(id, maxBitrate, offset = offset).execute()
}
/**
* TODO: Remove this in favour of handling the stream response inside RESTService
* Convenient method to get user avatar using [username].
*
* It detects the response `Content-Type` and tries to parse subsonic error if there is one.
*
* Prefer this method over [SubsonicAPIDefinition.getAvatar] as this handles error cases.
*/
fun getAvatar(username: String): StreamResponse = handleStreamResponse {
api.getAvatar(username).execute()
}
// TODO: Move this to response checker
private inline fun handleStreamResponse(apiCall: () -> Response<ResponseBody>): StreamResponse {
val response = apiCall()
return if (response.isSuccessful) {
val responseBody = response.body()
val contentType = responseBody?.contentType()
if (
contentType != null &&
contentType.type().equals("application", true) &&
contentType.subtype().equals("json", true)
) {
val error = jacksonMapper.readValue<SubsonicResponse>(responseBody.byteStream())
StreamResponse(apiError = error.error, responseHttpCode = response.code())
} else {
StreamResponse(
stream = responseBody?.byteStream(),
responseHttpCode = response.code()
)
}
} else {
StreamResponse(responseHttpCode = response.code())
}
}
/**
* Get stream url.
*
* Calling this method do actual connection to the backend, though not downloading all content.
*
* Consider do not use this method, but [stream] call.
*/
fun getStreamUrl(id: String): String {
val request = api.stream(id).execute()
val url = request.raw().request().url().toString()
if (request.isSuccessful) {
request.body()?.close()
}
return url
}
private fun OkHttpClient.Builder.addLogging() {
val loggingInterceptor = HttpLoggingInterceptor(okLogger)
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
@ -202,4 +128,19 @@ class SubsonicAPIClient(
hostnameVerifier { _, _ -> true }
}
/**
* This function is necessary because Mockito has problems with stubbing chained calls
*/
fun toStreamResponse(call: Response<ResponseBody>): StreamResponse {
return call.toStreamResponse()
}
companion object {
val jacksonMapper: ObjectMapper = ObjectMapper()
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
.registerModule(KotlinModule())
}
}

View File

@ -11,5 +11,6 @@ data class SubsonicClientConfiguration(
val clientID: String,
val allowSelfSignedCertificate: Boolean = false,
val enableLdapUserSupport: Boolean = false,
val debug: Boolean = false
val debug: Boolean = false,
val isRealProtocolVersion: Boolean = false
)

View File

@ -1,6 +1,4 @@
package org.moire.ultrasonic.service
import org.moire.ultrasonic.api.subsonic.SubsonicError
package org.moire.ultrasonic.api.subsonic
/**
* Exception returned by API with given `code`.

View File

@ -63,7 +63,6 @@ class VersionAwareJacksonConverterFactory(
}
}
@Suppress("SwallowedException")
class VersionAwareResponseBodyConverter<T> (
private val notifier: (SubsonicAPIVersions) -> Unit = {},
private val adapter: ObjectReader
@ -77,7 +76,7 @@ class VersionAwareJacksonConverterFactory(
if (response is SubsonicResponse) {
try {
notifier(response.version)
} catch (e: IllegalArgumentException) {
} catch (ignored: IllegalArgumentException) {
// no-op
}
}

View File

@ -14,7 +14,7 @@ import org.moire.ultrasonic.api.subsonic.models.AlbumListType.BY_GENRE
*/
class ApiVersionCheckWrapperTest {
private val apiMock = mock<SubsonicAPIDefinition>()
private val wrapper = ApiVersionCheckWrapper(apiMock, V1_1_0)
private val wrapper = ApiVersionCheckWrapper(apiMock, V1_1_0, isRealProtocolVersion = true)
@Test
fun `Should just call real api for ping`() {

View File

@ -5,14 +5,11 @@
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !background</ID>
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() &amp;&amp; Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.JELLY_BEAN &amp;&amp; ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
<ID>ComplexCondition:SongView.kt$SongView$TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo &amp;&amp; Util.getVideoPlayerType() !== VideoPlayerType.FLASH</ID>
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>EmptyCatchBlock:LocalMediaPlayer.kt$LocalMediaPlayer${ }</ID>
<ID>EmptyDefaultConstructor:VideoPlayer.kt$VideoPlayer$()</ID>
<ID>EmptyFunctionBlock:SongView.kt$SongView${}</ID>
<ID>FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent()</ID>
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID>
@ -29,7 +26,6 @@
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s &gt; %s", suffix, transcodedSuffix)</ID>
<ID>LargeClass:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
@ -59,7 +55,6 @@
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$5</ID>
<ID>MagicNumber:SongView.kt$SongView$3</ID>
<ID>MagicNumber:SongView.kt$SongView$4</ID>
<ID>MagicNumber:SongView.kt$SongView$60</ID>
@ -68,14 +63,10 @@
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() }</ID>
<ID>ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response&lt;out SubsonicResponse&gt;)</ID>
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$e: Throwable</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +0,0 @@
package org.moire.ultrasonic.Test.service;
import java.io.Reader;
import java.io.StringReader;
/**
* Created by rcocula on 11/03/2016.
*/
public class GetPodcastEpisodesTestReaderProvider {
private static String data = "<subsonic-response status=\"ok\" version=\"1.12.0\" xmlns=\"http://subsonic.org/restapi\">\n" +
" <podcasts>\n" +
" <channel id=\"0\" url=\"http://radiofrance-podcast.net/podcast09/rss_13183.xml\" title=\"La tribune des critiques de disques\" description=\"Sous la houlette de Jérémie Rousseau, d'éminents critiques musicaux écoutent à l'aveugle différentes versions d'une oeuvre du répertoire et la commentent\" status=\"completed\">\n" +
" <episode id=\"2551\" parent=\"42169\" isDir=\"false\" title=\"2ème Sonate, Op. 35 de Frédéric Chopin.\" album=\"2ème Sonate, Op. 35 de Frédéric Chopin.\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87140480\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5428\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-06.03.2016-ITEMA_20929680-0.mp3\" isVideo=\"false\" created=\"2016-03-06T21:13:09.000Z\" albumId=\"4089\" artistId=\"1457\" type=\"podcast\" streamId=\"54710\" description=\"durée : 01:30:16 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec la participation d'Elsa Fottorino, Stéphane Friédérich et Piotr Kaminski. - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-03-06T20:00:00.000Z\"/>\n" +
" <episode id=\"2513\" parent=\"42169\" isDir=\"false\" title=\"Harmonielehre de John Adams\" album=\"La tribune des critiques de disques\" coverArt=\"42169\" size=\"639\" contentType=\"audio/mpeg\" suffix=\"mp3\" path=\"La tribune des critiques de disques/13183-28.02.2016-ITEMA_20924164-0.mp3\" isVideo=\"false\" created=\"2016-02-28T21:38:56.000Z\" type=\"podcast\" streamId=\"54674\" description=\"durée : 01:30:27 - La tribune des critiques de disques - par : Jérémie Rousseau - Bertrand Dermoncourt, Classica - Emmanuelle Guiliani, La Croix - Jean-Charles Hoffelé, Diapason - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-02-28T20:00:00.000Z\"/>\n" +
" <episode id=\"2465\" parent=\"42169\" isDir=\"false\" title=\"Le Barbier de Séville (Acte I) de Gioachino Rossini\" album=\"Le Barbier de Séville (Acte I) de Gioachino Rossini\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87089280\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5424\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-21.02.2016-ITEMA_20918652-0.mp3\" isVideo=\"false\" created=\"2016-02-22T04:13:19.000Z\" albumId=\"3955\" artistId=\"1457\" type=\"podcast\" streamId=\"54629\" description=\"durée : 01:30:12 - La tribune des critiques de disques - par : Jérémie Rousseau - avec Chantal Cazaux, Emmanuel Dupuy et Sylvain Fort - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-02-21T20:00:00.000Z\"/>\n" +
" <episode id=\"2432\" parent=\"42169\" isDir=\"false\" title=\"Suite en La de Jean-Philippe Rameau\" album=\"Suite en La de Jean-Philippe Rameau\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86632576\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5396\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-14.02.2016-ITEMA_20913147-0.mp3\" isVideo=\"false\" created=\"2016-02-16T22:16:23.000Z\" albumId=\"3929\" artistId=\"1457\" type=\"podcast\" streamId=\"54563\" description=\"durée : 01:29:44 - La tribune des critiques de disques - par : Jérémie Rousseau - avec Elsa Fottorino, Piotr Kaminski, Philippe Venturini - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-02-14T20:00:00.000Z\"/>\n" +
" <episode id=\"2397\" parent=\"42169\" isDir=\"false\" title=\"La Valse de l'Empereur de Johann Strauss\" album=\"La Valse de l'Empereur de Johann Strauss\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87042176\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5421\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-07.02.2016-ITEMA_20907545-0.mp3\" isVideo=\"false\" created=\"2016-02-09T04:13:46.000Z\" albumId=\"3893\" artistId=\"1457\" type=\"podcast\" streamId=\"54491\" description=\"durée : 01:30:09 - La tribune des critiques de disques - par : Jérémie Rousseau - avec Séverine Garnier, Emmanuelle Giuliani, Christian Merlin - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-02-07T20:00:00.000Z\"/>\n" +
" <episode id=\"2328\" parent=\"42169\" isDir=\"false\" title=\"Concerto pour piano n° 22 en mi bémol majeur K 482 de Wolfgang Amadeus Mozart\" album=\"Concerto pour piano n° 22 en mi bémol majeur K 482 de Wolfgang Amadeus Mozart\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87261312\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5435\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-31.01.2016-ITEMA_20901964-00.mp3\" isVideo=\"false\" created=\"2016-02-04T04:13:08.000Z\" albumId=\"3872\" artistId=\"1457\" type=\"podcast\" streamId=\"54400\" description=\"durée : 01:30:23 - La tribune des critiques de disques - par : Jérémie Rousseau - Sylvain Fort, Forum Opéra - Elsa Fottorino, Revue Pianiste - Christian Merlin, Le Figaro - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-01-31T20:00:00.000Z\"/>\n" +
" <episode id=\"2321\" parent=\"42169\" isDir=\"false\" title=\"Concerto pour piano n° 22 en mi bémol majeur K 482 de Wolfgang Amadeus Mozart\" album=\"Concerto pour piano n° 22 en mi bémol majeur K 482 de Wolfgang Amadeus Mozart\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87261312\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5435\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-31.01.2016-ITEMA_20901964-0.mp3\" isVideo=\"false\" created=\"2016-02-03T21:23:06.000Z\" albumId=\"3872\" artistId=\"1457\" type=\"podcast\" streamId=\"54385\" description=\"durée : 01:30:23 - La tribune des critiques de disques - par : Jérémie Rousseau - Sylvain Fort, Forum Opéra - Elsa Fottorino, Revue Pianiste - Christian Merlin, Le Figaro - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-01-31T20:00:00.000Z\"/>\n" +
" <episode id=\"2267\" parent=\"42169\" isDir=\"false\" title=\"Symphonie n° 2 &quot;Le Double&quot; d'Henri Dutilleux\" album=\"Symphonie n° 2 ''Le Double'' d'Henri Dutilleux\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86952064\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5416\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-24.01.2016-ITEMA_20896460-0.mp3\" isVideo=\"false\" created=\"2016-01-25T04:13:51.000Z\" albumId=\"3829\" artistId=\"1457\" type=\"podcast\" streamId=\"54245\" description=\"durée : 01:30:04 - La tribune des critiques de disques - par : Jérémie Rousseau - avec Bertrand Dermoncourt, Emmanuelle Giuliani, Jean-Charles Hoffelé - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-01-24T20:00:00.000Z\"/>\n" +
" <episode id=\"2225\" parent=\"42169\" isDir=\"false\" title=\"Gymnopédies de Erik Satie\" album=\"Gymnopédies de Erik Satie\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86579328\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5392\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-17.01.2016-ITEMA_20890984-0.mp3\" isVideo=\"false\" created=\"2016-01-18T04:13:13.000Z\" albumId=\"3792\" artistId=\"1457\" type=\"podcast\" streamId=\"54089\" description=\"durée : 01:29:40 - La tribune des critiques de disques - par : Jérémie Rousseau - avec Bertrand Dermoncourt, Elsa Fottorino, Antoine Mignon - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-01-17T20:00:00.000Z\"/>\n" +
" <episode id=\"2189\" parent=\"42169\" isDir=\"false\" title=\"Quatuor n° 13 en la mineur, D. 804 (op. 29) 'Rosamunde&quot; de Franz Schubert\" album=\"Quatuor n° 13 en la mineur, D. 804 (op. 29) 'Rosamunde'' de Franz Schubert\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86990976\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5418\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-10.01.2016-ITEMA_20885378-0.mp3\" isVideo=\"false\" created=\"2016-01-11T04:14:15.000Z\" albumId=\"3764\" artistId=\"1457\" type=\"podcast\" streamId=\"53982\" description=\"durée : 01:30:06 - La tribune des critiques de disques - par : Jérémie Rousseau - avec Jérémie Cahen, Piotr Kaminski, Philippe Venturini - réalisé par : Marie Grout\" status=\"completed\" publishDate=\"2016-01-10T20:00:00.000Z\"/>\n" +
" <episode id=\"2146\" parent=\"42169\" isDir=\"false\" title=\"Les Quatre Derniers Lieder de Richard Strauss\" album=\"Les Quatre Derniers Lieder de Richard Strauss\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87040128\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5421\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-03.01.2016-ITEMA_20879946-0.mp3\" isVideo=\"false\" created=\"2016-01-04T04:12:55.000Z\" albumId=\"3735\" artistId=\"1457\" type=\"podcast\" streamId=\"53939\" description=\"durée : 01:30:09 - La tribune des critiques de disques - par : Jérémie Rousseau - avec Christian Merlin, Emmanuelle Giuliani et Eric Taver - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2016-01-03T20:00:00.000Z\"/>\n" +
" <episode id=\"2108\" parent=\"42169\" isDir=\"false\" title=\"&quot;Une petite musique de nuit&quot;, Sérénade n° 13 en sol Majeur de Wolfgang-Amadeus Mozart\" album=\"''Une petite musique de nuit'', Sérénade n° 13 en sol Majeur de Wolfgang-Amadeus Mozart\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86259840\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5372\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-27.12.2015-ITEMA_20874498-0.mp3\" isVideo=\"false\" created=\"2015-12-28T04:12:57.000Z\" albumId=\"3708\" artistId=\"1457\" type=\"podcast\" streamId=\"53865\" description=\"durée : 01:29:20 - La tribune des critiques de disques - par : Jérémie Rousseau - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-12-27T20:00:00.000Z\"/>\n" +
" <episode id=\"2070\" parent=\"42169\" isDir=\"false\" title=\"La Cantate BWV 61 &quot;Nun komm, der Heiden Heiland&quot; de Jean-Sébastien Bach\" album=\"La Cantate BWV 61 ''Nun komm, der Heiden Heiland'' de Jean-Sébastien Bach\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87079040\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5424\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-20.12.2015-ITEMA_20868893-0.mp3\" isVideo=\"false\" created=\"2015-12-21T04:13:10.000Z\" albumId=\"3670\" artistId=\"1457\" type=\"podcast\" streamId=\"53761\" description=\"durée : 01:30:12 - La tribune des critiques de disques - par : Jérémie Rousseau - Emission enregistrée en public jeudi 10 décembre au studio 109 à 19h. Avec la participation d'Emmanuel Dupuy, Séverine Garnier et Emmanuelle Giuliani. - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-12-20T20:00:00.000Z\"/>\n" +
" <episode id=\"2025\" parent=\"42169\" isDir=\"false\" title=\"Les Notations pour piano de Pierre Boulez\" album=\"Les Notations pour piano de Pierre Boulez\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86775936\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5405\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-13.12.2015-ITEMA_20863386-0.mp3\" isVideo=\"false\" created=\"2015-12-14T04:13:16.000Z\" albumId=\"3626\" artistId=\"1457\" type=\"podcast\" streamId=\"53522\" description=\"durée : 01:29:53 - La tribune des critiques de disques - par : Jérémie Rousseau - avec Jérémie Bigorie, Jérémie Cahen et Christian Merlin. - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-12-13T20:00:00.000Z\"/>\n" +
" <episode id=\"1983\" parent=\"42169\" isDir=\"false\" title=\"Les Scènes d'enfants op.15 de Robert Schumann\" album=\"Les Scènes d'enfants op.15 de Robert Schumann\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87011456\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5419\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-06.12.2015-ITEMA_20857958-0.mp3\" isVideo=\"false\" created=\"2015-12-07T04:12:55.000Z\" albumId=\"3593\" artistId=\"1457\" type=\"podcast\" streamId=\"53437\" description=\"durée : 01:30:07 - La tribune des critiques de disques - par : Jérémie Rousseau - avec Elsa Fottorino, Jean-Charles Hoffelé et Antoine Mignon. - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-12-06T20:00:00.000Z\"/>\n" +
" <episode id=\"1937\" parent=\"42169\" isDir=\"false\" title=\"Concerto pour piano n°1 en mi bémol Majeur de Franz Liszt\" album=\"Concerto pour piano n°1 en mi bémol Majeur de Franz Liszt\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86730880\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5402\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-29.11.2015-ITEMA_20852326-0.mp3\" isVideo=\"false\" created=\"2015-11-30T04:12:27.000Z\" albumId=\"3570\" artistId=\"1457\" type=\"podcast\" streamId=\"53388\" description=\"durée : 01:29:50 - La tribune des critiques de disques - par : Jérémie Rousseau - Emission enregistrée le jeudi 19 novembre au studio 109 à 19h. Avec la participation d'Elsa Fottorino, Stéphane Friédérich et Sylvain Fort. - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-11-29T20:00:00.000Z\"/>\n" +
" <episode id=\"1810\" parent=\"42169\" isDir=\"false\" title=\"Le Concerto pour deux mandolines en sol Majeur RV 532 de Vivaldi\" album=\"Le Concerto pour deux mandolines en sol Majeur RV 532 de Vivaldi\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86470784\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5386\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-15.11.2015-ITEMA_20841620-0.mp3\" isVideo=\"false\" created=\"2015-11-16T04:12:27.000Z\" albumId=\"3512\" artistId=\"1457\" type=\"podcast\" streamId=\"53214\" description=\"durée : 01:29:34 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jérémie Bigorie, Emmanuelle Giuliani, Piotr Kaminski - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-11-15T20:00:00.000Z\"/>\n" +
" <episode id=\"1778\" parent=\"42169\" isDir=\"false\" title=\"Les Contes d'Hoffmann d'Offenbach (acte II)\" album=\"Les Contes d'Hoffmann d'Offenbach (acte II)\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87195776\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5431\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-08.11.2015-ITEMA_20836265-0.mp3\" isVideo=\"false\" created=\"2015-11-09T04:12:48.000Z\" albumId=\"3478\" artistId=\"1457\" type=\"podcast\" streamId=\"53159\" description=\"durée : 01:30:19 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Chantal Cazaux, Emmanuel Dupuy, Sylvain Fort - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-11-08T20:00:00.000Z\"/>\n" +
" <episode id=\"1721\" parent=\"42169\" isDir=\"false\" title=\"Symphonie n°7 de Bruckner\" album=\"Symphonie n°7 de Bruckner\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87429248\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5446\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-01.11.2015-ITEMA_20830962-00.mp3\" isVideo=\"false\" created=\"2015-11-03T21:57:28.000Z\" albumId=\"3441\" artistId=\"1457\" type=\"podcast\" streamId=\"51952\" description=\"durée : 01:30:34 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jérémie Cahen, Christian Merlin, Philippe Venturini - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-11-01T20:00:00.000Z\"/>\n" +
" <episode id=\"1658\" parent=\"42169\" isDir=\"false\" title=\"Jean Sebastien Bach : Partita n°1 en si bémol Majeur BWV 825\" album=\"Jean Sebastien Bach : Partita n°1 en si bémol Majeur BWV 825\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86999168\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5419\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-25.10.2015-ITEMA_20825686-0.mp3\" isVideo=\"false\" created=\"2015-10-26T04:09:47.000Z\" albumId=\"3328\" artistId=\"1457\" type=\"podcast\" streamId=\"50164\" description=\"durée : 01:30:07 - La tribune des critiques de disques - par : Jérémie Rousseau - réalisé par : Sylvain Richard\" status=\"completed\" publishDate=\"2015-10-25T20:00:00.000Z\"/>\n" +
" <episode id=\"1618\" parent=\"42169\" isDir=\"false\" title=\"Tchaïkovsky : Sérénade pour cordes\" album=\"Tchaïkovsky : Sérénade pour cordes\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87337088\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5440\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-18.10.2015-ITEMA_20820416-0.mp3\" isVideo=\"false\" created=\"2015-10-19T03:10:05.000Z\" albumId=\"3327\" artistId=\"1457\" type=\"podcast\" streamId=\"49791\" description=\"durée : 01:30:28 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jean-Charles Hoffelé, Antoine Mignon, Eric Taver - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-10-18T19:00:00.000Z\"/>\n" +
" <episode id=\"1581\" parent=\"42169\" isDir=\"false\" title=\"Arvo Pärt : Magnificat\" album=\"Arvo Pärt : Magnificat\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86837376\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5408\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-11.10.2015-ITEMA_20815299-0.mp3\" isVideo=\"false\" created=\"2015-10-12T03:10:01.000Z\" albumId=\"3326\" artistId=\"1457\" type=\"podcast\" streamId=\"49731\" description=\"durée : 01:29:57 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Bertrand Dermoncourt, Séverine Garnier, Emmanuelle Giuliani - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-10-11T19:00:00.000Z\"/>\n" +
" <episode id=\"1538\" parent=\"42169\" isDir=\"false\" title=\"Scherzos de Chopin\" album=\"Scherzos de Chopin\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87048320\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5422\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-04.10.2015-ITEMA_20810281-0.mp3\" isVideo=\"false\" created=\"2015-10-04T20:20:32.000Z\" albumId=\"3325\" artistId=\"1457\" type=\"podcast\" streamId=\"49693\" description=\"durée : 01:30:10 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Elsa Fottorino, Sylvain Fort, Christian Merlin - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-10-04T19:00:00.000Z\"/>\n" +
" <episode id=\"1502\" parent=\"42169\" isDir=\"false\" title=\"Haendel : Dixit Dominus\" album=\"Haendel : Dixit Dominus\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87736448\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5465\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-27.09.2015-ITEMA_20805338-0.mp3\" isVideo=\"false\" created=\"2015-09-28T18:31:27.000Z\" albumId=\"3324\" artistId=\"1457\" type=\"podcast\" streamId=\"49634\" description=\"durée : 01:30:53 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Chantal Cazaux, Emmanuel Dupuy, Emmanuelle Giuliani - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-09-27T19:00:00.000Z\"/>\n" +
" <episode id=\"1458\" parent=\"42169\" isDir=\"false\" title=\"Beethoven : Sonate n°23 &quot;Appassionata&quot;\" album=\"Beethoven : Sonate n°23 ''Appassionata''\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86859904\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5410\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-20.09.2015-ITEMA_20800368-0.mp3\" isVideo=\"false\" created=\"2015-09-21T03:14:46.000Z\" albumId=\"3300\" artistId=\"1457\" type=\"podcast\" streamId=\"49559\" description=\"durée : 01:29:58 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Elsa Fottorino, Christian Merlin, Antoine Mignon - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-09-20T19:00:00.000Z\"/>\n" +
" <episode id=\"1417\" parent=\"42169\" isDir=\"false\" title=\"Verdi : Le Trouvère (acte II)\" album=\"Verdi : Le Trouvère (acte II)\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87050368\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5422\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-13.09.2015-ITEMA_20795506-0.mp3\" isVideo=\"false\" created=\"2015-09-14T03:16:50.000Z\" albumId=\"3269\" artistId=\"1457\" type=\"podcast\" streamId=\"49504\" description=\"durée : 01:30:10 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jérémie Bigorie, Chantal Cazaux, Piotr Kaminski - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-09-13T19:00:00.000Z\"/>\n" +
" <episode id=\"860\" parent=\"42169\" isDir=\"false\" title=\"Concerto pour clarinette en la majeur K 622 (1791) de Wolfgang-Amadeus Mozart\" album=\"Concerto pour clarinette en la majeur K 622 (1791) de Wolfgang-Amadeus Mozart\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86745216\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5403\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-06.09.2015-ITEMA_20790942-0.mp3\" isVideo=\"false\" created=\"2015-09-07T03:17:51.000Z\" albumId=\"3253\" artistId=\"1457\" type=\"podcast\" streamId=\"49461\" description=\"durée : 01:29:51 - La tribune des critiques de disques - par : Jérémie Rousseau - Stéphane Friédérich, Pianiste - Emmanuelle Giuliani, La Croix - Antoine Mignon, Classica - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-09-06T19:00:00.000Z\"/>\n" +
" <episode id=\"709\" parent=\"42169\" isDir=\"false\" title=\"Gnossiennes de Satie\" album=\"Gnossiennes de Satie\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86691968\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5399\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-30.08.2015-ITEMA_20787504-00.mp3\" isVideo=\"false\" created=\"2015-09-01T19:28:47.000Z\" albumId=\"2615\" artistId=\"1457\" type=\"podcast\" streamId=\"45554\" description=\"durée : 01:29:47 - La tribune des critiques de disques - par : Jérémie Rousseau - thème de la semaine : Rediffusion du 22 février 2015 - avec Christian Merlin, Bertrand Dermoncourt et Elsa Fottorino - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-08-30T19:00:00.000Z\"/>\n" +
" <episode id=\"710\" isDir=\"false\" title=\"Parsifal de Wagner, acte III\" description=\"durée : 01:29:46 - La tribune des critiques de disques - par : Jérémie Rousseau - thème de la semaine : Rediffusion du 23 novembre 2014 - Avec Chantal Cazaux, Emmanuel Dupuy, Christian Merlin - réalisé par : Géraldine Prutner\" status=\"skipped\" publishDate=\"2015-08-23T19:00:00.000Z\"/>\n" +
" <episode id=\"711\" isDir=\"false\" title=\"Sonate opus 111 de Beethoven\" description=\"durée : 01:29:43 - La tribune des critiques de disques - par : Jérémie Rousseau - thème de la semaine : Rediffusion du 11 janvier 2015 - Avec Elsa Fottorino, Christian Merlin, Antoine Mignon - réalisé par : Géraldine Prutner\" status=\"skipped\" publishDate=\"2015-08-16T19:00:00.000Z\"/>\n" +
" <episode id=\"712\" isDir=\"false\" title=\"Concerto pour piano &quot; Jeunehomme &quot; de Mozart\" description=\"durée : 01:29:42 - La tribune des critiques de disques - par : Jérémie Rousseau - thème de la semaine : Rediffusion du 1er mars 2015 - Concerto pour piano &amp;quot; Jeunehomme &amp;quot; de Mozart - réalisé par : Géraldine Prutner\" status=\"skipped\" publishDate=\"2015-08-09T19:00:00.000Z\"/>\n" +
" <episode id=\"713\" isDir=\"false\" title=\"Neuvième Symphonie de Beethoven\" description=\"durée : 01:29:40 - La tribune des critiques de disques - par : Jérémie Rousseau - thème de la semaine : Rediffusion du 26 octobre 2014 - Avec Stéphane Friederich , Emmanuelle Giuliani et Philippe Venturini - réalisé par : Cyrielle Weber\" status=\"skipped\" publishDate=\"2015-08-02T19:00:00.000Z\"/>\n" +
" <episode id=\"714\" isDir=\"false\" title=\"Dans les brumes de Leos Janacek\" description=\"durée : 01:29:54 - La tribune des critiques de disques - par : Jérémie Rousseau - thème de la semaine : Rediffusion du 2 novembre 2014 - Avec Bertrand Dermoncourt, Elsa Fottorino et Piotr Kaminski - réalisé par : Géraldine Prutner\" status=\"skipped\" publishDate=\"2015-07-26T19:00:00.000Z\"/>\n" +
" <episode id=\"715\" isDir=\"false\" title=\"&quot;Le Beau Danube Bleu&quot; de Johann Strauss\" description=\"durée : 01:29:41 - La tribune des critiques de disques - par : Jérémie Rousseau - thème de la semaine : Rediffusion - avec Emmanuelle Giuliani, Christian Merlin et Bertrand Dermoncourt - réalisé par : Géraldine Prutner\" status=\"skipped\" publishDate=\"2015-07-19T19:00:00.000Z\"/>\n" +
" <episode id=\"716\" isDir=\"false\" title=\"Gloria de Vivaldi\" description=\"durée : 01:29:44 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jérémie Bigorie, Classica, Chantal Cazaux, musicologue et enseignante et Philippe Venturini, Les Echos et Classica - réalisé par : Géraldine Prutner\" status=\"skipped\" publishDate=\"2015-07-12T19:00:00.000Z\"/>\n" +
" <episode id=\"698\" parent=\"42169\" isDir=\"false\" title=\"Symphonie n°8 de Chostakovitch\" album=\"Symphonie n°8 de Chostakovitch\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86915200\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5413\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-28.06.2015-ITEMA_20771947-0.mp3\" isVideo=\"false\" created=\"2015-06-28T22:18:40.000Z\" albumId=\"2614\" artistId=\"1456\" type=\"podcast\" streamId=\"44999\" description=\"durée : 01:30:01 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Bertrand Dermoncourt, Emmanuelle Giuliani, Xavier Lacavalerie - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-06-28T19:00:00.000Z\"/>\n" +
" <episode id=\"681\" parent=\"42169\" isDir=\"false\" title=\"Passion selon Saint Jean de Bach\" album=\"Passion selon Saint Jean de Bach\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86993024\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5418\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-21.06.2015-ITEMA_20769183-0.mp3\" isVideo=\"false\" created=\"2015-06-21T20:47:41.000Z\" albumId=\"2613\" artistId=\"1456\" type=\"podcast\" streamId=\"44877\" description=\"durée : 01:30:06 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jérémie Bigorie, Emmanuel Dupuy, Eric Taver\n" +
" - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-06-21T19:00:00.000Z\"/>\n" +
" <episode id=\"668\" parent=\"42169\" isDir=\"false\" title=\"Concerto pour violon de Sibelius\" album=\"Concerto pour violon de Sibelius\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86059136\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5360\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-14.06.2015-ITEMA_20766550-00.mp3\" isVideo=\"false\" created=\"2015-06-16T21:56:04.000Z\" albumId=\"2612\" artistId=\"1456\" type=\"podcast\" streamId=\"44869\" description=\"durée : 01:29:08 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Stéphane Friédérich, Jennifer Lesieur, Philippe Venturini - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-06-14T19:00:00.000Z\"/>\n" +
" <episode id=\"661\" parent=\"42169\" isDir=\"false\" title=\"Trio n°1 op. 49 de Mendelssohn\" album=\"Trio n°1 op. 49 de Mendelssohn\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86151296\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5366\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-07.06.2015-ITEMA_20763789-0.mp3\" isVideo=\"false\" created=\"2015-06-07T21:54:20.000Z\" albumId=\"2611\" artistId=\"1456\" type=\"podcast\" streamId=\"44850\" description=\"durée : 01:29:14 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jean-Charles Hoffelé, Antoine Mignon, Eric Taver - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-06-07T19:00:00.000Z\"/>\n" +
" <episode id=\"642\" parent=\"42169\" isDir=\"false\" title=\"Miroirs, de Maurice Ravel\" album=\"Miroirs, de Maurice Ravel\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86952064\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5416\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-31.05.2015-ITEMA_20761227-0.mp3\" isVideo=\"false\" created=\"2015-06-03T21:33:17.000Z\" albumId=\"2610\" artistId=\"1456\" type=\"podcast\" streamId=\"44727\" description=\"durée : 01:30:04 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jérémie Cahen, Stéphane Friédérich, Elsa Fottorino - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-05-31T19:00:00.000Z\"/>\n" +
" <episode id=\"643\" isDir=\"false\" title=\"La Walkyrie de Wagner, acte I\" description=\"durée : 01:30:06 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Chantal Cazaux, Emmanuel Dupuy, Christian Merlin - réalisé par : Cyrielle Weber\" status=\"skipped\" publishDate=\"2015-05-24T19:00:00.000Z\"/>\n" +
" <episode id=\"633\" parent=\"42169\" isDir=\"false\" title=\"Messe du Couronnement de Mozart\" album=\"Messe du Couronnement de Mozart\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86921344\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5414\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-17.05.2015-ITEMA_20756109-0.mp3\" isVideo=\"false\" created=\"2015-05-17T21:47:51.000Z\" albumId=\"2609\" artistId=\"1456\" type=\"podcast\" streamId=\"44636\" description=\"durée : 01:30:02 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Bertrand Dermoncourt (Classica), Emmanuelle Giuliani (La Croix) et Piotr Kaminski (Diapason). - réalisé par : Cyrielle Weber\" status=\"completed\" publishDate=\"2015-05-17T19:00:00.000Z\"/>\n" +
" <episode id=\"620\" parent=\"42169\" isDir=\"false\" title=\"Concerto pour piano n°2 de Rachmaninov\" album=\"Concerto pour piano n°2 de Rachmaninov\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86995072\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5418\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-10.05.2015-ITEMA_20753543-0.mp3\" isVideo=\"false\" created=\"2015-05-10T21:47:50.000Z\" albumId=\"2608\" artistId=\"1456\" type=\"podcast\" streamId=\"44626\" description=\"durée : 01:30:06 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Elsa Fottorino (Pianiste), Stéphane Friédérich (Pianiste), Jean-Charles Hoffelé (Diapason, LAvant-Scène Opéra) - réalisé par : Thomas Jost\" status=\"completed\" publishDate=\"2015-05-10T19:00:00.000Z\"/>\n" +
" <episode id=\"609\" parent=\"42169\" isDir=\"false\" title=\"Symphonie n°82 « LOurs » de Haydn\" album=\"Symphonie n°82 « L?Ours » de Haydn\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87052416\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5422\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-03.05.2015-ITEMA_20751006-0.mp3\" isVideo=\"false\" created=\"2015-05-03T21:48:13.000Z\" albumId=\"2607\" artistId=\"1456\" type=\"podcast\" streamId=\"44590\" description=\"durée : 01:30:10 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Philippe Venturini, Emmanuelle Giuliani, Christian Merlin - réalisé par : Cyrielle Weber\" status=\"completed\" publishDate=\"2015-05-03T19:00:00.000Z\"/>\n" +
" <episode id=\"599\" parent=\"42169\" isDir=\"false\" title=\"Leçons de Ténèbres de Couperin\" album=\"Leçons de Ténèbres de Couperin\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"87036032\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5421\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-26.04.2015-ITEMA_20748474-0.mp3\" isVideo=\"false\" created=\"2015-04-26T22:33:12.000Z\" albumId=\"2606\" artistId=\"1456\" type=\"podcast\" streamId=\"42807\" description=\"durée : 01:30:09 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Chantal Cazaux (LAvant-scène Opéra), Piotr Kaminski (Diapason), Philippe Venturini (Les Echos) \n" +
" - réalisé par : Sylvain Richard\" status=\"completed\" publishDate=\"2015-04-26T19:00:00.000Z\"/>\n" +
" <episode id=\"590\" parent=\"42169\" isDir=\"false\" title=\"Impromptus D. 899 de Schubert\" album=\"Impromptus D. 899 de Schubert\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86634624\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5396\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-19.04.2015-ITEMA_20745909-0.mp3\" isVideo=\"false\" created=\"2015-04-20T17:12:16.000Z\" albumId=\"2605\" artistId=\"1456\" type=\"podcast\" streamId=\"42716\" description=\"durée : 01:29:44 - La tribune des critiques de disques - par : Jérémie Rousseau - Bertrand Dermoncourt (Classica), Elsa Fottorino (Pianiste), Emmanuelle Giuliani (La Croix) - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-04-19T19:00:00.000Z\"/>\n" +
" <episode id=\"574\" parent=\"42169\" isDir=\"false\" title=\"Gloria d'Antonio Vivaldi\" album=\"Gloria d'Antonio Vivaldi\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86798464\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5406\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-15.03.2015-ITEMA_20733576-0.mp3\" isVideo=\"false\" created=\"2015-03-16T15:17:04.000Z\" albumId=\"2604\" artistId=\"1456\" type=\"podcast\" streamId=\"42263\" description=\"durée : 01:29:54 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jérémie Bigorie (Classica), Chantal Cazaux (LAvant-scène Opéra), Philippe Venturini (Les Echos) \n" +
" - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-03-15T20:00:00.000Z\"/>\n" +
" <episode id=\"565\" parent=\"42169\" isDir=\"false\" title=\"&quot;Lagrime mie&quot; de &quot;L'Eraclito Amoroso&quot; de Barbara Strozzi\" album=\"''Lagrime mie'' de ''L'Eraclito Amoroso'' de Barbara Strozzi\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86646912\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5397\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-08.03.2015-ITEMA_20731103-0.mp3\" isVideo=\"false\" created=\"2015-03-08T23:45:39.000Z\" albumId=\"2603\" artistId=\"1456\" type=\"podcast\" streamId=\"42217\" description=\"durée : 01:29:45 - La tribune des critiques de disques - par : Jérémie Rousseau - thème de la semaine : dans le cadre de la Journée internationale de la femme - Avec Jérémie Bigorie (Classica), Jérémie Cahen (disquaire chez Gibert Joseph) et Jean-Charles Hoffelé (Diapason, L'Avant-Scène opéra) - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-03-08T20:00:00.000Z\"/>\n" +
" <episode id=\"556\" parent=\"42169\" isDir=\"false\" title=\"Concerto pour piano « Jeunehomme » de Wolfgang Amadeus Mozart\" album=\"Concerto pour piano « Jeunehomme » de Wolfgang Amadeus Mozart\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86608000\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5394\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-01.03.2015-ITEMA_20728611-0.mp3\" isVideo=\"false\" created=\"2015-03-02T14:28:25.000Z\" albumId=\"2597\" artistId=\"1456\" type=\"podcast\" streamId=\"41746\" description=\"durée : 01:29:42 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Elsa Fottorino, Stéphane Friédérich, Antoine Mignon - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-03-01T20:00:00.000Z\"/>\n" +
" <episode id=\"546\" parent=\"42169\" isDir=\"false\" title=\"Gnossiennes d'Erik Satie\" album=\"Gnossiennes d'Erik Satie\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86866048\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5410\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-22.02.2015-ITEMA_20726133-0.mp3\" isVideo=\"false\" created=\"2015-02-23T17:47:40.000Z\" albumId=\"2596\" artistId=\"1456\" type=\"podcast\" streamId=\"41705\" description=\"durée : 01:29:58 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Christian Merlin, Bertrand Dermoncourt et Elsa Fottorino - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-02-22T20:00:00.000Z\"/>\n" +
" <episode id=\"513\" parent=\"42169\" isDir=\"false\" title=\"Sextuor n°1 de Johannes Brahms\" album=\"Sextuor n°1 de Johannes Brahms\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86612096\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5394\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-15.02.2015-ITEMA_20723560-0.mp3\" isVideo=\"false\" created=\"2015-02-16T15:30:30.000Z\" albumId=\"2595\" artistId=\"1456\" type=\"podcast\" streamId=\"41592\" description=\"durée : 01:29:42 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jean-Charles Hoffelé, Antoine Mignon, Eric Taver\n" +
" - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-02-15T20:00:00.000Z\"/>\n" +
" <episode id=\"506\" parent=\"42169\" isDir=\"false\" title=\"Cavalleria Rusticana de Pietro Mascagni\" album=\"Cavalleria Rusticana de Pietro Mascagni\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86425728\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5383\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-08.02.2015-ITEMA_20721030-0.mp3\" isVideo=\"false\" created=\"2015-02-09T17:03:05.000Z\" albumId=\"2594\" artistId=\"1456\" type=\"podcast\" streamId=\"40570\" description=\"durée : 01:29:31 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jérémie Bigorie (Classica), Chantal Cazaux (L'Avant-Scène Opéra) et Emmanuel Dupuy (Diapason) - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-02-08T20:00:00.000Z\"/>\n" +
" <episode id=\"498\" parent=\"42169\" isDir=\"false\" title=\"Cantate « Weinen, Klagen, Sorgen, Zagen » de Jean-Sébastien Bach\" album=\"Cantate « Weinen, Klagen, Sorgen, Zagen » de Jean-Sébastien Bach\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86556800\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5391\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-01.02.2015-ITEMA_20718534-0.mp3\" isVideo=\"false\" created=\"2015-02-01T21:45:28.000Z\" albumId=\"2598\" artistId=\"1456\" type=\"podcast\" streamId=\"42163\" description=\"durée : 01:29:39 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Bertrand Dermoncourt, Emmanuel Dupuy, Philippe Venturini - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-02-01T20:00:00.000Z\"/>\n" +
" <episode id=\"489\" parent=\"42169\" isDir=\"false\" title=\"Quatuor Américain d'Antonin Dvorak\" album=\"Quatuor Américain d'Antonin Dvorak\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86728832\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5402\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-25.01.2015-ITEMA_20716094-0.mp3\" isVideo=\"false\" created=\"2015-01-25T21:43:55.000Z\" albumId=\"2599\" artistId=\"1456\" type=\"podcast\" streamId=\"42164\" description=\"durée : 01:29:50 - La tribune des critiques de disques - par : Jérémie Rousseau - Emmanuelle Giuliani (La Croix), Antoine Mignon (Classica), Eric Taver (L'Etudiant) élisent la version de référence du Quatuor à cordes n°12 « Américain » dAnton Dvorak.\n" +
" - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-01-25T20:00:00.000Z\"/>\n" +
" <episode id=\"479\" parent=\"42169\" isDir=\"false\" title=\"LArlésienne de Georges Bizet\" album=\"L?Arlésienne de Georges Bizet\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86802560\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5406\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-18.01.2015-ITEMA_20713431-0.mp3\" isVideo=\"false\" created=\"2015-01-19T21:12:44.000Z\" albumId=\"2600\" artistId=\"1456\" type=\"podcast\" streamId=\"42165\" description=\"durée : 01:29:54 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Jérémie Cahen, Stéphane Friédérich, Emmanuelle Giuliani - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-01-18T20:00:00.000Z\"/>\n" +
" <episode id=\"470\" parent=\"42169\" isDir=\"false\" title=\"Sonate Opus 111 de Beethoven\" album=\"Sonate Opus 111 de Beethoven\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86657152\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5397\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-11.01.2015-ITEMA_20710790-0.mp3\" isVideo=\"false\" created=\"2015-01-11T21:15:24.000Z\" albumId=\"2601\" artistId=\"1456\" type=\"podcast\" streamId=\"42166\" description=\"durée : 01:29:45 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Elsa Fottorino, Christian Merlin, Antoine Mignon - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-01-11T20:00:00.000Z\"/>\n" +
" <episode id=\"461\" parent=\"42169\" isDir=\"false\" title=\"Le Beau Danube Bleu de Johann Strauss\" album=\"Le Beau Danube Bleu de Johann Strauss\" artist=\"Jérémie Rousseau\" genre=\"Podcast\" coverArt=\"42169\" size=\"86569088\" contentType=\"audio/mpeg\" suffix=\"mp3\" duration=\"5392\" bitRate=\"128\" path=\"La tribune des critiques de disques/13183-04.01.2015-ITEMA_20708450-0.mp3\" isVideo=\"false\" created=\"2015-01-04T20:49:53.000Z\" albumId=\"2602\" artistId=\"1456\" type=\"podcast\" streamId=\"42167\" description=\"durée : 01:29:40 - La tribune des critiques de disques - par : Jérémie Rousseau - Avec Emmanuelle Giuliani (La Croix), Christian Merlin (Le Figaro) et Bertrand Dermoncourt (Classica, L'Express). - réalisé par : Géraldine Prutner\" status=\"completed\" publishDate=\"2015-01-04T20:00:00.000Z\"/>\n" +
" </channel>\n" +
" </podcasts>\n" +
"</subsonic-response>\n";
public static Reader getReader() {
return new StringReader(data);
}
}

View File

@ -1,41 +0,0 @@
package org.moire.ultrasonic.Test.service;
import java.io.Reader;
import java.io.StringReader;
/**
* Created by rcocula on 11/03/2016.
*/
public class GetPodcastTestReaderProvider {
private static String data = "<subsonic-response status=\"ok\" version=\"1.12.0\" xmlns=\"http://subsonic.org/restapi\">\n" +
" <podcasts>\n" +
" <channel id=\"0\" url=\"http://radiofrance-podcast.net/podcast09/rss_13183.xml\" title=\"La tribune des critiques de disques\" description=\"Sous la houlette de Jérémie Rousseau, d'éminents critiques musicaux écoutent à l'aveugle différentes versions d'une oeuvre du répertoire et la commentent\" status=\"completed\"/>\n" +
" <channel id=\"1\" url=\"http://radiofrance-podcast.net/podcast09/rss_11874.xml\" title=\"La Matinale du samedi\" description=\"Une version détendue pour les matinaux du week end\" status=\"completed\"/>\n" +
" <channel id=\"2\" url=\"http://radiofrance-podcast.net/podcast09/rss_10467.xml\" title=\"LES NOUVEAUX CHEMINS DE LA CONNAISSANCE\" description=\"Une rencontre quotidienne entre philosophie et monde contemporain.\" status=\"completed\"/>\n" +
" <channel id=\"3\" url=\"http://radiofrance-podcast.net/podcast09/rss_12087.xml\" title=\"Alla Breve, l'intégrale\" description=\"Une oeuvre courte, commandée à un compositeur d aujourd hui, diffusée en 5 mouvements durant la semaine et proposée en podcast dans son intégralité.\" status=\"completed\"/>\n" +
" <channel id=\"4\" url=\"http://lescastcodeurs.libsyn.com/rss\" title=\"Les Cast Codeurs Podcast\" description=\"Le podcast Java en Français dans le texte\" status=\"completed\"/>\n" +
" <channel id=\"6\" url=\"http://radiofrance-podcast.net/podcast09/rss_14003.xml\" title=\"Le cri du patchwork\" description=\"Si le patchwork était un animal, quel serait son cri ?\" status=\"completed\"/>\n" +
" <channel id=\"7\" url=\"http://radiofrance-podcast.net/podcast09/rss_12289.xml\" title=\"Electromania\" description=\"Electromania continue de témoigner de toutes les musiques avant tout inventives et inclassables, de Pierre Henry à Nick Cave.\" status=\"completed\"/>\n" +
" <channel id=\"8\" url=\"http://radiofrance-podcast.net/podcast09/rss_11910.xml\" title=\"CONTINENT MUSIQUE\" description=\"Funk, baroque, jazz, électro, classique, chanson, musique concrète ou hip-hop abstrait...\" status=\"completed\"/>\n" +
" <channel id=\"9\" url=\"http://radiofrance-podcast.net/podcast09/rss_11985.xml\" title=\"SUPERSONIC\" description=\"Un homme ou une femme de son fait partager ses créations et son univers\" status=\"completed\"/>\n" +
" <channel id=\"10\" url=\"http://radiofrance-podcast.net/podcast09/rss_12668.xml\" title=\"Label pop\" description=\"Chaque semaine, une oreille attentive à l'actualité, pour restituer l'éclatante vitalité de la pop moderne, entendue au sens le plus large\" status=\"completed\"/>\n" +
" <channel id=\"11\" url=\"http://radiofrance-podcast.net/podcast09/rss_11224.xml\" title=\"Le magazine de la contemporaine\" description=\"Interviews et reportages autour de l'actualité de la musique contemporaine\" status=\"completed\"/>\n" +
" <channel id=\"12\" url=\"http://radiofrance-podcast.net/podcast09/rss_11393.xml\" title=\"Les greniers de la mémoire\" description=\"Une visite complice et nostalgique des archives musicales de Radio France\" status=\"completed\"/>\n" +
" <channel id=\"13\" url=\"http://radiofrance-podcast.net/podcast09/rss_14498.xml\" title=\"Musicopolis\" description=\"Une ville, un compositeur, une époque. Une histoire de la musique racontée chaque semaine.\" status=\"completed\"/>\n" +
" <channel id=\"14\" url=\"http://radiofrance-podcast.net/podcast09/rss_14603.xml\" title=\"On ne peut pas tout savoir\" description=\"Parce qu'on ne peut pas tout savoir, Arnaud Merlin vous propose chaque semaine un voyage dans un univers musical différent.\" status=\"completed\"/>\n" +
" <channel id=\"15\" url=\"http://radiofrance-podcast.net/podcast09/rss_12576.xml\" title=\"LES CARNETS DE L'ECONOMIE\" description=\"Un chercheur ou un acteur de la sphère économique et sociale nous livre un concentré de ses travaux et de sa réflexion\" status=\"completed\"/>\n" +
" <channel id=\"16\" url=\"http://radiofrance-podcast.net/podcast09/rss_14076.xml\" title=\"LES CARNETS DE LA CREATION\" description=\"LES CARNETS DE LA CREATION\" status=\"completed\"/>\n" +
" <channel id=\"18\" url=\"http://radiofrance-podcast.net/podcast09/rss_14663.xml\" title=\"LE MONDE SELON XAVIER DELAPORTE\" description=\"LE MONDE SELON XAVIER DELAPORTE\" status=\"completed\"/>\n" +
" <channel id=\"19\" url=\"http://radiofrance-podcast.net/podcast09/rss_16260.xml\" title=\"LA SUITE DANS LES IDEES\" description=\"Contribuer à alimenter le débat public par les idées\" status=\"completed\"/>\n" +
" <channel id=\"20\" url=\"http://radiofrance-podcast.net/podcast09/rss_13959.xml\" title=\"CULTURE MUSIQUE\" description=\"CULTURE MUSIQUE\" status=\"completed\"/>\n" +
" <channel id=\"21\" url=\"http://radiofrance-podcast.net/podcast09/rss_14009.xml\" title=\"Carnets de voyages\" description=\"Carnet de voyage est un atlas ouvert sur les musiques que l'on dit de tradition orale ou extra-européennes.\" status=\"completed\"/>\n" +
" </podcasts>\n" +
"</subsonic-response>\n";
public static Reader getReader() {
return new StringReader(data);
}
}

View File

@ -55,7 +55,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
private Preference addServerPreference;
private ListPreference theme;
private ListPreference videoPlayer;
private ListPreference maxBitrateWifi;
private ListPreference maxBitrateMobile;
private ListPreference cacheSize;
@ -110,7 +109,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
addServerPreference = findPreference(Constants.PREFERENCES_KEY_SERVERS_EDIT);
theme = findPreference(Constants.PREFERENCES_KEY_THEME);
videoPlayer = findPreference(Constants.PREFERENCES_KEY_VIDEO_PLAYER);
maxBitrateWifi = findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI);
maxBitrateMobile = findPreference(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE);
cacheSize = findPreference(Constants.PREFERENCES_KEY_CACHE_SIZE);
@ -411,7 +409,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
private void update() {
theme.setSummary(theme.getEntry());
videoPlayer.setSummary(videoPlayer.getEntry());
maxBitrateWifi.setSummary(maxBitrateWifi.getEntry());
maxBitrateMobile.setSummary(maxBitrateMobile.getEntry());
cacheSize.setSummary(cacheSize.getEntry());

View File

@ -30,6 +30,7 @@ import android.widget.Toast;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException;
import org.moire.ultrasonic.app.UApp;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.JukeboxStatus;

View File

@ -117,7 +117,6 @@ public final class Constants
public static final String PREFERENCES_KEY_CLEAR_PLAYLIST = "clearPlaylist";
public static final String PREFERENCES_KEY_CLEAR_BOOKMARK = "clearBookmark";
public static final String PREFERENCES_KEY_DISC_SORT = "discAndTrackSort";
public static final String PREFERENCES_KEY_VIDEO_PLAYER = "videoPlayer";
public static final String PREFERENCES_KEY_SEND_BLUETOOTH_NOTIFICATIONS = "sendBluetoothNotifications";
public static final String PREFERENCES_KEY_SEND_BLUETOOTH_ALBUM_ART = "sendBluetoothAlbumArt";
public static final String PREFERENCES_KEY_VIEW_REFRESH = "viewRefresh";

View File

@ -21,8 +21,9 @@ package org.moire.ultrasonic.util;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.*;
import android.content.pm.ApplicationInfo;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
@ -39,7 +40,6 @@ import android.os.Build;
import android.os.Environment;
import android.os.Parcelable;
import android.util.DisplayMetrics;
import timber.log.Timber;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
@ -51,19 +51,32 @@ import androidx.preference.PreferenceManager;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.app.UApp;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.*;
import org.moire.ultrasonic.domain.Bookmark;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicDirectory.Entry;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.domain.RepeatMode;
import org.moire.ultrasonic.domain.SearchResult;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.MediaPlayerService;
import java.io.*;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import timber.log.Timber;
/**
* @author Sindre Mehus
* @version $Id$
@ -1148,36 +1161,6 @@ public class Util
else return minutes > 0 ? String.format(Locale.getDefault(), "%d:%02d", minutes, seconds) : String.format(Locale.getDefault(), "0:%02d", seconds);
}
public static VideoPlayerType getVideoPlayerType()
{
SharedPreferences preferences = getPreferences();
return VideoPlayerType.forKey(preferences.getString(Constants.PREFERENCES_KEY_VIDEO_PLAYER, VideoPlayerType.MX.getKey()));
}
public static boolean isPackageInstalled(Context context, String packageName)
{
PackageManager pm = context.getPackageManager();
List<ApplicationInfo> packages = null;
if (pm != null)
{
packages = pm.getInstalledApplications(0);
}
if (packages != null)
{
for (ApplicationInfo packageInfo : packages)
{
if (packageInfo.packageName.equals(packageName))
{
return true;
}
}
}
return false;
}
public static String getVersionName(Context context)
{
String versionName = null;

View File

@ -1,139 +0,0 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2013 (C) Sindre Mehus
*/
package org.moire.ultrasonic.util;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.MusicServiceFactory;
/**
* @author Sindre Mehus
* @version $Id: VideoPlayerType.java 3473 2013-05-23 16:42:49Z sindre_mehus $
*/
public enum VideoPlayerType
{
MX("mx")
{
@Override
public void playVideo(final Context context, MusicDirectory.Entry entry) throws Exception
{
// Check if MX Player is installed.
boolean installedAd = Util.isPackageInstalled(context, PACKAGE_NAME_MX_AD);
boolean installedPro = Util.isPackageInstalled(context, PACKAGE_NAME_MX_PRO);
if (!installedAd && !installedPro)
{
new AlertDialog.Builder(context).setMessage(R.string.video_get_mx_player_text).setPositiveButton(R.string.video_get_mx_player_button, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int i)
{
try
{
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("market://details?id=%s", PACKAGE_NAME_MX_AD))));
}
catch (android.content.ActivityNotFoundException x)
{
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("http://play.google.com/store/apps/details?id=%s", PACKAGE_NAME_MX_AD))));
}
dialog.dismiss();
}
}).setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int i)
{
dialog.dismiss();
}
}).show();
}
else
{
// See documentation on https://sites.google.com/site/mxvpen/api
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setPackage(installedPro ? PACKAGE_NAME_MX_PRO : PACKAGE_NAME_MX_AD);
intent.putExtra("title", entry.getTitle());
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService().getVideoUrl(entry.getId(), false)), "video/*");
context.startActivity(intent);
}
}
},
FLASH("flash")
{
@Override
public void playVideo(Context context, MusicDirectory.Entry entry) throws Exception
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(MusicServiceFactory.getMusicService().getVideoUrl(entry.getId(), true)));
context.startActivity(intent);
}
},
DEFAULT("default")
{
@Override
public void playVideo(Context context, MusicDirectory.Entry entry) throws Exception
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(MusicServiceFactory.getMusicService().getVideoUrl(entry.getId(), false)), "video/*");
context.startActivity(intent);
}
};
private final String key;
VideoPlayerType(String key)
{
this.key = key;
}
public String getKey()
{
return key;
}
public static VideoPlayerType forKey(String key)
{
for (VideoPlayerType type : VideoPlayerType.values())
{
if (type.key.equals(key))
{
return type;
}
}
return null;
}
public abstract void playVideo(Context context, MusicDirectory.Entry entry) throws Exception;
private static final String PACKAGE_NAME_MX_AD = "com.mxtech.videoplayer.ad";
private static final String PACKAGE_NAME_MX_PRO = "com.mxtech.videoplayer.pro";
}

View File

@ -29,7 +29,6 @@ import androidx.preference.PreferenceManager
import com.google.android.material.navigation.NavigationView
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
@ -151,7 +150,12 @@ class NavigationActivity : AppCompatActivity() {
showWelcomeScreen = showWelcomeScreen and !areServersMigrated
loadSettings()
showInfoDialog(showWelcomeScreen)
// This is a first run with only the demo entry inside the database
// We set the active server to the demo one and show the welcome dialog
if (showWelcomeScreen) {
showWelcomeDialog()
}
nowPlayingEventListener = object : NowPlayingEventListener {
override fun onDismissNowPlaying() {
@ -313,19 +317,27 @@ class NavigationActivity : AppCompatActivity() {
finish()
}
private fun showInfoDialog(show: Boolean) {
private fun showWelcomeDialog() {
if (!infoDialogDisplayed) {
infoDialogDisplayed = true
if (show) {
AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_info)
.setTitle(R.string.main_welcome_title)
.setMessage(R.string.main_welcome_text)
.setPositiveButton(R.string.common_ok) { dialog, _ ->
dialog.dismiss()
findNavController(R.id.nav_host_fragment).navigate(R.id.settingsFragment)
}.show()
}
AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_info)
.setTitle(R.string.main_welcome_title)
.setMessage(R.string.main_welcome_text_demo)
.setNegativeButton(R.string.main_welcome_cancel) { dialog, _ ->
// Go to the settings screen
dialog.dismiss()
findNavController(R.id.nav_host_fragment).navigate(R.id.settingsFragment)
}
.setPositiveButton(R.string.common_ok) { dialog, _ ->
// Add the demo server
val activeServerProvider: ActiveServerProvider by inject()
val demoIndex = serverSettingsModel.addDemoServer()
activeServerProvider.setActiveServerByIndex(demoIndex)
findNavController(R.id.nav_host_fragment).navigate(R.id.mainFragment)
dialog.dismiss()
}.show()
}
}

View File

@ -37,12 +37,15 @@ class ActiveServerProvider(
cachedServer = repository.findById(serverId)
}
Timber.d(
"getActiveServer retrieved from DataBase, id: $serverId; " +
"cachedServer: $cachedServer"
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
serverId, cachedServer
)
}
if (cachedServer != null) return cachedServer!!
if (cachedServer != null) {
return cachedServer!!
}
setActiveServerId(0)
}
@ -105,7 +108,7 @@ class ActiveServerProvider(
* @param method: The Rest resource to use
* @return The Rest Url of the method on the server
*/
fun getRestUrl(method: String?): String? {
fun getRestUrl(method: String?): String {
val builder = StringBuilder(8192)
val activeServer = getActiveServer()
val serverUrl: String = activeServer.url

View File

@ -12,6 +12,7 @@ import org.moire.ultrasonic.fragment.ServerSettingsModel
import org.moire.ultrasonic.util.Util
const val SP_NAME = "Default_SP"
const val DB_FILENAME = "ultrasonic-database"
/**
* This Koin module contains registration of classes related to permanent storage
@ -23,11 +24,10 @@ val appPermanentStorage = module {
Room.databaseBuilder(
androidContext(),
AppDatabase::class.java,
"ultrasonic-database"
DB_FILENAME
)
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.fallbackToDestructiveMigrationOnDowngrade()
.build()
}

View File

@ -14,7 +14,6 @@ import org.moire.ultrasonic.cache.PermanentFileStorage
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.log.TimberOkHttpLogger
import org.moire.ultrasonic.service.ApiCallResponseChecker
import org.moire.ultrasonic.service.CachedMusicService
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.OfflineMusicService
@ -50,28 +49,29 @@ val musicServiceModule = module {
}
single {
val server = get<ActiveServerProvider>().getActiveServer()
return@single SubsonicClientConfiguration(
baseUrl = get<ActiveServerProvider>().getActiveServer().url,
username = get<ActiveServerProvider>().getActiveServer().userName,
password = get<ActiveServerProvider>().getActiveServer().password,
baseUrl = server.url,
username = server.userName,
password = server.password,
minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion(
get<ActiveServerProvider>().getActiveServer().minimumApiVersion
server.minimumApiVersion
?: Constants.REST_PROTOCOL_VERSION
),
clientID = Constants.REST_CLIENT_ID,
allowSelfSignedCertificate = get<ActiveServerProvider>()
.getActiveServer().allowSelfSignedCertificate,
enableLdapUserSupport = get<ActiveServerProvider>().getActiveServer().ldapSupport,
debug = BuildConfig.DEBUG
allowSelfSignedCertificate = server.allowSelfSignedCertificate,
enableLdapUserSupport = server.ldapSupport,
debug = BuildConfig.DEBUG,
isRealProtocolVersion = server.minimumApiVersion != null
)
}
single<HttpLoggingInterceptor.Logger> { TimberOkHttpLogger() }
single { SubsonicAPIClient(get(), get()) }
single { ApiCallResponseChecker(get(), get()) }
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
CachedMusicService(RESTMusicService(get(), get(), get(), get()))
CachedMusicService(RESTMusicService(get(), get(), get()))
}
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {

View File

@ -22,12 +22,13 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
import org.moire.ultrasonic.api.subsonic.falseOnFailure
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.api.subsonic.throwOnFailure
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.service.ApiCallResponseChecker
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.SubsonicRESTException
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.ErrorDialog
import org.moire.ultrasonic.util.ModalBackgroundTask
@ -360,7 +361,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
// Execute a ping to check the authentication, now using the correct API version.
pingResponse = subsonicApiClient.api.ping().execute()
ApiCallResponseChecker.checkResponseSuccessful(pingResponse)
pingResponse.throwOnFailure()
currentServerSetting!!.chatSupport = isServerFunctionAvailable {
subsonicApiClient.api.getChatMessages().execute()
@ -387,7 +388,8 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
updateProgress(getProgress())
val licenseResponse = subsonicApiClient.api.getLicense().execute()
ApiCallResponseChecker.checkResponseSuccessful(licenseResponse)
licenseResponse.throwOnFailure()
if (!licenseResponse.body()!!.license.valid) {
return getProgress() + "\n" +
resources.getString(R.string.settings_testing_unlicensed)
@ -438,9 +440,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
private fun isServerFunctionAvailable(function: () -> Response<out SubsonicResponse>): Boolean {
return try {
val response = function()
ApiCallResponseChecker.checkResponseSuccessful(response)
true
function().falseOnFailure()
} catch (_: IOException) {
false
} catch (_: SubsonicRESTException) {

View File

@ -10,8 +10,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.net.ConnectException
import java.net.UnknownHostException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -89,10 +87,8 @@ open class GenericListModel(application: Application) :
try {
load(isOffline, useId3Tags, musicService, refresh, bundle)
} catch (exception: ConnectException) {
handleException(exception, swipe.context)
} catch (exception: UnknownHostException) {
handleException(exception, swipe.context)
} catch (all: Exception) {
handleException(all, swipe.context)
}
}

View File

@ -11,6 +11,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.data.ServerSettingDao
@ -25,20 +27,6 @@ class ServerSettingsModel(
application: Application
) : AndroidViewModel(application) {
companion object {
private const val PREFERENCES_KEY_SERVER_MIGRATED = "serverMigrated"
// These constants were removed from Constants.java as they are deprecated and only used here
private const val PREFERENCES_KEY_JUKEBOX_BY_DEFAULT = "jukeboxEnabled"
private const val PREFERENCES_KEY_SERVER_NAME = "serverName"
private const val PREFERENCES_KEY_SERVER_URL = "serverUrl"
private const val PREFERENCES_KEY_ACTIVE_SERVERS = "activeServers"
private const val PREFERENCES_KEY_USERNAME = "username"
private const val PREFERENCES_KEY_PASSWORD = "password"
private const val PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate"
private const val PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport"
private const val PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId"
}
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
/**
@ -67,8 +55,8 @@ class ServerSettingsModel(
repository.insert(newServerSetting)
index++
Timber.i(
"Imported server from Preferences to Database:" +
" ${newServerSetting.name}"
"Imported server from Preferences to Database: %s",
newServerSetting.name
)
}
}
@ -187,6 +175,23 @@ class ServerSettingsModel(
}
}
/**
* Inserts a new Setting into the database
* @return The id of the demo server
*/
fun addDemoServer(): Int {
val demo = DEMO_SERVER_CONFIG.copy()
runBlocking {
demo.index = (repository.count() ?: 0) + 1
demo.id = (repository.getMaxId() ?: 0) + 1
repository.insert(demo)
Timber.d("Added demo server")
}
return demo.id
}
/**
* Reads up a Server Setting stored in the obsolete Preferences
*/
@ -262,4 +267,36 @@ class ServerSettingsModel(
editor.putBoolean(PREFERENCES_KEY_SERVER_MIGRATED + preferenceId, true)
editor.apply()
}
companion object {
private const val PREFERENCES_KEY_SERVER_MIGRATED = "serverMigrated"
// These constants were removed from Constants.java as they are deprecated and only used here
private const val PREFERENCES_KEY_JUKEBOX_BY_DEFAULT = "jukeboxEnabled"
private const val PREFERENCES_KEY_SERVER_NAME = "serverName"
private const val PREFERENCES_KEY_SERVER_URL = "serverUrl"
private const val PREFERENCES_KEY_ACTIVE_SERVERS = "activeServers"
private const val PREFERENCES_KEY_USERNAME = "username"
private const val PREFERENCES_KEY_PASSWORD = "password"
private const val PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate"
private const val PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport"
private const val PREFERENCES_KEY_MUSIC_FOLDER_ID = "musicFolderId"
private val DEMO_SERVER_CONFIG = ServerSetting(
id = 0,
index = 0,
name = UApp.applicationContext().getString(R.string.server_menu_demo),
url = "https://demo.ampache.dev",
userName = "ultrasonic_demo",
password = "W7DumQ3ZUR89Se3",
jukeboxByDefault = false,
allowSelfSignedCertificate = false,
ldapSupport = false,
musicFolderId = null,
minimumApiVersion = "1.13.0",
chatSupport = true,
bookmarkSupport = true,
shareSupport = true,
podcastSupport = true
)
}
}

View File

@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
* Loads avatars from subsonic api.
*/
class AvatarRequestHandler(
private val apiClient: SubsonicAPIClient
private val client: SubsonicAPIClient
) : RequestHandler() {
override fun canHandleRequest(data: Request): Boolean {
return with(data.uri) {
@ -23,7 +23,9 @@ class AvatarRequestHandler(
val username = request.uri.getQueryParameter(QUERY_USERNAME)
?: throw IllegalArgumentException("Nullable username")
val response = apiClient.getAvatar(username)
// Inverted call order, because Mockito has problems with chained calls.
val response = client.toStreamResponse(client.api.getAvatar(username).execute())
if (response.hasError() || response.stream == null) {
throw IOException("${response.apiError}")
} else {

View File

@ -7,13 +7,14 @@ import com.squareup.picasso.RequestHandler
import java.io.IOException
import okio.Okio
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.toStreamResponse
import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE
import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL
/**
* Loads cover arts from subsonic api.
*/
class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : RequestHandler() {
class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHandler() {
override fun canHandleRequest(data: Request): Boolean {
return with(data.uri) {
scheme == SCHEME &&
@ -38,7 +39,10 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request
}
// Try to fetch the image from the API
val response = apiClient.getCoverArt(id, size)
// Inverted call order, because Mockito has problems with chained calls.
val response = client.toStreamResponse(client.api.getCoverArt(id, size).execute())
// Handle the response
if (!response.hasError() && response.stream != null) {
return Result(Okio.source(response.stream!!), NETWORK)
}

View File

@ -13,8 +13,9 @@ import java.io.OutputStream
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.throwOnFailure
import org.moire.ultrasonic.api.subsonic.toStreamResponse
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util
import timber.log.Timber
@ -24,9 +25,12 @@ import timber.log.Timber
*/
class ImageLoader(
context: Context,
private val apiClient: SubsonicAPIClient,
apiClient: SubsonicAPIClient,
private val config: ImageLoaderConfig
) {
// Shortcut
@Suppress("VariableNaming", "PropertyName")
val API = apiClient.api
private val picasso = Picasso.Builder(context)
.addRequestHandler(CoverArtRequestHandler(apiClient))
@ -143,8 +147,8 @@ class ImageLoader(
// Query the API
Timber.d("Loading cover art for: %s", entry)
val response = apiClient.getCoverArt(id!!, size.toLong())
RESTMusicService.checkStreamResponseError(response)
val response = API.getCoverArt(id!!, size.toLong()).execute().toStreamResponse()
response.throwOnFailure()
// Check for failure
if (response.stream == null) return

View File

@ -1,66 +0,0 @@
package org.moire.ultrasonic.service
import java.io.IOException
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIDefinition
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.data.ActiveServerProvider
import retrofit2.Response
import timber.log.Timber
/**
* This call wraps Subsonic API calls so their results can be checked for errors, API version, etc
*/
class ApiCallResponseChecker(
private val subsonicAPIClient: SubsonicAPIClient,
private val activeServerProvider: ActiveServerProvider
) {
/**
* Executes a Subsonic API call with response check
*/
@Throws(SubsonicRESTException::class, IOException::class)
fun <T : Response<out SubsonicResponse>> callWithResponseCheck(
call: (SubsonicAPIDefinition) -> T
): T {
// Check for API version when first contacting the server
if (activeServerProvider.getActiveServer().minimumApiVersion == null) {
try {
val response = subsonicAPIClient.api.ping().execute()
if (response.body() != null) {
val restApiVersion = response.body()!!.version.restApiVersion
Timber.i("Server minimum API version set to %s", restApiVersion)
activeServerProvider.setMinimumApiVersion(restApiVersion)
}
} catch (ignored: Exception) {
// This Ping is only used to get the API Version, if it fails, that's no problem.
}
}
// This call will be now executed with the correct API Version, so it shouldn't fail
val result = call.invoke(subsonicAPIClient.api)
checkResponseSuccessful(result)
return result
}
/**
* Creates Exceptions from the results returned by the Subsonic API
*/
companion object {
@Throws(SubsonicRESTException::class, IOException::class)
fun checkResponseSuccessful(response: Response<out SubsonicResponse>) {
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
return
}
if (!response.isSuccessful) {
throw IOException("Server error, code: " + response.code())
} else if (
response.body()!!.status === SubsonicResponse.Status.ERROR &&
response.body()!!.error != null
) {
throw SubsonicRESTException(response.body()!!.error!!)
} else {
throw IOException("Failed to perform request: " + response.code())
}
}
}
}

View File

@ -264,8 +264,8 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
}
@Throws(Exception::class)
override fun getVideoUrl(id: String, useFlash: Boolean): String? {
return musicService.getVideoUrl(id, useFlash)
override fun getVideoUrl(id: String): String? {
return musicService.getVideoUrl(id)
}
@Throws(Exception::class)

View File

@ -28,6 +28,7 @@ import java.security.cert.CertificateException
import javax.net.ssl.SSLException
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
import org.moire.ultrasonic.util.Util
import timber.log.Timber

View File

@ -123,7 +123,7 @@ interface MusicService {
// TODO: Refactor and remove this call (see RestMusicService implementation)
@Throws(Exception::class)
fun getVideoUrl(id: String, useFlash: Boolean): String?
fun getVideoUrl(id: String): String?
@Throws(Exception::class)
fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus

View File

@ -396,7 +396,7 @@ class OfflineMusicService : MusicService, KoinComponent {
}
@Throws(OfflineException::class)
override fun getVideoUrl(id: String, useFlash: Boolean): String? {
override fun getVideoUrl(id: String): String? {
throw OfflineException("getVideoUrl isn't available in offline mode")
}

View File

@ -11,13 +11,15 @@ import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.io.InputStream
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.moire.ultrasonic.api.subsonic.throwOnFailure
import org.moire.ultrasonic.api.subsonic.toStreamResponse
import org.moire.ultrasonic.cache.PermanentFileStorage
import org.moire.ultrasonic.cache.serializers.getIndexesSerializer
import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer
@ -50,20 +52,23 @@ import timber.log.Timber
*/
@Suppress("LargeClass")
open class RESTMusicService(
private val subsonicAPIClient: SubsonicAPIClient,
val subsonicAPIClient: SubsonicAPIClient,
private val fileStorage: PermanentFileStorage,
private val activeServerProvider: ActiveServerProvider,
private val responseChecker: ApiCallResponseChecker
private val activeServerProvider: ActiveServerProvider
) : MusicService {
// Shortcut to the API
@Suppress("VariableNaming", "PropertyName")
val API = subsonicAPIClient.api
@Throws(Exception::class)
override fun ping() {
responseChecker.callWithResponseCheck { api -> api.ping().execute() }
API.ping().execute().throwOnFailure()
}
@Throws(Exception::class)
override fun isLicenseValid(): Boolean {
val response = responseChecker.callWithResponseCheck { api -> api.getLicense().execute() }
val response = API.getLicense().execute().throwOnFailure()
return response.body()!!.license.valid
}
@ -78,9 +83,7 @@ open class RESTMusicService(
if (cachedMusicFolders != null && !refresh) return cachedMusicFolders
val response = responseChecker.callWithResponseCheck { api ->
api.getMusicFolders().execute()
}
val response = API.getMusicFolders().execute().throwOnFailure()
val musicFolders = response.body()!!.musicFolders.toDomainEntityList()
fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer())
@ -98,9 +101,7 @@ open class RESTMusicService(
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
if (cachedIndexes != null && !refresh) return cachedIndexes
val response = responseChecker.callWithResponseCheck { api ->
api.getIndexes(musicFolderId, null).execute()
}
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
val indexes = response.body()!!.indexes.toDomainEntity()
fileStorage.store(indexName, indexes, getIndexesSerializer())
@ -114,9 +115,7 @@ open class RESTMusicService(
val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer())
if (cachedArtists != null && !refresh) return cachedArtists
val response = responseChecker.callWithResponseCheck { api ->
api.getArtists(null).execute()
}
val response = API.getArtists(null).execute().throwOnFailure()
val indexes = response.body()!!.indexes.toDomainEntity()
fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer())
@ -129,7 +128,7 @@ open class RESTMusicService(
albumId: String?,
artistId: String?
) {
responseChecker.callWithResponseCheck { api -> api.star(id, albumId, artistId).execute() }
API.star(id, albumId, artistId).execute().throwOnFailure()
}
@Throws(Exception::class)
@ -138,7 +137,7 @@ open class RESTMusicService(
albumId: String?,
artistId: String?
) {
responseChecker.callWithResponseCheck { api -> api.unstar(id, albumId, artistId).execute() }
API.unstar(id, albumId, artistId).execute().throwOnFailure()
}
@Throws(Exception::class)
@ -146,7 +145,7 @@ open class RESTMusicService(
id: String,
rating: Int
) {
responseChecker.callWithResponseCheck { api -> api.setRating(id, rating).execute() }
API.setRating(id, rating).execute().throwOnFailure()
}
@Throws(Exception::class)
@ -155,9 +154,7 @@ open class RESTMusicService(
name: String?,
refresh: Boolean
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api ->
api.getMusicDirectory(id).execute()
}
val response = API.getMusicDirectory(id).execute().throwOnFailure()
return response.body()!!.musicDirectory.toDomainEntity()
}
@ -168,7 +165,7 @@ open class RESTMusicService(
name: String?,
refresh: Boolean
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api -> api.getArtist(id).execute() }
val response = API.getArtist(id).execute().throwOnFailure()
return response.body()!!.artist.toMusicDirectoryDomainEntity()
}
@ -179,7 +176,7 @@ open class RESTMusicService(
name: String?,
refresh: Boolean
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api -> api.getAlbum(id).execute() }
val response = API.getAlbum(id).execute().throwOnFailure()
return response.body()!!.album.toMusicDirectoryDomainEntity()
}
@ -207,10 +204,9 @@ open class RESTMusicService(
private fun searchOld(
criteria: SearchCriteria
): SearchResult {
val response = responseChecker.callWithResponseCheck { api ->
api.search(null, null, null, criteria.query, criteria.songCount, null, null)
.execute()
}
val response =
API.search(null, null, null, criteria.query, criteria.songCount, null, null)
.execute().throwOnFailure()
return response.body()!!.searchResult.toDomainEntity()
}
@ -223,12 +219,10 @@ open class RESTMusicService(
criteria: SearchCriteria
): SearchResult {
requireNotNull(criteria.query) { "Query param is null" }
val response = responseChecker.callWithResponseCheck { api ->
api.search2(
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
criteria.songCount, null
).execute()
}
val response = API.search2(
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
criteria.songCount, null
).execute().throwOnFailure()
return response.body()!!.searchResult.toDomainEntity()
}
@ -238,12 +232,10 @@ open class RESTMusicService(
criteria: SearchCriteria
): SearchResult {
requireNotNull(criteria.query) { "Query param is null" }
val response = responseChecker.callWithResponseCheck { api ->
api.search3(
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
criteria.songCount, null
).execute()
}
val response = API.search3(
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
criteria.songCount, null
).execute().throwOnFailure()
return response.body()!!.searchResult.toDomainEntity()
}
@ -253,9 +245,7 @@ open class RESTMusicService(
id: String,
name: String
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api ->
api.getPlaylist(id).execute()
}
val response = API.getPlaylist(id).execute().throwOnFailure()
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity()
savePlaylist(name, playlist)
@ -300,9 +290,7 @@ open class RESTMusicService(
override fun getPlaylists(
refresh: Boolean
): List<Playlist> {
val response = responseChecker.callWithResponseCheck { api ->
api.getPlaylists(null).execute()
}
val response = API.getPlaylists(null).execute().throwOnFailure()
return response.body()!!.playlists.toDomainEntitiesList()
}
@ -318,16 +306,15 @@ open class RESTMusicService(
for ((id1) in entries) {
pSongIds.add(id1)
}
responseChecker.callWithResponseCheck { api ->
api.createPlaylist(id, name, pSongIds.toList()).execute()
}
API.createPlaylist(id, name, pSongIds.toList()).execute().throwOnFailure()
}
@Throws(Exception::class)
override fun deletePlaylist(
id: String
) {
responseChecker.callWithResponseCheck { api -> api.deletePlaylist(id).execute() }
API.deletePlaylist(id).execute().throwOnFailure()
}
@Throws(Exception::class)
@ -337,19 +324,15 @@ open class RESTMusicService(
comment: String?,
pub: Boolean
) {
responseChecker.callWithResponseCheck { api ->
api.updatePlaylist(id, name, comment, pub, null, null)
.execute()
}
API.updatePlaylist(id, name, comment, pub, null, null)
.execute().throwOnFailure()
}
@Throws(Exception::class)
override fun getPodcastsChannels(
refresh: Boolean
): List<PodcastsChannel> {
val response = responseChecker.callWithResponseCheck { api ->
api.getPodcasts(false, null).execute()
}
val response = API.getPodcasts(false, null).execute().throwOnFailure()
return response.body()!!.podcastChannels.toDomainEntitiesList()
}
@ -358,9 +341,7 @@ open class RESTMusicService(
override fun getPodcastEpisodes(
podcastChannelId: String?
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api ->
api.getPodcasts(true, podcastChannelId).execute()
}
val response = API.getPodcasts(true, podcastChannelId).execute().throwOnFailure()
val podcastEntries = response.body()!!.podcastChannels[0].episodeList
val musicDirectory = MusicDirectory()
@ -384,9 +365,7 @@ open class RESTMusicService(
artist: String,
title: String
): Lyrics {
val response = responseChecker.callWithResponseCheck { api ->
api.getLyrics(artist, title).execute()
}
val response = API.getLyrics(artist, title).execute().throwOnFailure()
return response.body()!!.lyrics.toDomainEntity()
}
@ -396,9 +375,7 @@ open class RESTMusicService(
id: String,
submission: Boolean
) {
responseChecker.callWithResponseCheck { api ->
api.scrobble(id, null, submission).execute()
}
API.scrobble(id, null, submission).execute().throwOnFailure()
}
@Throws(Exception::class)
@ -408,10 +385,15 @@ open class RESTMusicService(
offset: Int,
musicFolderId: String?
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api ->
api.getAlbumList(fromName(type), size, offset, null, null, null, musicFolderId)
.execute()
}
val response = API.getAlbumList(
fromName(type),
size,
offset,
null,
null,
null,
musicFolderId
).execute().throwOnFailure()
val childList = response.body()!!.albumList.toDomainEntityList()
val result = MusicDirectory()
@ -427,17 +409,15 @@ open class RESTMusicService(
offset: Int,
musicFolderId: String?
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api ->
api.getAlbumList2(
fromName(type),
size,
offset,
null,
null,
null,
musicFolderId
).execute()
}
val response = API.getAlbumList2(
fromName(type),
size,
offset,
null,
null,
null,
musicFolderId
).execute().throwOnFailure()
val result = MusicDirectory()
result.addAll(response.body()!!.albumList.toDomainEntityList())
@ -449,15 +429,13 @@ open class RESTMusicService(
override fun getRandomSongs(
size: Int
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api ->
api.getRandomSongs(
size,
null,
null,
null,
null
).execute()
}
val response = API.getRandomSongs(
size,
null,
null,
null,
null
).execute().throwOnFailure()
val result = MusicDirectory()
result.addAll(response.body()!!.songsList.toDomainEntityList())
@ -467,18 +445,14 @@ open class RESTMusicService(
@Throws(Exception::class)
override fun getStarred(): SearchResult {
val response = responseChecker.callWithResponseCheck { api ->
api.getStarred(null).execute()
}
val response = API.getStarred(null).execute().throwOnFailure()
return response.body()!!.starred.toDomainEntity()
}
@Throws(Exception::class)
override fun getStarred2(): SearchResult {
val response = responseChecker.callWithResponseCheck { api ->
api.getStarred2(null).execute()
}
val response = API.getStarred2(null).execute().throwOnFailure()
return response.body()!!.starred2.toDomainEntity()
}
@ -491,8 +465,10 @@ open class RESTMusicService(
): Pair<InputStream, Boolean> {
val songOffset = if (offset < 0) 0 else offset
val response = subsonicAPIClient.stream(song.id, maxBitrate, songOffset)
checkStreamResponseError(response)
val response = API.stream(song.id, maxBitrate, offset = songOffset)
.execute().toStreamResponse()
response.throwOnFailure()
if (response.stream == null) {
throw IOException("Null stream response")
@ -502,41 +478,51 @@ open class RESTMusicService(
return Pair(response.stream!!, partial)
}
/**
* We currently don't handle video playback in the app, but just create an Intent which video
* players can respond to. For this intent we need the full URL of the stream, including the
* authentication params. This is a bit tricky, because we want to avoid actually executing the
* call because that could take a long time.
*/
@Throws(Exception::class)
override fun getVideoUrl(
id: String,
useFlash: Boolean
id: String
): String {
// TODO This method should not exists as video should be loaded using stream method
// Previous method implementation uses assumption that video will be available
// by videoPlayer.view?id=<id>&maxBitRate=500&autoplay=true, but this url is not
// official Subsonic API call.
val expectedResult = arrayOfNulls<String>(1)
expectedResult[0] = null
// Create a new modified okhttp client to intercept the URL
val builder = subsonicAPIClient.okHttpClient.newBuilder()
val latch = CountDownLatch(1)
builder.addInterceptor { chain ->
// Returns a dummy response
Response.Builder()
.code(100)
.body(ResponseBody.create(null, ""))
.protocol(Protocol.HTTP_2)
.message("Empty response")
.request(chain.request())
.build()
}
Thread(
{
expectedResult[0] = subsonicAPIClient.getStreamUrl(id) + "&format=raw"
latch.countDown()
},
"Get-Video-Url"
).start()
// Create a new Okhttp client
val client = builder.build()
latch.await(5, TimeUnit.SECONDS)
// Get the request from Retrofit, but don't execute it!
val request = API.stream(id, format = "raw").request()
return expectedResult[0]!!
// Create a new call with the request, and execute ist on our custom client
val response = client.newCall(request).execute()
// The complete url :)
val url = response.request().url()
return url.toString()
}
@Throws(Exception::class)
override fun updateJukeboxPlaylist(
ids: List<String>?
): JukeboxStatus {
val response = responseChecker.callWithResponseCheck { api ->
api.jukeboxControl(JukeboxAction.SET, null, null, ids, null)
.execute()
}
val response = API.jukeboxControl(JukeboxAction.SET, null, null, ids, null)
.execute().throwOnFailure()
return response.body()!!.jukebox.toDomainEntity()
}
@ -546,40 +532,32 @@ open class RESTMusicService(
index: Int,
offsetSeconds: Int
): JukeboxStatus {
val response = responseChecker.callWithResponseCheck { api ->
api.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null)
.execute()
}
val response = API.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null)
.execute().throwOnFailure()
return response.body()!!.jukebox.toDomainEntity()
}
@Throws(Exception::class)
override fun stopJukebox(): JukeboxStatus {
val response = responseChecker.callWithResponseCheck { api ->
api.jukeboxControl(JukeboxAction.STOP, null, null, null, null)
.execute()
}
val response = API.jukeboxControl(JukeboxAction.STOP, null, null, null, null)
.execute().throwOnFailure()
return response.body()!!.jukebox.toDomainEntity()
}
@Throws(Exception::class)
override fun startJukebox(): JukeboxStatus {
val response = responseChecker.callWithResponseCheck { api ->
api.jukeboxControl(JukeboxAction.START, null, null, null, null)
.execute()
}
val response = API.jukeboxControl(JukeboxAction.START, null, null, null, null)
.execute().throwOnFailure()
return response.body()!!.jukebox.toDomainEntity()
}
@Throws(Exception::class)
override fun getJukeboxStatus(): JukeboxStatus {
val response = responseChecker.callWithResponseCheck { api ->
api.jukeboxControl(JukeboxAction.STATUS, null, null, null, null)
.execute()
}
val response = API.jukeboxControl(JukeboxAction.STATUS, null, null, null, null)
.execute().throwOnFailure()
return response.body()!!.jukebox.toDomainEntity()
}
@ -588,10 +566,8 @@ open class RESTMusicService(
override fun setJukeboxGain(
gain: Float
): JukeboxStatus {
val response = responseChecker.callWithResponseCheck { api ->
api.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain)
.execute()
}
val response = API.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain)
.execute().throwOnFailure()
return response.body()!!.jukebox.toDomainEntity()
}
@ -600,7 +576,7 @@ open class RESTMusicService(
override fun getShares(
refresh: Boolean
): List<Share> {
val response = responseChecker.callWithResponseCheck { api -> api.getShares().execute() }
val response = API.getShares().execute().throwOnFailure()
return response.body()!!.shares.toDomainEntitiesList()
}
@ -609,7 +585,7 @@ open class RESTMusicService(
override fun getGenres(
refresh: Boolean
): List<Genre>? {
val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() }
val response = API.getGenres().execute().throwOnFailure()
return response.body()!!.genresList.toDomainEntityList()
}
@ -620,9 +596,7 @@ open class RESTMusicService(
count: Int,
offset: Int
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api ->
api.getSongsByGenre(genre, count, offset, null).execute()
}
val response = API.getSongsByGenre(genre, count, offset, null).execute().throwOnFailure()
val result = MusicDirectory()
result.addAll(response.body()!!.songsList.toDomainEntityList())
@ -634,9 +608,7 @@ open class RESTMusicService(
override fun getUser(
username: String
): UserInfo {
val response = responseChecker.callWithResponseCheck { api ->
api.getUser(username).execute()
}
val response = API.getUser(username).execute().throwOnFailure()
return response.body()!!.user.toDomainEntity()
}
@ -645,9 +617,7 @@ open class RESTMusicService(
override fun getChatMessages(
since: Long?
): List<ChatMessage> {
val response = responseChecker.callWithResponseCheck { api ->
api.getChatMessages(since).execute()
}
val response = API.getChatMessages(since).execute().throwOnFailure()
return response.body()!!.chatMessages.toDomainEntitiesList()
}
@ -656,12 +626,12 @@ open class RESTMusicService(
override fun addChatMessage(
message: String
) {
responseChecker.callWithResponseCheck { api -> api.addChatMessage(message).execute() }
API.addChatMessage(message).execute().throwOnFailure()
}
@Throws(Exception::class)
override fun getBookmarks(): List<Bookmark> {
val response = responseChecker.callWithResponseCheck { api -> api.getBookmarks().execute() }
val response = API.getBookmarks().execute().throwOnFailure()
return response.body()!!.bookmarkList.toDomainEntitiesList()
}
@ -671,23 +641,21 @@ open class RESTMusicService(
id: String,
position: Int
) {
responseChecker.callWithResponseCheck { api ->
api.createBookmark(id, position.toLong(), null).execute()
}
API.createBookmark(id, position.toLong(), null).execute().throwOnFailure()
}
@Throws(Exception::class)
override fun deleteBookmark(
id: String
) {
responseChecker.callWithResponseCheck { api -> api.deleteBookmark(id).execute() }
API.deleteBookmark(id).execute().throwOnFailure()
}
@Throws(Exception::class)
override fun getVideos(
refresh: Boolean
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api -> api.getVideos().execute() }
val response = API.getVideos().execute().throwOnFailure()
val musicDirectory = MusicDirectory()
musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList())
@ -701,9 +669,7 @@ open class RESTMusicService(
description: String?,
expires: Long?
): List<Share> {
val response = responseChecker.callWithResponseCheck { api ->
api.createShare(ids, description, expires).execute()
}
val response = API.createShare(ids, description, expires).execute().throwOnFailure()
return response.body()!!.shares.toDomainEntitiesList()
}
@ -712,7 +678,7 @@ open class RESTMusicService(
override fun deleteShare(
id: String
) {
responseChecker.callWithResponseCheck { api -> api.deleteShare(id).execute() }
API.deleteShare(id).execute().throwOnFailure()
}
@Throws(Exception::class)
@ -726,8 +692,15 @@ open class RESTMusicService(
expiresValue = null
}
responseChecker.callWithResponseCheck { api ->
api.updateShare(id, description, expiresValue).execute()
API.updateShare(id, description, expiresValue).execute().throwOnFailure()
}
init {
// The client will notice if the minimum supported API version has changed
// By registering a callback we ensure this info is saved in the database as well
subsonicAPIClient.onProtocolChange = {
Timber.i("Server minimum API version set to %s", it)
activeServerProvider.setMinimumApiVersion(it.toString())
}
}
@ -735,19 +708,5 @@ open class RESTMusicService(
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
private const val INDEXES_STORAGE_NAME = "indexes"
private const val ARTISTS_STORAGE_NAME = "artists"
// TODO: Move to response checker
@Throws(SubsonicRESTException::class, IOException::class)
fun checkStreamResponseError(response: StreamResponse) {
if (response.hasError() || response.stream == null) {
if (response.apiError != null) {
throw SubsonicRESTException(response.apiError!!)
} else {
throw IOException(
"Failed to make endpoint request, code: " + response.responseHttpCode
)
}
}
}
}
}

View File

@ -12,7 +12,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicError.TokenAuthNotSupportedForL
import org.moire.ultrasonic.api.subsonic.SubsonicError.TrialPeriodIsOver
import org.moire.ultrasonic.api.subsonic.SubsonicError.UserNotAuthorizedForOperation
import org.moire.ultrasonic.api.subsonic.SubsonicError.WrongUsernameOrPassword
import org.moire.ultrasonic.service.SubsonicRESTException
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
/**
* Extension for [SubsonicRESTException] that returns localized error string, that can used to
@ -21,7 +21,7 @@ import org.moire.ultrasonic.service.SubsonicRESTException
fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String =
when (error) {
is Generic -> {
val message = error.message
val message = (error as Generic).message
val errorMessage = if (message == "") {
context.getString(R.string.api_subsonic_generic_no_message)
} else {

View File

@ -1,22 +1,30 @@
package org.moire.ultrasonic.subsonic
import android.content.Context
import android.content.Intent
import android.net.Uri
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Util
/**
* This utility class helps starting video playback
*/
class VideoPlayer() {
class VideoPlayer {
fun playVideo(context: Context, entry: MusicDirectory.Entry?) {
if (!Util.isNetworkConnected()) {
if (!Util.isNetworkConnected() || entry == null) {
Util.toast(context, R.string.select_album_no_network)
return
}
val player = Util.getVideoPlayerType()
try {
player.playVideo(context, entry)
val intent = Intent(Intent.ACTION_VIEW)
val url = MusicServiceFactory.getMusicService().getVideoUrl(entry.id)
intent.setDataAndType(
Uri.parse(url),
"video/*"
)
context.startActivity(intent)
} catch (e: Exception) {
Util.toast(context, e.toString(), false)
}

View File

@ -36,7 +36,6 @@ import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.VideoPlayerType
import org.moire.ultrasonic.view.EntryAdapter.SongViewHolder
import timber.log.Timber
@ -111,8 +110,7 @@ class SongView(context: Context) : UpdateView(context), Checkable, KoinComponent
val transcodedSuffix = song.transcodedSuffix
fileFormat = if (
TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix ||
song.isVideo && Util.getVideoPlayerType() !== VideoPlayerType.FLASH
TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo
) suffix else String.format("%s > %s", suffix, transcodedSuffix)
val artistName = song.artist

View File

@ -101,7 +101,6 @@
<string name="main.songs_starred">Označené hvězdičkou</string>
<string name="main.songs_title">Skladby</string>
<string name="main.videos">Videa</string>
<string name="main.welcome_text">Vítejte v aplikaci Ultrasonic! Aplikace není ještě nakonfigurována. Poté co nastavíte svůj osobní server (dostupný na <b>subsonic.org</b>), vyberte <i>Správa serverů</i> v <b>Nastavení</b> a připojte aplikaci.</string>
<string name="main.welcome_title">Vítejte!</string>
<string name="menu.about">O aplikaci</string>
<string name="menu.common">Další</string>
@ -130,16 +129,11 @@
<string name="search.search">Kliknout pro vyhledání</string>
<string name="search.songs">Skladby</string>
<string name="search.title">Hledat</string>
<string name="select_album.donate_dialog_0_trial_days_left">Zkušební doba vypršela</string>
<string name="select_album.donate_dialog_later">Pozdějí</string>
<string name="select_album.donate_dialog_message">Získejte neomezená stahování přispěním na Subsonic.</string>
<string name="select_album.donate_dialog_now">Hned</string>
<string name="select_album.empty">Média nenalezena</string>
<string name="select_album.n_selected">%d skladeb označeno.</string>
<string name="select_album.n_unselected">%d skladeb odznačeno.</string>
<string name="select_album.no_network">Varování: Připojení nedostupné.</string>
<string name="select_album.no_sdcard">Chyba: SD karta nedostupná.</string>
<string name="select_album.not_licensed">Server bez licence. Zbývá %d dní zkušební doby.</string>
<string name="select_album.play_all">Přehrát vše</string>
<string name="select_artist.all_folders">Všechny adresáře</string>
<string name="select_artist.folder">Vybrat adresář</string>
@ -311,8 +305,7 @@
<string name="settings.use_folder_for_album_artist_summary">Očekává jména hlavních adresářů obsahující jména umělců</string>
<string name="settings.use_id3">Procházet za použití ID3 tagů</string>
<string name="settings.use_id3_summary">Používat metodu ID3 tagů místo jmen na základě adresářové struktury</string>
<string name="settings.video_title">Video</string>
<string name="settings.video_player">Videopřehrávač</string>
<string name="main.video">Video</string>
<string name="settings.view_refresh">Obnovení náhledu</string>
<string name="settings.view_refresh_500">.5 sekundy</string>
<string name="settings.view_refresh_1000">1 sekunda</string>
@ -334,8 +327,6 @@
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">MX Player není nainstalován. Nainstalujte z Obchodu Play nebo změňte nastavení videí.</string>
<string name="video.get_mx_player_button">Stáhnout MX Player</string>
<string name="widget.initial_text">Ťuknutím vybrat hudbu</string>
<string name="widget.sdcard_busy">SD karta nedostupná</string>
<string name="widget.sdcard_missing">Chybí SD karta</string>
@ -371,9 +362,6 @@
<string name="settings.share_greeting_default">Výchozí pozdrav sdílení</string>
<string name="share_default_greeting">Mrkni na hudbu sdílenou z %s</string>
<string name="share_via">Sdílet skladby přes</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Výchozí</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Sdílení</string>
<string name="select_album_all_songs">Všechny skladby od %s</string>
<string name="settings.show_all_songs_by_artist">Zobrazit všechny skladby umělce</string>

View File

@ -100,7 +100,6 @@
<string name="main.songs_starred">Mit Stern</string>
<string name="main.songs_title">Titel</string>
<string name="main.videos">Filme</string>
<string name="main.welcome_text">Willkommen bei Ultrasonic! Die App ist zurzeit unkonfiguriert. Nachdem du deinen Server konfiguriert hast (siehe <b>subsonic.org</b>), bitte <i>Server hinzufügen</i> in den <b>Einstellungen</b> klicken um die Verbindun herzustellen.</string>
<string name="main.welcome_title">Willkommen</string>
<string name="menu.about">Über</string>
<string name="menu.common">Allgemein</string>
@ -129,16 +128,11 @@
<string name="search.search">Neue Suche</string>
<string name="search.songs">Titel</string>
<string name="search.title">Suche</string>
<string name="select_album.donate_dialog_0_trial_days_left">Testperiode zu Ende</string>
<string name="select_album.donate_dialog_later">Später</string>
<string name="select_album.donate_dialog_message">Unbegresnze Downloads bei Spende an Sunsonic</string>
<string name="select_album.donate_dialog_now">Jetzt</string>
<string name="select_album.empty">Keine Medien gefunden</string>
<string name="select_album.n_selected">%d Titel ausgewählt.</string>
<string name="select_album.n_unselected">%d Titel abgewählt.</string>
<string name="select_album.no_network">Warnung: kein Netz.</string>
<string name="select_album.no_sdcard">Fehler: Keine SD Karte verfügbar.</string>
<string name="select_album.not_licensed">Server nicht lizenziert. Noch %d Testtage</string>
<string name="select_album.play_all">Alles wiedergeben</string>
<string name="select_artist.all_folders">Alle Ordner</string>
<string name="select_artist.folder">Ordner wählen</string>
@ -308,8 +302,7 @@
<string name="settings.use_folder_for_album_artist_summary">Annehmen, dass der Ordner der obersten Ebene der Name des Albumkünstlers ist</string>
<string name="settings.use_id3">Durchsuchen von ID3-Tags</string>
<string name="settings.use_id3_summary">Nutze ID3 Tag Methode anstatt Dateisystem-Methode</string>
<string name="settings.video_title">Film</string>
<string name="settings.video_player">Filmwiedergabe</string>
<string name="main.video">Film</string>
<string name="settings.view_refresh">Aktualisierungsinterval</string>
<string name="settings.view_refresh_500">.5 Sekunden</string>
<string name="settings.view_refresh_1000">1 Sekunde</string>
@ -331,8 +324,6 @@
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">MX Player ist nicht installiert. Holen Sie ihn sich kostenlos im Play Store, oder ändern Sie die Filmeinstellungen.</string>
<string name="video.get_mx_player_button">MX Player holen</string>
<string name="widget.initial_text">Berühren, um Musik auszuwählen</string>
<string name="widget.sdcard_busy">SD Karte nicht verfügbar</string>
<string name="widget.sdcard_missing">Keine SD Karte</string>
@ -368,9 +359,6 @@
<string name="settings.share_greeting_default">Standard Begrüßung beim Teilen</string>
<string name="share_default_greeting">Hör dir mal die Musik an, die ich mit dir über %s geteilt habe.</string>
<string name="share_via">Titel teilen über</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Standard</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Freigabe</string>
<string name="select_album_all_songs">Alle Titel von %s</string>
<string name="settings.show_all_songs_by_artist">Alle Titel nach Künstler sortieren</string>

View File

@ -111,7 +111,6 @@
<string name="main.songs_starred">Me gusta</string>
<string name="main.songs_title">Canciones</string>
<string name="main.videos">Vídeos</string>
<string name="main.welcome_text">Te damos la bienvenida a Ultrasonic! La aplicación no está configurada actualmente. Después de que hayas configurado tu servidor personal (disponible desde <b>subsonic.org</b>), por favor haz click en <i>Administrar servidores</i> en <b>Configuración</b> para conectarte con él.</string>
<string name="main.welcome_title">¡Saludos!</string>
<string name="menu.about">Acerca de</string>
<string name="menu.common">Común</string>
@ -140,16 +139,11 @@
<string name="search.search">Haz click para buscar</string>
<string name="search.songs">Canciones</string>
<string name="search.title">Buscar</string>
<string name="select_album.donate_dialog_0_trial_days_left">El periodo de prueba ha finalizado</string>
<string name="select_album.donate_dialog_later">Mas tarde</string>
<string name="select_album.donate_dialog_message">Consigue descargas ilimitadas donando a Subsonic.</string>
<string name="select_album.donate_dialog_now">Ahora</string>
<string name="select_album.empty">No se han encontrado medios</string>
<string name="select_album.n_selected">%d pista(s) seleccionada(s).</string>
<string name="select_album.n_unselected">%d pista(s) deseleccionada(s).</string>
<string name="select_album.no_network">Atención: No hay red disponible.</string>
<string name="select_album.no_sdcard">Error: No hay tarjeta SD disponible.</string>
<string name="select_album.not_licensed">Servidor sin licencia. Quedan %d dia(s) de prueba.</string>
<string name="select_album.play_all">Reproducir todo</string>
<string name="select_artist.all_folders">Todas las carpetas</string>
<string name="select_artist.folder">Seleccionar la carpeta</string>
@ -325,8 +319,7 @@
<string name="settings.use_id3_summary">Usar el método de etiquetas ID3 en lugar del método basado en el sistema de ficheros</string>
<string name="settings.show_artist_picture">Mostrar la imagen del artista en la lista de artistas</string>
<string name="settings.show_artist_picture_summary">Muestra la imagen del artista en la lista de artistas si está disponible</string>
<string name="settings.video_title">Vídeo</string>
<string name="settings.video_player">Reproductor de vídeo</string>
<string name="main.video">Vídeo</string>
<string name="settings.view_refresh">Refresco de la vista</string>
<string name="settings.view_refresh_500">.5 segundos</string>
<string name="settings.view_refresh_1000">1 segundo</string>
@ -348,8 +341,6 @@
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">El MX Player no esta instalado. Descárgalo grátis de la Play Store, o cambia la configuración de vídeo.</string>
<string name="video.get_mx_player_button">Obtener MX Player</string>
<string name="widget.initial_text">Toca para seleccionar música</string>
<string name="widget.sdcard_busy">Tarjeta SD no disponible</string>
<string name="widget.sdcard_missing">No hay tarjeta SD</string>
@ -385,9 +376,6 @@
<string name="settings.share_greeting_default">Saludo predeterminado para los compartidos</string>
<string name="share_default_greeting">Echa un vistazo a esta música que te comparto desde %s</string>
<string name="share_via">Compartir canciones vía</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Por defecto</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Compartir</string>
<string name="select_album_all_songs">Todas las canciones por %s</string>
<string name="settings.show_all_songs_by_artist">Mostrar todas las canciones por artista</string>

View File

@ -101,7 +101,6 @@
<string name="main.songs_starred">Favoris</string>
<string name="main.songs_title">Titres</string>
<string name="main.videos">Vidéos</string>
<string name="main.welcome_text">Bienvenue dans Ultrasonic ! L\'application n\'est pas configurée. Après avoir configuré votre serveur personnel (disponible à partir de <b>subsonic.org</b>), veuillez accéder aux <b>Paramètres</b> et modifier la configuration pour vous y connecter.</string>
<string name="main.welcome_title">Bienvenue !</string>
<string name="menu.about">À propos</string>
<string name="menu.common">Général</string>
@ -130,16 +129,11 @@
<string name="search.search">Cliquer pour rechercher</string>
<string name="search.songs">Titres</string>
<string name="search.title">Recherche</string>
<string name="select_album.donate_dialog_0_trial_days_left">La période d\'essai est terminée</string>
<string name="select_album.donate_dialog_later">Plus tard</string>
<string name="select_album.donate_dialog_message">Obtenez des téléchargements illimités en faisant un don pour Subsonic.</string>
<string name="select_album.donate_dialog_now">Maintenant</string>
<string name="select_album.empty">Aucun titre trouvé</string>
<string name="select_album.n_selected">%d pistes sélectionnées.</string>
<string name="select_album.n_unselected">%d pistes non sélectionnés.</string>
<string name="select_album.no_network">Avertissement : Aucun réseau disponible.</string>
<string name="select_album.no_sdcard">Erreur : Aucune carte SD disponible.</string>
<string name="select_album.not_licensed">Serveur sans licence. %d jours d\'essai restants.</string>
<string name="select_album.play_all">Tout jouer</string>
<string name="select_artist.all_folders">Tous les dossiers</string>
<string name="select_artist.folder">Sélectionner le dossier</string>
@ -313,8 +307,7 @@
<string name="settings.use_id3_summary">Utiliser ID3 Tags à la place du système de fichier basique</string>
<string name="settings.show_artist_picture">Afficher limage de lartiste dans la liste</string>
<string name="settings.show_artist_picture_summary">Affiche limage de lartiste dans la liste des artistes si celle-ci est disponible</string>
<string name="settings.video_title">Vidéo</string>
<string name="settings.video_player">Lecteur vidéo</string>
<string name="main.video">Vidéo</string>
<string name="settings.view_refresh">Actualisation de la vue</string>
<string name="settings.view_refresh_500">0,5 secondes</string>
<string name="settings.view_refresh_1000">1 seconde</string>
@ -336,8 +329,6 @@
<string name="util.bytes_format.megabyte">0.00 Mo</string>
<string name="util.no_time">&#8212;:&#8212;&#8212;</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">MX Player n\'est pas installé. Récupérez le gratuitement sur Play Store, ou modifier les paramètres vidéo.</string>
<string name="video.get_mx_player_button">Obtenez MX Player</string>
<string name="widget.initial_text">Touchez pour sélectionner un titre</string>
<string name="widget.sdcard_busy">Carte SD non disponible</string>
<string name="widget.sdcard_missing">Aucune carte SD</string>
@ -373,9 +364,6 @@
<string name="settings.share_greeting_default">Texte par défaut lors d\'un partage</string>
<string name="share_default_greeting">Regardez cette musique que j\'ai partagée depuis %s</string>
<string name="share_via">Partager des titres via</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Défaut</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Partager</string>
<string name="select_album_all_songs">Tous les titres de %s</string>
<string name="settings.show_all_songs_by_artist">Voir tous les titres par artiste</string>

View File

@ -111,7 +111,6 @@
<string name="main.songs_starred">Csillaggal megjelölt</string>
<string name="main.songs_title">Dalok</string>
<string name="main.videos">Videók</string>
<string name="main.welcome_text">Üdvözli az Ultrasonic! Az alkalmazás még nincs beállítva. Miután konfigurálta saját kiszolgálóját (elérhető: <b>subsonic.org</b>), húzza balról jobbra az oldalsávot, lépjen be a <b>Beállítások</b> menüpontba, és adja meg csatlakozási adatokat!</string>
<string name="main.welcome_title">Üdvözlet!</string>
<string name="menu.about">Névjegy</string>
<string name="menu.common">Általános</string>
@ -140,16 +139,11 @@
<string name="search.search">Érintse meg a kereséshez</string>
<string name="search.songs">Dalok</string>
<string name="search.title">Keresés</string>
<string name="select_album.donate_dialog_0_trial_days_left">A próbaidőszak lejárt!</string>
<string name="select_album.donate_dialog_later">Később</string>
<string name="select_album.donate_dialog_message">Korlátlan letöltéshez juthat a Subsonic támogatásával.</string>
<string name="select_album.donate_dialog_now">Most</string>
<string name="select_album.empty">Nem található média!</string>
<string name="select_album.n_selected">%d dal kijelölve.</string>
<string name="select_album.n_unselected">%d dal visszavonva.</string>
<string name="select_album.no_network">Figyelem: Hálózat nem áll rendelkezésre!</string>
<string name="select_album.no_sdcard">Hiba: SD kártya nem áll rendelkezésre!</string>
<string name="select_album.not_licensed">A kiszolgálónak nincs licence! %d próba nap van hátra!</string>
<string name="select_album.play_all">Összes lejátszása</string>
<string name="select_artist.all_folders">Összes mappa</string>
<string name="select_artist.folder">Mappa kiválasztása</string>
@ -325,8 +319,7 @@
<string name="settings.use_id3_summary">ID3 Tag módszer használata a fájlredszer alapú mód helyett.</string>
<string name="settings.show_artist_picture">Előadó képének megjelenítése</string>
<string name="settings.show_artist_picture_summary">Az előadó listában megjeleníti a képeket, amennyiben elérhetőek</string>
<string name="settings.video_title">Videó</string>
<string name="settings.video_player">Videólejátszó</string>
<string name="main.video">Videó</string>
<string name="settings.view_refresh">Nézet frissítési gyakorisága</string>
<string name="settings.view_refresh_500">.5 másodperc</string>
<string name="settings.view_refresh_1000">1 másodperc</string>
@ -348,8 +341,6 @@
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">Az MX Player nincs telepítve. Töltse le díjmentesen a Play Áruházból, vagy módosítsa a videó beállításait!</string>
<string name="video.get_mx_player_button">MX Player letöltése</string>
<string name="widget.initial_text">Érintse meg a zene kiválasztásához</string>
<string name="widget.sdcard_busy">Az SD kártya nem elérhető!</string>
<string name="widget.sdcard_missing">Nincs SD kártya!</string>
@ -385,9 +376,6 @@
<string name="settings.share_greeting_default">Alapértelmezett megosztási üzenet</string>
<string name="share_default_greeting">Hallgasd meg ezt a zenét, megosztottam innen: %s</string>
<string name="share_via">Dalok megosztása ezzel</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Alapértelmezett</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Megosztás</string>
<string name="select_album_all_songs">%s minden dala</string>
<string name="settings.show_all_songs_by_artist">Az előadó összes dalának megjelenítése</string>

View File

@ -125,16 +125,11 @@
<string name="search.search">Selezione per cercare</string>
<string name="search.songs">Canzoni</string>
<string name="search.title">Cerca</string>
<string name="select_album.donate_dialog_0_trial_days_left">Periodo di prova terminato </string>
<string name="select_album.donate_dialog_later">Dopo</string>
<string name="select_album.donate_dialog_message">Ottieni download illimitato con una donazione a Subsonic.</string>
<string name="select_album.donate_dialog_now">Ora</string>
<string name="select_album.empty">Nessun media trovato</string>
<string name="select_album.n_selected">%dtracce selezionate.</string>
<string name="select_album.n_unselected">%d tracce non selezionate.</string>
<string name="select_album.no_network">Attenzione: nessuna rete disponibile.</string>
<string name="select_album.no_sdcard">Errore: Nessuna memoria SD disponibile.</string>
<string name="select_album.not_licensed">Nessuna licenza presente. %d giorni di prova rimanenti.</string>
<string name="select_album.play_all">Riproduci tutto</string>
<string name="select_artist.all_folders">Tutte le cartelle</string>
<string name="select_artist.folder">Seleziona cartella</string>
@ -299,8 +294,7 @@
<string name="settings.use_folder_for_album_artist_summary">Presumi che la cartella superiore sia il nome dell\'artista dell\'album</string>
<string name="settings.use_id3">Sfoglia Utilizzando Tag ID3</string>
<string name="settings.use_id3_summary">Usa metodi tag ID3 invece dei metodi basati sul filesystem</string>
<string name="settings.video_title">Video</string>
<string name="settings.video_player">Riproduttore video</string>
<string name="main.video">Video</string>
<string name="settings.view_refresh_500">.5 secondo</string>
<string name="settings.view_refresh_1000">1 secondo</string>
<string name="settings.view_refresh_1500">1.5 secondi</string>
@ -319,8 +313,6 @@
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">MX Player non è installato. Scaricalo gratuitamente dal Play Store, o cambia le impostazioni video.</string>
<string name="video.get_mx_player_button">Ottieni MX Player</string>
<string name="widget.initial_text">Tocca per selezionare musica</string>
<string name="widget.sdcard_busy">Scheda SD non disponibile</string>
<string name="widget.sdcard_missing">Nessuna scheda SD</string>
@ -336,8 +328,6 @@
<string name="share_comment">Commenta</string>
<string name="download_song_removed">\"%s\" è stato rimosso dalla playlist</string>
<string name="share_via">Condividi canzoni via</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Predefinito</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 canzone</item>
<item quantity="other">%d canzoni</item>

View File

@ -111,7 +111,6 @@
<string name="main.songs_starred">Favorieten</string>
<string name="main.songs_title">Nummers</string>
<string name="main.videos">Video\'s</string>
<string name="main.welcome_text">Welkom bij Ultrasonic! De app is nog niet ingesteld. Nadat je je persoonlijke server hebt opgezet (beschikbaar op <b>subsonic.org</b>), kun je naar de <b>Instellingen</b> gaan en drukken op <i>Server toevoegen</i>.</string>
<string name="main.welcome_title">Welkom!</string>
<string name="menu.about">Over</string>
<string name="menu.common">Algemeen</string>
@ -140,16 +139,11 @@
<string name="search.search">Druk om te zoeken</string>
<string name="search.songs">Nummers</string>
<string name="search.title">Zoeken</string>
<string name="select_album.donate_dialog_0_trial_days_left">Proefperiode is afgelopen</string>
<string name="select_album.donate_dialog_later">Later</string>
<string name="select_album.donate_dialog_message">Verkrijg ongelimiteerde downloads door te doneren aan Subsonic.</string>
<string name="select_album.donate_dialog_now">Nu</string>
<string name="select_album.empty">Geen media gevonden</string>
<string name="select_album.n_selected">%d nummers geselecteerd.</string>
<string name="select_album.n_unselected">%d nummers gedeselecteerd.</string>
<string name="select_album.no_network">Waarschuwing: geen internetverbinding.</string>
<string name="select_album.no_sdcard">Fout: geen SD-kaart beschikbaar.</string>
<string name="select_album.not_licensed">Geen serverlicentie; nog %d dagen resterend van de proefperiode.</string>
<string name="select_album.play_all">Alles afspelen</string>
<string name="select_artist.all_folders">Alle mappen</string>
<string name="select_artist.folder">Map kiezen</string>
@ -325,8 +319,7 @@
<string name="settings.use_id3_summary">ID3-labels gebruiken in plaats van systeemlabels</string>
<string name="settings.show_artist_picture">Artiestfoto tonen op artiestenlijst</string>
<string name="settings.show_artist_picture_summary">Toont de artiestfoto op de artiestenlijst (indien beschikbaar)</string>
<string name="settings.video_title">Video</string>
<string name="settings.video_player">Videospeler</string>
<string name="main.video">Video</string>
<string name="settings.view_refresh">Verversen</string>
<string name="settings.view_refresh_500">0,5 seconden</string>
<string name="settings.view_refresh_1000">1 seconde</string>
@ -348,8 +341,6 @@
<string name="util.bytes_format.megabyte">0,00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">MX Player is niet geïnstalleerd. Installeer deze gratis via de Play Store of wijzig de video-instellingen.</string>
<string name="video.get_mx_player_button">MX Player installeren</string>
<string name="widget.initial_text">Druk om muziek te selecteren</string>
<string name="widget.sdcard_busy">SD-kaart niet beschikbaar</string>
<string name="widget.sdcard_missing">Geen SD-kaart</string>
@ -385,9 +376,6 @@
<string name="settings.share_greeting_default">Standaard deelbericht</string>
<string name="share_default_greeting">Hé, luister eens naar de muziek die ik heb gedeeld via %s</string>
<string name="share_via">Nummers delen via</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Standaard</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Delen</string>
<string name="select_album_all_songs">Alle nummers van %s</string>
<string name="settings.show_all_songs_by_artist">Alle nummers van artiest tonen</string>

View File

@ -100,7 +100,6 @@
<string name="main.songs_starred">Ulubione</string>
<string name="main.songs_title">Utwory</string>
<string name="main.videos">Klipy wideo</string>
<string name="main.welcome_text">Witaj w Ultrasonic! Obecnie aplikacja nie jest skonfigurowana. Jeśli masz uruchomiony własny serwer (dostępny na <b>subsonic.org</b>), proszę wybrać <i>Dodaj serwer</i> w <b>Ustawieniach</b> aby się z nim połączyć.</string>
<string name="main.welcome_title">Witaj!</string>
<string name="menu.about">O aplikacji</string>
<string name="menu.common">Wspólne</string>
@ -128,16 +127,11 @@
<string name="search.search">Kliknij, aby wyszukać</string>
<string name="search.songs">Utwory</string>
<string name="search.title">Wyszukiwanie</string>
<string name="select_album.donate_dialog_0_trial_days_left">Okres próbny zakończył się</string>
<string name="select_album.donate_dialog_later">Później</string>
<string name="select_album.donate_dialog_message">Uzyskaj możliwość nieograniczonych pobrań przekazując darowiznę na rzecz Subsonic.</string>
<string name="select_album.donate_dialog_now">Teraz</string>
<string name="select_album.empty">Brak mediów</string>
<string name="select_album.n_selected">Zaznaczono %d utworów.</string>
<string name="select_album.n_unselected">Odznaczono %d utworów.</string>
<string name="select_album.no_network">Uwaga: sieć niedostępna.</string>
<string name="select_album.no_sdcard">Błąd: Niedostępna karta SD.</string>
<string name="select_album.not_licensed">Serwer bez licencji. Pozostało %d dni próbnych.</string>
<string name="select_album.play_all">Odtwórz wszystkie</string>
<string name="select_artist.all_folders">Wszystkie foldery</string>
<string name="select_artist.folder">Wybierz folder</string>
@ -308,8 +302,7 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników
<string name="settings.use_folder_for_album_artist_summary">Zakłada, że folder najwyższego poziomu jest nazwą artysty albumu</string>
<string name="settings.use_id3">Przeglądaj używając tagów ID3</string>
<string name="settings.use_id3_summary">Używa metod z tagów ID3 zamiast metod opartych na systemie plików</string>
<string name="settings.video_title">Wideo</string>
<string name="settings.video_player">Odtwarzacz wideo</string>
<string name="main.video">Wideo</string>
<string name="settings.view_refresh">Odświeżanie widoku</string>
<string name="settings.view_refresh_500">co pół sekundy</string>
<string name="settings.view_refresh_1000">co 1 sekundę</string>
@ -331,8 +324,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">MX Player nie jest zainstalowany, Pobierz go za darmo w Sklepie Play, lub zmień ustawiena wideo.</string>
<string name="video.get_mx_player_button">Pobierz MX Player</string>
<string name="widget.initial_text">Dotknij, aby wybrać muzykę</string>
<string name="widget.sdcard_busy">Karta SD jest niedostępna</string>
<string name="widget.sdcard_missing">Brak karty SD</string>
@ -368,9 +359,6 @@ ponieważ api Subsonic nie wspiera nowego sposobu autoryzacji dla użytkowników
<string name="settings.share_greeting_default">Domyślny tekst podczas udostępniania</string>
<string name="share_default_greeting">Sprawdź muzykę, którą udostępniam na %s</string>
<string name="share_via">Udostępnij utwory za pomocą</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Domyślny</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Udostępnianie</string>
<string name="select_album_all_songs">Wszystkie utwory %s</string>
<string name="settings.show_all_songs_by_artist">Wyświetlaj wszystkie utwory artysty</string>

View File

@ -101,7 +101,6 @@
<string name="main.songs_starred">Favoritas</string>
<string name="main.songs_title">Músicas</string>
<string name="main.videos">Vídeos</string>
<string name="main.welcome_text">Bem-vindo ao Ultrasonic! O aplicativo ainda não está configurado. Após configurar seu servidor pessoal (disponível em <b>subsonic.org</b>), clique em <i>Adicionar Servidor</i> em <b>Configurações</b> para a conexão.</string>
<string name="main.welcome_title">Bem-vindo!</string>
<string name="menu.about">Sobre</string>
<string name="menu.common">Comum</string>
@ -130,16 +129,11 @@
<string name="search.search">Clique para pesquisar</string>
<string name="search.songs">Músicas</string>
<string name="search.title">Pesquisar</string>
<string name="select_album.donate_dialog_0_trial_days_left">O período de teste acabou</string>
<string name="select_album.donate_dialog_later">Mais tarde</string>
<string name="select_album.donate_dialog_message">Obtenha downloads ilimitados fazendo uma doação ao Subsonic.</string>
<string name="select_album.donate_dialog_now">Agora</string>
<string name="select_album.empty">Nenhuma mídia encontrada</string>
<string name="select_album.n_selected">%d faixas selecionadas.</string>
<string name="select_album.n_unselected">%d faixas desselecionadas.</string>
<string name="select_album.no_network">Aviso: Nenhuma rede disponível.</string>
<string name="select_album.no_sdcard">Erro: Nenhum cartão SD disponível.</string>
<string name="select_album.not_licensed">Servidor não licenciado. Restam %d dias de teste.</string>
<string name="select_album.play_all">Tocar Tudo</string>
<string name="select_artist.all_folders">Todas as Pastas</string>
<string name="select_artist.folder">Selecionar Pasta</string>
@ -313,8 +307,7 @@
<string name="settings.use_id3_summary">Usar as etiquetas ID3 ao invés do sistema de arquivos</string>
<string name="settings.show_artist_picture">Mostrar Foto do Artista na Lista</string>
<string name="settings.show_artist_picture_summary">Mostrar a imagem do artista na lista de artistas, se disponível</string>
<string name="settings.video_title">Vídeo</string>
<string name="settings.video_player">Player de Vídeo</string>
<string name="main.video">Vídeo</string>
<string name="settings.view_refresh">Atualização da Tela</string>
<string name="settings.view_refresh_500">.5 segundos</string>
<string name="settings.view_refresh_1000">1 segundo</string>
@ -336,8 +329,6 @@
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">O MX Player não está instalado. Baixe da graça pela Play Store ou modifique as configurações de vídeo.</string>
<string name="video.get_mx_player_button">Baixar MX Player</string>
<string name="widget.initial_text">Toque para selecionar a música</string>
<string name="widget.sdcard_busy">Cartão SD indisponível</string>
<string name="widget.sdcard_missing">Sem cartão SD</string>
@ -373,9 +364,6 @@
<string name="settings.share_greeting_default">Saudação Padrão do Compartilhamento</string>
<string name="share_default_greeting">Confira esta música que compartilhei do %s</string>
<string name="share_via">Compartilhar músicas via</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Padrão</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Compartilhar</string>
<string name="select_album_all_songs">Todas as Músicas de %s</string>
<string name="settings.show_all_songs_by_artist">Mostrar Todas as Músicas por Artista</string>

View File

@ -100,7 +100,6 @@
<string name="main.songs_starred">Favoritas</string>
<string name="main.songs_title">Músicas</string>
<string name="main.videos">Vídeos</string>
<string name="main.welcome_text">Bem-vindo ao Ultrasonic! O aplicativo ainda não está configurado. Após configurar seu servidor pessoal (disponível em <b>subsonic.org</b>), clique em <i>Adicionar Servidor</i> em <b>Configurações</b> para a conexão.</string>
<string name="main.welcome_title">Bem-vindo!</string>
<string name="menu.about">Sobre</string>
<string name="menu.common">Comum</string>
@ -128,16 +127,11 @@
<string name="search.search">Clique para pesquisar</string>
<string name="search.songs">Músicas</string>
<string name="search.title">Pesquisar</string>
<string name="select_album.donate_dialog_0_trial_days_left">O período de teste acabou</string>
<string name="select_album.donate_dialog_later">Mais tarde</string>
<string name="select_album.donate_dialog_message">Obtenha downloads ilimitados fazendo uma doação ao Subsonic.</string>
<string name="select_album.donate_dialog_now">Agora</string>
<string name="select_album.empty">Nenhuma mídia encontrada</string>
<string name="select_album.n_selected">%d faixas selecionadas.</string>
<string name="select_album.n_unselected">%d faixas desselecionadas.</string>
<string name="select_album.no_network">Aviso: Nenhuma rede disponível.</string>
<string name="select_album.no_sdcard">Erro: Nenhum cartão SD disponível.</string>
<string name="select_album.not_licensed">Servidor não licenciado. Restam %d dias de teste.</string>
<string name="select_album.play_all">Tocar Tudo</string>
<string name="select_artist.all_folders">Todas as Pastas</string>
<string name="select_artist.folder">Selecionar Pasta</string>
@ -308,8 +302,7 @@
<string name="settings.use_folder_for_album_artist_summary">Assumir que a pasta mais acima é o nome do artista</string>
<string name="settings.use_id3">Navegar Usando Etiquetas ID3</string>
<string name="settings.use_id3_summary">Usa as etiquetas ID3 ao invés do sistema de ficheiros</string>
<string name="settings.video_title">Vídeo</string>
<string name="settings.video_player">Player de Vídeo</string>
<string name="main.video">Vídeo</string>
<string name="settings.view_refresh">Atualização do Ecrã</string>
<string name="settings.view_refresh_500">.5 segundos</string>
<string name="settings.view_refresh_1000">1 segundo</string>
@ -331,8 +324,6 @@
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">&#8212;:&#8212;&#8212;</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">O MX Player não está instalado. Descarregue da graça pela Play Store ou modifique as configurações de vídeo.</string>
<string name="video.get_mx_player_button">Descarregar MX Player</string>
<string name="widget.initial_text">Toque para selecionar a música</string>
<string name="widget.sdcard_busy">Cartão SD indisponível</string>
<string name="widget.sdcard_missing">Sem cartão SD</string>
@ -368,9 +359,6 @@
<string name="settings.share_greeting_default">Saudação Padrão</string>
<string name="share_default_greeting">Confira esta música que compartilhei do %s</string>
<string name="share_via">Compartilhar músicas via</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Padrão</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Compartilhar</string>
<string name="select_album_all_songs">Todas as Músicas de %s</string>
<string name="settings.show_all_songs_by_artist">Todas as Músicas do Artista</string>

View File

@ -125,16 +125,11 @@
<string name="search.search">Нажми для поиска</string>
<string name="search.songs">Песни</string>
<string name="search.title">Поиск</string>
<string name="select_album.donate_dialog_0_trial_days_left">Пробный период окончен</string>
<string name="select_album.donate_dialog_later">Позже</string>
<string name="select_album.donate_dialog_message">Получите неограниченное количество загрузок, пожертвовав Subsonic</string>
<string name="select_album.donate_dialog_now">Сейчас</string>
<string name="select_album.empty">Медиа не найдена</string>
<string name="select_album.n_selected">%d треки выбраны.</string>
<string name="select_album.n_unselected">%d треки не выбраны.</string>
<string name="select_album.no_network">Предупреждение: сеть недоступна.</string>
<string name="select_album.no_sdcard">Ошибка: нет SD-карты</string>
<string name="select_album.not_licensed">Сервер не лицензирован. %d пробные дни остались.</string>
<string name="select_album.play_all">Воспроизвести все</string>
<string name="select_artist.all_folders">Все папки</string>
<string name="select_artist.folder">Выбрать папку</string>
@ -300,8 +295,7 @@
<string name="settings.use_folder_for_album_artist_summary">Предположим, папка верхнего уровня - это имя исполнителя альбома</string>
<string name="settings.use_id3">Обзор с использованием тегов ID3</string>
<string name="settings.use_id3_summary">Используйте методы тегов ID3 ​​вместо методов на основе файловой системы</string>
<string name="settings.video_title">Видео</string>
<string name="settings.video_player">Видеоплеер</string>
<string name="main.video">Видео</string>
<string name="settings.view_refresh">Посмотреть Обновить</string>
<string name="settings.view_refresh_500">.5 секунд</string>
<string name="settings.view_refresh_1000">1 секунда</string>
@ -323,8 +317,6 @@
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">MX Player не установлен. Получите его бесплатно в магазине Play Store или измените настройки видео.</string>
<string name="video.get_mx_player_button">Получить MX Player</string>
<string name="widget.initial_text">Нажмите, чтобы выбрать музыку</string>
<string name="widget.sdcard_busy">SD-карта недоступна</string>
<string name="widget.sdcard_missing">Нет SD-карты</string>
@ -360,9 +352,6 @@
<string name="settings.share_greeting_default">Поделиться приветствием по умолчанию</string>
<string name="share_default_greeting">Проверьте эту музыку, с которой я поделился %s</string>
<string name="share_via">Поделиться треками через</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">По умолчанию</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Поделиться</string>
<string name="select_album_all_songs">Все треки %s</string>
<string name="settings.show_all_songs_by_artist">Показать все треки исполнителя</string>

View File

@ -106,10 +106,6 @@
<string name="search.search">点击搜索</string>
<string name="search.songs">歌曲</string>
<string name="search.title">搜索</string>
<string name="select_album.donate_dialog_0_trial_days_left">试用已结束</string>
<string name="select_album.donate_dialog_later">稍后</string>
<string name="select_album.donate_dialog_message">通过捐赠 Subsonic 得到无限制的下载。</string>
<string name="select_album.donate_dialog_now">现在</string>
<string name="select_album.empty">找不到歌曲</string>
<string name="select_album.no_network">警告:网络不可用</string>
<string name="select_album.no_sdcard">错误没有SD卡</string>
@ -224,8 +220,7 @@
<string name="settings.testing_unlicensed">连接正常, 服务器未授权。</string>
<string name="settings.theme_title">主题</string>
<string name="settings.title.allow_self_signed_certificate">允许自签名 HTTPS 证书</string>
<string name="settings.video_title">视频</string>
<string name="settings.video_player">视频播放器</string>
<string name="main.video">视频</string>
<string name="settings.view_refresh">刷新视图</string>
<string name="settings.view_refresh_500">.5 秒</string>
<string name="settings.view_refresh_1000">1 秒</string>
@ -261,8 +256,6 @@
<string name="share_comment">评论</string>
<string name="download_song_removed">%s已从播放列表中移除</string>
<string name="download.share_playlist">分享播放列表</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">默认</string>
<string name="menu.share">分享</string>
<string name="settings.playback.bluetooth_disabled">已禁用</string>
<string name="settings.debug.log_delete">删除文件</string>

View File

@ -224,16 +224,6 @@
<item>@string/settings.search_250</item>
<item>@string/settings.search_500</item>
</string-array>
<string-array name="videoPlayerValues" translatable="false">
<item>mx</item>
<item>default</item>
<item>flash</item>
</string-array>
<string-array name="videoPlayerNames" translatable="false">
<item>@string/settings.video_mx_player</item>
<item>@string/settings.video_default</item>
<item>@string/settings.video_flash</item>
</string-array>
<string-array name="viewRefreshNames" translatable="false">
<item>@string/settings.view_refresh_500</item>
<item>@string/settings.view_refresh_1000</item>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Loading&#8230;</string>
<string name="background_task.network_error">A network error occurred. Please check the server address or try again later.</string>
@ -111,8 +111,9 @@
<string name="main.songs_starred">Starred</string>
<string name="main.songs_title">Songs</string>
<string name="main.videos">Videos</string>
<string name="main.welcome_text">Welcome to Ultrasonic! The app is currently not configured. After you\'ve set up your personal server (available from <b>subsonic.org</b>), please click <i>Manage Servers</i> in <b>Settings</b> to connect to it.</string>
<string name="main.welcome_title">Welcome!</string>
<string name="main.welcome_text_demo">To use Ultrasonic with your own music you will need your <b>own server</b>. \n\n➤ In case you want to try out the app first, it can add a demo server now. \n\n➤ Otherwise you can configure your server in the <b>settings</b>.</string>
<string name="main.welcome_title">Welcome to Ultrasonic!</string>
<string name="main.welcome_cancel">Take me to the settings</string>
<string name="menu.about">About</string>
<string name="menu.common">Common</string>
<string name="menu.deleted_playlist">Deleted playlist %s</string>
@ -140,16 +141,11 @@
<string name="search.search">Click to search</string>
<string name="search.songs">Songs</string>
<string name="search.title">Search</string>
<string name="select_album.donate_dialog_0_trial_days_left">Trial period is over</string>
<string name="select_album.donate_dialog_later">Later</string>
<string name="select_album.donate_dialog_message">Get unlimited downloads by donating to Subsonic.</string>
<string name="select_album.donate_dialog_now">Now</string>
<string name="select_album.empty">No media found</string>
<string name="select_album.n_selected">%d tracks selected.</string>
<string name="select_album.n_unselected">%d tracks unselected.</string>
<string name="select_album.no_network">Warning: No network available.</string>
<string name="select_album.no_sdcard">Error: No SD card available.</string>
<string name="select_album.not_licensed">Server not licensed. %d trial days left.</string>
<string name="select_album.play_all">Play All</string>
<string name="select_artist.all_folders">All Folders</string>
<string name="select_artist.folder">Select Folder</string>
@ -327,8 +323,7 @@
<string name="settings.use_id3_summary">Use ID3 tag methods instead of file system based methods</string>
<string name="settings.show_artist_picture">Show artist picture in artist list</string>
<string name="settings.show_artist_picture_summary">Displays the artist picture in the artist list if available</string>
<string name="settings.video_title">Video</string>
<string name="settings.video_player">Video player</string>
<string name="main.video" tools:ignore="UnusedResources">Video</string>
<string name="settings.view_refresh">View Refresh</string>
<string name="settings.view_refresh_500">.5 seconds</string>
<string name="settings.view_refresh_1000">1 second</string>
@ -348,10 +343,8 @@
<string name="util.bytes_format.gigabyte">0.00 GB</string>
<string name="util.bytes_format.kilobyte">0 KB</string>
<string name="util.bytes_format.megabyte">0.00 MB</string>
<string name="util.no_time">-:--</string>
<string name="util.no_time" tools:ignore="TypographyDashes">-:--</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">MX Player is not installed. Get it for free on Play Store, or change video settings.</string>
<string name="video.get_mx_player_button">Get MX Player</string>
<string name="widget.initial_text">Touch to select music</string>
<string name="widget.sdcard_busy">SD card unavailable</string>
<string name="widget.sdcard_missing">No SD card</string>
@ -387,9 +380,6 @@
<string name="settings.share_greeting_default">Default Share Greeting</string>
<string name="share_default_greeting">Check out this music I shared from %s</string>
<string name="share_via">Share songs via</string>
<string name="settings.video_mx_player">MX Player</string>
<string name="settings.video_default">Default</string>
<string name="settings.video_flash">Flash</string>
<string name="menu.share">Share</string>
<string name="select_album_all_songs">All Songs by %s</string>
<string name="settings.show_all_songs_by_artist">Show All Songs By Artist</string>
@ -449,6 +439,7 @@
<string name="server_editor.authentication">Authentication</string>
<string name="server_editor.advanced">Advanced settings</string>
<string name="server_editor.disabled_feature">One or more features were disabled because the server doesn\'t support them.\nYou can run this test again anytime.</string>
<string name="server_menu.demo">Demo Server</string>
<plurals name="select_album_n_songs">
<item quantity="one">1 song</item>

View File

@ -70,13 +70,13 @@
a:key="playbackControlSettings"
app:iconSpaceReserved="false">
<CheckBoxPreference
a:defaultValue="false"
a:defaultValue="true"
a:key="useId3Tags"
a:summary="@string/settings.use_id3_summary"
a:title="@string/settings.use_id3"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="false"
a:defaultValue="true"
a:key="showArtistPicture"
a:summary="@string/settings.show_artist_picture_summary"
a:title="@string/settings.show_artist_picture"
@ -180,17 +180,6 @@
a:title="@string/settings.send_bluetooth_album_art"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
a:title="@string/settings.video_title"
app:iconSpaceReserved="false">
<ListPreference
a:defaultValue="default"
a:entries="@array/videoPlayerNames"
a:entryValues="@array/videoPlayerValues"
a:key="videoPlayer"
a:title="@string/settings.video_player"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
a:title="@string/settings.sharing_title"
app:iconSpaceReserved="false">

View File

@ -9,18 +9,20 @@ import org.amshove.kluent.`should throw`
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Answers
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.moire.ultrasonic.api.subsonic.toStreamResponse
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class AvatarRequestHandlerTest {
private val mockApiClient: SubsonicAPIClient = mock()
private val mockApiClient: SubsonicAPIClient = mock(defaultAnswer = Answers.RETURNS_DEEP_STUBS)
private val handler = AvatarRequestHandler(mockApiClient)
@Test
@ -59,8 +61,10 @@ class AvatarRequestHandlerTest {
apiError = null,
responseHttpCode = 200
)
whenever(mockApiClient.getAvatar(any()))
.thenReturn(streamResponse)
whenever(
mockApiClient.toStreamResponse(any())
).thenReturn(streamResponse)
val response = handler.load(
createLoadAvatarRequest("some-username").buildRequest(), 0

View File

@ -10,8 +10,8 @@ import org.amshove.kluent.`should throw`
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Answers
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
@ -20,7 +20,7 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class CoverArtRequestHandlerTest {
private val mockApiClient: SubsonicAPIClient = mock()
private val mockApiClient: SubsonicAPIClient = mock(defaultAnswer = Answers.RETURNS_DEEP_STUBS)
private val handler = CoverArtRequestHandler(mockApiClient)
@Test
@ -56,7 +56,9 @@ class CoverArtRequestHandlerTest {
fun `Should throw IOException when request to api failed`() {
val streamResponse = StreamResponse(null, null, 500)
whenever(mockApiClient.getCoverArt(any(), anyOrNull())).thenReturn(streamResponse)
whenever(
mockApiClient.toStreamResponse(any())
).thenReturn(streamResponse)
val fail = {
handler.load(createLoadCoverArtRequest("some").buildRequest(), 0)
@ -73,7 +75,9 @@ class CoverArtRequestHandlerTest {
responseHttpCode = 200
)
whenever(mockApiClient.getCoverArt(any(), anyOrNull())).thenReturn(streamResponse)
whenever(
mockApiClient.toStreamResponse(any())
).thenReturn(streamResponse)
val response = handler.load(
createLoadCoverArtRequest("some").buildRequest(), 0