Merge pull request #89 from ultrasonic/develop

New 2.0.0 release
This commit is contained in:
Óscar García Amor 2017-12-18 14:51:55 +01:00 committed by GitHub
commit 4f44977e55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
257 changed files with 15579 additions and 4099 deletions

41
.circleci/config.yml Normal file
View File

@ -0,0 +1,41 @@
version: 2
jobs:
build:
docker:
- image: circleci/android:api-26-alpha
working_directory: ~/ultrasonic
envoronment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- restore_cache:
key: gradle-cache-{{ checksum "dependencies.gradle" }}
- run:
name: checkstyle
command: ./gradlew -Pqc ktlintCheck
- run:
name: build
command: ./gradlew assembleDebug
- run:
name: unit-tests
command: |
./gradlew :subsonic-api:test :ultrasonic:testDebugUnitTest
./gradlew jacocoFullReport
bash <(curl -s https://codecov.io/bash)
- run:
name: lint
command: ./gradlew lint
- run:
name: static analysis
command: ./gradlew -Pqc detektCheck
- save_cache:
paths:
- ~/.gradle
key: gradle-cache-{{ checksum "dependencies.gradle" }}
- store_artifacts:
path: ultrasonic/build/reports
path: subsonic-api/build/reports
destination: reports
- store_artifacts:
path: build/reports/jacoco/jacocoFullReport/

27
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,27 @@
# Contributing
Ultrasonic development is a community project, and contributions are welcomed.
First, see if your issue havent been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
then, please, first discuss the change you wish to make via [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
By default Pull Request should be opened against **develop** branch, PR against **master** branch should be used only
for critical bugfixes.
### Here are a few guidelines you should follow before submitting:
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
Use `git commit --signoff` to acknowledge this.
2. **App is migrating to [Kotlin](https://kotlinlang.org/) programming language:** new Pull Requests
should be written in this programming language.
3. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
4. **Coding standards** best-practices should be followed, comment generously, and avoid "clever" algorithms.
Refactoring existing messes is great, but watch out for breakage.
5. **No large PR:*** Try to limit the scope of PR only to the related issue, so it will be easier to review
and test.
## Pull Request Process
1. Ensure all commits are signed-off.
2. Check tests for the new code are added.
3. Check code style is passing.
4. Check code static analysis is passing.

View File

@ -1 +1,20 @@
Please, take note that this project is STALLED. See README.md for more info.
## Problem description
Describe your problem here. Describe what you want to happen, and what happens
if you try to do it. If you have a stack trace or any logs, please format them using
github triple backquote notation
### Steps to reproduce
Describe how somebody else could observe the same behavior you do. Don't share here any logins and
passwords!
## System information
* **Ultrasonic version**: *version of the app*
* **Android version**: *Version of Android OS on the device*
* **Device info**: *Device manufacturer, model*
## Additional notes
Include any extra notes here. Otherwise you may remove this section.

View File

@ -1,12 +1,28 @@
# Ultrasonic
Subsonic Music Streamer Android client
[![Build Status](https://circleci.com/gh/ultrasonic/ultrasonic/tree/develop.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/ultrasonic)
[![Codecov branch](https://img.shields.io/codecov/c/github/ultrasonic/ultrasonic/develop.svg)]()
[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)
## About project status
Ultrasonic is free, open-source [Subsonic](http://www.subsonic.org/) [API](http://www.subsonic.org/pages/api.jsp) compatible music streaming Android client.
WARNING: This project is **STALLED**
## Download
This means that I don't have time to fix bugs or add new features, but you can send me **Pull Requests** and I **promise** that I shall merge and upload to Play Store.
App is available to download at following stores:
If you want contribute at project you can see the [issues](https://github.com/ogarcia/ultrasonic/issues) page and try to fix the discovered bugs or implement the enhancements that users report there.
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="70">](https://play.google.com/store/apps/details?id=org.moire.ultrasonic)
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="70">](https://f-droid.org/packages/org.moire.ultrasonic/)
## Bugs and issue
If you want give more active development, please contact with me cause I can give you access to repository to do work faster.
First, see if your issue havent been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
otherwise open [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
## Contributing
See [CONTRIBUTING](CONTRIBUTING.md).
## License
This software is licensed under the terms of the GNU General Public License version 3 (GPLv3).
Full text of the license is available in the [LICENSE](LICENSE) file and [online](https://opensource.org/licenses/gpl-3.0.html).

View File

@ -1,19 +1,39 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: 'dependencies.gradle'
buildscript {
apply from: 'dependencies.gradle'
repositories {
jcenter()
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.jacocoAndroid
}
}
allprojects {
// Buildscript here is required by detekt
buildscript {
repositories {
jcenter()
}
}
repositories {
jcenter()
}
}
apply from: 'gradle_scripts/jacoco.gradle'
task wrapper(type: Wrapper) {
gradleVersion(versions.gradle)
distributionType("all")
}

View File

@ -2,18 +2,61 @@ ext.versions = [
minSdk : 14,
targetSdk : 22,
compileSdk : 22,
gradle : '4.3.1',
buildTools : "25.0.2",
androidTools : "2.2.3",
buildTools : "25.0.3",
androidTools : "2.3.3",
ktlint : "0.12.1",
ktlintGradle : "2.3.0",
detekt : "1.0.0.RC5-4",
jacoco : "0.7.9",
jacocoAndroid : "0.1.2",
androidSupport : "22.2.1",
kotlin : "1.1.60",
retrofit : "2.1.0",
jackson : "2.9.0",
okhttp : "3.9.0",
junit : "4.12",
mockito : "2.12.0",
mockitoKotlin : "1.5.0",
kluent : "1.26",
apacheCodecs : "1.10",
]
ext.gradlePlugins = [
androidTools : "com.android.tools.build:gradle:$versions.androidTools"
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"
]
ext.androidSupport = [
support : "com.android.support:support-v4:$versions.androidSupport",
design : "com.android.support:design:$versions.androidSupport",
]
ext.other = [
kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib:$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",
jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson",
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
]
ext.testing = [
junit : "junit:junit:$versions.junit",
kotlinJunit : "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin",
mockitoKotlin : "com.nhaarman:mockito-kotlin:$versions.mockitoKotlin",
mockito : "org.mockito:mockito-core:$versions.mockito",
mockitoInline : "org.mockito:mockito-inline:$versions.mockito",
kluent : "org.amshove.kluent:kluent:$versions.kluent",
mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp",
apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs",
]

111
detekt-config.yml Normal file
View File

@ -0,0 +1,111 @@
autoCorrect: true
failFast: false
build:
warningThreshold: 0
failThreshold: 0
weights:
complexity: 2
formatting: 1
LongParameterList: 1
comments: 1
potential-bugs:
active: true
DuplicateCaseInWhenExpression:
active: true
EqualsWithHashCodeExist:
active: true
ExplicitGarbageCollectionCall:
active: true
LateinitUsage:
active: false
UnsafeCallOnNullableType:
active: false
UnsafeCast:
active: false
performance:
active: true
ForEachOnRange:
active: true
SpreadOperator:
active: true
exceptions:
active: true
empty-blocks:
active: true
complexity:
active: true
LongMethod:
threshold: 20
LongParameterList:
threshold: 5
LargeClass:
threshold: 150
ComplexMethod:
threshold: 10
TooManyFunctions:
threshold: 20
ComplexCondition:
threshold: 3
LabeledExpression:
active: false
code-smell:
active: true
FeatureEnvy:
threshold: 0.5
weight: 0.45
base: 0.5
formatting:
active: false
style:
active: true
NewLineAtEndOfFile:
active: true
ForbiddenComment:
active: true
values: 'TODO:,FIXME:,STOPSHIP:'
WildcardImport:
active: true
MaxLineLength:
active: true
maxLineLength: 120
excludePackageStatements: false
excludeImportStatements: false
NamingConventionViolation:
active: true
variablePattern: '^(_)?[a-z$][a-zA-Z$0-9]*$'
constantPattern: '^([A-Z_]*|serialVersionUID)$'
methodPattern: '^[a-z\s`$][a-zA-Z\s$0-9`]*$'
classPattern: '[A-Z$][a-zA-Z$]*'
enumEntryPattern: '^[A-Z$][a-zA-Z_$0-9]*$'
comments:
active: true
CommentOverPrivateMethod:
active: true
CommentOverPrivateProperty:
active: true
UndocumentedPublicClass:
active: false
searchInNestedClass: true
searchInInnerClass: true
searchInInnerInterface: true
UndocumentedPublicFunction:
active: false
# *experimental feature*
# Migration rules can be defined in the same config file or a new one
migration:
active: false
imports:
# your.package.Class: new.package.or.Class
# for example:
# io.gitlab.arturbosch.detekt.api.Rule: io.gitlab.arturbosch.detekt.rule.Rule

Binary file not shown.

View File

@ -1,6 +1,5 @@
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-all.zip

View File

@ -0,0 +1,29 @@
// Applies code quality plugins when -Pqc is passed to the gradle
def isCodeQualityEnabled = project.hasProperty('qc')
// KtLint
if (isCodeQualityEnabled) {
apply plugin: "org.jlleitschuh.gradle.ktlint"
ktlint {
version = versions.ktlint
android = true
}
}
// Detekt
if (isCodeQualityEnabled) {
if (!project.rootProject.plugins.hasPlugin("io.gitlab.arturbosch.detekt")) {
Project rootProject = project.rootProject
rootProject.apply {
apply plugin: "io.gitlab.arturbosch.detekt"
detekt {
version = versions.detekt
profile("main") {
config = "${rootProject.projectDir}/detekt-config.yml"
}
}
}
}
}

View File

@ -0,0 +1,57 @@
apply plugin: 'jacoco'
task jacocoMergeReports(type: JacocoMerge) {
group = "Reporting"
description = "Merge all jacoco reports from projects into one."
def subsonicApi = project.findProject("subsonic-api")
def ultrasonicApp = project.findProject("ultrasonic")
executionData(
"${subsonicApi.buildDir}/jacoco/test.exec",
"${ultrasonicApp.buildDir}/jacoco/testDebugUnitTest.exec",
)
destinationFile(file("${project.buildDir}/jacoco/jacoco.exec"))
}
def createJacocoFullReportTask() {
tasks.create(name: 'jacocoFullReport', type: JacocoReport, dependsOn: 'jacocoMergeReports') {
group = "Reporting"
description = "Generate full Jacoco coverage report including all modules."
def subsonicApi = project.findProject("subsonic-api")
def ultrasonicApp = project.findProject("ultrasonic")
classDirectories = files(
fileTree(
dir: "${subsonicApi.buildDir}/classes/main",
excludes: subsonicApi.jacocoExclude
),
fileTree(
dir: "${ultrasonicApp.buildDir}/intermediates/classes/debug/org",
excludes: ultrasonicApp.jacocoExclude
)
)
sourceDirectories = files(subsonicApi.sourceSets.main.getAllSource(),
ultrasonicApp.extensions.getByName('android').sourceSets.main.java.sourceFiles)
executionData = files("${buildDir}/jacoco/jacoco.exec")
reports {
xml.enabled = true
html.enabled = true
csv.enabled = false
}
}
}
// We need to wait to all subprojects configuration finish or we don't get sources and exclusions
def subprojectsCount = allprojects.size()
allprojects {
afterEvaluate { subproject ->
subprojectsCount--
if (subprojectsCount == 0) {
apply {
createJacocoFullReportTask()
}
}
}
}

110
gradlew vendored
View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
##############################################################################
##
@ -6,47 +6,6 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@ -61,9 +20,49 @@ while [ -h "$PRG" ] ; do
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -90,7 +89,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@ -114,6 +113,7 @@ fi
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@ -154,11 +154,19 @@ if $cygwin ; then
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

14
gradlew.bat vendored
View File

@ -8,14 +8,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@ -46,10 +46,9 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@ -60,11 +59,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line

View File

@ -15,6 +15,11 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
baselineFile file("lint-baseline.xml")
abortOnError true
}
}
dependencies {

59
library/lint-baseline.xml Normal file
View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="4" by="lint 2.3.3">
<issue
id="NewApi"
message="Call requires API level 21 (current min is 14): android.widget.AbsListView#setSelectionFromTop"
errorLine1=" setSelectionFromTop(movePos, top - padTop);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/mobeta/android/dslv/DragSortListView.java"
line="2936"
column="13"/>
</issue>
<issue
id="OldTargetApi"
message="Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the `android.os.Build.VERSION_CODES` javadoc for details."
errorLine1=" &lt;uses-sdk android:targetSdkVersion=&quot;7&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="6"
column="15"/>
</issue>
<issue
id="GradleOverrides"
message="This `targetSdkVersion` value (`7`) is not used; it is always overridden by the value specified in the Gradle build script (`22`)"
errorLine1=" &lt;uses-sdk android:targetSdkVersion=&quot;7&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="6"
column="15"/>
</issue>
<issue
id="GradleOverrides"
message="This `minSdkVersion` value (`7`) is not used; it is always overridden by the value specified in the Gradle build script (`14`)"
errorLine1=" android:minSdkVersion=&quot;7&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="7"/>
</issue>
<issue
id="ClickableViewAccessibility"
message="`com/mobeta/android/dslv/DragSortController#onTouch` should call `View#performClick` when a click is detected"
errorLine1=" public boolean onTouch(View v, MotionEvent ev) {"
errorLine2=" ~~~~~~~">
<location
file="src/main/java/com/mobeta/android/dslv/DragSortController.java"
line="238"
column="20"/>
</issue>
</issues>

View File

@ -15,4 +15,9 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
baselineFile file("lint-baseline.xml")
abortOnError true
}
}

View File

@ -0,0 +1,246 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="4" by="lint 2.3.3">
<issue
id="InlinedApi"
message="Field requires API level 17 (current min is 14): `android.view.View#LAYOUT_DIRECTION_RTL`"
errorLine1=" if (mSlideDrawable != null) mSlideDrawable.setIsRtl(layoutDirection == LAYOUT_DIRECTION_RTL);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/MenuDrawer.java"
line="882"
column="80"/>
</issue>
<issue
id="InlinedApi"
message="Field requires API level 17 (current min is 14): `android.view.View#LAYOUT_DIRECTION_RTL`"
errorLine1=" mSlideDrawable.setIsRtl(ViewHelper.getLayoutDirection(this) == LAYOUT_DIRECTION_RTL);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/MenuDrawer.java"
line="1325"
column="72"/>
</issue>
<issue
id="OldTargetApi"
message="Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the `android.os.Build.VERSION_CODES` javadoc for details."
errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;7&quot; android:targetSdkVersion=&quot;16&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="41"/>
</issue>
<issue
id="GradleOverrides"
message="This `minSdkVersion` value (`7`) is not used; it is always overridden by the value specified in the Gradle build script (`14`)"
errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;7&quot; android:targetSdkVersion=&quot;16&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="15"/>
</issue>
<issue
id="GradleOverrides"
message="This `targetSdkVersion` value (`16`) is not used; it is always overridden by the value specified in the Gradle build script (`22`)"
errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;7&quot; android:targetSdkVersion=&quot;16&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="41"/>
</issue>
<issue
id="ParcelClassLoader"
message="Using the default class loader will not work if you are restoring your own classes. Consider using for example `readBundle(getClass().getClassLoader())` instead."
errorLine1=" mState = in.readBundle();"
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/MenuDrawer.java"
line="1630"
column="25"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="41"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="43"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="51"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="53"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="59"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="61"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="67"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="69"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is never &lt; 14"
errorLine1=" if (mUsesCompat &amp;&amp; Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.ICE_CREAM_SANDWICH) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="77"
column="28"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/compat/ActionBarHelper.java"
line="79"
column="20"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/DraggableDrawer.java"
line="572"
column="13"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/DraggableDrawer.java"
line="580"
column="13"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/DraggableDrawer.java"
line="588"
column="13"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/DraggableDrawer.java"
line="596"
column="13"/>
</issue>
<issue
id="FloatMath"
message="Use `java.lang.Math#sqrt` instead of `android.util.FloatMath#sqrt()` since it is faster as of API 8"
errorLine1=" float hyp = FloatMath.sqrt(dx * dx + dy * dy);"
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/Scroller.java"
line="374"
column="25"/>
</issue>
<issue
id="FloatMath"
message="Use `java.lang.Math#sqrt` instead of `android.util.FloatMath#sqrt()` since it is faster as of API 8"
errorLine1=" float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);"
errorLine2=" ~~~~~~~~~~~~~~">
<location
file="src/main/java/net/simonvt/menudrawer/Scroller.java"
line="391"
column="26"/>
</issue>
</issues>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="md__drawerOpenIndicatorDesc">Close drawer</string>
<string name="md__drawerOpenIndicatorDesc" tools:ignore="MissingTranslation">Close drawer</string>
<string name="md__drawerClosedIndicatorDesc">Open drawer</string>
<string name="md__drawerClosedIndicatorDesc" tools:ignore="MissingTranslation">Open drawer</string>
</resources>

View File

@ -15,4 +15,9 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
baselineFile file("lint-baseline.xml")
abortOnError true
}
}

View File

@ -0,0 +1,326 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="4" by="lint 2.3.3">
<issue
id="LocaleFolder"
message="The locale folder &quot;`he`&quot; should be called &quot;`iw`&quot; instead; see the `java.util.Locale` documentation">
<location
file="src/main/res/values-he"/>
</issue>
<issue
id="OldTargetApi"
message="Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the `android.os.Build.VERSION_CODES` javadoc for details."
errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;4&quot; android:targetSdkVersion=&quot;16&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="41"/>
</issue>
<issue
id="GradleOverrides"
message="This `minSdkVersion` value (`4`) is not used; it is always overridden by the value specified in the Gradle build script (`14`)"
errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;4&quot; android:targetSdkVersion=&quot;16&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="15"/>
</issue>
<issue
id="GradleOverrides"
message="This `targetSdkVersion` value (`16`) is not used; it is always overridden by the value specified in the Gradle build script (`22`)"
errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;4&quot; android:targetSdkVersion=&quot;16&quot; />"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/AndroidManifest.xml"
line="7"
column="41"/>
</issue>
<issue
id="Deprecated"
message="`android:singleLine` is deprecated: Use `maxLines=&quot;1&quot;` instead"
errorLine1=" android:singleLine=&quot;true&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/pull_to_refresh_header_vertical.xml"
line="45"
column="17"/>
</issue>
<issue
id="Deprecated"
message="`android:singleLine` is deprecated: Use `maxLines=&quot;1&quot;` instead"
errorLine1=" android:singleLine=&quot;true&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/pull_to_refresh_header_vertical.xml"
line="53"
column="17"/>
</issue>
<issue
id="MissingTranslation"
message="&quot;`pull_to_refresh_from_bottom_pull_label`&quot; is not translated in &quot;es&quot; (Spanish), &quot;fr&quot; (French), &quot;pt&quot; (Portuguese), &quot;pt-BR&quot; (Portuguese: Brazil)"
errorLine1=" &lt;string name=&quot;pull_to_refresh_from_bottom_pull_label&quot;>@string/pull_to_refresh_pull_label&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/pull_refresh_strings.xml"
line="9"
column="13"/>
</issue>
<issue
id="MissingTranslation"
message="&quot;`pull_to_refresh_from_bottom_release_label`&quot; is not translated in &quot;es&quot; (Spanish), &quot;fr&quot; (French), &quot;pt&quot; (Portuguese), &quot;pt-BR&quot; (Portuguese: Brazil)"
errorLine1=" &lt;string name=&quot;pull_to_refresh_from_bottom_release_label&quot;>@string/pull_to_refresh_release_label&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/pull_refresh_strings.xml"
line="10"
column="13"/>
</issue>
<issue
id="MissingTranslation"
message="&quot;`pull_to_refresh_from_bottom_refreshing_label`&quot; is not translated in &quot;es&quot; (Spanish), &quot;fr&quot; (French), &quot;pt&quot; (Portuguese), &quot;pt-BR&quot; (Portuguese: Brazil)"
errorLine1=" &lt;string name=&quot;pull_to_refresh_from_bottom_refreshing_label&quot;>@string/pull_to_refresh_refreshing_label&lt;/string>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/pull_refresh_strings.xml"
line="11"
column="13"/>
</issue>
<issue
id="AddJavascriptInterface"
message="`WebView.addJavascriptInterface` should not be called with minSdkVersion &lt; 17 for security reasons: JavaScript can use reflection to manipulate application"
errorLine1=" webView.addJavascriptInterface(mJsCallback, JS_INTERFACE_PKG);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/extras/PullToRefreshWebView2.java"
line="90"
column="11"/>
</issue>
<issue
id="JavascriptInterface"
message="None of the methods in the added interface (JsValueCallback) have been annotated with `@android.webkit.JavascriptInterface`; they will not be visible in API 17"
errorLine1=" webView.addJavascriptInterface(mJsCallback, JS_INTERFACE_PKG);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/extras/PullToRefreshWebView2.java"
line="90"
column="11"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" return VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD &amp;&amp; mOverScrollEnabled"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshBase.java"
line="211"
column="10"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshExpandableListView.java"
line="54"
column="7"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshGridView.java"
line="54"
column="7"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshHorizontalScrollView.java"
line="53"
column="7"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshListView.java"
line="207"
column="7"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshScrollView.java"
line="52"
column="7"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshWebView.java"
line="98"
column="7"/>
</issue>
<issue
id="ObsoleteSdkInt"
message="Unnecessary; SDK_INT is always >= 14"
errorLine1=" if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/internal/ViewCompat.java"
line="44"
column="7"/>
</issue>
<issue
id="FloatMath"
message="Use `java.lang.Math#floor` instead of `android.util.FloatMath#floor()` since it is faster as of API 8"
errorLine1=" float exactContentHeight = FloatMath.floor(mRefreshableView.getContentHeight() * mRefreshableView.getScale());"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshWebView.java"
line="115"
column="30"/>
</issue>
<issue
id="FloatMath"
message="Use `java.lang.Math#floor` instead of `android.util.FloatMath#floor()` since it is faster as of API 8"
errorLine1=" return (int) Math.max(0, FloatMath.floor(mRefreshableView.getContentHeight() * mRefreshableView.getScale())"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshWebView.java"
line="161"
column="29"/>
</issue>
<issue
id="IconMissingDensityFolder"
message="Missing density variation folders in `src/main/res`: drawable-xxhdpi">
<location
file="src/main/res"/>
</issue>
<issue
id="ViewConstructor"
message="Custom view `RotateLoadingLayout` is missing constructor used by tools: `(Context)` or `(Context,AttributeSet)` or `(Context,AttributeSet,int)`"
errorLine1="public class RotateLoadingLayout extends LoadingLayout {"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/internal/RotateLoadingLayout.java"
line="30"
column="14"/>
</issue>
<issue
id="ContentDescription"
message="[Accessibility] Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView"
errorLine2=" ^">
<location
file="src/main/res/layout/pull_to_refresh_header_horizontal.xml"
line="13"
column="9"/>
</issue>
<issue
id="ContentDescription"
message="[Accessibility] Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView"
errorLine2=" ^">
<location
file="src/main/res/layout/pull_to_refresh_header_vertical.xml"
line="18"
column="13"/>
</issue>
<issue
id="RtlHardcoded"
message="Use &quot;`Gravity.START`&quot; instead of &quot;`Gravity.LEFT`&quot; to ensure correct behavior in right-to-left locales"
errorLine1=" lp.gravity = scrollDirection == Orientation.VERTICAL ? Gravity.TOP : Gravity.LEFT;"
errorLine2=" ~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/internal/LoadingLayout.java"
line="92"
column="82"/>
</issue>
<issue
id="RtlHardcoded"
message="Use &quot;`Gravity.END`&quot; instead of &quot;`Gravity.RIGHT`&quot; to ensure correct behavior in right-to-left locales"
errorLine1=" lp.gravity = scrollDirection == Orientation.VERTICAL ? Gravity.BOTTOM : Gravity.RIGHT;"
errorLine2=" ~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/internal/LoadingLayout.java"
line="102"
column="85"/>
</issue>
<issue
id="RtlHardcoded"
message="Use &quot;`Gravity.END`&quot; instead of &quot;`Gravity.RIGHT`&quot; to ensure correct behavior in right-to-left locales"
errorLine1=" params.gravity = Gravity.TOP | Gravity.RIGHT;"
errorLine2=" ~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshAdapterViewBase.java"
line="344"
column="43"/>
</issue>
<issue
id="RtlHardcoded"
message="Use &quot;`Gravity.END`&quot; instead of &quot;`Gravity.RIGHT`&quot; to ensure correct behavior in right-to-left locales"
errorLine1=" params.gravity = Gravity.BOTTOM | Gravity.RIGHT;"
errorLine2=" ~~~~~">
<location
file="src/main/java/com/handmark/pulltorefresh/library/PullToRefreshAdapterViewBase.java"
line="359"
column="46"/>
</issue>
<issue
id="RtlHardcoded"
message="Use &quot;`start`&quot; instead of &quot;`left`&quot; to ensure correct behavior in right-to-left locales"
errorLine1=" android:layout_gravity=&quot;left|center_vertical&quot; >"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/pull_to_refresh_header_vertical.xml"
line="16"
column="37"/>
</issue>
</issues>

View File

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="pull_to_refresh_pull_label">Pull to refresh…</string>
<string name="pull_to_refresh_release_label">Release to refresh…</string>
<string name="pull_to_refresh_refreshing_label">Loading…</string>
<string name="pull_to_refresh_pull_label" tools:ignore="MissingTranslation">Pull to refresh…</string>
<string name="pull_to_refresh_release_label" tools:ignore="MissingTranslation">Release to refresh…</string>
<string name="pull_to_refresh_refreshing_label" tools:ignore="MissingTranslation">Loading…</string>
<!-- Just use standard Pull Down String when pulling up. These can be set for languages which require it -->
<string name="pull_to_refresh_from_bottom_pull_label">@string/pull_to_refresh_pull_label</string>
<string name="pull_to_refresh_from_bottom_release_label">@string/pull_to_refresh_release_label</string>
<string name="pull_to_refresh_from_bottom_refreshing_label">@string/pull_to_refresh_refreshing_label</string>
<string name="pull_to_refresh_from_bottom_pull_label" tools:ignore="MissingTranslation">@string/pull_to_refresh_pull_label</string>
<string name="pull_to_refresh_from_bottom_release_label" tools:ignore="MissingTranslation">@string/pull_to_refresh_release_label</string>
<string name="pull_to_refresh_from_bottom_refreshing_label" tools:ignore="MissingTranslation">@string/pull_to_refresh_refreshing_label</string>
</resources>

View File

@ -1,4 +1,4 @@
include ':library'
include ':library', ':subsonic-api'
include ':menudrawer'
include ':pulltorefresh'
include ':ultrasonic'

2
subsonic-api/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/build
*.iml

62
subsonic-api/build.gradle Normal file
View File

@ -0,0 +1,62 @@
apply plugin: 'kotlin'
apply plugin: 'java-library'
apply plugin: 'jacoco'
apply from: '../gradle_scripts/code_quality.gradle'
sourceSets {
main.java.srcDirs += "${projectDir}/src/main/kotlin"
test.java.srcDirs += "${projectDir}/src/integrationTest/kotlin"
test.resources.srcDirs += "${projectDir}/src/integrationTest/resources"
}
dependencies {
api other.kotlinStdlib
api other.retrofit
implementation other.jacksonConverter
implementation(other.jacksonKotlin) {
exclude module: 'kotlin-reflect'
}
implementation other.kotlinReflect // for jackson kotlin, but to use the same version
implementation other.okhttpLogging
testImplementation testing.junit
testImplementation testing.kotlinJunit
testImplementation testing.mockito
testImplementation testing.mockitoInline
testImplementation testing.mockitoKotlin
testImplementation testing.kluent
testImplementation testing.mockWebServer
testImplementation testing.apacheCodecs
}
jacoco {
toolVersion(versions.jacoco)
}
ext {
// Excluding data classes
jacocoExclude = [
'**/models/**'
]
}
jacocoTestReport {
reports {
html.enabled true
csv.enabled false
xml.enabled true
}
afterEvaluate {
classDirectories = files(classDirectories.files.collect {
fileTree(dir: it, excludes: jacocoExclude)
})
}
}
test.finalizedBy jacocoTestReport
test {
jacoco {
excludes += jacocoExclude
}
}

View File

@ -0,0 +1,78 @@
package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse
import okio.Okio
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should not be`
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
import retrofit2.Response
import java.nio.charset.Charset
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
const val USERNAME = "some-user"
const val PASSWORD = "some-password"
val CLIENT_VERSION = SubsonicAPIVersions.V1_16_0
const val CLIENT_ID = "test-client"
val dateFormat by lazy(LazyThreadSafetyMode.NONE, {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US)
})
fun MockWebServerRule.enqueueResponse(resourceName: String) {
this.mockWebServer.enqueue(MockResponse()
.setBody(loadJsonResponse(resourceName))
.setHeader("Content-Type", "application/json;charset=UTF-8"))
}
fun MockWebServerRule.loadJsonResponse(name: String): String {
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
return source.readString(Charset.forName("UTF-8"))
}
fun <T> assertResponseSuccessful(response: Response<T>) {
response.isSuccessful `should be` true
response.body() `should not be` null
}
fun parseDate(dateAsString: String): Calendar {
val result = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
result.time = dateFormat.parse(dateAsString.replace("Z$".toRegex(), "+0000"))
return result
}
fun <T : SubsonicResponse> checkErrorCallParsed(mockWebServerRule: MockWebServerRule,
apiRequest: () -> Response<T>): T {
mockWebServerRule.enqueueResponse("generic_error_response.json")
val response = apiRequest()
assertResponseSuccessful(response)
with(response.body()) {
status `should be` SubsonicResponse.Status.ERROR
error `should be` SubsonicError.GENERIC
}
return response.body()
}
fun SubsonicResponse.assertBaseResponseOk() {
status `should be` SubsonicResponse.Status.OK
version `should be` SubsonicAPIVersions.V1_13_0
error `should be` null
}
fun MockWebServerRule.assertRequestParam(responseResourceName: String = "ping_ok.json",
expectedParam: String,
apiRequest: () -> Response<out Any>) {
this.enqueueResponse(responseResourceName)
apiRequest()
val request = this.mockWebServer.takeRequest()
request.requestLine `should contain` expectedParam
}

View File

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

View File

@ -0,0 +1,20 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Before
import org.junit.Rule
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
/**
* Base class for integration tests for [SubsonicAPIClient] class.
*/
abstract class SubsonicAPIClientTest {
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
protected lateinit var client: SubsonicAPIClient
@Before
fun setUp() {
client = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME, PASSWORD,
CLIENT_VERSION, CLIENT_ID)
}
}

View File

@ -0,0 +1,33 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
/**
* Integration test for [SubsonicAPIDefinition.addChatMessage] call.
*/
class SubsonicApiAddChatMessageTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.addChatMessage("some").execute()
}
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.addChatMessage("some").execute()
assertResponseSuccessful(response)
}
@Test
fun `Should pass message in request param`() {
val message = "Youuhuuu"
mockWebServerRule.assertRequestParam(expectedParam = "message=$message") {
client.api.addChatMessage(message = message).execute()
}
}
}

View File

@ -0,0 +1,51 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
/**
* Integration test for [SubsonicAPIDefinition.createBookmark] call.
*/
class SubsonicApiCreateBookmarkTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.createBookmark("1", 1).execute()
}
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.createBookmark("213", 123213L).execute()
assertResponseSuccessful(response)
}
@Test
fun `Should pass id in request params`() {
val id = "544"
mockWebServerRule.assertRequestParam(expectedParam = "id=$id") {
client.api.createBookmark(id = id, position = 123).execute()
}
}
@Test
fun `Should pass position in request params`() {
val position = 4412333L
mockWebServerRule.assertRequestParam(expectedParam = "position=$position") {
client.api.createBookmark(id = "12", position = position).execute()
}
}
@Test
fun `Should pass comment in request params`() {
val comment = "some-comment"
mockWebServerRule.assertRequestParam(expectedParam = "comment=$comment") {
client.api.createBookmark(id = "1", position = 1, comment = comment).execute()
}
}
}

View File

@ -0,0 +1,54 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient] for createPlaylist call.
*/
class SubsonicApiCreatePlaylistTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.createPlaylist().execute()
}
}
@Test
fun `Should hanlde ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.createPlaylist().execute()
assertResponseSuccessful(response)
}
@Test
fun `Should pass id param in request`() {
val id = "56"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "playlistId=$id") {
client.api.createPlaylist(id = id).execute()
}
}
@Test
fun `Should pass name param in request`() {
val name = "some-name"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "name=$name") {
client.api.createPlaylist(name = name).execute()
}
}
@Test
fun `Should pass song id param in request`() {
val songId = listOf("4410", "852")
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "songId=${songId[0]}&songId=${songId[1]}") {
client.api.createPlaylist(songIds = songId).execute()
}
}
}

View File

@ -0,0 +1,78 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
import java.util.Calendar
/**
* Instrumentation test for [SubsonicAPIDefinition.createShare] call.
*/
class SubsonicApiCreateShareTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error responce`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.createShare(listOf("some-id")).execute()
}
response.shares `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_shares_ok.json")
val response = client.api.createShare(listOf("some-id")).execute()
assertResponseSuccessful(response)
response.body().shares.size `should equal to` 1
with(response.body().shares[0]) {
id `should equal to` "0"
url `should equal to` "https://subsonic.com/ext/share/awdwo?jwt=eyJhbGciOiJIUzI1NiJ9." +
"eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8hJ692" +
"UxorHdHWFU2RB-fMCmCA4IJ_dTw"
username `should equal to` "admin"
created `should equal` parseDate("2017-11-07T21:33:51.748Z")
expires `should equal` parseDate("2018-11-07T21:33:51.748Z")
lastVisited `should equal` parseDate("2018-11-07T21:33:51.748Z")
description `should equal to` "Awesome link!"
visitCount `should equal to` 0
items.size `should equal to` 1
items[0] `should equal` MusicDirectoryChild(id = "4212", parent = "4186", isDir = false,
title = "Heaven Knows", album = "Going to Hell", artist = "The Pretty Reckless",
track = 3, year = 2014, genre = "Hard Rock", coverArt = "4186", size = 9025090,
contentType = "audio/mpeg", suffix = "mp3", duration = 225, bitRate = 320,
path = "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3", isVideo = false,
playCount = 2, discNumber = 1, created = parseDate("2016-10-23T21:30:40.000Z"),
albumId = "388", artistId = "238", type = "music")
}
}
@Test
fun `Should pass ids in request param`() {
val idsList = listOf("some-id1", "some-id2")
mockWebServerRule.assertRequestParam(expectedParam = "id=${idsList[0]}&id=${idsList[1]}") {
client.api.createShare(idsList).execute()
}
}
@Test
fun `Should pass description in request param`() {
val description = "description-banana"
mockWebServerRule.assertRequestParam(expectedParam = "description=$description") {
client.api.createShare(idsToShare = listOf("id1", "id2"), description = description)
.execute()
}
}
@Test
fun `Should pass expires in request param`() {
val expires = Calendar.getInstance().timeInMillis
mockWebServerRule.assertRequestParam(expectedParam = "expires=$expires") {
client.api.createShare(idsToShare = listOf("id1"), expires = expires).execute()
}
}
}

View File

@ -0,0 +1,33 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
/**
* Integration test for [SubsonicAPIDefinition.deleteBookmark] call.
*/
class SubsonicApiDeleteBookmarkTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.deleteBookmark("1").execute()
}
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.deleteBookmark("1").execute()
assertResponseSuccessful(response)
}
@Test
fun `Should pass id in request params`() {
val id = "233"
mockWebServerRule.assertRequestParam(expectedParam = "id=$id") {
client.api.deleteBookmark(id).execute()
}
}
}

View File

@ -0,0 +1,34 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
/**
* Instrumentation test for [SubsonicAPIClient] for deletePlaylist call.
*/
class SubsonicApiDeletePlaylistTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.deletePlaylist("10").execute()
}
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.deletePlaylist("10").execute()
assertResponseSuccessful(response)
}
@Test
fun `Should pass id param in request`() {
val id = "534"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "id=$id") {
client.api.deletePlaylist(id).execute()
}
}
}

View File

@ -0,0 +1,33 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
/**
* Integration test for [SubsonicAPIDefinition.deleteShare] call.
*/
class SubsonicApiDeleteShareTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.deleteShare("123").execute()
}
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.deleteShare("12").execute()
assertResponseSuccessful(response)
}
@Test
fun `Should pass id in request params`() {
val id = "224"
mockWebServerRule.assertRequestParam(expectedParam = "id=$id") {
client.api.deleteShare(id).execute()
}
}
}

View File

@ -0,0 +1,113 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Album
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.api.subsonic.models.AlbumListType.STARRED
/**
* Integration test for [SubsonicAPIClient] for getAlbumList2() call.
*/
@Suppress("NamingConventionViolation")
class SubsonicApiGetAlbumList2Test : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getAlbumList2(STARRED).execute()
}
response.albumList `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_album_list_2_ok.json")
val response = client.api.getAlbumList2(STARRED).execute()
assertResponseSuccessful(response)
with(response.body().albumList) {
this.size `should equal to` 2
this[0] `should equal` Album(id = "962", name = "Fury", artist = "Sick Puppies",
artistId = "473", coverArt = "al-962", songCount = 13, duration = 2591,
created = parseDate("2017-09-02T17:34:51.000Z"), year = 2016,
genre = "Alternative Rock")
this[1] `should equal` Album(id = "961", name = "Endless Forms Most Beautiful",
artist = "Nightwish", artistId = "559", coverArt = "al-961", songCount = 22,
duration = 9469, created = parseDate("2017-09-02T16:22:47.000Z"),
year = 2015, genre = "Symphonic Metal")
}
}
@Test
fun `Should pass type in request params`() {
val type = AlbumListType.SORTED_BY_NAME
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json",
expectedParam = "type=${type.typeName}") {
client.api.getAlbumList2(type = type).execute()
}
}
@Test
fun `Should pass size in request param`() {
val size = 45
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json",
expectedParam = "size=$size") {
client.api.getAlbumList2(STARRED, size = size).execute()
}
}
@Test
fun `Should pass offset in request param`() {
val offset = 33
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json",
expectedParam = "offset=$offset") {
client.api.getAlbumList2(STARRED, offset = offset).execute()
}
}
@Test
fun `Should pass from year in request params`() {
val fromYear = 3030
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json",
expectedParam = "fromYear=$fromYear") {
client.api.getAlbumList2(STARRED, fromYear = fromYear).execute()
}
}
@Test
fun `Should pass toYear in request param`() {
val toYear = 2014
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json",
expectedParam = "toYear=$toYear") {
client.api.getAlbumList2(STARRED, toYear = toYear).execute()
}
}
@Test
fun `Should pass genre in request param`() {
val genre = "MathRock"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json",
expectedParam = "genre=$genre") {
client.api.getAlbumList2(STARRED, genre = genre).execute()
}
}
@Test
fun `Should pass music folder id in request param`() {
val musicFolderId = "9422"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_2_ok.json",
expectedParam = "musicFolderId=$musicFolderId") {
client.api.getAlbumList2(STARRED, musicFolderId = musicFolderId).execute()
}
}
}

View File

@ -0,0 +1,109 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.api.subsonic.models.AlbumListType.BY_GENRE
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration tests for [SubsonicAPIDefinition] for getAlbumList call.
*/
class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getAlbumList(BY_GENRE).execute()
}
response.albumList `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_album_list_ok.json")
val response = client.api.getAlbumList(BY_GENRE).execute()
assertResponseSuccessful(response)
with(response.body().albumList) {
size `should equal to` 2
this[1] `should equal` MusicDirectoryChild(id = "9997", parent = "9996", isDir = true,
title = "Endless Forms Most Beautiful", album = "Endless Forms Most Beautiful",
artist = "Nightwish", year = 2015, genre = "Symphonic Metal",
coverArt = "9997", playCount = 11,
created = parseDate("2017-09-02T16:22:49.000Z"))
}
}
@Test
fun `Should pass type in request params`() {
val listType = AlbumListType.HIGHEST
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json",
expectedParam = "type=${listType.typeName}") {
client.api.getAlbumList(type = listType).execute()
}
}
@Test
fun `Should pass size in request params`() {
val size = 45
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json",
expectedParam = "size=$size") {
client.api.getAlbumList(type = BY_GENRE, size = size).execute()
}
}
@Test
fun `Should pass offset in request params`() {
val offset = 3
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json",
expectedParam = "offset=$offset") {
client.api.getAlbumList(type = BY_GENRE, offset = offset).execute()
}
}
@Test
fun `Should pass from year in request params`() {
val fromYear = 2001
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json",
expectedParam = "fromYear=$fromYear") {
client.api.getAlbumList(type = BY_GENRE, fromYear = fromYear).execute()
}
}
@Test
fun `Should pass to year in request params`() {
val toYear = 2017
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json",
expectedParam = "toYear=$toYear") {
client.api.getAlbumList(type = BY_GENRE, toYear = toYear).execute()
}
}
@Test
fun `Should pass genre in request params`() {
val genre = "Rock"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json",
expectedParam = "genre=$genre") {
client.api.getAlbumList(type = BY_GENRE, genre = genre).execute()
}
}
@Test
fun `Should pass music folder id in request params`() {
val folderId = "545"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_list_ok.json",
expectedParam = "musicFolderId=$folderId") {
client.api.getAlbumList(type = BY_GENRE, musicFolderId = folderId).execute()
}
}
}

View File

@ -0,0 +1,69 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Album
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration test for [SubsonicAPIClient] for getAlbum call.
*/
class SubsonicApiGetAlbumTest : SubsonicAPIClientTest() {
@Test
fun `Should parse error responce`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getAlbum("56").execute()
}
response.album `should not be` null
response.album `should equal` Album()
}
@Test
fun `Should add id to request params`() {
val id = "76"
mockWebServerRule.assertRequestParam(responseResourceName = "get_album_ok.json",
expectedParam = "id=$id") {
client.api.getAlbum(id).execute()
}
}
@Test
fun `Should parse ok response`() {
mockWebServerRule.enqueueResponse("get_album_ok.json")
val response = client.api.getAlbum("512").execute()
assertResponseSuccessful(response)
with(response.body().album) {
id `should equal to` "618"
name `should equal to` "Black Ice"
artist `should equal to` "AC/DC"
artistId `should equal to` "362"
coverArt `should equal to` "al-618"
songCount `should equal to` 15
duration `should equal to` 3331
created `should equal` parseDate("2016-10-23T15:31:22.000Z")
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"),
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,
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"),
albumId = "618", artistId = "362", type = "music")
}
}
}

View File

@ -0,0 +1,57 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Album
import org.moire.ultrasonic.api.subsonic.models.Artist
/**
* Integration test for [SubsonicAPIClient] for getArtist call.
*/
class SubsonicApiGetArtistTest : SubsonicAPIClientTest() {
@Test
fun `Should parse error call`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getArtist("101").execute()
}
response.artist `should not be` null
response.artist `should equal` Artist()
}
@Test
fun `Should pass id param in request`() {
val id = "929"
mockWebServerRule.assertRequestParam(responseResourceName = "get_artist_ok.json",
expectedParam = "id=$id") {
client.api.getArtist(id).execute()
}
}
@Test
fun `Should parse ok response`() {
mockWebServerRule.enqueueResponse("get_artist_ok.json")
val response = client.api.getArtist("100").execute()
assertResponseSuccessful(response)
with(response.body().artist) {
id `should equal to` "362"
name `should equal to` "AC/DC"
coverArt `should equal to` "ar-362"
albumCount `should equal to` 2
albumsList.size `should equal to` 2
albumsList[0] `should equal` Album(id = "618", name = "Black Ice", artist = "AC/DC",
artistId = "362", coverArt = "al-618", songCount = 15, duration = 3331,
created = parseDate("2016-10-23T15:31:22.000Z"),
year = 2008, genre = "Hard Rock")
albumsList[1] `should equal` Album(id = "617", name = "Rock or Bust", artist = "AC/DC",
artistId = "362", coverArt = "al-617", songCount = 11, duration = 2095,
created = parseDate("2016-10-23T15:31:23.000Z"),
year = 2014, genre = "Hard Rock")
}
}
}

View File

@ -0,0 +1,60 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Artist
import org.moire.ultrasonic.api.subsonic.models.Index
import org.moire.ultrasonic.api.subsonic.models.Indexes
/**
* Integration test for [SubsonicAPIClient] for getArtists() request.
*/
class SubsonicApiGetArtistsTest : SubsonicAPIClientTest() {
@Test
fun `Should parse get artists error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getArtists(null).execute()
}
response.indexes `should not be` null
response.indexes `should equal` Indexes()
}
@Test
fun `Should parse get artists ok reponse`() {
mockWebServerRule.enqueueResponse("get_artists_ok.json")
val response = client.api.getArtists(null).execute()
assertResponseSuccessful(response)
with(response.body().indexes) {
lastModified `should equal to` 0L
ignoredArticles `should equal to` "The El La Los Las Le Les"
shortcutList `should equal` emptyList()
indexList.size `should equal to` 2
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)
)),
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)
))
)
}
}
@Test
fun `Should pass param on query for get artists call`() {
mockWebServerRule.enqueueResponse("get_artists_ok.json")
val musicFolderId = "101"
mockWebServerRule.assertRequestParam(responseResourceName = "get_artists_ok.json",
expectedParam = "musicFolderId=$musicFolderId") {
client.api.getArtists(musicFolderId).execute()
}
}
}

View File

@ -0,0 +1,65 @@
package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient.getAvatar] call.
*/
class SubsonicApiGetAvatarTest : SubsonicAPIClientTest() {
@Test
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("generic_error_response.json")
val response = client.getAvatar("some")
with(response) {
stream `should be` null
responseHttpCode `should equal to` 200
apiError `should equal` SubsonicError.GENERIC
}
}
@Test
fun `Should handle server error`() {
val httpErrorCode = 500
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
val response = client.getAvatar("some")
with(response) {
stream `should equal` null
responseHttpCode `should equal to` httpErrorCode
apiError `should be` null
}
}
@Test
fun `Should return successful call stream`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse()
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")))
val response = client.stream("some")
with(response) {
responseHttpCode `should equal to` 200
apiError `should be` null
stream `should not be` null
val expectedContent = mockWebServerRule.loadJsonResponse("ping_ok.json")
stream!!.bufferedReader().readText() `should equal to` expectedContent
}
}
@Test
fun `Should pass username as param`() {
val username = "Guardian"
mockWebServerRule.assertRequestParam(expectedParam = "username=$username") {
client.api.getAvatar(username).execute()
}
}
}

View File

@ -0,0 +1,46 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration test for [SubsonicAPIDefinition.getBookmarks] call.
*/
class SubsonicApiGetBookmarksTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getBookmarks().execute()
}
response.bookmarkList `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_bookmarks_ok.json")
val response = client.api.getBookmarks().execute()
assertResponseSuccessful(response)
response.body().bookmarkList.size `should equal to` 1
with(response.body().bookmarkList[0]) {
position `should equal to` 107914
username `should equal to` "CaptainEurope"
comment `should equal to` "Look at this"
created `should equal` parseDate("2017-11-18T15:22:22.144Z")
changed `should equal` parseDate("2017-11-18T15:22:22.144Z")
entry `should equal` MusicDirectoryChild(id = "10349", parent = "10342",
isDir = false, title = "Amerika", album = "Home of the Strange",
artist = "Young the Giant", track = 1, year = 2016, genre = "Indie Rock",
coverArt = "10342", size = 9628673, contentType = "audio/mpeg",
suffix = "mp3", duration = 240, bitRate = 320,
path = "Young the Giant/Home of the Strange/01 Amerika.mp3",
isVideo = false, playCount = 2, discNumber = 1,
created = parseDate("2017-11-01T17:46:52.000Z"),
albumId = "984", artistId = "571", type = "music")
}
}
}

View File

@ -0,0 +1,45 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.ChatMessage
/**
* Integration test for [SubsonicAPIDefinition.getChatMessages] call.
*/
class SubsonicApiGetChatMessagesTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getChatMessages().execute()
}
response.chatMessages `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_chat_messages_ok.json")
val response = client.api.getChatMessages().execute()
assertResponseSuccessful(response)
with(response.body().chatMessages) {
size `should equal to` 2
this[0] `should equal` ChatMessage(username = "sindre", time = 1269771845310,
message = "Sindre was here")
this[1] `should equal` ChatMessage(username = "ben", time = 1269771842504,
message = "Ben too")
}
}
@Test
fun `Should pass since in request param`() {
val since = 21388L
mockWebServerRule.assertRequestParam(expectedParam = "since=$since") {
client.api.getChatMessages(since = since).execute()
}
}
}

View File

@ -0,0 +1,74 @@
package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient] for [SubsonicAPIDefinition.getCoverArt] call.
*/
class SubsonicApiGetCoverArtTest : SubsonicAPIClientTest() {
@Test
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("generic_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
}
}
@Test
fun `Should handle server error`() {
val httpErrorCode = 404
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
val response = client.getCoverArt("some-id")
with(response) {
stream `should be` null
responseHttpCode `should equal` 404
apiError `should be` null
}
}
@Test
fun `Should return successful call stream`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse()
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")))
val response = client.getCoverArt("some-id")
with(response) {
responseHttpCode `should equal to` 200
apiError `should be` null
stream `should not be` null
val expectedContent = mockWebServerRule.loadJsonResponse("ping_ok.json")
stream!!.bufferedReader().readText() `should equal to` expectedContent
}
}
@Test
fun `Should pass id as parameter`() {
val id = "ca123994"
mockWebServerRule.assertRequestParam("ping_ok.json", id) {
client.api.getCoverArt(id).execute()
}
}
@Test
fun `Should pass size as a parameter`() {
val size = 45600L
mockWebServerRule.assertRequestParam("ping_ok.json", size.toString()) {
client.api.getCoverArt("some-id", size).execute()
}
}
}

View File

@ -0,0 +1,37 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Genre
/**
* Integration test for [SubsonicAPIDefinition.getGenres] call.
*/
class SubsonicApiGetGenresTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getGenres().execute()
}
response.genresList `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_genres_ok.json")
val response = client.api.getGenres().execute()
assertResponseSuccessful(response)
with(response.body().genresList) {
size `should equal to` 5
this[0] `should equal` Genre(1186, 103, "Rock")
this[1] `should equal` Genre(896, 72, "Electronic")
this[2] `should equal` Genre(790, 59, "Alternative Rock")
this[3] `should equal` Genre(622, 97, "Trance")
this[4] `should equal` Genre(476, 36, "Hard Rock")
}
}
}

View File

@ -0,0 +1,72 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Artist
import org.moire.ultrasonic.api.subsonic.models.Index
import org.moire.ultrasonic.api.subsonic.models.Indexes
/**
* Integration test for [SubsonicAPIClient] for getIndexes() request.
*/
class SubsonicApiGetIndexesTest : SubsonicAPIClientTest() {
@Test
fun `Should parse get indexes ok response`() {
mockWebServerRule.enqueueResponse("get_indexes_ok.json")
val response = client.api.getIndexes(null, null).execute()
assertResponseSuccessful(response)
response.body().indexes `should not be` null
with(response.body().indexes) {
lastModified `should equal` 1491069027523
ignoredArticles `should equal` "The El La Los Las Le Les"
shortcutList `should equal` listOf(
Artist(id = "889", name = "podcasts"),
Artist(id = "890", name = "audiobooks")
)
indexList `should equal` mutableListOf(
Index("A", listOf(
Artist(id = "50", name = "Ace Of Base",
starred = parseDate("2017-04-02T20:16:29.815Z")),
Artist(id = "379", name = "A Perfect Circle")
)),
Index("H", listOf(
Artist(id = "299", name = "Haddaway"),
Artist(id = "297", name = "Halestorm")
))
)
}
}
@Test
fun `Should add music folder id as a query param for getIndexes api call`() {
val musicFolderId = "9"
mockWebServerRule.assertRequestParam(responseResourceName = "get_indexes_ok.json",
expectedParam = "musicFolderId=$musicFolderId") {
client.api.getIndexes(musicFolderId, null).execute()
}
}
@Test
fun `Should add ifModifiedSince as a query param for getIndexes api call`() {
val ifModifiedSince = System.currentTimeMillis()
mockWebServerRule.assertRequestParam(responseResourceName = "get_indexes_ok.json",
expectedParam = "ifModifiedSince=$ifModifiedSince") {
client.api.getIndexes(null, ifModifiedSince).execute()
}
}
@Test
fun `Should parse get indexes error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getIndexes(null, null).execute()
}
response.indexes `should not be` null
response.indexes `should equal` Indexes()
}
}

View File

@ -0,0 +1,38 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.License
/**
* Integration test [SubsonicAPIClient] for getLicense() request.
*/
class SubsonicApiGetLicenseTest : SubsonicAPIClientTest() {
@Test
fun `Should parse get license ok response`() {
mockWebServerRule.enqueueResponse("license_ok.json")
val response = client.api.getLicense().execute()
assertResponseSuccessful(response)
with(response.body()) {
assertBaseResponseOk()
license `should equal` License(valid = true,
trialExpires = parseDate("2016-11-23T20:17:15.206Z"),
email = "someone@example.net",
licenseExpires = parseDate("8994-08-17T07:12:55.807Z"))
}
}
@Test
fun `Should parse get license error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getLicense().execute()
}
response.license `should not be` null
response.license.email `should equal` License().email
response.license.valid `should equal` License().valid
}
}

View File

@ -0,0 +1,51 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient] for getLyrics() call.
*/
class SubsonicApiGetLyricsTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.getLyrics().execute()
}
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_lyrics_ok.json")
val response = client.api.getLyrics().execute()
assertResponseSuccessful(response)
with(response.body().lyrics) {
artist `should equal to` "Amorphis"
title `should equal to` "Alone"
text `should equal to` "Tear dimmed rememberance\nIn a womb of time\nBreath upon " +
"me\nPossessed by the"
}
}
@Test
fun `Should pass artist param in request`() {
val artist = "some-artist"
mockWebServerRule.assertRequestParam(responseResourceName = "get_lyrics_ok.json",
expectedParam = "artist=$artist") {
client.api.getLyrics(artist = artist).execute()
}
}
@Test
fun `Should pass title param in request`() {
val title = "some-title"
mockWebServerRule.assertRequestParam(responseResourceName = "get_lyrics_ok.json",
expectedParam = "title=$title") {
client.api.getLyrics(title = title).execute()
}
}
}

View File

@ -0,0 +1,69 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectory
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration test for [SubsonicAPIClient] for getMusicDirectory request.
*/
class SubsonicApiGetMusicDirectoryTest : SubsonicAPIClientTest() {
@Test
fun `Should parse getMusicDirectory error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getMusicDirectory("1").execute()
}
response.musicDirectory `should not be` null
response.musicDirectory `should equal` MusicDirectory()
}
@Test
fun `GetMusicDirectory should add directory id to query params`() {
val directoryId = "124"
mockWebServerRule.assertRequestParam(responseResourceName = "get_music_directory_ok.json",
expectedParam = "id=$directoryId") {
client.api.getMusicDirectory(directoryId).execute()
}
}
@Test
fun `Should parse get music directory ok response`() {
mockWebServerRule.enqueueResponse("get_music_directory_ok.json")
val response = client.api.getMusicDirectory("1").execute()
assertResponseSuccessful(response)
response.body().musicDirectory `should not be` null
with(response.body().musicDirectory) {
id `should equal to` "4836"
parent `should equal to` "300"
name `should equal` "12 Stones"
userRating `should equal to` 5
averageRating `should equal to` 5.0f
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"),
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"),
albumId = "454", artistId = "288", type = "music")
}
}
}

View File

@ -0,0 +1,34 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicFolder
/**
* Integration test for [SubsonicAPIClient] for getMusicFolders() request.
*/
class SubsonicApiGetMusicFoldersTest : SubsonicAPIClientTest() {
@Test
fun `Should parse get music folders ok response`() {
mockWebServerRule.enqueueResponse("get_music_folders_ok.json")
val response = client.api.getMusicFolders().execute()
assertResponseSuccessful(response)
with(response.body()) {
assertBaseResponseOk()
musicFolders `should equal` listOf(
MusicFolder("0", "Music"),
MusicFolder("2", "Test"))
}
}
@Test
fun `Should parse get music folders error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getMusicFolders().execute()
}
response.musicFolders `should equal` emptyList()
}
}

View File

@ -0,0 +1,63 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
import org.moire.ultrasonic.api.subsonic.models.Playlist
/**
* Integration test for [SubsonicAPIClient] for getPlaylist call.
*/
class SubsonicApiGetPlaylistTest : SubsonicAPIClientTest() {
@Test
fun `Should parse error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getPlaylist("10").execute()
}
response.playlist `should not be` null
response.playlist `should equal` Playlist()
}
@Test
fun `Should parse ok response`() {
mockWebServerRule.enqueueResponse("get_playlist_ok.json")
val response = client.api.getPlaylist("4").execute()
assertResponseSuccessful(response)
with(response.body().playlist) {
id `should equal to` "0"
name `should equal to` "Aug 27, 2017 11:17 AM"
owner `should equal to` "admin"
public `should equal to` false
songCount `should equal to` 16
duration `should equal to` 3573
created `should equal` parseDate("2017-08-27T11:17:26.216Z")
changed `should equal` parseDate("2017-08-27T11:17:26.218Z")
coverArt `should equal to` "pl-0"
entriesList.size `should equal to` 2
entriesList[1] `should equal` MusicDirectoryChild(id = "4215", parent = "4186",
isDir = false, title = "Going to Hell", album = "Going to Hell",
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,
created = parseDate("2016-10-23T21:30:41.000Z"),
albumId = "388", artistId = "238", type = "music")
}
}
@Test
fun `Should pass id as request param`() {
val playlistId = "453"
mockWebServerRule.assertRequestParam(responseResourceName = "get_playlist_ok.json",
expectedParam = "id=$playlistId") {
client.api.getPlaylist(playlistId).execute()
}
}
}

View File

@ -0,0 +1,50 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Playlist
/**
* Integration test for [SubsonicAPIClient] for getPlaylists call.
*/
class SubsonicApiGetPlaylistsTest : SubsonicAPIClientTest() {
@Test
fun `Should parse error call`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getPlaylists().execute()
}
response.playlists `should not be` null
response.playlists `should equal` emptyList()
}
@Test
fun `Should parse ok response`() {
mockWebServerRule.enqueueResponse("get_playlists_ok.json")
val response = client.api.getPlaylists().execute()
assertResponseSuccessful(response)
with(response.body().playlists) {
size `should equal to` 1
this[0] `should equal` Playlist(id = "0", name = "Aug 27, 2017 11:17 AM",
owner = "admin", public = false, songCount = 16, duration = 3573,
comment = "Some comment",
created = parseDate("2017-08-27T11:17:26.216Z"),
changed = parseDate("2017-08-27T11:17:26.218Z"),
coverArt = "pl-0")
}
}
@Test
fun `Should pass username as a parameter`() {
val username = "SomeUsername"
mockWebServerRule.assertRequestParam(responseResourceName = "get_playlists_ok.json",
expectedParam = "username=$username") {
client.api.getPlaylists(username = username).execute()
}
}
}

View File

@ -0,0 +1,82 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration test for [SubsonicAPIClient] for getPodcasts call.
*/
class SubsonicApiGetPodcastsTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getPodcasts().execute()
}
response.podcastChannels `should not be` null
response.podcastChannels `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_podcasts_ok.json")
val response = client.api.getPodcasts().execute()
assertResponseSuccessful(response)
val podcastChannelsList = response.body().podcastChannels
podcastChannelsList.size `should equal to` 1
with(podcastChannelsList[0]) {
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."
coverArt `should equal to` "pod-2"
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,
title = "S1:EP3 How to teach yourself computer science (Vaidehi Joshi)",
album = "CodeNewbie", artist = "podcasts", coverArt = "9959",
size = 38274221, contentType = "audio/mpeg", suffix = "mp3",
duration = 2397, bitRate = 128, isVideo = false, playCount = 0,
created = parseDate("2017-08-30T09:33:39.000Z"), type = "podcast",
streamId = "9982", channelId = "2",
description = "Vaidehi decided to take on a year-long challenge. " +
"She'd pick a computer science topic every week, do tons of research " +
"and write a technical blog post explaining it in simple terms and " +
"beautiful illustrations. And then she actually did it. She tells us " +
"about her project, basecs, how it's changed her as a developer, and " +
"how she handles the trolls and negativity from people who don't " +
"appreciate her work. Show Notes Technical Writer position at " +
"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"))
}
}
@Test
fun `Should pass include episodes in request`() {
val includeEpisodes = true
mockWebServerRule.assertRequestParam(responseResourceName = "get_podcasts_ok.json",
expectedParam = "includeEpisodes=$includeEpisodes") {
client.api.getPodcasts(includeEpisodes = includeEpisodes).execute()
}
}
@Test
fun `Should pass id in request param`() {
val id = "249"
mockWebServerRule.assertRequestParam(responseResourceName = "get_podcasts_ok.json",
expectedParam = "id=$id") {
client.api.getPodcasts(id = id).execute()
}
}
}

View File

@ -0,0 +1,90 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration test for [SubsonicAPIClient] for getRandomSongs call.
*/
class SubsonicApiGetRandomSongsTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getRandomSongs().execute()
}
response.songsList `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_random_songs_ok.json")
val response = client.api.getRandomSongs().execute()
assertResponseSuccessful(response)
with(response.body().songsList) {
size `should equal to` 3
this[1] `should equal` MusicDirectoryChild(id = "3061", parent = "3050", isDir = false,
title = "Sure as Hell", album = "Who Are You Now?", artist = "This Providence",
track = 1, year = 2009, genre = "Indie Rock", coverArt = "3050",
size = 1969692, contentType = "audio/mpeg", suffix = "mp3", duration = 110,
bitRate = 142, path = "This Providence/Who Are You Now_/01 Sure as Hell.mp3",
isVideo = false, playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T21:32:46.000Z"), albumId = "272",
artistId = "152", type = "music")
}
}
@Test
fun `Should pass size in request param`() {
val size = 384433
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json",
expectedParam = "size=$size") {
client.api.getRandomSongs(size = size).execute()
}
}
@Test
fun `Should pass genre in request param`() {
val genre = "PostRock"
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json",
expectedParam = "genre=$genre") {
client.api.getRandomSongs(genre = genre).execute()
}
}
@Test
fun `Should pass from year in request param`() {
val fromYear = 1919
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json",
expectedParam = "fromYear=$fromYear") {
client.api.getRandomSongs(fromYear = fromYear).execute()
}
}
@Test
fun `Should pass to year in request params`() {
val toYear = 2012
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json",
expectedParam = "toYear=$toYear") {
client.api.getRandomSongs(toYear = toYear).execute()
}
}
@Test
fun `Should pass music folder id in request param`() {
val musicFolderId = "4919"
mockWebServerRule.assertRequestParam(responseResourceName = "get_random_songs_ok.json",
expectedParam = "musicFolderId=$musicFolderId") {
client.api.getRandomSongs(musicFolderId = musicFolderId).execute()
}
}
}

View File

@ -0,0 +1,50 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration test for [SubsonicAPIDefinition.getShares] call.
*/
class SubsonicApiGetSharesTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getShares().execute()
}
response.shares `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_shares_ok.json")
val response = client.api.getShares().execute()
assertResponseSuccessful(response)
response.body().shares.size `should equal to` 1
with(response.body().shares[0]) {
id `should equal to` "0"
url `should equal to` "https://subsonic.com/ext/share/awdwo?jwt=eyJhbGciOiJIUzI1" +
"NiJ9.eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8" +
"hJ692UxorHdHWFU2RB-fMCmCA4IJ_dTw"
username `should equal to` "admin"
created `should equal` parseDate("2017-11-07T21:33:51.748Z")
expires `should equal` parseDate("2018-11-07T21:33:51.748Z")
lastVisited `should equal` parseDate("2018-11-07T21:33:51.748Z")
visitCount `should equal to` 0
description `should equal to` "Awesome link!"
items.size `should equal to` 1
items[0] `should equal` MusicDirectoryChild(id = "4212", parent = "4186", isDir = false,
title = "Heaven Knows", album = "Going to Hell", artist = "The Pretty Reckless",
track = 3, year = 2014, genre = "Hard Rock", coverArt = "4186", size = 9025090,
contentType = "audio/mpeg", suffix = "mp3", duration = 225, bitRate = 320,
path = "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3", isVideo = false,
playCount = 2, discNumber = 1, created = parseDate("2016-10-23T21:30:40.000Z"),
albumId = "388", artistId = "238", type = "music")
}
}
}

View File

@ -0,0 +1,82 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration test for [SubsonicAPIDefinition.getSongsByGenre] call.
*/
class SubsonicApiGetSongsByGenreTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getSongsByGenre("Metal").execute()
}
response.songsList `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_songs_by_genre_ok.json")
val response = client.api.getSongsByGenre("Trance").execute()
assertResponseSuccessful(response)
response.body().songsList.size `should equal to` 2
with(response.body().songsList) {
this[0] `should equal` MusicDirectoryChild(id = "575", parent = "576", isDir = false,
title = "Time Machine (Vadim Zhukov Remix)", album = "668",
artist = "Tasadi", year = 2008, genre = "Trance", size = 22467672,
contentType = "audio/mpeg", suffix = "mp3", duration = 561, bitRate = 320,
path = "Tasadi/668/00 Time Machine (Vadim Zhukov Remix).mp3",
isVideo = false, playCount = 0, created = parseDate("2016-10-23T21:58:29.000Z"),
albumId = "0", artistId = "0", type = "music")
this[1] `should equal` MusicDirectoryChild(id = "621", parent = "622", isDir = false,
title = "My Heart (Vadim Zhukov Remix)", album = "668",
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"),
albumId = "5", artistId = "4", type = "music")
}
}
@Test
fun `Should pass genre in request param`() {
val genre = "Rock"
mockWebServerRule.assertRequestParam(expectedParam = "genre=$genre") {
client.api.getSongsByGenre(genre = genre).execute()
}
}
@Test
fun `Should pass count in request param`() {
val count = 494
mockWebServerRule.assertRequestParam(expectedParam = "count=$count") {
client.api.getSongsByGenre("Trance", count = count).execute()
}
}
@Test
fun `Should pass offset in request param`() {
val offset = 31
mockWebServerRule.assertRequestParam(expectedParam = "offset=$offset") {
client.api.getSongsByGenre("Trance", offset = offset).execute()
}
}
@Test
fun `Should pass music folder id in request param`() {
val musicFolderId = "1010"
mockWebServerRule.assertRequestParam(expectedParam = "musicFolderId=$musicFolderId") {
client.api.getSongsByGenre("Trance", musicFolderId = musicFolderId).execute()
}
}
}

View File

@ -0,0 +1,48 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Artist
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
/**
* Integration test for [SubsonicAPIClient] for getStarred2 call.
*/
@Suppress("NamingConventionViolation")
class SubsonicApiGetStarred2Test : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getStarred2().execute()
}
response.starred2 `should equal` SearchTwoResult()
}
@Test
fun `Should handle ok reponse`() {
mockWebServerRule.enqueueResponse("get_starred_2_ok.json")
val response = client.api.getStarred2().execute()
assertResponseSuccessful(response)
with(response.body().starred2) {
albumList `should equal` emptyList()
artistList.size `should equal to` 1
artistList[0] `should equal` Artist(id = "364", name = "Parov Stelar",
starred = parseDate("2017-08-12T18:32:58.768Z"))
songList `should equal` emptyList()
}
}
@Test
fun `Should pass music folder id in request param`() {
val musicFolderId = "441"
mockWebServerRule.assertRequestParam(responseResourceName = "get_starred_2_ok.json",
expectedParam = "musicFolderId=$musicFolderId") {
client.api.getStarred2(musicFolderId = musicFolderId).execute()
}
}
}

View File

@ -0,0 +1,47 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Artist
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
/**
* Integration test for [SubsonicAPIClient] for getStarred call.
*/
class SubsonicApiGetStarredTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getStarred().execute()
}
response.starred `should equal` SearchTwoResult()
}
@Test
fun `Should handle ok reponse`() {
mockWebServerRule.enqueueResponse("get_starred_ok.json")
val response = client.api.getStarred().execute()
assertResponseSuccessful(response)
with(response.body().starred) {
albumList `should equal` emptyList()
artistList.size `should equal to` 1
artistList[0] `should equal` Artist(id = "364", name = "Parov Stelar",
starred = parseDate("2017-08-12T18:32:58.768Z"))
songList `should equal` emptyList()
}
}
@Test
fun `Should pass music folder id in request param`() {
val musicFolderId = "441"
mockWebServerRule.assertRequestParam(responseResourceName = "get_starred_ok.json",
expectedParam = "musicFolderId=$musicFolderId") {
client.api.getStarred(musicFolderId = musicFolderId).execute()
}
}
}

View File

@ -0,0 +1,57 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.User
/**
* Integration test for [SubsonicAPIDefinition.getUser] call.
*/
class SubsonicApiGetUserTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getUser("some").execute()
}
response.user `should equal` User()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_user_ok.json")
val response = client.api.getUser("some").execute()
assertResponseSuccessful(response)
with(response.body().user) {
username `should equal to` "GodOfUniverse"
email `should equal to` "some.mail@example.com"
scrobblingEnabled `should equal to` false
adminRole `should equal to` true
settingsRole `should equal to` true
downloadRole `should equal to` true
uploadRole `should equal to` true
playlistRole `should equal to` true
coverArtRole `should equal to` true
commentRole `should equal to` true
podcastRole `should equal to` true
streamRole `should equal to` true
jukeboxRole `should equal to` true
shareRole `should equal to` true
videoConverstionRole `should equal to` false
folderList.size `should equal to` 1
folderList[0] `should equal to` 0
}
}
@Test
fun `Should pass username in request param`() {
val username = "Mighty"
mockWebServerRule.assertRequestParam(expectedParam = "username=$username") {
client.api.getUser(username).execute()
}
}
}

View File

@ -0,0 +1,37 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration test for [SubsonicAPIDefinition.getVideos] call.
*/
class SubsonicApiGetVideosListTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.getVideos().execute()
}
response.videosList `should equal` emptyList()
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("get_videos_ok.json")
val response = client.api.getVideos().execute()
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")
}
}
}

View File

@ -0,0 +1,110 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction.GET
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction.STATUS
import org.moire.ultrasonic.api.subsonic.models.JukeboxStatus
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Integration test for [SubsonicAPIDefinition.jukeboxControl] call.
*/
class SubsonicApiJukeboxControlTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.jukeboxControl(GET).execute()
}
response.jukebox `should equal` JukeboxStatus()
}
@Test
fun `Should handle ok response with jukebox status`() {
mockWebServerRule.enqueueResponse("jukebox_control_status_ok.json")
val response = client.api.jukeboxControl(STATUS).execute()
assertResponseSuccessful(response)
with(response.body().jukebox) {
currentIndex `should equal to` 94
playing `should equal to` true
gain `should equal to` 0.32f
position `should equal to` 3
playlistEntries `should equal` emptyList()
}
}
@Test
fun `Should handle ok response with jukebox playlist`() {
mockWebServerRule.enqueueResponse("jukebox_control_playlist_ok.json")
val response = client.api.jukeboxControl(GET).execute()
assertResponseSuccessful(response)
with(response.body().jukebox) {
currentIndex `should equal to` 887
playing `should equal to` false
gain `should equal to` 0.88f
position `should equal to` 2
playlistEntries.size `should equal to` 2
playlistEntries[1] `should equal` MusicDirectoryChild(id = "4215", parent = "4186",
isDir = false, title = "Going to Hell", album = "Going to Hell",
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,
created = parseDate("2016-10-23T21:30:41.000Z"), albumId = "388",
artistId = "238", type = "music")
}
}
@Test
fun `Should pass action in request params`() {
val action = JukeboxAction.SET_GAIN
mockWebServerRule.assertRequestParam(expectedParam = "action=$action") {
client.api.jukeboxControl(action).execute()
}
}
@Test
fun `Should pass index in request params`() {
val index = 440
mockWebServerRule.assertRequestParam(expectedParam = "index=$index") {
client.api.jukeboxControl(GET, index = index).execute()
}
}
@Test
fun `Should pass offset in request params`() {
val offset = 58223
mockWebServerRule.assertRequestParam(expectedParam = "offset=$offset") {
client.api.jukeboxControl(GET, offset = offset).execute()
}
}
@Test
fun `Should pass ids in request params`() {
val id = listOf("some-id1", "some-id2")
mockWebServerRule.assertRequestParam(expectedParam = "id=${id[0]}&id=${id[1]}") {
client.api.jukeboxControl(GET, ids = id).execute()
}
}
@Test
fun `Should pass gain in request params`() {
val gain = 0.73f
mockWebServerRule.assertRequestParam(expectedParam = "gain=$gain") {
client.api.jukeboxControl(GET, gain = gain).execute()
}
}
}

View File

@ -0,0 +1,40 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should not contain`
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient] that checks proper user password handling.
*/
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)
mockWebServerRule.enqueueResponse("ping_ok.json")
clientV12.api.ping().execute()
with(mockWebServerRule.mockWebServer.takeRequest()) {
requestLine `should contain` "&s="
requestLine `should contain` "&t="
requestLine `should not contain` "&p=enc:"
}
}
@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)
mockWebServerRule.enqueueResponse("ping_ok.json")
clientV11.api.ping().execute()
with(mockWebServerRule.mockWebServer.takeRequest()) {
requestLine `should not contain` "&s="
requestLine `should not contain` "&t="
requestLine `should contain` "&p=enc:"
}
}
}

View File

@ -0,0 +1,27 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient] that checks ping api call.
*/
class SubsonicApiPingRequestTest : SubsonicAPIClientTest() {
@Test
fun `Should parse ping ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.ping().execute()
assertResponseSuccessful(response)
with(response.body()) {
assertBaseResponseOk()
}
}
@Test
fun `Should parse ping error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.ping().execute()
}
}
}

View File

@ -0,0 +1,55 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
import java.util.Calendar
/**
* Integration test for [SubsonicAPIClient] for scrobble call.
*/
class SubsonicApiScrobbleTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.scrobble("id").execute()
}
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.scrobble("id").execute()
assertResponseSuccessful(response)
}
@Test
fun `Should pass id param in request`() {
val id = "some-id"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "id=$id") {
client.api.scrobble(id = id).execute()
}
}
@Test
fun `Should pass time param in request`() {
val time = Calendar.getInstance().timeInMillis
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "time=$time") {
client.api.scrobble(id = "some-id", time = time).execute()
}
}
@Test
fun `Should pass submission param in request`() {
val submission = false
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "submission=$submission") {
client.api.scrobble(id = "some-id", submission = submission).execute()
}
}
}

View File

@ -0,0 +1,118 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
import org.moire.ultrasonic.api.subsonic.models.SearchResult
import java.util.Calendar
/**
* Integration test for [SubsonicAPIClient] for search call.
*/
class SubsonicApiSearchTest : SubsonicAPIClientTest() {
@Test
fun `Should parse error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.search().execute()
}
response.searchResult `should not be` null
response.searchResult `should equal` SearchResult()
}
@Test
fun `Should parse ok response`() {
mockWebServerRule.enqueueResponse("search_ok.json")
val response = client.api.search().execute()
assertResponseSuccessful(response)
with(response.body().searchResult) {
offset `should equal to` 10
totalHits `should equal to` 53
matchList.size `should equal to` 1
matchList[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",
isVideo = false, playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T20:09:02.000Z"), albumId = "568",
artistId = "505", type = "music")
}
}
@Test
fun `Should pass artist param`() {
val artist = "some-artist"
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json",
expectedParam = "artist=$artist") {
client.api.search(artist = artist).execute()
}
}
@Test
fun `Should pass album param`() {
val album = "some-album"
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json",
expectedParam = "album=$album") {
client.api.search(album = album).execute()
}
}
@Test
fun `Should pass title param`() {
val title = "some-title"
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json",
expectedParam = "title=$title") {
client.api.search(title = title).execute()
}
}
@Test
fun `Should contain any param`() {
val any = "AnyString"
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json",
expectedParam = "any=$any") {
client.api.search(any = any).execute()
}
}
@Test
fun `Should contain count param`() {
val count = 11
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json",
expectedParam = "count=$count") {
client.api.search(count = count).execute()
}
}
@Test
fun `Should contain offset param`() {
val offset = 54
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json",
expectedParam = "offset=$offset") {
client.api.search(offset = offset).execute()
}
}
@Test
fun `Should contain newerThan param`() {
val newerThan = Calendar.getInstance()
mockWebServerRule.assertRequestParam(responseResourceName = "search_ok.json",
expectedParam = "newerThan=${newerThan.time.time}") {
client.api.search(newerThan = newerThan.time.time).execute()
}
}
}

View File

@ -0,0 +1,123 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Album
import org.moire.ultrasonic.api.subsonic.models.Artist
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult
/**
* Integration test for [SubsonicAPIClient] for search3 call.
*/
class SubsonicApiSearchThreeTest : SubsonicAPIClientTest() {
@Test
fun `Should parse error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.search3("some-query").execute()
}
response.searchResult `should not be` null
response.searchResult `should equal` SearchThreeResult()
}
@Test
fun `Should parse ok response`() {
mockWebServerRule.enqueueResponse("search3_ok.json")
val response = client.api.search3("some-query").execute()
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)
albumList.size `should equal to` 1
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,
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",
isVideo = false, playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T20:09:02.000Z"), albumId = "568",
artistId = "505", type = "music")
}
}
@Test
fun `Should pass query as request param`() {
val query = "some-wip-query"
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json", apiRequest = {
client.api.search3(query = query).execute()
}, expectedParam = "query=$query")
}
@Test
fun `Should pass artist count as request param`() {
val artistCount = 67
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json",
expectedParam = "artistCount=$artistCount") {
client.api.search3("some", artistCount = artistCount).execute()
}
}
@Test
fun `Should pass artist offset as request param`() {
val artistOffset = 34
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json",
expectedParam = "artistOffset=$artistOffset") {
client.api.search3("some", artistOffset = artistOffset).execute()
}
}
@Test
fun `Should pass album count as request param`() {
val albumCount = 21
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json",
expectedParam = "albumCount=$albumCount") {
client.api.search3("some", albumCount = albumCount).execute()
}
}
@Test
fun `Should pass album offset as request param`() {
val albumOffset = 43
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json",
expectedParam = "albumOffset=$albumOffset") {
client.api.search3("some", albumOffset = albumOffset).execute()
}
}
@Test
fun `Should pass song count as request param`() {
val songCount = 15
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json",
expectedParam = "songCount=$songCount") {
client.api.search3("some", songCount = songCount).execute()
}
}
@Test
fun `Should pass music folder id as request param`() {
val musicFolderId = "43"
mockWebServerRule.assertRequestParam(responseResourceName = "search3_ok.json",
expectedParam = "musicFolderId=$musicFolderId") {
client.api.search3("some", musicFolderId = musicFolderId).execute()
}
}
}

View File

@ -0,0 +1,123 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.models.Artist
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
/**
* Integration test for [SubsonicAPIClient] for search2 call.
*/
class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
val response = checkErrorCallParsed(mockWebServerRule) {
client.api.search2("some-query").execute()
}
response.searchResult `should not be` null
response.searchResult `should equal` SearchTwoResult()
}
@Test
fun `Should parse ok response`() {
mockWebServerRule.enqueueResponse("search2_ok.json")
val response = client.api.search2("some-query").execute()
assertResponseSuccessful(response)
with(response.body().searchResult) {
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",
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,
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",
isVideo = false, playCount = 0, discNumber = 1,
created = parseDate("2016-10-23T20:09:02.000Z"),
albumId = "568", artistId = "505", type = "music")
}
}
@Test
fun `Should pass query id in request param`() {
val query = "some"
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json",
expectedParam = "query=$query") {
client.api.search2(query).execute()
}
}
@Test
fun `Should pass artist count in request param`() {
val artistCount = 45
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json",
expectedParam = "artistCount=$artistCount") {
client.api.search2("some", artistCount = artistCount).execute()
}
}
@Test
fun `Should pass artist offset in request param`() {
val artistOffset = 13
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json",
expectedParam = "artistOffset=$artistOffset") {
client.api.search2("some", artistOffset = artistOffset).execute()
}
}
@Test
fun `Should pass album count in request param`() {
val albumCount = 30
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json",
expectedParam = "albumCount=$albumCount") {
client.api.search2("some", albumCount = albumCount).execute()
}
}
@Test
fun `Should pass album offset in request param`() {
val albumOffset = 91
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json",
expectedParam = "albumOffset=$albumOffset") {
client.api.search2("some", albumOffset = albumOffset).execute()
}
}
@Test
fun `Should pass song count in request param`() {
val songCount = 22
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json",
expectedParam = "songCount=$songCount") {
client.api.search2("some", songCount = songCount).execute()
}
}
@Test
fun `Should pass music folder id in request param`() {
val musicFolderId = "565"
mockWebServerRule.assertRequestParam(responseResourceName = "search2_ok.json",
expectedParam = "musicFolderId=$musicFolderId") {
client.api.search2("some", musicFolderId = musicFolderId).execute()
}
}
}

View File

@ -0,0 +1,57 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
/**
* Integration test for [SubsonicAPIClient] for star request.
*/
class SubsonicApiStarTest : SubsonicAPIClientTest() {
@Test
fun `Should parse star ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.star().execute()
assertResponseSuccessful(response)
response.body().status `should be` SubsonicResponse.Status.OK
}
@Test
fun `Should parse star error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.star().execute()
}
}
@Test
fun `Should pass id param`() {
val id = "110"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "id=$id") {
client.api.star(id = id).execute()
}
}
@Test
fun `Should pass artist id param`() {
val artistId = "123"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "artistId=$artistId") {
client.api.star(artistId = artistId).execute()
}
}
@Test
fun `Should pass album id param`() {
val albumId = "1001"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "albumId=$albumId") {
client.api.star(albumId = albumId).execute()
}
}
}

View File

@ -0,0 +1,125 @@
package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should equal`
import org.amshove.kluent.`should not be`
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient] for [SubsonicAPIDefinition.stream] call.
*/
class SubsonicApiStreamTest : SubsonicAPIClientTest() {
@Test
fun `Should handle api error response`() {
mockWebServerRule.enqueueResponse("generic_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
}
}
@Test
fun `Should handle server error`() {
val httpErrorCode = 404
mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(httpErrorCode))
val response = client.stream("some-id")
with(response) {
stream `should be` null
responseHttpCode `should equal to` httpErrorCode
apiError `should be` null
}
}
@Test
fun `Should return successfull call stream`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse()
.setBody(mockWebServerRule.loadJsonResponse("ping_ok.json")))
val response = client.stream("some-id")
with(response) {
responseHttpCode `should equal to` 200
apiError `should be` null
stream `should not be` null
val expectedContent = mockWebServerRule.loadJsonResponse("ping_ok.json")
stream!!.bufferedReader().readText() `should equal to` expectedContent
}
}
@Test
fun `Should pass id as parameter`() {
val id = "asdo123"
mockWebServerRule.assertRequestParam("ping_ok.json", id) {
client.api.stream(id = id).execute()
}
}
@Test
fun `Should pass max bit rate as param`() {
val maxBitRate = 360
mockWebServerRule.assertRequestParam("ping_ok.json",
"maxBitRate=$maxBitRate") {
client.api.stream("some-id", maxBitRate = maxBitRate).execute()
}
}
@Test
fun `Should pass format as param`() {
val format = "aac"
mockWebServerRule.assertRequestParam("ping_ok.json",
"format=$format") {
client.api.stream("some-id", format = format).execute()
}
}
@Test
fun `Should pass time offset as param`() {
val timeOffset = 155
mockWebServerRule.assertRequestParam("ping_ok.json",
"timeOffset=$timeOffset") {
client.api.stream("some-id", timeOffset = timeOffset).execute()
}
}
@Test
fun `Should pass video size as param`() {
val videoSize = "44144"
mockWebServerRule.assertRequestParam("ping_ok.json",
"size=$videoSize") {
client.api.stream("some-id", videoSize = videoSize).execute()
}
}
@Test
fun `Should pass estimate content length as param`() {
val estimateContentLength = true
mockWebServerRule.assertRequestParam("ping_ok.json",
"estimateContentLength=$estimateContentLength") {
client.api.stream("some-id", estimateContentLength = estimateContentLength).execute()
}
}
@Test
fun `Should pass converted as param`() {
val converted = false
mockWebServerRule.assertRequestParam("ping_ok.json",
"converted=$converted") {
client.api.stream("some-id", converted = converted).execute()
}
}
}

View File

@ -0,0 +1,57 @@
package org.moire.ultrasonic.api.subsonic
import org.amshove.kluent.`should be`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
/**
* Integration test for [SubsonicAPIClient] for unstar call.
*/
class SubsonicApiUnstarTest : SubsonicAPIClientTest() {
@Test
fun `Should parse ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.unstar().execute()
assertResponseSuccessful(response)
response.body().status `should be` SubsonicResponse.Status.OK
}
@Test
fun `Should parse error response`() {
checkErrorCallParsed(mockWebServerRule, {
client.api.unstar().execute()
})
}
@Test
fun `Should pass id param`() {
val id = "545"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "id=$id") {
client.api.unstar(id = id).execute()
}
}
@Test
fun `Should pass artistId param`() {
val artistId = "644"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "artistId=$artistId") {
client.api.unstar(artistId = artistId).execute()
}
}
@Test
fun `Should pass albumId param`() {
val albumId = "3344"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "albumId=$albumId") {
client.api.unstar(albumId = albumId).execute()
}
}
}

View File

@ -0,0 +1,85 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
/**
* Integration test for [SubsonicAPIClient] for updatePlaylist call.
*/
class SubsonicApiUpdatePlaylistTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.updatePlaylist("10").execute()
}
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.updatePlaylist("15").execute()
assertResponseSuccessful(response)
}
@Test
fun `Should pass playlist id param in request`() {
val id = "5453"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "playlistId=$id") {
client.api.updatePlaylist(id = id).execute()
}
}
@Test
fun `Should pass name param in request`() {
val name = "some-name"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "name=$name") {
client.api.updatePlaylist("22", name = name).execute()
}
}
@Test
fun `Should pass comment param in request`() {
val comment = "some-unusual-comment"
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "comment=$comment") {
client.api.updatePlaylist("42", comment = comment).execute()
}
}
@Test
fun `Should pass public param in request`() {
val public = true
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "public=$public") {
client.api.updatePlaylist("53", public = public).execute()
}
}
@Test
fun `Should pass song ids to update in request`() {
val songIds = listOf("45", "81")
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "songIdToAdd=${songIds[0]}&songIdToAdd=${songIds[1]}") {
client.api.updatePlaylist("25", songIdsToAdd = songIds).execute()
}
}
@Test
fun `Should pass song indexes to remove in request`() {
val songIndexesToRemove = listOf(129, 1)
mockWebServerRule.assertRequestParam(responseResourceName = "ping_ok.json",
expectedParam = "songIndexToRemove=${songIndexesToRemove[0]}&" +
"songIndexToRemove=${songIndexesToRemove[1]}") {
client.api.updatePlaylist("49", songIndexesToRemove = songIndexesToRemove).execute()
}
}
}

View File

@ -0,0 +1,51 @@
package org.moire.ultrasonic.api.subsonic
import org.junit.Test
/**
* Integration test for [SubsonicAPIDefinition.updateShare] call.
*/
class SubsonicApiUpdateShareTest : SubsonicAPIClientTest() {
@Test
fun `Should handle error response`() {
checkErrorCallParsed(mockWebServerRule) {
client.api.updateShare("11").execute()
}
}
@Test
fun `Should handle ok response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val response = client.api.updateShare("12").execute()
assertResponseSuccessful(response)
}
@Test
fun `Should pass id in request params`() {
val id = "4432"
mockWebServerRule.assertRequestParam(expectedParam = "id=$id") {
client.api.updateShare(id = id).execute()
}
}
@Test
fun `Should pass description in request params`() {
val description = "some-description"
mockWebServerRule.assertRequestParam(expectedParam = "description=$description") {
client.api.updateShare("123", description = description).execute()
}
}
@Test
fun `Should pass expires in request params`() {
val expires = 223123123L
mockWebServerRule.assertRequestParam(expectedParam = "expires=$expires") {
client.api.updateShare("12", expires = expires).execute()
}
}
}

View File

@ -0,0 +1,35 @@
package org.moire.ultrasonic.api.subsonic.interceptors
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import org.junit.Before
import org.junit.Rule
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
/**
* Base class for testing [okhttp3.Interceptor] implementations.
*/
abstract class BaseInterceptorTest {
@Rule @JvmField val mockWebServerRule = MockWebServerRule()
lateinit var client: OkHttpClient
abstract val interceptor: Interceptor
@Before
fun setUp() {
client = OkHttpClient.Builder().addInterceptor(interceptor).build()
}
/**
* Creates [Request] to use with [mockWebServerRule].
*
* @param additionalParams passes [Request.Builder] to add additionally required
* params to the [Request].
*/
fun createRequest(additionalParams: (Request.Builder) -> Unit): Request = Request.Builder()
.url(mockWebServerRule.mockWebServer.url("/"))
.also { additionalParams(it) }
.build()
}

View File

@ -0,0 +1,33 @@
package org.moire.ultrasonic.api.subsonic.interceptors
import okhttp3.Interceptor
import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should not contain`
import org.apache.commons.codec.binary.Hex
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.PASSWORD
/**
* Integration test for [PasswordHexInterceptor].
*/
class PasswordHexInterceptorTest : BaseInterceptorTest() {
private val password = "some-password"
override val interceptor: Interceptor get() = PasswordHexInterceptor(password)
@Test
fun `Should pass hex encoded password in query params`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse())
val request = createRequest { }
client.newCall(request).execute()
with(mockWebServerRule.mockWebServer.takeRequest()) {
requestLine `should not contain` "s="
requestLine `should not contain` "t="
val encodedPassword = String(Hex.encodeHex(PASSWORD.toByteArray(), false))
requestLine `should contain` "p=enc:$encodedPassword"
}
}
}

View File

@ -0,0 +1,37 @@
package org.moire.ultrasonic.api.subsonic.interceptors
import okhttp3.Interceptor
import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should not contain`
import org.apache.commons.codec.binary.Hex
import org.junit.Test
import java.security.MessageDigest
/**
* Integration test for [PasswordMD5Interceptor].
*/
class PasswordMD5InterceptorTest : BaseInterceptorTest() {
private val password = "some-password"
override val interceptor: Interceptor get() = PasswordMD5Interceptor(password)
@Test
fun `Should pass password hash and salt in query params`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse())
val request = createRequest { }
client.newCall(request).execute()
with(mockWebServerRule.mockWebServer.takeRequest()) {
requestLine `should contain` "s="
requestLine `should contain` "t="
requestLine `should not contain` "p=enc:"
val salt = requestLine.split('&').find { it.startsWith("s=") }
?.substringAfter('=')?.substringBefore(" ")
val expectedToken = String(Hex.encodeHex(MessageDigest.getInstance("MD5")
.digest("$password$salt".toByteArray()), true))
requestLine `should contain` "t=$expectedToken"
}
}
}

View File

@ -0,0 +1,59 @@
package org.moire.ultrasonic.api.subsonic.interceptors
import okhttp3.Interceptor
import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should equal to`
import org.amshove.kluent.`should not contain`
import org.junit.Test
/**
* Unit test for [RangeHeaderInterceptor].
*/
class RangeHeaderInterceptorTest : BaseInterceptorTest() {
override val interceptor: Interceptor
get() = RangeHeaderInterceptor()
@Test
fun `Should update uppercase range header`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse())
val offset = 111
val request = createRequest {
it.addHeader("Range", "$offset")
}
client.newCall(request).execute()
val executedRequest = mockWebServerRule.mockWebServer.takeRequest()
executedRequest.headers.names() `should contain` "Range"
executedRequest.headers["Range"]!! `should equal to` "bytes=$offset-"
}
@Test
fun `Should not add range header if request doesnt contain it`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse())
val request = createRequest { }
client.newCall(request).execute()
val executedRequest = mockWebServerRule.mockWebServer.takeRequest()
executedRequest.headers.names() `should not contain` "Range"
executedRequest.headers.names() `should not contain` "range"
}
@Test
fun `Should update lowercase range header`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse())
val offset = 51233
val request = createRequest {
it.addHeader("range", "$offset")
}
client.newCall(request).execute()
val executedRequest = mockWebServerRule.mockWebServer.takeRequest()
executedRequest.headers.names() `should contain` "Range"
executedRequest.headers["Range"]!! `should equal to` "bytes=$offset-"
}
}

View File

@ -0,0 +1,74 @@
package org.moire.ultrasonic.api.subsonic.interceptors
import okhttp3.Interceptor
import okhttp3.mockwebserver.MockResponse
import org.amshove.kluent.`should contain`
import org.amshove.kluent.`should equal`
import org.junit.Test
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.enqueueResponse
import kotlin.LazyThreadSafetyMode.NONE
/**
* Integration test for [VersionInterceptor].
*/
class VersionInterceptorTest : BaseInterceptorTest() {
private val initialProtocolVersion = SubsonicAPIVersions.V1_1_0
private var updatedProtocolVersion = SubsonicAPIVersions.V1_1_0
override val interceptor: Interceptor by lazy(NONE) {
VersionInterceptor(initialProtocolVersion) {
updatedProtocolVersion = it
}
}
@Test
fun `Should add initial protocol version to request`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
val request = createRequest {}
client.newCall(request).execute()
val requestLine = mockWebServerRule.mockWebServer.takeRequest().requestLine
requestLine `should contain` "v=${initialProtocolVersion.restApiVersion}"
}
@Test
fun `Should update version from response`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
client.newCall(createRequest {}).execute()
(interceptor as VersionInterceptor).protocolVersion `should equal` SubsonicAPIVersions.V1_13_0
}
@Test
fun `Should not update version if response json doesn't contain version`() {
mockWebServerRule.enqueueResponse("non_subsonic_response.json")
client.newCall(createRequest {}).execute()
(interceptor as VersionInterceptor).protocolVersion `should equal` initialProtocolVersion
}
@Test
fun `Should not update version on non-json response`() {
mockWebServerRule.mockWebServer.enqueue(MockResponse()
.setBody("asdqwnekjnqwkjen")
.setHeader("Content-Type", "application/octet-stream"))
client.newCall(createRequest {}).execute()
(interceptor as VersionInterceptor).protocolVersion `should equal` initialProtocolVersion
}
@Test
fun `Should notify notifier on version change`() {
mockWebServerRule.enqueueResponse("ping_ok.json")
client.newCall(createRequest {}).execute()
updatedProtocolVersion `should equal` SubsonicAPIVersions.V1_13_0
}
}

View File

@ -0,0 +1,27 @@
package org.moire.ultrasonic.api.subsonic.rules
import okhttp3.mockwebserver.MockWebServer
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* Starts mock web server for test and shut it down after.
*/
class MockWebServerRule : TestRule {
val mockWebServer = MockWebServer()
override fun apply(base: Statement?, description: Description?): Statement {
val ruleStatement = object : Statement() {
override fun evaluate() {
try {
mockWebServer.start()
base?.evaluate()
} finally {
mockWebServer.shutdown()
}
}
}
return ruleStatement
}
}

View File

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

View File

@ -0,0 +1,31 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"albumList2" : {
"album" : [ {
"id" : "962",
"name" : "Fury",
"artist" : "Sick Puppies",
"artistId" : "473",
"coverArt" : "al-962",
"songCount" : 13,
"duration" : 2591,
"created" : "2017-09-02T17:34:51.000Z",
"year" : 2016,
"genre" : "Alternative Rock"
}, {
"id" : "961",
"name" : "Endless Forms Most Beautiful",
"artist" : "Nightwish",
"artistId" : "559",
"coverArt" : "al-961",
"songCount" : 22,
"duration" : 9469,
"created" : "2017-09-02T16:22:47.000Z",
"year" : 2015,
"genre" : "Symphonic Metal"
} ]
}
}
}

View File

@ -0,0 +1,33 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"albumList" : {
"album" : [ {
"id" : "10020",
"parent" : "490",
"isDir" : true,
"title" : "Fury",
"album" : "Fury",
"artist" : "Sick Puppies",
"year" : 2016,
"genre" : "Alternative Rock",
"coverArt" : "10020",
"playCount" : 13,
"created" : "2017-09-02T17:34:51.000Z"
}, {
"id" : "9997",
"parent" : "9996",
"isDir" : true,
"title" : "Endless Forms Most Beautiful",
"album" : "Endless Forms Most Beautiful",
"artist" : "Nightwish",
"year" : 2015,
"genre" : "Symphonic Metal",
"coverArt" : "9997",
"playCount" : 11,
"created" : "2017-09-02T16:22:49.000Z"
} ]
}
}
}

View File

@ -0,0 +1,379 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"album" : {
"id" : "618",
"name" : "Black Ice",
"artist" : "AC/DC",
"artistId" : "362",
"coverArt" : "al-618",
"songCount" : 15,
"duration" : 3331,
"created" : "2016-10-23T15:31:22.000Z",
"year" : 2008,
"genre" : "Hard Rock",
"song" : [ {
"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" : "2016-10-23T15:31:20.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6500",
"parent" : "6475",
"isDir" : false,
"title" : "Skies on Fire",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 2,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 5634607,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 214,
"bitRate" : 209,
"path" : "AC_DC/Black Ice/02 Skies on Fire.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:22.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6496",
"parent" : "6475",
"isDir" : false,
"title" : "Big Jack",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 3,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 6274247,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 237,
"bitRate" : 211,
"path" : "AC_DC/Black Ice/03 Big Jack.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:22.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6488",
"parent" : "6475",
"isDir" : false,
"title" : "Anything Goes",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 4,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 5763074,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 202,
"bitRate" : 227,
"path" : "AC_DC/Black Ice/04 Anything Goes.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:20.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6501",
"parent" : "6475",
"isDir" : false,
"title" : "War Machine",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 5,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 4962101,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 189,
"bitRate" : 208,
"path" : "AC_DC/Black Ice/05 War Machine.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:20.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"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" : "2016-10-23T15:31:20.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6499",
"parent" : "6475",
"isDir" : false,
"title" : "Spoilin' for a Fight",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 7,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 5481070,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 197,
"bitRate" : 222,
"path" : "AC_DC/Black Ice/07 Spoilin' for a Fight.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:21.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6487",
"parent" : "6475",
"isDir" : false,
"title" : "Wheels",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 8,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 5837241,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 208,
"bitRate" : 223,
"path" : "AC_DC/Black Ice/08 Wheels.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:22.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6495",
"parent" : "6475",
"isDir" : false,
"title" : "Decibel",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 9,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 5251982,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 213,
"bitRate" : 195,
"path" : "AC_DC/Black Ice/09 Decibel.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:19.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6494",
"parent" : "6475",
"isDir" : false,
"title" : "Stormy May Day",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 10,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 5105168,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 190,
"bitRate" : 214,
"path" : "AC_DC/Black Ice/10 Stormy May Day.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:20.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6493",
"parent" : "6475",
"isDir" : false,
"title" : "She Likes Rock 'n' Roll",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 11,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 6135766,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 232,
"bitRate" : 210,
"path" : "AC_DC/Black Ice/11 She Likes Rock 'n' Roll.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:22.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6497",
"parent" : "6475",
"isDir" : false,
"title" : "Money Made",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 12,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 6816386,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 255,
"bitRate" : 213,
"path" : "AC_DC/Black Ice/12 Money Made.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:21.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6489",
"parent" : "6475",
"isDir" : false,
"title" : "Rock 'n' Roll Dream",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 13,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 7486707,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 280,
"bitRate" : 212,
"path" : "AC_DC/Black Ice/13 Rock 'n' Roll Dream.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:20.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6498",
"parent" : "6475",
"isDir" : false,
"title" : "Rocking All the Way",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 14,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 4927026,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 202,
"bitRate" : 194,
"path" : "AC_DC/Black Ice/14 Rocking All the Way.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:22.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
}, {
"id" : "6490",
"parent" : "6475",
"isDir" : false,
"title" : "Black Ice",
"album" : "Black Ice",
"artist" : "AC/DC",
"track" : 15,
"year" : 2008,
"genre" : "Hard Rock",
"coverArt" : "6475",
"size" : 5192799,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 205,
"bitRate" : 202,
"path" : "AC_DC/Black Ice/15 Black Ice.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:22.000Z",
"albumId" : "618",
"artistId" : "362",
"type" : "music"
} ]
}
}
}

View File

@ -0,0 +1,35 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"artist" : {
"id" : "362",
"name" : "AC/DC",
"coverArt" : "ar-362",
"albumCount" : 2,
"album" : [ {
"id" : "618",
"name" : "Black Ice",
"artist" : "AC/DC",
"artistId" : "362",
"coverArt" : "al-618",
"songCount" : 15,
"duration" : 3331,
"created" : "2016-10-23T15:31:22.000Z",
"year" : 2008,
"genre" : "Hard Rock"
}, {
"id" : "617",
"name" : "Rock or Bust",
"artist" : "AC/DC",
"artistId" : "362",
"coverArt" : "al-617",
"songCount" : 11,
"duration" : 2095,
"created" : "2016-10-23T15:31:23.000Z",
"year" : 2014,
"genre" : "Hard Rock"
} ]
}
}
}

View File

@ -0,0 +1,43 @@
{
"subsonic-response": {
"status": "ok",
"version": "1.15.0",
"artists": {
"ignoredArticles": "The El La Los Las Le Les",
"index": [
{
"name": "A",
"artist": [
{
"id": "362",
"name": "AC/DC",
"coverArt": "ar-362",
"albumCount": 2
},
{
"id": "254",
"name": "Acceptance",
"coverArt": "ar-254",
"albumCount": 1
}
]
}, {
"name": "T",
"artist": [
{
"id": "516",
"name": "Tangerine Dream",
"coverArt": "ar-516",
"albumCount": 1
},
{
"id": "242",
"name": "Taproot",
"coverArt": "ar-242",
"albumCount": 2
}
]
} ]
}
}
}

View File

@ -0,0 +1,40 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"bookmarks" : {
"bookmark" : [ {
"position" : 107914,
"username" : "CaptainEurope",
"comment" : "Look at this",
"created" : "2017-11-18T15:22:22.144Z",
"changed" : "2017-11-18T15:22:22.144Z",
"entry" : {
"id" : "10349",
"parent" : "10342",
"isDir" : false,
"title" : "Amerika",
"album" : "Home of the Strange",
"artist" : "Young the Giant",
"track" : 1,
"year" : 2016,
"genre" : "Indie Rock",
"coverArt" : "10342",
"size" : 9628673,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 240,
"bitRate" : 320,
"path" : "Young the Giant/Home of the Strange/01 Amerika.mp3",
"isVideo" : false,
"playCount" : 2,
"discNumber" : 1,
"created" : "2017-11-01T17:46:52.000Z",
"albumId" : "984",
"artistId" : "571",
"type" : "music"
}
} ]
}
}
}

View File

@ -0,0 +1,17 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"chatMessages" : {
"chatMessage" : [ {
"username" : "sindre",
"time" : 1269771845310,
"message" : "Sindre was here"
}, {
"username" : "ben",
"time" : 1269771842504,
"message" : "Ben too"
} ]
}
}
}

View File

@ -0,0 +1,29 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"genres" : {
"genre" : [ {
"songCount" : 1186,
"albumCount" : 103,
"value" : "Rock"
}, {
"songCount" : 896,
"albumCount" : 72,
"value" : "Electronic"
}, {
"songCount" : 790,
"albumCount" : 59,
"value" : "Alternative Rock"
}, {
"songCount" : 622,
"albumCount" : 97,
"value" : "Trance"
}, {
"songCount" : 476,
"albumCount" : 36,
"value" : "Hard Rock"
} ]
}
}
}

View File

@ -0,0 +1,38 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.13.0",
"indexes" : {
"lastModified" : 1491069027523,
"ignoredArticles" : "The El La Los Las Le Les",
"shortcut" : [ {
"id" : "889",
"name" : "podcasts"
},
{
"id" : "890",
"name" : "audiobooks"
} ],
"index" : [ {
"name" : "A",
"artist" : [ {
"id" : "50",
"name" : "Ace Of Base",
"starred" : "2017-04-02T20:16:29.815Z"
}, {
"id" : "379",
"name" : "A Perfect Circle"
} ]
}, {
"name" : "H",
"artist" : [ {
"id" : "299",
"name" : "Haddaway"
}, {
"id" : "297",
"name" : "Halestorm"
} ]
} ]
}
}
}

View File

@ -0,0 +1,11 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"lyrics" : {
"artist" : "Amorphis",
"title" : "Alone",
"value" : "Tear dimmed rememberance\nIn a womb of time\nBreath upon me\nPossessed by the"
}
}
}

View File

@ -0,0 +1,63 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"directory" : {
"id" : "4836",
"parent" : "300",
"name" : "12 Stones",
"userRating" : 5,
"averageRating" : 5.0,
"playCount" : 1,
"child" : [ {
"id" : "4844",
"parent" : "4836",
"isDir" : false,
"title" : "Crash",
"album" : "12 Stones",
"artist" : "12 Stones",
"track" : 1,
"year" : 2002,
"genre" : "Alternative Rock",
"coverArt" : "4836",
"size" : 5348318,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 222,
"bitRate" : 192,
"path" : "12 Stones/12 Stones/01 Crash.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:19:10.000Z",
"albumId" : "454",
"artistId" : "288",
"type" : "music"
}, {
"id" : "4845",
"parent" : "4836",
"isDir" : false,
"title" : "Broken",
"album" : "12 Stones",
"artist" : "12 Stones",
"track" : 2,
"year" : 2002,
"genre" : "Alternative Rock",
"coverArt" : "4836",
"size" : 4309043,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 179,
"bitRate" : 192,
"path" : "12 Stones/12 Stones/02 Broken.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:19:09.000Z",
"albumId" : "454",
"artistId" : "288",
"type" : "music"
} ]
}
}
}

View File

@ -0,0 +1,15 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.13.0",
"musicFolders" : {
"musicFolder" : [ {
"id" : 0,
"name" : "Music"
}, {
"id" : 2,
"name" : "Test"
} ]
}
}
}

View File

@ -0,0 +1,66 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"playlist" : {
"id" : "0",
"name" : "Aug 27, 2017 11:17 AM",
"owner" : "admin",
"public" : false,
"songCount" : 16,
"duration" : 3573,
"created" : "2017-08-27T11:17:26.216Z",
"changed" : "2017-08-27T11:17:26.218Z",
"coverArt" : "pl-0",
"entry" : [ {
"id" : "4209",
"parent" : "4186",
"isDir" : false,
"title" : "Follow Me Down",
"album" : "Going to Hell",
"artist" : "The Pretty Reckless",
"track" : 1,
"year" : 2014,
"genre" : "Hard Rock",
"coverArt" : "4186",
"size" : 11229681,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 280,
"bitRate" : 320,
"path" : "The Pretty Reckless/Going to Hell/01 Follow Me Down.mp3",
"isVideo" : false,
"playCount" : 1,
"discNumber" : 1,
"created" : "2016-10-23T21:30:43.000Z",
"albumId" : "388",
"artistId" : "238",
"type" : "music"
}, {
"id" : "4215",
"parent" : "4186",
"isDir" : false,
"title" : "Going to Hell",
"album" : "Going to Hell",
"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,
"created" : "2016-10-23T21:30:41.000Z",
"albumId" : "388",
"artistId" : "238",
"type" : "music"
} ]
}
}
}

View File

@ -0,0 +1,20 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"playlists" : {
"playlist" : [ {
"id" : "0",
"name" : "Aug 27, 2017 11:17 AM",
"comment" : "Some comment",
"owner" : "admin",
"public" : false,
"songCount" : 16,
"duration" : 3573,
"created" : "2017-08-27T11:17:26.216Z",
"changed" : "2017-08-27T11:17:26.218Z",
"coverArt" : "pl-0"
} ]
}
}
}

View File

@ -0,0 +1,154 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"podcasts" : {
"channel" : [ {
"id" : "2",
"url" : "http://feeds.codenewbie.org/cnpodcast.xml",
"title" : "CodeNewbie",
"description" : "Stories and interviews from people on their coding journey.",
"coverArt" : "pod-2",
"originalImageUrl" : "http://codenewbie.blubrry.com/wp-content/uploads/powerpress/220808.jpg",
"status" : "completed",
"episode" : [ {
"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",
"duration" : 2397,
"bitRate" : 128,
"isVideo" : false,
"playCount" : 0,
"created" : "2017-08-30T09:33:39.000Z",
"type" : "podcast",
"streamId" : "9982",
"channelId" : "2",
"description" : "Vaidehi decided to take on a year-long challenge. She'd pick a computer science topic every week, do tons of research and write a technical blog post explaining it in simple terms and beautiful illustrations. And then she actually did it. She tells us about her project, basecs, how it's changed her as a developer, and how she handles the trolls and negativity from people who don't appreciate her work. Show Notes Technical Writer position at 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" : "2017-08-29T00:01:01.000Z"
}, {
"id" : "147",
"parent" : "9959",
"isDir" : false,
"title" : "S1:EP2 Building community in a virtual world: Moderation tools in VR (Cameron Brown)",
"album" : "CodeNewbie",
"artist" : "podcasts",
"coverArt" : "9959",
"size" : 52657014,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 3298,
"bitRate" : 128,
"isVideo" : false,
"playCount" : 0,
"created" : "2017-08-25T19:32:53.000Z",
"type" : "podcast",
"streamId" : "9963",
"channelId" : "2",
"description" : "Rec Room is the most popular virtual reality game. It's a social space where you can play dodgeball, ping pong, darts and more with people from all over the world. But when you're inviting everyone to play, how do you make sure that everyone is safe? What happens when a player attacks someone? What does an attack even look like in a virtual world? Cameron Brown, Chief Creative Officer at Against Gravity, the creators of Rec Room, takes us through the world of social virtual reality and shows how they've designed a system to make their game a welcoming place for all. Show Links Incapsula (sponsor) Dice (sponsor) Technical Writer position at CodeNewbie HTC Vive Rec Room Rec Room's Code of Conduct",
"status" : "completed",
"publishDate" : "2017-08-24T00:01:01.000Z"
}, {
"id" : "146",
"parent" : "9959",
"isDir" : false,
"title" : "S1:EP1 Intro to Accessibility (Stephanie Slattery)",
"album" : "CodeNewbie",
"artist" : "podcasts",
"coverArt" : "9959",
"size" : 50708305,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 3176,
"bitRate" : 128,
"isVideo" : false,
"playCount" : 0,
"created" : "2017-08-16T19:32:53.000Z",
"type" : "podcast",
"streamId" : "9962",
"channelId" : "2",
"description" : "We kick off the first episode of our official first season with Stephanie Slattery, a front-end engineer who specializes in accessibility. She breaks down the world of accessibility, giving you the perfect introduction to this topic. She explains the five categories of disabilities, shows us how to implement suggestions from the Web Content Accessibility Guidelines, and shares why shes so passionate about helping more people experience tech. Show Links Incapsula (sponsor) Hover (sponsor) An Introduction to Web Accessibility (Stephanie's Blog Post) Codeland, CodeNewbie's conference - April 21 & 22 in NYC NeoPets Dev Bootcamp Illinois Institute of Technology W3C WCAG (Web Content Accessibility Guidelines) ADA 1990 Rehabilitation Act of 1973",
"status" : "completed",
"publishDate" : "2017-08-16T00:01:01.000Z"
}, {
"id" : "0",
"parent" : "9959",
"isDir" : false,
"title" : "Ep. 146 Codeland - Mentorship, Technical Blogging, and Open Source Talks from Katrina Owen, Quincy Larson, and Nell Shamrell-Harrington",
"album" : "CodeNewbie",
"artist" : "podcasts",
"coverArt" : "9959",
"size" : 47779050,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 2993,
"bitRate" : 128,
"isVideo" : false,
"playCount" : 0,
"created" : "2017-08-12T18:40:06.000Z",
"type" : "podcast",
"streamId" : "9960",
"channelId" : "2",
"description" : "In our final episode of our Codeland mini-series, Katrina Owen shares what it really takes to get that mentor you've always wanted, Quincy Larson gives us his best practices for writing technical blog posts people will actually read, and Nell Shamrell-Harrington explores what it really takes for an open source project to be successful and what you should know as a future contributor. Show Links Flatiron School (sponsor) Incapsula (sponsor) Hover (sponsor) CodeNewbie YouTube channel Codeland, CodeNewbie's conference - April 21 & 22 in NYC Be Lucky—its an easy skill to learn by Richard Wiseman How to read Medium articles people will actually read Sample Contribution Guide Sample Testing Guide Travis CI Sample Code of Conduct Open Source Governance Continuous Integration (CI) System",
"status" : "completed",
"publishDate" : "2017-08-01T00:01:01.000Z"
}, {
"id" : "1",
"isDir" : false,
"title" : "Ep. 145 Codeland - NYPL and Khan Academy talks from Courteney Ervin and Celia La",
"channelId" : "2",
"description" : "Courteney Ervin shares the ups and downs of building a product for one of the largest library systems in the world. Celia La walks us through the technical challenges (and solutions) of bringing Khan Academy's high quality content to people all over the world. Show Links Flatiron School (sponsor) Hover (sponsor) Incapsula (sponsor) CodeNewbie YouTube channel Codeland, CodeNewbie's conference - April 21 & 22 in NYC Khan Academy Memcached Git Version Control System New York Public Library Integrated Library System",
"status" : "skipped",
"publishDate" : "2017-07-25T00:01:01.000Z"
}, {
"id" : "2",
"isDir" : false,
"title" : "Ep. 144 Codeland - Accessibility and Education talks from Sterling Walker and Maurice Rogers",
"channelId" : "2",
"description" : "We wrap up our community talks with Sterlings story of her very first project at her first dev job: making the app accessible to two blind students. Maurice kicks off our education talks with the story of Abacus, his side project that became the learning system used by thousands of students in his country of Belize. Show Links Flatiron School (sponsor) Hover (sponsor)Incapsula (sponsor) CodeNewbie YouTube channel Codeland, CodeNewbie's conference - April 21 & 22 in NYC An Alphabet of Accessibility Issues Web Content Accessibility Guidelines (WCAG) Abacus Grails Java xkcd comic \"Standards\"",
"status" : "skipped",
"publishDate" : "2017-07-18T00:01:01.000Z"
}, {
"id" : "3",
"isDir" : false,
"title" : "Ep. 143 Codeland - Community Talks from Valerie Woolard Srinivasan and Rapi Castillo",
"channelId" : "2",
"description" : "Valerie explores the importance of security in creating powerful and engaged communities, and breaks down three ways your code might be vulnerable. Rapi shares his story of creating a toy coding project in D3.js that sparked a movement and helped thousands of people become more politically engaged. Checkout the videos of these talks on the CodeNewbie YouTube channel. Show Links Flatiron School (sponsor) Hover (sponsor) Incapsula (sponsor) CodeNewbie YouTube channel Codeland, CodeNewbie's conference - April 21 & 22 in NYC Rapi's Talk [VIDEO] Valerie's Talk [VIDEO] Progressive Coders Network D3.js SQL mass assignment man in the middle SQL injection validating inputs sanitizing inputs strong parameters",
"status" : "skipped",
"publishDate" : "2017-07-06T00:01:01.000Z"
}, {
"id" : "4",
"isDir" : false,
"title" : "Ep. 142 Codeland - Codeland - Mental Health talks from Michelle Morales and Greg Baugues",
"channelId" : "2",
"description" : "This episode features two talks on mental health that explore two very different sides of this important topic. Michelles talk is a technical showcase of how her research project uses open source tools to better diagnose depression. Greg shares his personal struggles with ADHD and bipolar disorder, and how important it is for us to openly talk about mental health. Show Links Flatiron School (sponsor) Hover (sponsor) Incapsula (sponsor) CodeNewbie YouTube channel Codeland, CodeNewbie's conference - April 21 & 22 in NYC CUNY Graduate Center Tom Insel's TED talk on depression Audio/Visual Emotion and Depression Recognition dataset DAIC-WOZ Database Covarep OpenFace OpenMM IBM Watson Speech to Text Natural Language Processing Machine Learning Feature Extraction Automatic Speech Recognition ZocDoc 718-312-8335 (Greg's mental health resource number)",
"status" : "skipped",
"publishDate" : "2017-06-28T00:01:01.000Z"
}, {
"id" : "5",
"isDir" : false,
"title" : "Ep. 141 Codeland - Interview with NYC's first CTO (Minerva Tantoco)",
"channelId" : "2",
"description" : "When Minerva Tantoco was first offered the CTO position for New York City, she thought it was a prank. But in 2014, she became the citys first Chief Technology Officer. She sits down with Codelands emcee, Nikhil Paul, to talk about how she started her long, impressive tech career, what programming looked like back her coding days, and how she hopes tech will transform cities for the better. Show Links Flatiron School (sponsor) Hover (sponsor) Incapsula (sponsor) CodeNewbie YouTube channel Mayor de Blasios announcement Codeland, CodeNewbie's conference - April 21 & 22 in NYC",
"status" : "skipped",
"publishDate" : "2017-06-21T00:01:01.000Z"
}, {
"id" : "6",
"isDir" : false,
"title" : "Ep. 140 Codeland - Gaming and City Talks from Chris Algoo, Kate Rabinowitz, Eric Brelsford",
"channelId" : "2",
"description" : "Chris Algoo shares how he co-created “Breakup Squad,” the game where you have to keep two exes from getting back together. Kate Rabinowitz shows us how open data can help build powerful, insightful tools to better understand and improve your city. Eric Brelsford shares how he used mapping tools to help community members turn vacant lots into beautiful neighborhood spaces. Show Links Flatiron School (sponsor) Hover (sponsor) Incapsula (sponsor) Breakup Squad (trailer) Codeland, CodeNewbie's conference - April 21 & 22 in NYC CodeNewbie YouTube channel Unity Twine FMOD Open Game Art Freesound.org The Big List of Game Making Tools An Introduction to Statistical Learning Interactive Data Visualization for the Web Code for America Brigades DataKind Maptime NYC Urban Reviewer 596 Acres Living Lots NYC NYCommons Open Data R Python Tableau Carto D3 API",
"status" : "skipped",
"publishDate" : "2017-06-13T00:01:01.000Z"
} ]
} ]
}
}
}

View File

@ -0,0 +1,81 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"randomSongs" : {
"song" : [ {
"id" : "4426",
"parent" : "4417",
"isDir" : false,
"title" : "Permanent",
"album" : "Phantoms",
"artist" : "Acceptance",
"track" : 11,
"year" : 2005,
"genre" : "Rock",
"coverArt" : "4417",
"size" : 4444381,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 185,
"bitRate" : 192,
"path" : "Acceptance/Phantoms/11 Permanent.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T15:31:49.000Z",
"albumId" : "409",
"artistId" : "254",
"type" : "music"
}, {
"id" : "3061",
"parent" : "3050",
"isDir" : false,
"title" : "Sure as Hell",
"album" : "Who Are You Now?",
"artist" : "This Providence",
"track" : 1,
"year" : 2009,
"genre" : "Indie Rock",
"coverArt" : "3050",
"size" : 1969692,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 110,
"bitRate" : 142,
"path" : "This Providence/Who Are You Now_/01 Sure as Hell.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T21:32:46.000Z",
"albumId" : "272",
"artistId" : "152",
"type" : "music"
}, {
"id" : "2455",
"parent" : "2451",
"isDir" : false,
"title" : "I Dare You",
"album" : "Us and Them",
"artist" : "Shinedown",
"track" : 4,
"year" : 2005,
"genre" : "Rock",
"coverArt" : "2451",
"size" : 6489382,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 234,
"bitRate" : 221,
"path" : "Shinedown/Us and Them/04 I Dare You.mp3",
"isVideo" : false,
"playCount" : 0,
"discNumber" : 1,
"created" : "2016-10-23T21:16:21.000Z",
"albumId" : "209",
"artistId" : "112",
"type" : "music"
} ]
}
}
}

View File

@ -0,0 +1,43 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"shares" : {
"share" : [ {
"id" : "0",
"url" : "https://subsonic.com/ext/share/awdwo?jwt=eyJhbGciOiJIUzI1NiJ9.eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8hJ692UxorHdHWFU2RB-fMCmCA4IJ_dTw",
"username" : "admin",
"created" : "2017-11-07T21:33:51.748Z",
"expires" : "2018-11-07T21:33:51.748Z",
"lastVisited" : "2018-11-07T21:33:51.748Z",
"description" : "Awesome link!",
"visitCount" : 0,
"entry" : [ {
"id" : "4212",
"parent" : "4186",
"isDir" : false,
"title" : "Heaven Knows",
"album" : "Going to Hell",
"artist" : "The Pretty Reckless",
"track" : 3,
"year" : 2014,
"genre" : "Hard Rock",
"coverArt" : "4186",
"size" : 9025090,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 225,
"bitRate" : 320,
"path" : "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3",
"isVideo" : false,
"playCount" : 2,
"discNumber" : 1,
"created" : "2016-10-23T21:30:40.000Z",
"albumId" : "388",
"artistId" : "238",
"type" : "music"
} ]
} ]
}
}
}

View File

@ -0,0 +1,51 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"songsByGenre" : {
"song" : [ {
"id" : "575",
"parent" : "576",
"isDir" : false,
"title" : "Time Machine (Vadim Zhukov Remix)",
"album" : "668",
"artist" : "Tasadi",
"year" : 2008,
"genre" : "Trance",
"size" : 22467672,
"contentType" : "audio/mpeg",
"suffix" : "mp3",
"duration" : 561,
"bitRate" : 320,
"path" : "Tasadi/668/00 Time Machine (Vadim Zhukov Remix).mp3",
"isVideo" : false,
"playCount" : 0,
"created" : "2016-10-23T21:58:29.000Z",
"albumId" : "0",
"artistId" : "0",
"type" : "music"
}, {
"id" : "621",
"parent" : "622",
"isDir" : false,
"title" : "My Heart (Vadim Zhukov Remix)",
"album" : "668",
"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" : "2016-10-23T21:58:29.000Z",
"albumId" : "5",
"artistId" : "4",
"type" : "music"
} ]
}
}
}

View File

@ -0,0 +1,13 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"starred2" : {
"artist" : [ {
"id" : "364",
"name" : "Parov Stelar",
"starred" : "2017-08-12T18:32:58.768Z"
} ]
}
}
}

View File

@ -0,0 +1,13 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"starred" : {
"artist" : [ {
"id" : "364",
"name" : "Parov Stelar",
"starred" : "2017-08-12T18:32:58.768Z"
} ]
}
}
}

View File

@ -0,0 +1,24 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"user" : {
"username" : "GodOfUniverse",
"email" : "some.mail@example.com",
"scrobblingEnabled" : false,
"adminRole" : true,
"settingsRole" : true,
"downloadRole" : true,
"uploadRole" : true,
"playlistRole" : true,
"coverArtRole" : true,
"commentRole" : true,
"podcastRole" : true,
"streamRole" : true,
"jukeboxRole" : true,
"shareRole" : true,
"videoConversionRole" : false,
"folder" : [ 0 ]
}
}
}

View File

@ -0,0 +1,25 @@
{
"subsonic-response" : {
"status" : "ok",
"version" : "1.15.0",
"videos" : {
"video" : [ {
"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" : "2017-11-19T12:34:33.000Z",
"type" : "video"
} ]
}
}
}

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