Merge pull request #142 from ultrasonic/develop

New release 2.2.0
This commit is contained in:
Óscar García Amor 2018-01-22 14:50:44 +01:00 committed by GitHub
commit cae5635833
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 1625 additions and 1890 deletions

View File

@ -2,7 +2,7 @@ version: 2
jobs:
build:
docker:
- image: circleci/android:api-26-alpha
- image: circleci/android:api-27-alpha
working_directory: ~/ultrasonic
envoronment:
JVM_OPTS: -Xmx3200m

View File

@ -21,6 +21,15 @@ otherwise open [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new
See [CONTRIBUTING](CONTRIBUTING.md).
## Supported (tested) Subsonic API implementations
- [Subsonic](http://www.subsonic.org/pages/index.jsp)
- [Airsonic](https://github.com/airsonic/airsonic)
- [Supysonic](https://github.com/spl0k/supysonic)
Other *Subsonic API* implementations should work as well as long as they follow API
[documentation](http://www.subsonic.org/pages/api.jsp).
## License
This software is licensed under the terms of the GNU General Public License version 3 (GPLv3).

View File

@ -4,17 +4,16 @@ buildscript {
repositories {
jcenter()
google()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath gradlePlugins.androidTools
classpath gradlePlugins.kotlin
classpath gradlePlugins.ktlintGradle
classpath(gradlePlugins.detekt) {
exclude module: 'kotlin-compiler-embeddable'
exclude module: 'kotlin-stdlib'
}
classpath gradlePlugins.detekt
classpath gradlePlugins.jacocoAndroid
classpath gradlePlugins.buildVersioning
}
}
@ -23,6 +22,7 @@ allprojects {
buildscript {
repositories {
jcenter()
google()
}
}

View File

@ -1,20 +1,20 @@
ext.versions = [
minSdk : 14,
targetSdk : 22,
compileSdk : 22,
gradle : '4.3.1',
compileSdk : 27,
gradle : '4.4.1',
buildTools : "25.0.3",
androidTools : "2.3.3",
ktlint : "0.12.1",
androidTools : "3.0.1",
ktlint : "0.14.0",
ktlintGradle : "2.3.0",
detekt : "1.0.0.RC5-4",
detekt : "1.0.0.RC6",
jacoco : "0.7.9",
jacocoAndroid : "0.1.2",
buildVersioning : "1.6.0",
androidSupport : "22.2.1",
kotlin : "1.1.60",
kotlin : "1.2.10",
retrofit : "2.1.0",
jackson : "2.9.0",
@ -28,11 +28,12 @@ ext.versions = [
]
ext.gradlePlugins = [
androidTools : "com.android.tools.build:gradle:$versions.androidTools",
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
ktlintGradle : "gradle.plugin.org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle",
detekt : "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt",
jacocoAndroid : "com.dicedmelon.gradle:jacoco-android:$versions.jacocoAndroid"
androidTools : "com.android.tools.build:gradle:$versions.androidTools",
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
ktlintGradle : "gradle.plugin.org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle",
detekt : "gradle.plugin.io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt",
jacocoAndroid : "com.dicedmelon.gradle:jacoco-android:$versions.jacocoAndroid",
buildVersioning : "org.moallemi.gradle.advanced-build-version:gradle-plugin:$versions.buildVersioning",
]
ext.androidSupport = [
@ -42,7 +43,7 @@ ext.androidSupport = [
ext.other = [
kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin",
kotlinReflect : "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin",
kotlinReflect : "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin",
retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
gsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit",
jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit",

3
gradle.properties Normal file
View File

@ -0,0 +1,3 @@
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.caching=true

Binary file not shown.

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip

View File

@ -2,7 +2,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools
defaultConfig {
minSdkVersion versions.minSdk
@ -23,5 +22,5 @@ android {
}
dependencies {
compile androidSupport.support
api androidSupport.support
}

View File

@ -2,7 +2,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools
defaultConfig {
minSdkVersion versions.minSdk

View File

@ -19,7 +19,6 @@ package net.simonvt.menudrawer;
import android.content.Context;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.FloatMath;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
@ -371,7 +370,7 @@ class Scroller {
float dx = (float) (mFinalX - mStartX);
float dy = (float) (mFinalY - mStartY);
float hyp = FloatMath.sqrt(dx * dx + dy * dy);
float hyp = (float) Math.sqrt(dx * dx + dy * dy);
float ndx = dx / hyp;
float ndy = dy / hyp;
@ -388,7 +387,7 @@ class Scroller {
mMode = FLING_MODE;
mFinished = false;
float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
float velocity = (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY);
mVelocity = velocity;
final double l = Math.log(START_TENSION * velocity / ALPHA);

View File

@ -2,7 +2,6 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools
defaultConfig {
minSdkVersion versions.minSdk

View File

@ -21,7 +21,6 @@ import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
@ -112,7 +111,7 @@ public class PullToRefreshWebView extends PullToRefreshBase<WebView> {
@Override
protected boolean isReadyForPullEnd() {
float exactContentHeight = FloatMath.floor(mRefreshableView.getContentHeight() * mRefreshableView.getScale());
float exactContentHeight = (float) Math.floor(mRefreshableView.getContentHeight() * mRefreshableView.getScale());
return mRefreshableView.getScrollY() >= (exactContentHeight - mRefreshableView.getHeight());
}
@ -158,7 +157,7 @@ public class PullToRefreshWebView extends PullToRefreshBase<WebView> {
}
private int getScrollRange() {
return (int) Math.max(0, FloatMath.floor(mRefreshableView.getContentHeight() * mRefreshableView.getScale())
return (int) Math.max(0, Math.floor(mRefreshableView.getContentHeight() * mRefreshableView.getScale())
- (getHeight() - getPaddingBottom() - getPaddingTop()));
}
}

View File

@ -12,7 +12,7 @@ sourceSets {
dependencies {
api other.kotlinStdlib
api other.retrofit
implementation other.jacksonConverter
api other.jacksonConverter
implementation(other.jacksonKotlin) {
exclude module: 'kotlin-reflect'
}

View File

@ -59,14 +59,14 @@ fun parseDate(dateAsString: String): Calendar {
fun <T : SubsonicResponse> checkErrorCallParsed(mockWebServerRule: MockWebServerRule,
apiRequest: () -> Response<T>): T {
mockWebServerRule.enqueueResponse("generic_error_response.json")
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
val response = apiRequest()
assertResponseSuccessful(response)
with(response.body()) {
status `should be` SubsonicResponse.Status.ERROR
error `should be` SubsonicError.GENERIC
error `should be` SubsonicError.RequestedDataWasNotFound
}
return response.body()
}

View File

@ -14,7 +14,7 @@ abstract class SubsonicAPIClientTest {
@Before
fun setUp() {
client = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME, PASSWORD,
CLIENT_VERSION, CLIENT_ID)
client = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME, PASSWORD, CLIENT_VERSION, CLIENT_ID)
}
}

View File

@ -0,0 +1,157 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.amshove.kluent.`should throw`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.SubsonicError.Generic
import org.moire.ultrasonic.api.subsonic.SubsonicError.IncompatibleClientProtocolVersion
import org.moire.ultrasonic.api.subsonic.SubsonicError.IncompatibleServerProtocolVersion
import org.moire.ultrasonic.api.subsonic.SubsonicError.RequestedDataWasNotFound
import org.moire.ultrasonic.api.subsonic.SubsonicError.RequiredParamMissing
import org.moire.ultrasonic.api.subsonic.SubsonicError.TokenAuthNotSupportedForLDAP
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.api.subsonic.response.SubsonicResponse
import retrofit2.Response
import java.io.IOException
/**
* Integration test that checks validity of api errors parsing.
*/
class SubsonicApiErrorsTest : SubsonicAPIClientTest() {
@Test
fun `Should parse wrong username or password error`() {
mockWebServerRule.enqueueResponse("wrong_username_or_password_error.json")
val response = client.api.ping().execute()
response.assertError(WrongUsernameOrPassword)
}
@Test
fun `Should parse generic error with message`() {
mockWebServerRule.enqueueResponse("generic_error.json")
val response = client.api.ping().execute()
response.assertError(Generic("Some generic error message."))
}
@Test
fun `Should fail on unknown error`() {
mockWebServerRule.enqueueResponse("unexpected_error.json")
val fail = {
client.api.ping().execute()
}
fail `should throw` IOException::class
}
@Test
fun `Should parse required param missing error`() {
mockWebServerRule.enqueueResponse("required_param_missing_error.json")
val response = client.api.ping().execute()
response.assertError(RequiredParamMissing)
}
@Test
fun `Should parse incompatible client protocol version error`() {
mockWebServerRule.enqueueResponse("incompatible_client_protocol_version_error.json")
val response = client.api.ping().execute()
response.assertError(IncompatibleClientProtocolVersion)
}
@Test
fun `Should parse incompatible server protocol version error`() {
mockWebServerRule.enqueueResponse("incompatible_server_protocol_version_error.json")
val response = client.api.ping().execute()
response.assertError(IncompatibleServerProtocolVersion)
}
@Test
fun `Should parse token auth not supported for ldap error`() {
mockWebServerRule.enqueueResponse("token_auth_not_supported_for_ldap_error.json")
val response = client.api.ping().execute()
response.assertError(TokenAuthNotSupportedForLDAP)
}
@Test
fun `Should parse user not authorized for operation error`() {
mockWebServerRule.enqueueResponse("user_not_authorized_for_operation_error.json")
val response = client.api.ping().execute()
response.assertError(UserNotAuthorizedForOperation)
}
@Test
fun `Should parse trial period is over error`() {
mockWebServerRule.enqueueResponse("trial_period_is_over_error.json")
val response = client.api.ping().execute()
response.assertError(TrialPeriodIsOver)
}
@Test
fun `Should parse requested data was not found error`() {
mockWebServerRule.enqueueResponse("requested_data_was_not_found_error.json")
val response = client.api.ping().execute()
response.assertError(RequestedDataWasNotFound)
}
@Test
fun `Should parse error with reversed tokens order`() {
mockWebServerRule.enqueueResponse("reversed_tokens_generic_error.json")
val response = client.api.ping().execute()
response.assertError(Generic("Video streaming not supported"))
}
@Test
fun `Should parse error if json contains error first before other fields`() {
mockWebServerRule.enqueueResponse("error_first_generic_error.json")
val response = client.api.ping().execute()
response.assertError(Generic("Video streaming not supported"))
}
@Test
fun `Should parse error if json doesn't contain message field`() {
mockWebServerRule.enqueueResponse("without_message_generic_error.json")
val response = client.api.ping().execute()
response.assertError(Generic(""))
}
@Test
fun `Should parse error if error json contains additional object`() {
mockWebServerRule.enqueueResponse("with_additional_json_object_generic_error.json")
val response = client.api.ping().execute()
response.assertError(Generic(""))
}
private fun Response<SubsonicResponse>.assertError(expectedError: SubsonicError) =
with(body()) {
error `should not be` null
error `should equal` expectedError
}
}

View File

@ -50,16 +50,18 @@ class SubsonicApiGetAlbumTest : SubsonicAPIClientTest() {
year `should equal to` 2008
genre `should equal to` "Hard Rock"
songList.size `should equal to` 15
songList[0] `should equal` MusicDirectoryChild(id = "6491", parent = "6475", isDir = false,
title = "Rock 'n' Roll Train", album = "Black Ice", artist = "AC/DC",
track = 1, year = 2008, genre = "Hard Rock", coverArt = "6475", size = 7205451,
contentType = "audio/mpeg", suffix = "mp3", duration = 261, bitRate = 219,
path = "AC_DC/Black Ice/01 Rock 'n' Roll Train.mp3", isVideo = false,
playCount = 0, discNumber = 1, created = parseDate("2016-10-23T15:31:20.000Z"),
songList[0] `should equal` MusicDirectoryChild(id = "6491", parent = "6475",
isDir = false, title = "Rock 'n' Roll Train", album = "Black Ice",
artist = "AC/DC", track = 1, year = 2008, genre = "Hard Rock",
coverArt = "6475", size = 7205451, contentType = "audio/mpeg", suffix = "mp3",
duration = 261, bitRate = 219,
path = "AC_DC/Black Ice/01 Rock 'n' Roll Train.mp3",
isVideo = false, playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T15:31:20.000Z"),
albumId = "618", artistId = "362", type = "music")
songList[5] `should equal` MusicDirectoryChild(id = "6492", parent = "6475", isDir = false,
title = "Smash 'n' Grab", album = "Black Ice", artist = "AC/DC", track = 6,
year = 2008, genre = "Hard Rock", coverArt = "6475", size = 6697204,
songList[5] `should equal` MusicDirectoryChild(id = "6492", parent = "6475",
isDir = false, title = "Smash 'n' Grab", album = "Black Ice", artist = "AC/DC",
track = 6, year = 2008, genre = "Hard Rock", coverArt = "6475", size = 6697204,
contentType = "audio/mpeg", suffix = "mp3", duration = 246, bitRate = 216,
path = "AC_DC/Black Ice/06 Smash 'n' Grab.mp3", isVideo = false, playCount = 0,
discNumber = 1, created = parseDate("2016-10-23T15:31:20.000Z"),

View File

@ -37,11 +37,14 @@ class SubsonicApiGetArtistsTest : SubsonicAPIClientTest() {
indexList `should equal` listOf(
Index(name = "A", artists = listOf(
Artist(id = "362", name = "AC/DC", coverArt = "ar-362", albumCount = 2),
Artist(id = "254", name = "Acceptance", coverArt = "ar-254", albumCount = 1)
Artist(id = "254", name = "Acceptance", coverArt = "ar-254",
albumCount = 1)
)),
Index(name = "T", artists = listOf(
Artist(id = "516", name = "Tangerine Dream", coverArt = "ar-516", albumCount = 1),
Artist(id = "242", name = "Taproot", coverArt = "ar-242", albumCount = 2)
Artist(id = "516", name = "Tangerine Dream", coverArt = "ar-516",
albumCount = 1),
Artist(id = "242", name = "Taproot", coverArt = "ar-242",
albumCount = 2)
))
)
}

View File

@ -13,14 +13,14 @@ import org.junit.Test
class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
@Test
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("generic_error_response.json")
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
val response = client.getAvatar("some")
with(response) {
stream `should be` null
responseHttpCode `should equal to` 200
apiError `should equal` SubsonicError.GENERIC
apiError `should equal` SubsonicError.RequestedDataWasNotFound
}
}

View File

@ -13,14 +13,14 @@ import org.junit.Test
class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
@Test
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("generic_error_response.json")
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
val response = client.getCoverArt("some-id")
with(response) {
stream `should be` null
responseHttpCode `should equal to` 200
apiError `should equal` SubsonicError.GENERIC
apiError `should equal` SubsonicError.RequestedDataWasNotFound
}
}

View File

@ -50,19 +50,21 @@ class SubsonicApiGetMusicDirectoryTest : SubsonicAPIClientTest() {
starred `should equal` null
playCount `should equal to` 1
childList.size `should be` 2
childList[0] `should equal` MusicDirectoryChild(id = "4844", parent = "4836", isDir = false,
title = "Crash", album = "12 Stones", artist = "12 Stones", track = 1, year = 2002,
genre = "Alternative Rock", coverArt = "4836", size = 5348318L,
contentType = "audio/mpeg", suffix = "mp3", duration = 222, bitRate = 192,
path = "12 Stones/12 Stones/01 Crash.mp3", isVideo = false, playCount = 0,
discNumber = 1, created = parseDate("2016-10-23T15:19:10.000Z"),
childList[0] `should equal` MusicDirectoryChild(id = "4844", parent = "4836",
isDir = false, title = "Crash", album = "12 Stones", artist = "12 Stones",
track = 1, year = 2002, genre = "Alternative Rock", coverArt = "4836",
size = 5348318L, contentType = "audio/mpeg", suffix = "mp3", duration = 222,
bitRate = 192, path = "12 Stones/12 Stones/01 Crash.mp3", isVideo = false,
playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T15:19:10.000Z"),
albumId = "454", artistId = "288", type = "music")
childList[1] `should equal` MusicDirectoryChild(id = "4845", parent = "4836", isDir = false,
title = "Broken", album = "12 Stones", artist = "12 Stones", track = 2, year = 2002,
genre = "Alternative Rock", coverArt = "4836", size = 4309043L,
contentType = "audio/mpeg", suffix = "mp3", duration = 179, bitRate = 192,
path = "12 Stones/12 Stones/02 Broken.mp3", isVideo = false, playCount = 0,
discNumber = 1, created = parseDate("2016-10-23T15:19:09.000Z"),
childList[1] `should equal` MusicDirectoryChild(id = "4845", parent = "4836",
isDir = false, title = "Broken", album = "12 Stones", artist = "12 Stones",
track = 2, year = 2002, genre = "Alternative Rock", coverArt = "4836",
size = 4309043L, contentType = "audio/mpeg", suffix = "mp3", duration = 179,
bitRate = 192, path = "12 Stones/12 Stones/02 Broken.mp3", isVideo = false,
playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T15:19:09.000Z"),
albumId = "454", artistId = "288", type = "music")
}
}

View File

@ -33,13 +33,16 @@ class SubsonicApiGetPodcastsTest : SubsonicAPIClientTest() {
id `should equal to` "2"
url `should equal to` "http://feeds.codenewbie.org/cnpodcast.xml"
title `should equal to` "CodeNewbie"
description `should equal to` "Stories and interviews from people on their coding journey."
description `should equal to` "Stories and interviews from people on their coding " +
"journey."
coverArt `should equal to` "pod-2"
originalImageUrl `should equal to` "http://codenewbie.blubrry.com/wp-content/uploads/powerpress/220808.jpg"
originalImageUrl `should equal to` "http://codenewbie.blubrry.com/wp-content/uploads/" +
"powerpress/220808.jpg"
status `should equal to` "completed"
errorMessage `should equal to` ""
episodeList.size `should equal to` 10
episodeList[0] `should equal` MusicDirectoryChild(id = "148", parent = "9959", isDir = false,
episodeList[0] `should equal` MusicDirectoryChild(id = "148", parent = "9959",
isDir = false,
title = "S1:EP3 How to teach yourself computer science (Vaidehi Joshi)",
album = "CodeNewbie", artist = "podcasts", coverArt = "9959",
size = 38274221, contentType = "audio/mpeg", suffix = "mp3",
@ -56,7 +59,8 @@ class SubsonicApiGetPodcastsTest : SubsonicAPIClientTest() {
"CodeNewbie basecs 100 Days of Code Conway's Game of Life Hexes and " +
"Other Magical Numbers (Vaidehi's blog post) Bits, Bytes, Building " +
"With Binary (Vaidehi's blog post) Rust",
status = "completed", publishDate = parseDate("2017-08-29T00:01:01.000Z"))
status = "completed",
publishDate = parseDate("2017-08-29T00:01:01.000Z"))
}
}

View File

@ -39,8 +39,10 @@ class SubsonicApiGetSongsByGenreTest : SubsonicAPIClientTest() {
artist = "DJ Polyakov PPK Feat Kate Cameron", year = 2009, genre = "Trance",
size = 26805932, contentType = "audio/mpeg", suffix = "mp3", duration = 670,
bitRate = 320,
path = "DJ Polyakov PPK Feat Kate Cameron/668/00 My Heart (Vadim Zhukov Remix).mp3",
isVideo = false, playCount = 2, created = parseDate("2016-10-23T21:58:29.000Z"),
path = "DJ Polyakov PPK Feat Kate Cameron/668/00 My Heart (Vadim Zhukov " +
"Remix).mp3",
isVideo = false, playCount = 2,
created = parseDate("2016-10-23T21:58:29.000Z"),
albumId = "5", artistId = "4", type = "music")
}
}

View File

@ -27,11 +27,13 @@ class SubsonicApiGetVideosListTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response)
with(response.body().videosList) {
size `should equal to` 1
this[0] `should equal` MusicDirectoryChild(id = "10402", parent = "10401", isDir = false,
title = "MVI_0512", album = "Incoming", size = 21889646,
contentType = "video/avi", suffix = "avi", transcodedContentType = "video/x-flv",
transcodedSuffix = "flv", path = "Incoming/MVI_0512.avi", isVideo = true,
playCount = 0, created = parseDate("2017-11-19T12:34:33.000Z"), type = "video")
this[0] `should equal` MusicDirectoryChild(id = "10402", parent = "10401",
isDir = false, title = "MVI_0512", album = "Incoming", size = 21889646,
contentType = "video/avi", suffix = "avi",
transcodedContentType = "video/x-flv", transcodedSuffix = "flv",
path = "Incoming/MVI_0512.avi", isVideo = true,
playCount = 0, created = parseDate("2017-11-19T12:34:33.000Z"),
type = "video")
}
}
}

View File

@ -56,8 +56,8 @@ class SubsonicApiJukeboxControlTest : SubsonicAPIClientTest() {
artist = "The Pretty Reckless", track = 2, year = 2014, genre = "Hard Rock",
coverArt = "4186", size = 11089627, contentType = "audio/mpeg",
suffix = "mp3", duration = 277, bitRate = 320,
path = "The Pretty Reckless/Going to Hell/02 Going to Hell.mp3", isVideo = false,
playCount = 0, discNumber = 1,
path = "The Pretty Reckless/Going to Hell/02 Going to Hell.mp3",
isVideo = false, playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T21:30:41.000Z"), albumId = "388",
artistId = "238", type = "music")
}

View File

@ -10,8 +10,8 @@ import org.junit.Test
class SubsonicApiPasswordTest : SubsonicAPIClientTest() {
@Test
fun `Should pass PasswordMD5Interceptor in query params for api version 1 13 0`() {
val clientV12 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME,
PASSWORD, SubsonicAPIVersions.V1_14_0, CLIENT_ID)
val clientV12 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME, PASSWORD, SubsonicAPIVersions.V1_14_0, CLIENT_ID)
mockWebServerRule.enqueueResponse("ping_ok.json")
clientV12.api.ping().execute()
@ -25,8 +25,8 @@ class SubsonicApiPasswordTest : SubsonicAPIClientTest() {
@Test
fun `Should pass PasswordHexInterceptor in query params for api version 1 12 0`() {
val clientV11 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME,
PASSWORD, SubsonicAPIVersions.V1_12_0, CLIENT_ID)
val clientV11 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME, PASSWORD, SubsonicAPIVersions.V1_12_0, CLIENT_ID)
mockWebServerRule.enqueueResponse("ping_ok.json")
clientV11.api.ping().execute()

View File

@ -48,8 +48,8 @@ class SubsonicApiSSLTest {
cert = (CertificateFactory.getInstance("X.509")
.generateCertificate(certificatePemStream)) as X509Certificate
}
val alias = cert?.subjectX500Principal?.name ?:
throw IllegalStateException("Failed to load certificate")
val alias = cert?.subjectX500Principal?.name
?: throw IllegalStateException("Failed to load certificate")
trustStore.setCertificateEntry(alias, cert)
val tmf = TrustManagerFactory.getInstance("X509")

View File

@ -39,7 +39,8 @@ class SubsonicApiSearchTest : SubsonicAPIClientTest() {
track = 17, year = 2005, genre = "Rap", coverArt = "5766",
size = 5607024, contentType = "audio/mpeg", suffix = "mp3", duration = 233,
bitRate = 192,
path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels.mp3",
path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels" +
".mp3",
isVideo = false, playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T20:09:02.000Z"), albumId = "568",
artistId = "505", type = "music")

View File

@ -32,20 +32,23 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
assertResponseSuccessful(response)
with(response.body().searchResult) {
artistList.size `should equal to` 1
artistList[0] `should equal` Artist(id = "505", name = "The Prodigy", coverArt = "ar-505",
albumCount = 5)
artistList[0] `should equal` Artist(id = "505", name = "The Prodigy",
coverArt = "ar-505", albumCount = 5)
albumList.size `should equal to` 1
albumList[0] `should equal` Album(id = "855", name = "Always Outnumbered, Never Outgunned",
albumList[0] `should equal` Album(id = "855",
name = "Always Outnumbered, Never Outgunned",
artist = "The Prodigy", artistId = "505", coverArt = "al-855", songCount = 12,
duration = 3313, created = parseDate("2016-10-23T20:57:27.000Z"),
year = 2004, genre = "Electronic")
songList.size `should equal to` 1
songList[0] `should equal` MusicDirectoryChild(id = "5831", parent = "5766", isDir = false,
songList[0] `should equal` MusicDirectoryChild(id = "5831", parent = "5766",
isDir = false,
title = "You'll Be Under My Wheels", album = "Need for Speed Most Wanted",
artist = "The Prodigy", track = 17, year = 2005, genre = "Rap",
coverArt = "5766", size = 5607024, contentType = "audio/mpeg",
suffix = "mp3", duration = 233, bitRate = 192,
path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels.mp3",
path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels" +
".mp3",
isVideo = false, playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T20:09:02.000Z"), albumId = "568",
artistId = "505", type = "music")
@ -56,9 +59,10 @@ class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
fun `Should pass query as request param`() {
val query = "some-wip-query"
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json", apiRequest = {
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json",
expectedParam = "query=$query") {
client.api.search3(query = query).execute()
}, expectedParam = "query=$query")
}
}
@Test

View File

@ -33,18 +33,20 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
artistList.size `should equal to` 1
artistList[0] `should equal` Artist(id = "522", name = "The Prodigy")
albumList.size `should equal to` 1
albumList[0] `should equal` MusicDirectoryChild(id = "8867", parent = "522", isDir = true,
title = "Always Outnumbered, Never Outgunned",
albumList[0] `should equal` MusicDirectoryChild(id = "8867", parent = "522",
isDir = true, title = "Always Outnumbered, Never Outgunned",
album = "Always Outnumbered, Never Outgunned", artist = "The Prodigy",
year = 2004, genre = "Electronic", coverArt = "8867", playCount = 0,
created = parseDate("2016-10-23T20:57:27.000Z"))
songList.size `should equal to` 1
songList[0] `should equal` MusicDirectoryChild(id = "5831", parent = "5766", isDir = false,
songList[0] `should equal` MusicDirectoryChild(id = "5831", parent = "5766",
isDir = false,
title = "You'll Be Under My Wheels", album = "Need for Speed Most Wanted",
artist = "The Prodigy", track = 17, year = 2005, genre = "Rap",
coverArt = "5766", size = 5607024, contentType = "audio/mpeg",
suffix = "mp3", duration = 233, bitRate = 192,
path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels.mp3",
path = "Compilations/Need for Speed Most Wanted/17 You'll Be Under My Wheels" +
".mp3",
isVideo = false, playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T20:09:02.000Z"),
albumId = "568", artistId = "505", type = "music")

View File

@ -13,14 +13,14 @@ import org.junit.Test
class SubsonicApiStreamTest : SubsonicAPIClientTest() {
@Test
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("generic_error_response.json")
mockWebServerRule.enqueueResponse("request_data_not_found_error_response.json")
val response = client.stream("some-id")
with(response) {
stream `should be` null
responseHttpCode `should equal to` 200
apiError `should equal` SubsonicError.GENERIC
apiError `should equal` SubsonicError.RequestedDataWasNotFound
}
}

View File

@ -40,7 +40,8 @@ class VersionInterceptorTest : BaseInterceptorTest() {
client.newCall(createRequest {}).execute()
(interceptor as VersionInterceptor).protocolVersion `should equal` SubsonicAPIVersions.V1_13_0
(interceptor as VersionInterceptor)
.protocolVersion `should equal` SubsonicAPIVersions.V1_13_0
}
@Test

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"error": {
"message": "Video streaming not supported",
"code": 0
},
"version": "1.8.0",
"status": "failed"
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 0,
"message": "Some generic error message."
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 20,
"message": "Client protocol version 1.17.0 is not supported."
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 30,
"message": "Server doesn't support 1.10.0 protocol version."
}
}
}

View File

@ -3,8 +3,8 @@
"status" : "failed",
"version" : "1.13.0",
"error" : {
"code" : 0,
"message" : "Generic error."
"code" : 70,
"message" : "Requested data was not found."
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 70,
"message": "Requested data was not found."
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 10,
"message": "Param musicFolderId is missing."
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.8.0",
"error": {
"message": "Video streaming not supported",
"code": 0
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 41,
"message": "Token auth is not supported for ldap users."
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 60,
"message": "Trial period is over."
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 1000000,
"message": "New funky error message."
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 50,
"message": "User is not authorized for this operation."
}
}
}

View File

@ -0,0 +1,13 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.8.0",
"error": {
"code": 0,
"unicorn" : {
"code": 41,
"message": "Unicorns doesn't exist!"
}
}
}
}

View File

@ -0,0 +1,9 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.8.0",
"error": {
"code": 0
}
}
}

View File

@ -0,0 +1,10 @@
{
"subsonic-response": {
"status": "failed",
"version": "1.15.0",
"error": {
"code": 40,
"message": "Wrong username or password."
}
}
}

View File

@ -271,7 +271,9 @@ internal class ApiVersionCheckWrapper(
return api.getBookmarks()
}
override fun createBookmark(id: String, position: Long, comment: String?): Call<SubsonicResponse> {
override fun createBookmark(id: String,
position: Long,
comment: String?): Call<SubsonicResponse> {
checkVersion(V1_9_0)
return api.createBookmark(id, position, comment)
}

View File

@ -41,13 +41,17 @@ class SubsonicAPIClient(baseUrl: String,
minimalProtocolVersion: SubsonicAPIVersions,
clientID: String,
allowSelfSignedCertificate: Boolean = false,
enableLdapUserSupport: Boolean = false,
debug: Boolean = false) {
private val versionInterceptor = VersionInterceptor(minimalProtocolVersion) {
protocolVersion = it
}
private val proxyPasswordInterceptor = ProxyPasswordInterceptor(minimalProtocolVersion,
PasswordHexInterceptor(password), PasswordMD5Interceptor(password))
private val proxyPasswordInterceptor = ProxyPasswordInterceptor(
minimalProtocolVersion,
PasswordHexInterceptor(password),
PasswordMD5Interceptor(password),
enableLdapUserSupport)
/**
* Get currently used protocol version.

View File

@ -127,7 +127,8 @@ interface SubsonicAPIDefinition {
@Query("comment") comment: String? = null,
@Query("public") public: Boolean? = null,
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null): Call<SubsonicResponse>
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null):
Call<SubsonicResponse>
@GET("getPodcasts.view")
fun getPodcasts(@Query("includeEpisodes") includeEpisodes: Boolean? = null,
@ -143,35 +144,39 @@ interface SubsonicAPIDefinition {
@Query("submission") submission: Boolean? = null): Call<SubsonicResponse>
@GET("getAlbumList.view")
fun getAlbumList(@Query("type") type: AlbumListType,
@Query("size") size: Int? = null,
@Query("offset") offset: Int? = null,
@Query("fromYear") fromYear: Int? = null,
@Query("toYear") toYear: Int? = null,
@Query("genre") genre: String? = null,
@Query("musicFolderId") musicFolderId: String? = null): Call<GetAlbumListResponse>
fun getAlbumList(
@Query("type") type: AlbumListType,
@Query("size") size: Int? = null,
@Query("offset") offset: Int? = null,
@Query("fromYear") fromYear: Int? = null,
@Query("toYear") toYear: Int? = null,
@Query("genre") genre: String? = null,
@Query("musicFolderId") musicFolderId: String? = null): Call<GetAlbumListResponse>
@GET("getAlbumList2.view")
fun getAlbumList2(@Query("type") type: AlbumListType,
@Query("size") size: Int? = null,
@Query("offset") offset: Int? = null,
@Query("fromYear") fromYear: Int? = null,
@Query("toYear") toYear: Int? = null,
@Query("genre") genre: String? = null,
@Query("musicFolderId") musicFolderId: String? = null): Call<GetAlbumList2Response>
fun getAlbumList2(
@Query("type") type: AlbumListType,
@Query("size") size: Int? = null,
@Query("offset") offset: Int? = null,
@Query("fromYear") fromYear: Int? = null,
@Query("toYear") toYear: Int? = null,
@Query("genre") genre: String? = null,
@Query("musicFolderId") musicFolderId: String? = null): Call<GetAlbumList2Response>
@GET("getRandomSongs.view")
fun getRandomSongs(@Query("size") size: Int? = null,
@Query("genre") genre: String? = null,
@Query("fromYear") fromYear: Int? = null,
@Query("toYear") toYear: Int? = null,
@Query("musicFolderId") musicFolderId: String? = null): Call<GetRandomSongsResponse>
fun getRandomSongs(
@Query("size") size: Int? = null,
@Query("genre") genre: String? = null,
@Query("fromYear") fromYear: Int? = null,
@Query("toYear") toYear: Int? = null,
@Query("musicFolderId") musicFolderId: String? = null): Call<GetRandomSongsResponse>
@GET("getStarred.view")
fun getStarred(@Query("musicFolderId") musicFolderId: String? = null): Call<GetStarredResponse>
@GET("getStarred2.view")
fun getStarred2(@Query("musicFolderId") musicFolderId: String? = null): Call<GetStarredTwoResponse>
fun getStarred2(
@Query("musicFolderId") musicFolderId: String? = null): Call<GetStarredTwoResponse>
@Streaming
@GET("getCoverArt.view")

View File

@ -55,7 +55,8 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
}
class SubsonicAPIVersionsDeserializer : JsonDeserializer<SubsonicAPIVersions>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): SubsonicAPIVersions {
override fun deserialize(p: JsonParser,
ctxt: DeserializationContext?): SubsonicAPIVersions {
if (p.currentName != "version") {
throw JsonParseException(p, "Not valid token for API version!")
}

View File

@ -1,6 +1,8 @@
package org.moire.ultrasonic.api.subsonic
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken.END_OBJECT
import com.fasterxml.jackson.core.JsonToken.START_OBJECT
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
@ -9,29 +11,45 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize
* Common API errors.
*/
@JsonDeserialize(using = SubsonicError.Companion.SubsonicErrorDeserializer::class)
enum class SubsonicError(val code: Int) {
GENERIC(0),
REQUIRED_PARAM_MISSING(10),
INCOMPATIBLE_CLIENT_PROTOCOL_VERSION(20),
INCOMPATIBLE_SERVER_PROTOCOL_VERSION(30),
WRONG_USERNAME_OR_PASSWORD(40),
TOKEN_AUTH_NOT_SUPPORTED_FOR_LDAP(41),
USER_NOT_AUTHORIZED_FOR_OPERATION(50),
TRIAL_PERIOD_IS_OVER(60),
REQUESTED_DATA_WAS_NOT_FOUND(70);
sealed class SubsonicError(val code: Int) {
data class Generic(val message: String) : SubsonicError(0)
object RequiredParamMissing : SubsonicError(10)
object IncompatibleClientProtocolVersion : SubsonicError(20)
object IncompatibleServerProtocolVersion : SubsonicError(30)
object WrongUsernameOrPassword : SubsonicError(40)
object TokenAuthNotSupportedForLDAP : SubsonicError(41)
object UserNotAuthorizedForOperation : SubsonicError(50)
object TrialPeriodIsOver : SubsonicError(60)
object RequestedDataWasNotFound : SubsonicError(70)
companion object {
fun parseErrorFromJson(jsonErrorCode: Int) = SubsonicError.values()
.filter { it.code == jsonErrorCode }.firstOrNull()
?: throw IllegalArgumentException("Unknown code $jsonErrorCode")
fun getError(code: Int, message: String) = when (code) {
0 -> Generic(message)
10 -> RequiredParamMissing
20 -> IncompatibleClientProtocolVersion
30 -> IncompatibleServerProtocolVersion
40 -> WrongUsernameOrPassword
41 -> TokenAuthNotSupportedForLDAP
50 -> UserNotAuthorizedForOperation
60 -> TrialPeriodIsOver
70 -> RequestedDataWasNotFound
else -> throw IllegalArgumentException("Unknown code $code")
}
class SubsonicErrorDeserializer : JsonDeserializer<SubsonicError>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): SubsonicError {
p.nextToken() // "code"
val error = parseErrorFromJson(p.valueAsInt)
p.nextToken() // "message"
p.nextToken() // end of error object
return error
var code = -1
var message = ""
while (p.nextToken() != END_OBJECT) {
when {
p.currentToken == START_OBJECT -> p.skipChildren()
"code".equals(p.currentName, ignoreCase = true) ->
code = p.nextIntValue(-1)
"message".equals(p.currentName, ignoreCase = true) ->
message = p.nextTextValue()
}
}
return getError(code, message)
}
}
}

View File

@ -7,15 +7,21 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
/**
* Proxy [Interceptor] that uses one of [hexInterceptor] or [mD5Interceptor] depends on [apiVersion].
*
* To force [hexInterceptor] set [forceHexPassword] to `true`. Usually it should be done only for
* ldap users.
*/
internal class ProxyPasswordInterceptor(
initialAPIVersions: SubsonicAPIVersions,
private val hexInterceptor: PasswordHexInterceptor,
private val mD5Interceptor: PasswordMD5Interceptor) : Interceptor {
private val mD5Interceptor: PasswordMD5Interceptor,
private val forceHexPassword: Boolean = false
) : Interceptor {
var apiVersion: SubsonicAPIVersions = initialAPIVersions
override fun intercept(chain: Chain): Response =
if (apiVersion < SubsonicAPIVersions.V1_13_0) {
if (apiVersion < SubsonicAPIVersions.V1_13_0 ||
forceHexPassword) {
hexInterceptor.intercept(chain)
} else {
mD5Interceptor.intercept(chain)

View File

@ -36,6 +36,6 @@ internal class RangeHeaderInterceptor : Interceptor {
// to avoid the thrashing effect seen when offset is combined with transcoding/downsampling
// on the server. In that case, the server uses a long time before sending any data,
// causing the client to time out.
private fun getReadTimeout(offset: Int)
= (SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE).toInt()
private fun getReadTimeout(offset: Int) =
(SOCKET_READ_TIMEOUT_DOWNLOAD + offset * TIMEOUT_MILLIS_PER_OFFSET_BYTE).toInt()
}

View File

@ -2,6 +2,7 @@ package org.moire.ultrasonic.api.subsonic.models
import com.fasterxml.jackson.annotation.JsonProperty
data class SearchResult(val offset: Int = 0,
val totalHits: Int = 0,
@JsonProperty("match") val matchList: List<MusicDirectoryChild> = emptyList())
data class SearchResult(
val offset: Int = 0,
val totalHits: Int = 0,
@JsonProperty("match") val matchList: List<MusicDirectoryChild> = emptyList())

View File

@ -16,4 +16,5 @@ class GetPlaylistsResponse(status: Status,
get() = playlistsWrapper.playlistList
}
private class PlaylistsWrapper(@JsonProperty("playlist") val playlistList: List<Playlist> = emptyList())
private class PlaylistsWrapper(
@JsonProperty("playlist") val playlistList: List<Playlist> = emptyList())

View File

@ -5,8 +5,9 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicError
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
class SearchTwoResponse(status: Status,
version: SubsonicAPIVersions,
error: SubsonicError?,
@JsonProperty("searchResult2") val searchResult: SearchTwoResult = SearchTwoResult())
class SearchTwoResponse(
status: Status,
version: SubsonicAPIVersions,
error: SubsonicError?,
@JsonProperty("searchResult2") val searchResult: SearchTwoResult = SearchTwoResult())
: SubsonicResponse(status, version, error)

View File

@ -1,28 +0,0 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/**
* Unit test for [SubsonicError].
*/
@RunWith(Parameterized::class)
class SubsonicErrorTest(private val error: SubsonicError) {
companion object {
@JvmStatic
@Parameterized.Parameters
fun data(): List<SubsonicError> = SubsonicError.values().toList()
}
@Test
fun `Should proper convert error code to error`() {
SubsonicError.parseErrorFromJson(error.code) `should equal` error
}
@Test(expected = IllegalArgumentException::class)
fun `Should throw IllegalArgumentException from unknown error code`() {
SubsonicError.parseErrorFromJson(error.code + 10000)
}
}

View File

@ -6,6 +6,7 @@ import okhttp3.Interceptor.Chain
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_12_0
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_13_0
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_16_0
/**
* Unit test for [ProxyPasswordInterceptor].
@ -16,7 +17,7 @@ class ProxyPasswordInterceptorTest {
private val mockChain = mock<Chain>()
private val proxyInterceptor = ProxyPasswordInterceptor(V1_12_0,
mockPasswordHexInterceptor, mockPasswordMd5Interceptor)
mockPasswordHexInterceptor, mockPasswordMd5Interceptor, false)
@Test
fun `Should use hex password on versions less then 1 13 0`() {
@ -33,4 +34,14 @@ class ProxyPasswordInterceptorTest {
verify(mockPasswordMd5Interceptor).intercept(mockChain)
}
@Test
fun `Should use hex password if forceHex is true`() {
val interceptor = ProxyPasswordInterceptor(V1_16_0, mockPasswordHexInterceptor,
mockPasswordMd5Interceptor, true)
interceptor.intercept(mockChain)
verify(mockPasswordHexInterceptor).intercept(mockChain)
}
}

View File

@ -2,7 +2,7 @@ package org.moire.ultrasonic.api.subsonic.response
import org.amshove.kluent.`should equal to`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.SubsonicError.GENERIC
import org.moire.ultrasonic.api.subsonic.SubsonicError.RequestedDataWasNotFound
/**
* Unit test for [StreamResponse].
@ -10,7 +10,8 @@ import org.moire.ultrasonic.api.subsonic.SubsonicError.GENERIC
class StreamResponseTest {
@Test
fun `Should have error if subsonic error is not null`() {
StreamResponse(apiError = GENERIC, responseHttpCode = 200).hasError() `should equal to` true
StreamResponse(apiError = RequestedDataWasNotFound, responseHttpCode = 200)
.hasError() `should equal to` true
}
@Test

View File

@ -1,14 +1,31 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'jacoco-android'
apply plugin: 'org.moallemi.advanced-build-version'
apply from: "../gradle_scripts/code_quality.gradle"
advancedVersioning {
nameOptions {
versionMajor 2
versionMinor 2
versionPatch 0
}
codeOptions {
versionCodeType org.moallemi.gradle.internal.VersionCodeType.AUTO_INCREMENT_ONE_STEP
}
outputOptions {
renameOutput true
}
}
android {
compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools
defaultConfig {
applicationId "org.moire.ultrasonic"
versionCode advancedVersioning.versionCode
versionName advancedVersioning.versionName
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
@ -42,21 +59,21 @@ android {
}
dependencies {
compile project(':menudrawer')
compile project(':pulltorefresh')
compile project(':library')
compile project(':subsonic-api')
implementation project(':menudrawer')
implementation project(':pulltorefresh')
implementation project(':library')
implementation project(':subsonic-api')
compile androidSupport.support
compile androidSupport.design
implementation androidSupport.support
implementation androidSupport.design
compile other.kotlinStdlib
implementation other.kotlinStdlib
testCompile other.kotlinReflect
testCompile testing.junit
testCompile testing.kotlinJunit
testCompile testing.mockitoKotlin
testCompile testing.kluent
testImplementation other.kotlinReflect
testImplementation testing.junit
testImplementation testing.kotlinJunit
testImplementation testing.mockitoKotlin
testImplementation testing.kluent
}
// Excluding all non-kotlin classes

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:a="http://schemas.android.com/apk/res/android"
package="org.moire.ultrasonic"
a:installLocation="auto"
a:versionCode="60"
a:versionName="2.1.0">
a:installLocation="auto">
<uses-permission a:name="android.permission.INTERNET"/>
<uses-permission a:name="android.permission.ACCESS_NETWORK_STATE"/>

View File

@ -38,6 +38,7 @@ public class ServerSettingsFragment extends PreferenceFragment
private CheckBoxPreference equalizerPref;
private CheckBoxPreference jukeboxPref;
private CheckBoxPreference allowSelfSignedCertificatePref;
private CheckBoxPreference enableLdapUserSupportPref;
private Preference removeServerPref;
private Preference testConnectionPref;
@ -77,6 +78,9 @@ public class ServerSettingsFragment extends PreferenceFragment
testConnectionPref = findPreference(getString(R.string.settings_test_connection_title));
allowSelfSignedCertificatePref = (CheckBoxPreference) findPreference(
getString(R.string.settings_allow_self_signed_certificate));
enableLdapUserSupportPref = (CheckBoxPreference) findPreference(
getString(R.string.settings_enable_ldap_user_support)
);
setupPreferencesValues();
setupPreferencesListeners();
@ -140,6 +144,11 @@ public class ServerSettingsFragment extends PreferenceFragment
.putBoolean(Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + serverId, (Boolean) newValue)
.apply();
return true;
} else if (preference == enableLdapUserSupportPref) {
sharedPreferences.edit()
.putBoolean(Constants.PREFERENCES_KEY_LDAP_SUPPORT + serverId, (Boolean) newValue)
.apply();
return true;
}
return false;
}
@ -175,6 +184,9 @@ public class ServerSettingsFragment extends PreferenceFragment
allowSelfSignedCertificatePref.setChecked(sharedPreferences
.getBoolean(Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + serverId, false));
enableLdapUserSupportPref.setChecked(sharedPreferences
.getBoolean(Constants.PREFERENCES_KEY_LDAP_SUPPORT + serverId, false));
}
private void updatePassword() {
@ -213,6 +225,7 @@ public class ServerSettingsFragment extends PreferenceFragment
equalizerPref.setOnPreferenceChangeListener(this);
jukeboxPref.setOnPreferenceChangeListener(this);
allowSelfSignedCertificatePref.setOnPreferenceChangeListener(this);
enableLdapUserSupportPref.setOnPreferenceChangeListener(this);
removeServerPref.setOnPreferenceClickListener(this);
testConnectionPref.setOnPreferenceClickListener(this);

View File

@ -2071,6 +2071,7 @@ public class DownloadServiceImpl extends Service implements DownloadService
}
}
@SuppressWarnings("IconColors")
private Notification buildForegroundNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.drawable.ic_stat_ultrasonic);

View File

@ -18,17 +18,22 @@
*/
package org.moire.ultrasonic.service;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.KeyEvent;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.util.CacheCleaner;
@ -128,32 +133,9 @@ public class DownloadServiceLifecycleSupport
executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS);
// Pause when headset is unplugged.
headsetEventReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
Bundle extras = intent.getExtras();
registerHeadsetReceiver();
if (extras == null)
{
return;
}
Log.i(TAG, String.format("Headset event for: %s", extras.get("name")));
if (extras.getInt("state") == 0)
{
if (!downloadService.isJukeboxEnabled())
{
downloadService.pause();
}
}
}
};
downloadService.registerReceiver(headsetEventReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
// Stop when SD card is ejected.
// Stop when SD card is ejected.
ejectEventReceiver = new BroadcastReceiver()
{
@Override
@ -202,7 +184,44 @@ public class DownloadServiceLifecycleSupport
new CacheCleaner(downloadService, downloadService).clean();
}
public void onStart(Intent intent)
private void registerHeadsetReceiver() {
// Pause when headset is unplugged.
final SharedPreferences sp = Util.getPreferences(downloadService);
final String spKey = downloadService
.getString(R.string.settings_playback_resume_play_on_headphones_plug);
headsetEventReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final Bundle extras = intent.getExtras();
if (extras == null) {
return;
}
Log.i(TAG, String.format("Headset event for: %s", extras.get("name")));
final int state = extras.getInt("state");
if (state == 0) {
if (!downloadService.isJukeboxEnabled()) {
downloadService.pause();
}
} else if (state == 1) {
if (!downloadService.isJukeboxEnabled() &&
sp.getBoolean(spKey, false) &&
downloadService.getPlayerState() == PlayerState.PAUSED) {
downloadService.start();
}
}
}
};
@SuppressLint("InlinedApi")
IntentFilter headsetIntentFilter = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) ?
new IntentFilter(AudioManager.ACTION_HEADSET_PLUG) :
new IntentFilter(Intent.ACTION_HEADSET_PLUG);
downloadService.registerReceiver(headsetEventReceiver, headsetIntentFilter);
}
public void onStart(Intent intent)
{
if (intent != null && intent.getExtras() != null)
{

View File

@ -31,7 +31,6 @@ import org.moire.ultrasonic.R;
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException;
import org.moire.ultrasonic.domain.JukeboxStatus;
import org.moire.ultrasonic.domain.PlayerState;
import org.moire.ultrasonic.service.parser.SubsonicRESTException;
import org.moire.ultrasonic.util.Util;
import java.util.ArrayList;

View File

@ -21,7 +21,6 @@ package org.moire.ultrasonic.service;
import android.content.Context;
import android.graphics.Bitmap;
import org.apache.http.HttpResponse;
import org.moire.ultrasonic.domain.Bookmark;
import org.moire.ultrasonic.domain.ChatMessage;
import org.moire.ultrasonic.domain.Genre;

View File

@ -86,6 +86,8 @@ public class MusicServiceFactory {
String password = preferences.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
boolean allowSelfSignedCertificate = preferences
.getBoolean(Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + instance, false);
boolean enableLdapUserSupport = preferences
.getBoolean(Constants.PREFERENCES_KEY_LDAP_SUPPORT + instance , false);
if (serverUrl == null ||
username == null ||
@ -93,11 +95,13 @@ public class MusicServiceFactory {
Log.i("MusicServiceFactory", "Server credentials is not available");
return new SubsonicAPIClient("http://localhost", "", "",
SubsonicAPIVersions.fromApiVersion(Constants.REST_PROTOCOL_VERSION),
Constants.REST_CLIENT_ID, allowSelfSignedCertificate, BuildConfig.DEBUG);
Constants.REST_CLIENT_ID, allowSelfSignedCertificate,
enableLdapUserSupport, BuildConfig.DEBUG);
}
return new SubsonicAPIClient(serverUrl, username, password,
SubsonicAPIVersions.fromApiVersion(Constants.REST_PROTOCOL_VERSION),
Constants.REST_CLIENT_ID, allowSelfSignedCertificate, BuildConfig.DEBUG);
Constants.REST_CLIENT_ID, allowSelfSignedCertificate,
enableLdapUserSupport, BuildConfig.DEBUG);
}
}

View File

@ -89,7 +89,6 @@ import org.moire.ultrasonic.domain.SearchCriteria;
import org.moire.ultrasonic.domain.SearchResult;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.domain.UserInfo;
import org.moire.ultrasonic.service.parser.SubsonicRESTException;
import org.moire.ultrasonic.util.CancellableTask;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.ProgressListener;
@ -698,7 +697,7 @@ public class RESTMusicService implements MusicService {
throws SubsonicRESTException, IOException {
if (response.hasError() || response.getStream() == null) {
if (response.getApiError() != null) {
throw new SubsonicRESTException(response.getApiError().getCode(), "rest error");
throw new SubsonicRESTException(response.getApiError());
} else {
throw new IOException("Failed to make endpoint request, code: " +
response.getResponseHttpCode());
@ -1077,7 +1076,7 @@ public class RESTMusicService implements MusicService {
}
private void checkResponseSuccessful(@NonNull final Response<? extends SubsonicResponse> response)
throws IOException {
throws SubsonicRESTException, IOException {
if (response.isSuccessful() &&
response.body().getStatus() == SubsonicResponse.Status.OK) {
return;
@ -1087,7 +1086,7 @@ public class RESTMusicService implements MusicService {
throw new IOException("Server error, code: " + response.code());
} else if (response.body().getStatus() == SubsonicResponse.Status.ERROR &&
response.body().getError() != null) {
throw new IOException("Server error: " + response.body().getError().getCode());
throw new SubsonicRESTException(response.body().getError());
} else {
throw new IOException("Failed to perform request: " + response.code());
}

View File

@ -1,26 +0,0 @@
package org.moire.ultrasonic.service.parser;
/**
* @author Sindre Mehus
* @version $Id$
*/
public class SubsonicRESTException extends Exception
{
/**
*
*/
private static final long serialVersionUID = 859440717343258203L;
private final int code;
public SubsonicRESTException(int code, String message)
{
super(message);
this.code = code;
}
public int getCode()
{
return code;
}
}

View File

@ -22,12 +22,18 @@ import android.app.Activity;
import android.os.Handler;
import android.util.Log;
import org.moire.ultrasonic.R;
import com.fasterxml.jackson.core.JsonParseException;
import org.xmlpull.v1.XmlPullParserException;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.service.SubsonicRESTException;
import org.moire.ultrasonic.subsonic.RestErrorMapper;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import javax.net.ssl.SSLException;
/**
* @author Sindre Mehus
@ -67,36 +73,34 @@ public abstract class BackgroundTask<T> implements ProgressListener
new ErrorDialog(activity, getErrorMessage(error), true);
}
protected String getErrorMessage(Throwable error)
{
protected String getErrorMessage(Throwable error) {
if (error instanceof IOException && !Util.isNetworkConnected(activity)) {
return activity.getResources().getString(R.string.background_task_no_network);
} else if (error instanceof FileNotFoundException) {
return activity.getResources().getString(R.string.background_task_not_found);
} else if (error instanceof JsonParseException) {
return activity.getResources().getString(R.string.background_task_parse_error);
} else if (error instanceof SSLException) {
if (error.getCause() instanceof CertificateException &&
error.getCause().getCause() instanceof CertPathValidatorException) {
return activity.getResources()
.getString(R.string.background_task_ssl_cert_error,
error.getCause().getCause().getMessage());
} else {
return activity.getResources().getString(R.string.background_task_ssl_error);
}
} else if (error instanceof IOException) {
return activity.getResources().getString(R.string.background_task_network_error);
} else if (error instanceof SubsonicRESTException) {
return RestErrorMapper.getLocalizedErrorMessage((SubsonicRESTException) error, activity);
}
if (error instanceof IOException && !Util.isNetworkConnected(activity))
{
return activity.getResources().getString(R.string.background_task_no_network);
}
if (error instanceof FileNotFoundException)
{
return activity.getResources().getString(R.string.background_task_not_found);
}
if (error instanceof IOException)
{
return activity.getResources().getString(R.string.background_task_network_error);
}
if (error instanceof XmlPullParserException)
{
return activity.getResources().getString(R.string.background_task_parse_error);
}
String message = error.getMessage();
if (message != null)
{
return message;
}
return error.getClass().getSimpleName();
}
String message = error.getMessage();
if (message != null) {
return message;
}
return error.getClass().getSimpleName();
}
@Override
public abstract void updateProgress(final String message);

View File

@ -77,6 +77,7 @@ public final class Constants
public static final String PREFERENCES_KEY_USERNAME = "username";
public static final String PREFERENCES_KEY_PASSWORD = "password";
public static final String PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE = "allowSSCertificate";
public static final String PREFERENCES_KEY_LDAP_SUPPORT = "enableLdapSupport";
public static final String PREFERENCES_KEY_INSTALL_TIME = "installTime";
public static final String PREFERENCES_KEY_THEME = "theme";
public static final String PREFERENCES_KEY_DISPLAY_BITRATE_WITH_ARTIST = "displayBitrateWithArtist";

View File

@ -6,9 +6,6 @@ import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.DownloadService;
import org.apache.http.HttpRequest;
import org.apache.http.message.BasicHttpRequest;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
@ -108,81 +105,66 @@ public class StreamProxy implements Runnable
Log.i(TAG, "Proxy interrupted. Shutting down.");
}
private class StreamToMediaPlayerTask implements Runnable
{
private class StreamToMediaPlayerTask implements Runnable {
String localPath;
Socket client;
int cbSkip;
String localPath;
Socket client;
int cbSkip;
StreamToMediaPlayerTask(Socket client) {
this.client = client;
}
public StreamToMediaPlayerTask(Socket client)
{
this.client = client;
}
private String readRequest() {
InputStream is;
String firstLine;
try {
is = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192);
firstLine = reader.readLine();
} catch (IOException e) {
Log.e(TAG, "Error parsing request", e);
return null;
}
private HttpRequest readRequest()
{
HttpRequest request;
InputStream is;
String firstLine;
try
{
is = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is), 8192);
firstLine = reader.readLine();
}
catch (IOException e)
{
Log.e(TAG, "Error parsing request", e);
return null;
}
if (firstLine == null) {
Log.i(TAG, "Proxy client closed connection without a request.");
return null;
}
if (firstLine == null)
{
Log.i(TAG, "Proxy client closed connection without a request.");
return null;
}
StringTokenizer st = new StringTokenizer(firstLine);
st.nextToken(); // method
String uri = st.nextToken();
String realUri = uri.substring(1);
Log.i(TAG, realUri);
StringTokenizer st = new StringTokenizer(firstLine);
String method = st.nextToken();
String uri = st.nextToken();
String realUri = uri.substring(1);
Log.i(TAG, realUri);
request = new BasicHttpRequest(method, realUri);
return request;
}
return realUri;
}
public boolean processRequest()
{
HttpRequest request = readRequest();
if (request == null)
{
return false;
}
boolean processRequest() {
final String uri = readRequest();
if (uri == null || uri.isEmpty()) {
return false;
}
// Read HTTP headers
Log.i(TAG, "Processing request");
// Read HTTP headers
Log.i(TAG, "Processing request: " + uri);
try
{
localPath = URLDecoder.decode(request.getRequestLine().getUri(), Constants.UTF_8);
}
catch (UnsupportedEncodingException e)
{
Log.e(TAG, "Unsupported encoding", e);
return false;
}
try {
localPath = URLDecoder.decode(uri, Constants.UTF_8);
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Unsupported encoding", e);
return false;
}
Log.i(TAG, String.format("Processing request for file %s", localPath));
File file = new File(localPath);
if (!file.exists())
{
Log.e(TAG, String.format("File %s does not exist", localPath));
return false;
}
Log.i(TAG, String.format("Processing request for file %s", localPath));
File file = new File(localPath);
if (!file.exists()) {
Log.e(TAG, String.format("File %s does not exist", localPath));
return false;
}
return true;
}
return true;
}
@Override
public void run()

View File

@ -65,8 +65,6 @@ import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver;
import org.moire.ultrasonic.service.DownloadFile;
import org.moire.ultrasonic.service.DownloadService;
import org.moire.ultrasonic.service.DownloadServiceImpl;
import org.apache.http.HttpEntity;
import org.moire.ultrasonic.service.MusicServiceFactory;
import java.io.ByteArrayOutputStream;
@ -405,15 +403,6 @@ public class Util extends DownloadActivity
return context.getSharedPreferences(Constants.PREFERENCES_FILE_NAME, 0);
}
public static String getContentType(HttpEntity entity)
{
if (entity == null || entity.getContentType() == null)
{
return null;
}
return entity.getContentType().getValue();
}
public static int getRemainingTrialDays(Context context)
{
SharedPreferences preferences = getPreferences(context);

View File

@ -8,5 +8,5 @@ import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
fun APIMusicFolder.toDomainEntity(): MusicFolder = MusicFolder(this.id, this.name)
fun List<APIMusicFolder>.toDomainEntityList(): List<MusicFolder>
= this.map { it.toDomainEntity() }
fun List<APIMusicFolder>.toDomainEntityList(): List<MusicFolder> =
this.map { it.toDomainEntity() }

View File

@ -0,0 +1,10 @@
package org.moire.ultrasonic.service
import org.moire.ultrasonic.api.subsonic.SubsonicError
/**
* Exception returned by API with given `code`.
*/
class SubsonicRESTException(val error: SubsonicError) : Exception("Api error: ${error.code}") {
val code: Int get() = error.code
}

View File

@ -0,0 +1,45 @@
@file:JvmName("RestErrorMapper")
package org.moire.ultrasonic.subsonic
import android.content.Context
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicError.Generic
import org.moire.ultrasonic.api.subsonic.SubsonicError.IncompatibleClientProtocolVersion
import org.moire.ultrasonic.api.subsonic.SubsonicError.IncompatibleServerProtocolVersion
import org.moire.ultrasonic.api.subsonic.SubsonicError.RequestedDataWasNotFound
import org.moire.ultrasonic.api.subsonic.SubsonicError.RequiredParamMissing
import org.moire.ultrasonic.api.subsonic.SubsonicError.TokenAuthNotSupportedForLDAP
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
/**
* Extension for [SubsonicRESTException] that returns localized error string, that can used to
* display error reason for user.
*/
fun SubsonicRESTException.getLocalizedErrorMessage(context: Context): String =
when (error) {
is Generic -> {
val message = error.message
val errorMessage = if (message == "") {
context.getString(R.string.api_subsonic_generic_no_message)
} else {
message
}
context.getString(R.string.api_subsonic_generic, errorMessage)
}
RequiredParamMissing -> context.getString(R.string.api_subsonic_param_missing)
IncompatibleClientProtocolVersion -> context
.getString(R.string.api_subsonic_upgrade_client)
IncompatibleServerProtocolVersion -> context
.getString(R.string.api_subsonic_upgrade_server)
WrongUsernameOrPassword -> context.getString(R.string.api_subsonic_not_authenticated)
TokenAuthNotSupportedForLDAP -> context
.getString(R.string.api_subsonic_token_auth_not_supported_for_ldap)
UserNotAuthorizedForOperation -> context
.getString(R.string.api_subsonic_not_authorized)
TrialPeriodIsOver -> context.getString(R.string.api_subsonic_trial_period_is_over)
RequestedDataWasNotFound -> context
.getString(R.string.api_subsonic_requested_data_was_not_found)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 B

After

Width:  |  Height:  |  Size: 926 B

View File

@ -15,7 +15,7 @@
a:layout_width="fill_parent"
a:layout_height="fill_parent"
a:layout_weight="1"
a:gravity="left"
a:gravity="start"
a:orientation="vertical" >
<ImageView

View File

@ -6,6 +6,8 @@
<string name="background_task.no_network">Este programa requiere acceso a la red. Por favor enciende la Wi-Fi o la red móvil.</string>
<string name="background_task.not_found">Recurso no encontrado. Por favor comprueba la dirección del servidor.</string>
<string name="background_task.parse_error">No se entiende la respuesta. Por favor comprueba la dirección del servidor.</string>
<string name="background_task.ssl_cert_error">Error del certificado HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Excepción de conexión SSL. Compruebe el certificado del servidor.</string>
<string name="background_task.wait">Por favor espera&#8230;</string>
<string name="button_bar.bookmarks">Marcadores</string>
<string name="button_bar.browse">Biblioteca</string>
@ -108,12 +110,8 @@
<string name="music_library.label_offline">Medios sin conexión</string>
<string name="music_service.retry">Se ha producido un error de red. Reintento %1$d de %2$d.</string>
<string name="parser.artist_count">Obtenido(s) %d artista(s).</string>
<string name="parser.not_authenticated">Nombre de usuario o contraseña incorrectos.</string>
<string name="parser.not_authorized">No autorizado. Comprueba los permisos de usuario en el servidor de Subsonic.</string>
<string name="parser.reading">Leyendo del servidor.</string>
<string name="parser.reading_done">Leyendo del servidor. ¡Hecho!</string>
<string name="parser.upgrade_client">Versiones incompatibles. Por favor actualiza la aplicación de Android UltraSonic.</string>
<string name="parser.upgrade_server">Versiones incompatibles. Por favor actualiza el servidor de Subsonic.</string>
<string name="playlist.label">Listas de reproducción</string>
<string name="playlist.update_info">Actualizar Información</string>
<string name="playlist.updated_info">Actualizada la información de la lista de reproducción para %s</string>
@ -249,6 +247,8 @@
<string name="settings.preload_3">3 canciónes</string>
<string name="settings.preload_5">5 canciónes</string>
<string name="settings.preload_unlimited">Ilimitado</string>
<string name="settings.playback.resume_play_on_headphones_plug.title">Reanudación de la inserción de auriculares</string>
<string name="settings.playback.resume_play_on_headphones_plug.summary">La aplicación reanudará la reproducción en pausa al insertar los auriculares en el dispositivo.</string>
<string name="settings.screen_lit_summary">Mantener la pantalla encendida mientras descarga mejora la velocidad de la misma.</string>
<string name="settings.screen_lit_title">Mantener la pantalla encendida</string>
<string name="settings.scrobble_summary">Recuerda configurar tu nombre de usuario y contraseña de Last.fm en el servidor de Subsonic</string>
@ -301,6 +301,9 @@
<string name="settings.theme_light">Claro</string>
<string name="settings.theme_title">Tema</string>
<string name="settings.title.allow_self_signed_certificate">Permir certificado HTTPS autofirmado</string>
<string name="settings.title.enable_ldap_users_support">Habilitar soporte para usuarios LDAP</string>
<string name="settings.summary.enable_ldap_users_support">Esto obliga a la aplicación a enviar siempre la contraseña en modo antiguo,
porque Subsonic api no soporta nueva autorización para usuarios LDAP.</string>
<string name="settings.use_folder_for_album_artist">Usar carpetas para el nombre del artista</string>
<string name="settings.use_folder_for_album_artist_summary">Se asume que la carpeta en el nivel mal alto es el nombre del artista del álbum</string>
<string name="settings.use_id3">Navegar usando las etiquetas ID3</string>
@ -424,4 +427,16 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">No hay canales de Podcasts registrados</string>
<!-- Subsonic api errors -->
<string name="api.subsonic.generic">Error genérico de api: %1$s</string>
<string name="api.subsonic.generic.no.message">ningún mensaje dado desde el servidor</string>
<string name="api.subsonic.token_auth_not_supported_for_ldap">La autenticación por token no es compatible con usuarios LDAP.</string>
<string name="api.subsonic.not_authenticated">Nombre de usuario o contraseña incorrectos.</string>
<string name="api.subsonic.not_authorized">No autorizado. Comprueba los permisos de usuario en el servidor de Subsonic.</string>
<string name="api.subsonic.param_missing">Falta el parámetro requerido.</string>
<string name="api.subsonic.requested_data_was_not_found">No se encontraron los datos solicitados.</string>
<string name="api.subsonic.trial_period_is_over">El período de prueba ha terminado.</string>
<string name="api.subsonic.upgrade_client">Versiones incompatibles. Por favor actualiza la aplicación de Android UltraSonic.</string>
<string name="api.subsonic.upgrade_server">Versiones incompatibles. Por favor actualiza el servidor de Subsonic.</string>
</resources>

View File

@ -6,6 +6,8 @@
<string name="background_task.no_network">Cette application requiert un accès au réseau. Veuillez activer le Wi-Fi ou le réseau mobile.</string>
<string name="background_task.not_found">Ressources introuvables. Veuillez vérifier l\'adresse du serveur.</string>
<string name="background_task.parse_error">Réponse incorrecte. Veuillez vérifier l\'adresse du serveur.</string>
<string name="background_task.ssl_cert_error">Erreur de certificat HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Exception de connexion SSL. Veuillez vérifier le certificat du serveur.</string>
<string name="background_task.wait">Veuillez patienter&#8230;</string>
<string name="button_bar.bookmarks">Signets</string>
<string name="button_bar.browse">Bibliothèque musicale</string>
@ -108,12 +110,8 @@
<string name="music_library.label_offline">Musique hors-ligne</string>
<string name="music_service.retry">Une erreur de réseau s\'est produite. Essai %1$d de %2$d.</string>
<string name="parser.artist_count">%d artistes récupérés.</string>
<string name="parser.not_authenticated">Mauvais nom d\'usager ou mot de passe.</string>
<string name="parser.not_authorized">Non autorisé. Vérifiez les permissions de l\'utilisateur dans le serveur Subsonic.</string>
<string name="parser.reading">Lecture du serveur.</string>
<string name="parser.reading_done">Lecture du serveur. Terminé!</string>
<string name="parser.upgrade_client">Versions incompatible. Veuillez mette à jour l\'application Android UltraSonic.</string>
<string name="parser.upgrade_server">Versions incompatible. Veuillez mette à jour le serveur Subsonic.</string>
<string name="playlist.label">Playlists</string>
<string name="playlist.update_info">Mise à jour des informations</string>
<string name="playlist.updated_info">Informations de la playlist %s mises à jour</string>
@ -249,6 +247,8 @@
<string name="settings.preload_3">3 morceaux</string>
<string name="settings.preload_5">5 morceaux</string>
<string name="settings.preload_unlimited">Illimité</string>
<string name="settings.playback.resume_play_on_headphones_plug.title">Reprise de l\'insertion des écouteurs</string>
<string name="settings.playback.resume_play_on_headphones_plug.summary">L\'application reprendra la lecture en pause lors de l\'insertion du casque dans l\'appareil.</string>
<string name="settings.screen_lit_summary">Garder l\'écran allumé pendant le téléchargement permet d\'améliorer la vitesse de téléchargement.</string>
<string name="settings.screen_lit_title">Garder écran allumé</string>
<string name="settings.scrobble_summary">N\'oubliez pas de définir votre nom d\'utilisateur et mot de passe Last.fm sur le serveur Subsonic</string>
@ -301,6 +301,9 @@
<string name="settings.theme_light">Clair</string>
<string name="settings.theme_title">Thème</string>
<string name="settings.title.allow_self_signed_certificate">Autoriser le certificat HTTPS auto-signé</string>
<string name="settings.title.enable_ldap_users_support">Activer la prise en charge des utilisateurs LDAP</string>
<string name="settings.summary.enable_ldap_users_support">Cela force l\'application à toujours envoyer le mot de passe à l\'ancienne,
parce que Subsonic api ne supporte pas les nouvelles autorisations pour les utilisateurs LDAP.</string>
<string name="settings.use_folder_for_album_artist">Utilisez des dossiers pour les noms d\'artistes</string>
<string name="settings.use_folder_for_album_artist_summary">Dossier de niveau supérieur devient le nom de l\'artiste de l\'album</string>
<string name="settings.use_id3">Naviguer en utilisant ID3 Tags</string>
@ -326,7 +329,7 @@
<string name="util.bytes_format.gigabyte">0.00 Go</string>
<string name="util.bytes_format.kilobyte">0 Ko</string>
<string name="util.bytes_format.megabyte">0.00 Mo</string>
<string name="util.no_time">-:--</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é. Recevez gratuitement sur Play Store, ou modifier les paramètres vidéo.</string>
<string name="video.get_mx_player_button">Obtenez MX Player</string>
@ -424,4 +427,16 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">No podcasts channels registered</string>
<!-- Subsonic api errors -->
<string name="api.subsonic.generic">Erreur api générique: %1$s</string>
<string name="api.subsonic.generic.no.message">aucun message donné par le serveur</string>
<string name="api.subsonic.token_auth_not_supported_for_ldap">L\'authentification par jeton n\'est pas prise en charge pour les utilisateurs LDAP.</string>
<string name="api.subsonic.not_authenticated">Mauvais nom d\'usager ou mot de passe.</string>
<string name="api.subsonic.not_authorized">Non autorisé. Vérifiez les permissions de l\'utilisateur dans le serveur Subsonic.</string>
<string name="api.subsonic.param_missing">Param nécessaire manquant.</string>
<string name="api.subsonic.requested_data_was_not_found">Les données demandées n\'ont pas été trouvées.</string>
<string name="api.subsonic.trial_period_is_over">La période d\'essai est terminée.</string>
<string name="api.subsonic.upgrade_client">Versions incompatible. Veuillez mette à jour l\'application Android UltraSonic.</string>
<string name="api.subsonic.upgrade_server">Versions incompatible. Veuillez mette à jour le serveur Subsonic.</string>
</resources>

View File

@ -6,6 +6,8 @@
<string name="background_task.no_network">Az alkalmazás hálózati hozzáférést igényel. Kérjük, kapcsolja be a Wi-Fi-t vagy a mobilhálózatot!</string>
<string name="background_task.not_found">Az erőforrás nem található! Kérjük, ellenőrizze a kiszolgáló címét!</string>
<string name="background_task.parse_error">Értelmezhetetlen válasz! Kérjük, ellenőrizze a kiszolgáló címét!</string>
<string name="background_task.ssl_cert_error">HTTPS tanúsítványhiba: %1$s.</string>
<string name="background_task.ssl_error">SSL kapcsolat kivétel. Kérjük, ellenőrizze a szerver tanúsítványát.</string>
<string name="background_task.wait">Kérem várjon!&#8230;</string>
<string name="button_bar.bookmarks">Könyvjelzők</string>
<string name="button_bar.browse">Médiakönyvtár</string>
@ -108,12 +110,8 @@
<string name="music_library.label_offline">Kapcsolat nélküli médiák</string>
<string name="music_service.retry">Hálózati hiba történt! Újrapróbálkozás %1$d - %2$d.</string>
<string name="parser.artist_count">%d előadó található a médiakönyvtárban.</string>
<string name="parser.not_authenticated">Hibás felhasználónév vagy jelszó!</string>
<string name="parser.not_authorized">Nem engedélyezett! Ellenőrizze a felhasználó jogosultságait a Subsonic kiszolgálón!</string>
<string name="parser.reading">Olvasás a kiszolgálóról&#8230;</string>
<string name="parser.reading_done">Olvasás a kiszolgálóról&#8230; Kész!</string>
<string name="parser.upgrade_client">Nem kompatibilis verzió. Kérjük, frissítse az UltraSonic Android alkalmazást!</string>
<string name="parser.upgrade_server">Nem kompatibilis verzió. Kérjük, frissítse a Subsonic kiszolgálót!</string>
<string name="playlist.label">Lejátszási listák</string>
<string name="playlist.update_info">Módosítás</string>
<string name="playlist.updated_info">Módosított lejátszási lista %s</string>
@ -249,6 +247,8 @@
<string name="settings.preload_3">3 dal</string>
<string name="settings.preload_5">5 dal</string>
<string name="settings.preload_unlimited">Korlátlan</string>
<string name="settings.playback.resume_play_on_headphones_plug.title">Folytatás a fejhallgató behelyezésekor</string>
<string name="settings.playback.resume_play_on_headphones_plug.summary">Az alkalmazás folytatja a szüneteltetett lejátszást a fejhallgató behelyezésekor a készülékbe.</string>
<string name="settings.screen_lit_summary">Képernyő ébrentartása a letöltés alatt, a magasabb letöltési sebesség érdekében.</string>
<string name="settings.screen_lit_title">Képernyő ébrentartása</string>
<string name="settings.scrobble_summary">A Last.fm felhasználónevet és jelszót be kell állítani a Subsonic kiszolgálón!</string>
@ -301,6 +301,9 @@
<string name="settings.theme_light">Világos</string>
<string name="settings.theme_title">Téma</string>
<string name="settings.title.allow_self_signed_certificate">Engedélyezze az önaláírt HTTPS tanúsítványt</string>
<string name="settings.title.enable_ldap_users_support">Az LDAP-felhasználók támogatásának engedélyezése</string>
<string name="settings.summary.enable_ldap_users_support">Ez arra kényszeríti az alkalmazást, hogy mindig jelszót küldjön régi módon,
mert a Subsonic api nem támogatja az LDAP-felhasználók új engedélyezését.</string>
<string name="settings.use_folder_for_album_artist">Mappanevek használata az előadók neveként</string>
<string name="settings.use_folder_for_album_artist_summary">Feltételezi, hogy a legfelső szintű mappa az előadó neve.</string>
<string name="settings.use_id3">Böngészés ID3 Tag használatával</string>
@ -424,4 +427,16 @@
<string name="podcasts.label">Podcast</string>
<string name="podcasts_channels.empty">No podcasts channels registered</string>
<!-- Subsonic api errors -->
<string name="api.subsonic.generic">Általános api hiba: %1$s</string>
<string name="api.subsonic.generic.no.message">nincs üzenet a szerverről</string>
<string name="api.subsonic.token_auth_not_supported_for_ldap">Az LDAP-felhasználók számára nem támogatott a token-hitelesítés.</string>
<string name="api.subsonic.not_authenticated">Hibás felhasználónév vagy jelszó!</string>
<string name="api.subsonic.not_authorized">Nem engedélyezett! Ellenőrizze a felhasználó jogosultságait a Subsonic kiszolgálón!</string>
<string name="api.subsonic.param_missing">A szükséges param hiányzik.</string>
<string name="api.subsonic.requested_data_was_not_found">A keresett adatokat nem találtuk.</string>
<string name="api.subsonic.trial_period_is_over">A próbaidő vége.</string>
<string name="api.subsonic.upgrade_client">Nem kompatibilis verzió. Kérjük, frissítse az UltraSonic Android alkalmazást!</string>
<string name="api.subsonic.upgrade_server">Nem kompatibilis verzió. Kérjük, frissítse a Subsonic kiszolgálót!</string>
</resources>

View File

@ -6,6 +6,8 @@
<string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string>
<string name="background_task.not_found">Recurso não encontrado. Verifique o endereço do servidor.</string>
<string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string>
<string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string>
<string name="background_task.wait">Por favor aguarde&#8230;</string>
<string name="button_bar.bookmarks">Favoritos</string>
<string name="button_bar.browse">Biblioteca de Mídia</string>
@ -111,12 +113,8 @@
<string name="music_library.label_offline">Mídia Offline</string>
<string name="music_service.retry">Ocorreu um erro de rede. Tentativa %1$d de %2$d.</string>
<string name="parser.artist_count">Obtive %d Artistas.</string>
<string name="parser.not_authenticated">Login ou senha errada.</string>
<string name="parser.not_authorized">Não autorizado. Verifique as permissões do usuário no servidor Subsonic.</string>
<string name="parser.reading">Lendo do servidor.</string>
<string name="parser.reading_done">Lendo do servidor. Pronto!</string>
<string name="parser.upgrade_client">Versões incompativeis. Atualize o aplicativo UltraSonic para Android.</string>
<string name="parser.upgrade_server">Versões incompativeis. Atualize o servidor UltraSonic.</string>
<string name="playlist.label">Playlists</string>
<string name="playlist.update_info">Atualizar Informação</string>
<string name="playlist.updated_info">Informação da playlist atualizada para %s</string>
@ -252,6 +250,8 @@
<string name="settings.preload_3">3 músicas</string>
<string name="settings.preload_5">5 músicas</string>
<string name="settings.preload_unlimited">Ilimitado</string>
<string name="settings.playback.resume_play_on_headphones_plug.title">Currículo na inserção de fone de ouvido</string>
<string name="settings.playback.resume_play_on_headphones_plug.summary">O aplicativo retomará a reprodução em pausa na inserção dos fones de ouvido no dispositivo.</string>
<string name="settings.screen_lit_summary">Manter a tela ligada enquanto baixando aumenta a velocidade de download.</string>
<string name="settings.screen_lit_title">Manter a Tela Ligada</string>
<string name="settings.scrobble_summary">Lembre-se de definir seu usuário e senha do Last.fm no servidor Subsonic</string>
@ -304,6 +304,9 @@
<string name="settings.theme_light">Claro</string>
<string name="settings.theme_title">Tema</string>
<string name="settings.title.allow_self_signed_certificate">Permitir o certificado HTTPS auto-assinado</string>
<string name="settings.title.enable_ldap_users_support">Ative o suporte para usuários LDAP</string>
<string name="settings.summary.enable_ldap_users_support">Isso força o aplicativo a enviar sempre a senha de forma antiga,
porque o Subsonic api não suporta nova autorização para usuários LDAP.</string>
<string name="settings.use_folder_for_album_artist">Pasta para Nome do Artista</string>
<string name="settings.use_folder_for_album_artist_summary">Assume que a pasta mais acima é o nome do artista</string>
<string name="settings.use_id3">Navegar Usando Etiquetas ID3</string>
@ -424,4 +427,16 @@
<item quantity="other">Restam %d dias para o fim do período de teste</item>
</plurals>
<!-- Subsonic api errors -->
<string name="api.subsonic.generic">Erro de api genérico: %1$s</string>
<string name="api.subsonic.generic.no.message">nenhuma mensagem fornecida pelo servidor</string>
<string name="api.subsonic.token_auth_not_supported_for_ldap">A autenticação por token não é suportada para usuários LDAP.</string>
<string name="api.subsonic.not_authenticated">Login ou senha errada.</string>
<string name="api.subsonic.not_authorized">Não autorizado. Verifique as permissões do usuário no servidor Subsonic.</string>
<string name="api.subsonic.param_missing">O parâmetro requerido está faltando.</string>
<string name="api.subsonic.requested_data_was_not_found">Os dados solicitados não foram encontrados.</string>
<string name="api.subsonic.trial_period_is_over">O período de avaliação acabou.</string>
<string name="api.subsonic.upgrade_client">Versões incompativeis. Atualize o aplicativo UltraSonic para Android.</string>
<string name="api.subsonic.upgrade_server">Versões incompativeis. Atualize o servidor UltraSonic.</string>
</resources>

View File

@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="background_task.loading">Carregando&#8230;</string>
<string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string>
<string name="background_task.no_network">Este aplicativo requer acesso à rede. Ligue o Wi-Fi ou a rede de dados.</string>
<string name="background_task.not_found">Recurso não encontrado. Verifique o endereço do servidor.</string>
<string name="background_task.parse_error">Não entendi a resposta. Verifique o endereço do servidor.</string>
<string name="background_task.ssl_cert_error">Erro de certificado HTTPS: %1$s.</string>
<string name="background_task.ssl_error">Exceção de conexão SSL. Verifique o certificado do servidor.</string>
<string name="background_task.wait">Por favor aguarde&#8230;</string>
<string name="button_bar.bookmarks">Favoritos</string>
<string name="button_bar.browse">Biblioteca de Mídia</string>
@ -111,12 +113,8 @@
<string name="music_library.label_offline">Mídia Offline</string>
<string name="music_service.retry">Ocorreu um erro de rede. Tentativa %1$d de %2$d.</string>
<string name="parser.artist_count">Obtive %d Artistas.</string>
<string name="parser.not_authenticated">Login ou senha errada.</string>
<string name="parser.not_authorized">Não autorizado. Verifique as permissões do usuário no servidor Subsonic.</string>
<string name="parser.reading">Lendo do servidor.</string>
<string name="parser.reading_done">Lendo do servidor. Pronto!</string>
<string name="parser.upgrade_client">Versões incompativeis. Atualize o aplicativo UltraSonic para Android.</string>
<string name="parser.upgrade_server">Versões incompativeis. Atualize o servidor UltraSonic.</string>
<string name="playlist.label">Playlists</string>
<string name="playlist.update_info">Atualizar Informação</string>
<string name="playlist.updated_info">Informação da playlist atualizada para %s</string>
@ -252,6 +250,8 @@
<string name="settings.preload_3">3 músicas</string>
<string name="settings.preload_5">5 músicas</string>
<string name="settings.preload_unlimited">Ilimitado</string>
<string name="settings.playback.resume_play_on_headphones_plug.title">Currículo na inserção de fone de ouvido</string>
<string name="settings.playback.resume_play_on_headphones_plug.summary">O aplicativo retomará a reprodução em pausa na inserção dos fones de ouvido no dispositivo.</string>
<string name="settings.screen_lit_summary">Manter o ecrã ligado enquanto descarrega aumenta a velocidade de download.</string>
<string name="settings.screen_lit_title">Manter o Ecrã Ligado</string>
<string name="settings.scrobble_summary">Lembre-se de definir seu usuário e senha do Last.fm no servidor Subsonic</string>
@ -304,6 +304,9 @@
<string name="settings.theme_light">Claro</string>
<string name="settings.theme_title">Tema</string>
<string name="settings.title.allow_self_signed_certificate">Permitir o certificado HTTPS auto-assinado</string>
<string name="settings.title.enable_ldap_users_support">Ative o suporte para usuários LDAP</string>
<string name="settings.summary.enable_ldap_users_support">Isso força o aplicativo a enviar sempre a senha de forma antiga,
porque o Subsonic api não suporta nova autorização para usuários LDAP.</string>
<string name="settings.use_folder_for_album_artist">Pasta para Nome do Artista</string>
<string name="settings.use_folder_for_album_artist_summary">Assume que a pasta mais acima é o nome do artista</string>
<string name="settings.use_id3">Navegar Usando Etiquetas ID3</string>
@ -329,7 +332,7 @@
<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">&#8212;:&#8212;&#8212;</string>
<string name="util.zero_time">0:00</string>
<string name="video.get_mx_player_text">O player MX 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 Player MX</string>
@ -394,34 +397,46 @@
<string name="albumArt">albumArt</string>
<string name="common_multiple_years">Múltiplos Anos</string>
<plurals name="select_album_n_songs">
<plurals name="select_album_n_songs" tools:ignore="UnusedQuantity">
<item quantity="zero">Nenhuma música</item>
<item quantity="one">1 música</item>
<item quantity="one">%d música</item>
<item quantity="other">%d músicas</item>
</plurals>
<plurals name="select_album_n_songs_pinned">
<item quantity="one">1 música selecionada para ser fixada.</item>
<item quantity="one">%d música selecionada para ser fixada.</item>
<item quantity="other">%d músicas selecionadas para serem fixadas.</item>
</plurals>
<plurals name="select_album_n_songs_downloaded">
<item quantity="one">1 música selecionada para descarregar.</item>
<item quantity="one">%d música selecionada para descarregar.</item>
<item quantity="other">%d músicas selecionadas para serem descarregadas.</item>
</plurals>
<plurals name="select_album_n_songs_unpinned">
<item quantity="one">1 música selecionada para ser desafixada.</item>
<item quantity="one">%d música selecionada para ser desafixada.</item>
<item quantity="other">%d músicas selecionadas para serem desfixadas.</item>
</plurals>
<plurals name="select_album_n_songs_added">
<item quantity="one">1 música adicionada ao fim da fila.</item>
<item quantity="one">%d música adicionada ao fim da fila.</item>
<item quantity="other">%d músicas adicionadas ao fim da fila.</item>
</plurals>
<plurals name="select_album_n_songs_play_next">
<item quantity="one">1 música inserida após a atual.</item>
<item quantity="one">%d música inserida após a atual.</item>
<item quantity="other">%d músicas inseridas após a atual.</item>
</plurals>
<plurals name="select_album_donate_dialog_n_trial_days_left">
<item quantity="one">Resta 1 dia para o fim do período de teste</item>
<item quantity="one">Resta %d dia para o fim do período de teste</item>
<item quantity="other">Restam %d dias para o fim do período de teste</item>
</plurals>
<!-- Subsonic api errors -->
<string name="api.subsonic.generic">Erro de api genérico: %1$s</string>
<string name="api.subsonic.generic.no.message">nenhuma mensagem fornecida pelo servidor</string>
<string name="api.subsonic.token_auth_not_supported_for_ldap">A autenticação por token não é suportada para usuários LDAP.</string>
<string name="api.subsonic.not_authenticated">Login ou senha errada.</string>
<string name="api.subsonic.not_authorized">Não autorizado. Verifique as permissões do usuário no servidor Subsonic.</string>
<string name="api.subsonic.param_missing">O parâmetro requerido está faltando.</string>
<string name="api.subsonic.requested_data_was_not_found">Os dados solicitados não foram encontrados.</string>
<string name="api.subsonic.trial_period_is_over">O período de avaliação acabou.</string>
<string name="api.subsonic.upgrade_client">Versões incompativeis. Atualize o aplicativo UltraSonic para Android.</string>
<string name="api.subsonic.upgrade_server">Versões incompativeis. Atualize o servidor UltraSonic.</string>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="settings.playback.resume_play_on_headphones_plug" translatable="false">playback.resume_play_on_headphones_plug</string>
</resources>

View File

@ -6,6 +6,8 @@
<string name="background_task.no_network">This program requires network access. Please turn on Wi-Fi or mobile network.</string>
<string name="background_task.not_found">Resource not found. Please check the server address.</string>
<string name="background_task.parse_error">Didn\'t understand the reply. Please check the server address.</string>
<string name="background_task.ssl_cert_error">HTTPS certificate error: %1$s.</string>
<string name="background_task.ssl_error">SSL connection exception. Please check server certificate.</string>
<string name="background_task.wait">Please wait&#8230;</string>
<string name="button_bar.bookmarks">Bookmarks</string>
<string name="button_bar.browse">Media Library</string>
@ -111,12 +113,8 @@
<string name="music_library.label_offline">Offline Media</string>
<string name="music_service.retry">A network error occurred. Retrying %1$d of %2$d.</string>
<string name="parser.artist_count">Got %d Artists.</string>
<string name="parser.not_authenticated">Wrong username or password.</string>
<string name="parser.not_authorized">Not authorized. Check user permissions in Subsonic server.</string>
<string name="parser.reading">Reading from server.</string>
<string name="parser.reading_done">Reading from server. Done!</string>
<string name="parser.upgrade_client">Incompatible versions. Please upgrade UltraSonic Android app.</string>
<string name="parser.upgrade_server">Incompatible versions. Please upgrade Subsonic server.</string>
<string name="playlist.label">Playlists</string>
<string name="playlist.update_info">Update Information</string>
<string name="playlist.updated_info">Updated playlist information for %s</string>
@ -147,6 +145,7 @@
<string name="select_playlist.empty">No saved playlists on server</string>
<string name="service.connecting">Contacting server, please wait.</string>
<string name="settings.allow_self_signed_certificate" translatable="false">allowSelfSignedCertificate</string>
<string name="settings.enable_ldap_user_support" translatable="false">enableLdapUserSupport</string>
<string name="settings.appearance_title">Appearance</string>
<string name="settings.buffer_length">Buffer Length</string>
<string name="settings.buffer_length_0">Disabled</string>
@ -253,6 +252,8 @@
<string name="settings.preload_3">3 songs</string>
<string name="settings.preload_5">5 songs</string>
<string name="settings.preload_unlimited">Unlimited</string>
<string name="settings.playback.resume_play_on_headphones_plug.title">Resume on headphones insertion</string>
<string name="settings.playback.resume_play_on_headphones_plug.summary">App will resume paused playback on headphones insertion into device.</string>
<string name="settings.screen_lit_summary">Keeping the screen on while downloading improves download speed.</string>
<string name="settings.screen_lit_title">Keep Screen On</string>
<string name="settings.scrobble_summary">Remember to set up your Last.fm user and password on the Subsonic server</string>
@ -305,6 +306,9 @@
<string name="settings.theme_light">Light</string>
<string name="settings.theme_title">Theme</string>
<string name="settings.title.allow_self_signed_certificate">Allow self-signed HTTPS certificate</string>
<string name="settings.title.enable_ldap_users_support">Enable support for LDAP users</string>
<string name="settings.summary.enable_ldap_users_support">This forces app to always send password in old-way,
because Subsonic api does not support new authorization for LDAP users.</string>
<string name="settings.use_folder_for_album_artist">Use Folders For Artist Name</string>
<string name="settings.use_folder_for_album_artist_summary">Assume top-level folder is the name of the album artist</string>
<string name="settings.use_id3">Browse Using ID3 Tags</string>
@ -426,4 +430,16 @@
<item quantity="other">%d days left of trial period</item>
</plurals>
<!-- Subsonic api errors -->
<string name="api.subsonic.generic">Generic api error: %1$s</string>
<string name="api.subsonic.generic.no.message">no message given from server</string>
<string name="api.subsonic.token_auth_not_supported_for_ldap">Authentication by token is not supported for LDAP users.</string>
<string name="api.subsonic.not_authenticated">Wrong username or password.</string>
<string name="api.subsonic.not_authorized">Not authorized. Check user permissions in Subsonic server.</string>
<string name="api.subsonic.param_missing">Required param is missing.</string>
<string name="api.subsonic.requested_data_was_not_found">Requested data was not found.</string>
<string name="api.subsonic.trial_period_is_over">Trial period is over.</string>
<string name="api.subsonic.upgrade_client">Incompatible versions. Please upgrade UltraSonic Android app.</string>
<string name="api.subsonic.upgrade_server">Incompatible versions. Please upgrade Subsonic server.</string>
</resources>

View File

@ -44,6 +44,13 @@
android:defaultValue="false"
android:title="@string/settings.title.allow_self_signed_certificate"
/>
<CheckBoxPreference
android:key="@string/settings.enable_ldap_user_support"
android:persistent="false"
android:defaultValue="false"
android:title="@string/settings.title.enable_ldap_users_support"
android:summary="@string/settings.summary.enable_ldap_users_support"
/>
<CheckBoxPreference
android:key="@string/jukebox.is_default"
android:persistent="false"

View File

@ -100,6 +100,12 @@
a:entryValues="@array/incrementTimeValues"
a:key="incrementTime"
a:title="@string/settings.increment_time"/>
<CheckBoxPreference
a:defaultValue="false"
a:key="@string/settings.playback.resume_play_on_headphones_plug"
a:title="@string/settings.playback.resume_play_on_headphones_plug.title"
a:summary="@string/settings.playback.resume_play_on_headphones_plug.summary"
/>
</PreferenceCategory>
<PreferenceCategory a:title="@string/settings.notifications_title">
<CheckBoxPreference

Some files were not shown because too many files have changed in this diff Show More