mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-21 19:47:41 +03:00
Compare commits
No commits in common. "develop" and "2.22.0" have entirely different histories.
140
.circleci/config.yml
Normal file
140
.circleci/config.yml
Normal file
@ -0,0 +1,140 @@
|
||||
version: 3
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/android:api-29
|
||||
working_directory: ~/ultrasonic
|
||||
environment:
|
||||
JVM_OPTS: -Xmx3200m
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-ultrasonic-{{ .Branch }}-{{ checksum "dependencies.gradle" }}
|
||||
- v1-ultrasonic-{{ .Branch }}
|
||||
- v1-ultrasonic
|
||||
- run:
|
||||
name: configure gradle.properties for CI building
|
||||
command: |
|
||||
sed -i '/^org.gradle.jvmargs/d' gradle.properties
|
||||
sed -i 's/^org.gradle.daemon=true/org.gradle.daemon=false/g' gradle.properties
|
||||
- run:
|
||||
name: checkstyle
|
||||
command: ./gradlew -Pqc ktlintCheck
|
||||
- run:
|
||||
name: static analysis
|
||||
command: ./gradlew -Pqc detekt
|
||||
- run:
|
||||
name: build
|
||||
command: ./gradlew assembleDebug
|
||||
- run:
|
||||
name: unit-tests
|
||||
command: |
|
||||
./gradlew ciTest testDebugUnitTest
|
||||
./gradlew jacocoFullReport
|
||||
- run:
|
||||
name: lint
|
||||
command: ./gradlew :ultrasonic:lintRelease
|
||||
- run:
|
||||
name: assemble release build
|
||||
command: ./gradlew build assembleRelease
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: v1-ultrasonic-{{ .Branch }}-{{ checksum "dependencies.gradle" }}
|
||||
- store_artifacts:
|
||||
path: ultrasonic/build/reports
|
||||
destination: reports
|
||||
- store_artifacts:
|
||||
path: subsonic-api/build/reports
|
||||
destination: reports
|
||||
- store_artifacts:
|
||||
path: build/reports/jacoco/jacocoFullReport/
|
||||
push_translations:
|
||||
docker:
|
||||
- image: circleci/python:3.6
|
||||
working_directory: ~/ultrasonic
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: install transifex client
|
||||
command: |
|
||||
python -m venv ~/venv
|
||||
. ~/venv/bin/activate
|
||||
pip install transifex-client
|
||||
- run:
|
||||
name: configure transifex client
|
||||
command: echo $'[https://www.transifex.com]\nhostname = https://www.transifex.com\nusername = api\npassword = '"${TRANSIFEX_PASSWORD}"$'\n' > ~/.transifexrc
|
||||
- run:
|
||||
name: push changes in translation files
|
||||
command: |
|
||||
. ~/venv/bin/activate
|
||||
tx push -s
|
||||
generate_signed_apk:
|
||||
docker:
|
||||
- image: circleci/android:api-28
|
||||
working_directory: ~/ultrasonic
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-ultrasonic-{{ .Branch }}-{{ checksum "dependencies.gradle" }}
|
||||
- v1-ultrasonic-{{ .Branch }}
|
||||
- v1-ultrasonic
|
||||
- run:
|
||||
name: decrypt ultrasonic-keystore
|
||||
command: openssl aes-256-cbc -K ${ULTRASONIC_KEYSTORE_KEY} -iv ${ULTRASONIC_KEYSTORE_IV} -in ultrasonic-keystore.enc -out ultrasonic-keystore -d
|
||||
- run:
|
||||
name: build release apk
|
||||
command: ./gradlew build assembleRelease
|
||||
- run:
|
||||
name: sign release apk
|
||||
command: |
|
||||
mkdir -p /tmp/ultrasonic-release
|
||||
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/ultrasonic/ultrasonic-keystore -storepass ${ULTRASONIC_KEYSTORE_STOREPASS} -keypass ${ULTRASONIC_KEYSTORE_KEYPASS} ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk ultrasonic
|
||||
jarsigner -verify ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk
|
||||
${ANDROID_HOME}/build-tools/27.0.0/zipalign -v 4 ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
|
||||
- persist_to_workspace:
|
||||
root: /tmp/ultrasonic-release
|
||||
paths:
|
||||
- ultrasonic-*.apk
|
||||
publish_github_signed_apk:
|
||||
docker:
|
||||
- image: circleci/golang
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /tmp/ultrasonic-release
|
||||
- run:
|
||||
name: install ghr
|
||||
command: go get -v github.com/tcnksm/ghr
|
||||
- run:
|
||||
name: publish release on github tag
|
||||
command: ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} ${CIRCLE_TAG} /tmp/ultrasonic-release
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_push_translations:
|
||||
jobs:
|
||||
- build
|
||||
- push_translations:
|
||||
requires:
|
||||
- build
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
- master
|
||||
- generate_signed_apk:
|
||||
filters:
|
||||
tags:
|
||||
only: /^[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- publish_github_signed_apk:
|
||||
requires:
|
||||
- generate_signed_apk
|
||||
filters:
|
||||
tags:
|
||||
only: /^[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
@ -1,2 +0,0 @@
|
||||
[*.{kt,kts}]
|
||||
ktlint_code_style = android_studio
|
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gradle" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -18,7 +18,6 @@ out/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
.kotlin/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
@ -40,7 +39,6 @@ captures/
|
||||
*.iml
|
||||
.idea/
|
||||
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
|
||||
|
151
.gitlab-ci.yml
151
.gitlab-ci.yml
@ -1,151 +0,0 @@
|
||||
default:
|
||||
image: registry.gitlab.com/ultrasonic/ci-android:1.2.0
|
||||
cache: &global_cache
|
||||
key:
|
||||
files:
|
||||
- gradle/wrapper/gradle-wrapper.properties
|
||||
paths:
|
||||
- .gradle/
|
||||
|
||||
variables:
|
||||
CACHE_FALLBACK_KEY: develop-protected
|
||||
MEMORY_CONFIG: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g"
|
||||
MEMORY_CONFIG_DEBUG: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*"
|
||||
JVM_OPTS: ${MEMORY_CONFIG}
|
||||
JAVA_TOOL_OPTIONS: ${MEMORY_CONFIG}
|
||||
GRADLE_OPTS: ${MEMORY_CONFIG}
|
||||
PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/Ultrasonic/${CI_COMMIT_TAG}"
|
||||
PACKAGE_APK: "ultrasonic-${CI_COMMIT_TAG}.apk"
|
||||
PACKAGE_APK_IDSIG: "ultrasonic-${CI_COMMIT_TAG}.apk.idsig"
|
||||
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"
|
||||
# The project id of https://gitlab.com/ultrasonic/ultrasonic/
|
||||
ROOT_PROJECT_ID: 37671564
|
||||
|
||||
stages:
|
||||
- Check
|
||||
- Build
|
||||
- Sign APK
|
||||
- Publish
|
||||
- Release
|
||||
|
||||
Check Style:
|
||||
stage: Check
|
||||
script: ./gradlew -Pqc ktlintCheck
|
||||
cache:
|
||||
# inherit all global cache settings
|
||||
<<: *global_cache
|
||||
policy: pull
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
Static Analysis:
|
||||
stage: Check
|
||||
script: ./gradlew -Pqc detekt
|
||||
cache:
|
||||
# inherit all global cache settings
|
||||
<<: *global_cache
|
||||
policy: pull
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
Lint:
|
||||
stage: Check
|
||||
script: ./gradlew :ultrasonic:lintRelease
|
||||
cache:
|
||||
# inherit all global cache settings
|
||||
<<: *global_cache
|
||||
policy: pull-push
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
Unit Tests:
|
||||
stage: Check
|
||||
script: ./gradlew ciTest testDebugUnitTest
|
||||
cache:
|
||||
# inherit all global cache settings
|
||||
<<: *global_cache
|
||||
policy: pull-push
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
Assemble Release:
|
||||
stage: Build
|
||||
script:
|
||||
- sed -i 's/applicationId \"org.moire.ultrasonic\"/applicationId "org.moire.ultrasonic.gitlab"/' ultrasonic/build.gradle
|
||||
- ./gradlew assembleRelease
|
||||
artifacts:
|
||||
name: ultrasonic-release-unsigned-${CI_COMMIT_SHA}
|
||||
paths:
|
||||
- ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" || $CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_TAG || $CI_PROJECT_ID != $ROOT_PROJECT_ID
|
||||
|
||||
# We generate a signed package for each commit to develop as well as when making a release.
|
||||
# Since the develop signed apk are not persistent they can be downloaded for around 3 weeks before Gitlab deletes them.
|
||||
Generate Signed APK:
|
||||
stage: Sign APK
|
||||
# We don't need the gradle cache here
|
||||
cache: []
|
||||
script:
|
||||
- openssl aes-256-cbc -K ${ULTRASONIC_KEYSTORE_KEY} -iv ${ULTRASONIC_KEYSTORE_IV} -in ultrasonic-keystore.enc -out ultrasonic-keystore -d
|
||||
- mkdir -p ${CI_PROJECT_DIR}/ultrasonic-release
|
||||
- ${ANDROID_HOME}/build-tools/*/zipalign -v 4 ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk ${CI_PROJECT_DIR}/ultrasonic-release/${PACKAGE_APK}
|
||||
- ${ANDROID_HOME}/build-tools/*/apksigner sign --verbose --ks ${CI_PROJECT_DIR}/ultrasonic-keystore --ks-pass pass:${ULTRASONIC_KEYSTORE_STOREPASS} --key-pass pass:${ULTRASONIC_KEYSTORE_KEYPASS} ${CI_PROJECT_DIR}/ultrasonic-release/${PACKAGE_APK}
|
||||
- ${ANDROID_HOME}/build-tools/*/apksigner verify --verbose ${CI_PROJECT_DIR}/ultrasonic-release/${PACKAGE_APK}
|
||||
artifacts:
|
||||
name: $PACKAGE_APK
|
||||
paths:
|
||||
- ultrasonic-release/
|
||||
rules:
|
||||
# Run when releasing a new tag
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||
# Or when adding a new commit to develop (but never inside merge events)
|
||||
- if: $CI_COMMIT_REF_NAME == "develop" && $CI_PROJECT_ID == $ROOT_PROJECT_ID && $CI_PIPELINE_SOURCE != "merge_request_event"
|
||||
variables:
|
||||
PACKAGE_APK: ultrasonic-${CI_COMMIT_SHA}.apk
|
||||
|
||||
|
||||
Publish Signed APK:
|
||||
stage: Publish
|
||||
image: curlimages/curl:latest
|
||||
script:
|
||||
- |
|
||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ultrasonic-release/${PACKAGE_APK} "${PACKAGE_REGISTRY_URL}/${PACKAGE_APK}"
|
||||
- |
|
||||
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file ultrasonic-release/${PACKAGE_APK_IDSIG} "${PACKAGE_REGISTRY_URL}/${PACKAGE_APK_IDSIG}"
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||
|
||||
Release:
|
||||
stage: Release
|
||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
script: |
|
||||
release-cli create --name "Release ${CI_COMMIT_TAG}" --tag-name ${CI_COMMIT_TAG} \
|
||||
--assets-link "{\"name\":\"${PACKAGE_APK}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${PACKAGE_APK}\"}" \
|
||||
--assets-link "{\"name\":\"${PACKAGE_APK_IDSIG}\",\"url\":\"${PACKAGE_REGISTRY_URL}/${PACKAGE_APK_IDSIG}\"}"
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||
|
||||
RoboTest:
|
||||
stage: Release
|
||||
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:latest
|
||||
# We don't need the gradle cache here
|
||||
cache: []
|
||||
script:
|
||||
- curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash
|
||||
- gcloud auth activate-service-account --key-file .secure_files/firebase-key.json
|
||||
- gcloud firebase test android run --project ultrasonic-61089 --type robo --app ultrasonic-release/${PACKAGE_APK} --robo-directives click:button1= --device model=Nexus6,version=21,locale=en,orientation=portrait --device model=Pixel3,version=28,locale=fr,orientation=landscape
|
||||
rules:
|
||||
# Run when releasing a new tag
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||
# or when requested by using [ROBO] inside the commit message and merging to develop
|
||||
# Would be nice to be able to run it in a MR as well, but currently not possible
|
||||
# Because it would not have access to the protected keys.
|
||||
- if: $CI_COMMIT_MESSAGE =~ /^\[ROBO\].*/ && $CI_PROJECT_ID == $ROOT_PROJECT_ID && $CI_COMMIT_REF_NAME == "develop" && $CI_PIPELINE_SOURCE != "merge_request_event"
|
||||
variables:
|
||||
PACKAGE_APK: ultrasonic-${CI_COMMIT_SHA}.apk
|
||||
|
@ -1,35 +0,0 @@
|
||||
## 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 Markdown 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!
|
||||
|
||||
### Please provide a log!
|
||||
|
||||
Many issues are hard to reproduce without a log file. Please enable logging by opening the setting and scrolling to the bottom.
|
||||
There you will find an option to activate loging and to export the file. Please note that the log might contain personal information,
|
||||
such as the name of the tracks you listened to. If you don't want to post it publicly, let us know, and we will find a way.
|
||||
|
||||
|
||||
## System information
|
||||
|
||||
### Ultrasonic client
|
||||
|
||||
* **Ultrasonic version**: *version of the app*
|
||||
* **Android version**: *Version of Android OS on the device*
|
||||
* **Device info**: *Device manufacturer, model*
|
||||
|
||||
### Server
|
||||
|
||||
* **Server name**: *Airsonic, Ampache, Supysonic...*
|
||||
* **Server version**: *version of server software*
|
||||
* **Protocol used**: *http or https (self certificate, letsencrypt...)*
|
||||
|
||||
## Additional notes
|
||||
|
||||
Include any extra notes here. Otherwise you may remove this section.
|
@ -1,32 +0,0 @@
|
||||
## Motivation
|
||||
|
||||
Describe here what motivated you to make this enhancement request. What is
|
||||
currently happening and what would you like to improve. If you have a stack
|
||||
trace or any logs, please format them using Markdown triple backquote
|
||||
notation.
|
||||
|
||||
### Proposal
|
||||
|
||||
Tell us in detail what you propose. How you want to achieve what you would
|
||||
like to improve.
|
||||
|
||||
## System information
|
||||
|
||||
Include this section only if you consider that is relevant to this ticket,
|
||||
otherwise you may remove it.
|
||||
|
||||
### Ultrasonic client
|
||||
|
||||
* **Ultrasonic version**: *version of the app*
|
||||
* **Android version**: *Version of Android OS on the device*
|
||||
* **Device info**: *Device manufacturer, model*
|
||||
|
||||
### Server
|
||||
|
||||
* **Server name**: *Airsonic, Ampache, Supysonic...*
|
||||
* **Server version**: *version of server software*
|
||||
* **Protocol used**: *http or https (self certificate, letsencrypt...)*
|
||||
|
||||
## Additional notes
|
||||
|
||||
Include any extra notes here. Otherwise you may remove this section.
|
@ -1,27 +0,0 @@
|
||||
## Proposal
|
||||
|
||||
Describe your proposed feature request here. Use this template only to
|
||||
request a new feature that does not exist in Ultrasonic. If you are
|
||||
requesting an enhancement to a current feature, please use the enhancement
|
||||
template.
|
||||
|
||||
## System information
|
||||
|
||||
Include this section only if you consider that is relevant to this ticket,
|
||||
otherwise you may remove it.
|
||||
|
||||
### Ultrasonic client
|
||||
|
||||
* **Ultrasonic version**: *version of the app*
|
||||
* **Android version**: *Version of Android OS on the device*
|
||||
* **Device info**: *Device manufacturer, model*
|
||||
|
||||
### Server
|
||||
|
||||
* **Server name**: *Airsonic, Ampache, Supysonic...*
|
||||
* **Server version**: *version of server software*
|
||||
* **Protocol used**: *http or https (self certificate, letsencrypt...)*
|
||||
|
||||
## Additional notes
|
||||
|
||||
Include any extra notes here. Otherwise you may remove this section.
|
@ -1,12 +0,0 @@
|
||||
<!-- Please describe your changes here -->
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
- [ ] I have opened an issue where I discuss the change I wish to make.
|
||||
- [ ] I ran `./gradlew -Pqc ktlintCheck`, `./gradlew -Pqc detekt` and
|
||||
`./gradlew :ultrasonic:lintRelease` and no problems found. See
|
||||
[CONTRIBUTING](CONTRIBUTING.md) for further information.
|
||||
- [ ] All commits [are
|
||||
signed](https://docs.gitlab.com/ee/user/project/repository/gpg_signed_commits/).
|
||||
- [ ] I agree to release my code and all other changes of this MR under the
|
||||
[GNU GPLv3](LICENSE) license.
|
@ -1,10 +0,0 @@
|
||||
#### Before merge:
|
||||
- [ ] MR is targetting the master branch
|
||||
- [ ] **Squash commits must be disabled!**
|
||||
- [ ] RoboTests (5 physical, 10 virtual) on a Release apk return no errors
|
||||
- [ ] Release notes present
|
||||
|
||||
#### After merge
|
||||
- [ ] ``git fetch``
|
||||
- [ ] Create an annotated and signed tag: ``git tag -sa``
|
||||
- [ ] Push the tag to git:``git push --tags``
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:base"],
|
||||
"baseBranches": [ "develop" ],
|
||||
"labels": [ "housekeeping", "dependencies" ]
|
||||
}
|
123
.idea/codeStyles/Project.xml
generated
123
.idea/codeStyles/Project.xml
generated
@ -1,123 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
6
.idea/copyright/Default.xml
generated
6
.idea/copyright/Default.xml
generated
@ -1,6 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="&#36;file.fileName Copyright (C) 2009-&#36;today.year Ultrasonic developers Distributed under terms of the GNU GPLv3 license." />
|
||||
<option name="myName" value="Default" />
|
||||
</copyright>
|
||||
</component>
|
7
.idea/copyright/profiles_settings.xml
generated
7
.idea/copyright/profiles_settings.xml
generated
@ -1,7 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="Default">
|
||||
<LanguageOptions name="Kotlin">
|
||||
<option name="fileTypeOverride" value="3" />
|
||||
</LanguageOptions>
|
||||
</settings>
|
||||
</component>
|
8
.idea/inspectionProfiles/Project_Default.xml
generated
8
.idea/inspectionProfiles/Project_Default.xml
generated
@ -1,8 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Reformat" enabled="false" level="WEAK WARNING" enabled_by_default="false">
|
||||
<option name="processChangedFilesOnly" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
10
.tx/config
Normal file
10
.tx/config
Normal file
@ -0,0 +1,10 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = nl_NL:nl,pl_PL:pl,fr_CA:fr-rCA,pt_BR:pt-rBR,pt_PT:pt,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW,da_DK:da-rDK,de_DE:de,tr_TR:tr,fr_FR:fr,es_ES:es,hu_HU:hu,sv_SE:sv-rSE,bg_BG:bg,el_GR:el,kn_IN:kn-rIN,cs_CZ:cs,sr:sr,he:iw,id:in,lt_LT:lt,km_KH:km-rKH,th_TH:th,ru_RU:ru,it_IT:it
|
||||
|
||||
[ultrasonic.app]
|
||||
file_filter = ultrasonic/src/main/res/values-<lang>/strings.xml
|
||||
source_file = ultrasonic/src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
type = ANDROID
|
||||
|
@ -1,83 +1,35 @@
|
||||
# Contributing
|
||||
|
||||
Ultrasonic development is a community project, and contributions are
|
||||
welcomed.
|
||||
Ultrasonic development is a community project, and contributions are welcomed.
|
||||
|
||||
First, see if your issue haven’t been yet reported
|
||||
[here](https://gitlab.com/ultrasonic/ultrasonic/issues), then, please, first
|
||||
discuss the change you wish to make via [a new
|
||||
issue](https://gitlab.com/ultrasonic/ultrasonic/issues/new).
|
||||
First, see if your issue haven’t 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).
|
||||
|
||||
## Contributing Translations
|
||||
|
||||
Interested in help to translate Ultrasonic? You can contribute in our
|
||||
[Weblate team](https://hosted.weblate.org/projects/ultrasonic/).
|
||||
[Transifex team](https://www.transifex.com/ultrasonic/ultrasonic/).
|
||||
|
||||
## Contributing Code
|
||||
|
||||
By default, merge requests should be opened against the **develop** branch
|
||||
from your own branch, MR against the **master** branch should only be used
|
||||
for critical bug fixes.
|
||||
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. **No Breakage**: New features or changes to existing ones must not
|
||||
degrade the user experience.
|
||||
3. **Coding standards**: best-practices should be followed, comment
|
||||
generously, and avoid "clever" algorithms. Refactoring existing messes is
|
||||
great, but watch out for breakage.
|
||||
4. **No large PR**: Try to limit the scope of PR only to the related issue,
|
||||
so it will be easier to review and test.
|
||||
5. **Make your own branch**: When you send us a merge request, please do it
|
||||
from your own branch. Avoid using the `develop` branch.
|
||||
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.
|
||||
|
||||
### Merge request process
|
||||
### Pull Request Process
|
||||
|
||||
On each merge request GitLab runs a number of checks to make sure there are
|
||||
no problems.
|
||||
|
||||
Take special note of point five of the previous paragraph. Do not use the
|
||||
`develop` branch, make your own. Not following this step will cause your
|
||||
merge request to be rejected without even checking it.
|
||||
|
||||
#### Signed commits
|
||||
|
||||
Commits must be signed. [See here how to set it
|
||||
up](https://docs.gitlab.com/ee/user/project/repository/gpg_signed_commits/).
|
||||
|
||||
#### KtLint
|
||||
|
||||
This programm checks if the source code is formatted correctly. You can run
|
||||
it yourself locally with
|
||||
```
|
||||
./gradlew -Pqc ktlintFormat
|
||||
```
|
||||
Running this command will fix common problems and will notify you of
|
||||
problems it couldn't fix automatically.
|
||||
|
||||
#### Detekt
|
||||
|
||||
Detekt is a static analyser. It helps to find potential bugs in our code.
|
||||
You can run it yourself locally with
|
||||
```
|
||||
./gradlew -Pqc detekt
|
||||
```
|
||||
There is a "baseline" file, in which errors which have been in the code base
|
||||
before are noted. Sometimes it is necessary to regenerate this file by
|
||||
running:
|
||||
```
|
||||
./gradlew -Pqc detektBaseline
|
||||
```
|
||||
|
||||
#### Lint
|
||||
|
||||
Lint looks for general problems in the code or unused resources etc. You can
|
||||
run it with
|
||||
```
|
||||
./gradlew -Pqc lintRelease
|
||||
```
|
||||
If there is a need to regenerate the baseline, remove
|
||||
`ultrasonic/lint-baseline.xml` and rerun the command.
|
||||
1. Ensure [all commits are signed-off](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/about-commit-signature-verification).
|
||||
2. Check tests for the new code are added.
|
||||
3. Check code style is passing.
|
||||
4. Check code static analysis is passing.
|
||||
|
20
ISSUE_TEMPLATE.md
Normal file
20
ISSUE_TEMPLATE.md
Normal file
@ -0,0 +1,20 @@
|
||||
## 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.
|
52
README.md
52
README.md
@ -1,14 +1,14 @@
|
||||
# Ultrasonic
|
||||
[](https://circleci.com/gh/ultrasonic)
|
||||
[]()
|
||||
[](https://ktlint.github.io/)
|
||||
|
||||
Ultrasonic is free and open-source music streaming Android client for
|
||||
[Subsonic][subsonic] [API][subapi] (version 1.7.0 or higher) compatible
|
||||
servers.
|
||||
Ultrasonic is free and open-source music streaming Android client for [Subsonic](http://www.subsonic.org/) [API](http://www.subsonic.org/pages/api.jsp) (version 1.7.0 or higher) compatible servers.
|
||||
|
||||
## Help wanted
|
||||
|
||||
We currently don't have that much time to spend developing Subsonic, so any
|
||||
contributions or active developers are always welcomed.
|
||||
Have a look at [CONTRIBUTING](CONTRIBUTING.md) to get started.
|
||||
|
||||
## Download
|
||||
|
||||
@ -16,20 +16,22 @@ App is available to download at following stores:
|
||||
|
||||
[<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/)
|
||||
[<img src="https://ultrasonic.gitlab.io/assets/img/get-it-on-gitlab.png" alt="Get it on GitLab" height="70">](https://gitlab.com/ultrasonic/ultrasonic/-/releases)
|
||||
[<img src="https://ultrasonic.github.io/assets/img/get-it-on-github.png" alt="Get it on GitHub" height="70">](https://github.com/ultrasonic/ultrasonic/releases)
|
||||
|
||||
**Warning**: All three versions (Google Play, F-Droid and the APKs) are not
|
||||
compatible (not signed by the same key)! You must uninstall one to install
|
||||
the other, which will delete all your data.
|
||||
|
||||
If you want to use the version downloaded from F-Droid or from GitLab with
|
||||
**Android Auto**, you must enable Unknown Sources as it is described in
|
||||
[this wiki page][wikiaa].
|
||||
|
||||
## Bugs and issues
|
||||
|
||||
First, see if your issue haven’t been yet reported [here][issues], otherwise
|
||||
open [a new issue][newissue].
|
||||
First, see if your issue haven’t been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
|
||||
otherwise open [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
|
||||
|
||||
### Known (not our) bugs
|
||||
|
||||
If you are using *Madsonic 5.1.X* several sections of Ultrasonic will not
|
||||
work. This is caused by bad implementation of Subsonic API by Madsonic. For
|
||||
more info about this you can read [this bug](https://github.com/ultrasonic/ultrasonic/issues/129).
|
||||
|
||||
## Contributing
|
||||
|
||||
@ -37,28 +39,16 @@ See [CONTRIBUTING](CONTRIBUTING.md).
|
||||
|
||||
## Supported (tested) Subsonic API implementations
|
||||
|
||||
- [Subsonic][subsonic]
|
||||
- [Airsonic-Advanced][airsonic]
|
||||
- [Supysonic][supysonic]
|
||||
- [Ampache][ampache]
|
||||
- [Subsonic](http://www.subsonic.org/pages/index.jsp)
|
||||
- [Airsonic](https://github.com/airsonic/airsonic)
|
||||
- [Supysonic](https://github.com/spl0k/supysonic)
|
||||
- [Ampache](https://ampache.org/)
|
||||
|
||||
Other *Subsonic API* implementations should work as well as long as they
|
||||
follow API [documentation][subapi].
|
||||
Other *Subsonic API* implementations should work as well as long as they follow API
|
||||
[documentation](http://www.subsonic.org/pages/api.jsp).
|
||||
|
||||
## License
|
||||
|
||||
This software is licensed under the terms of the GNU General Public License
|
||||
version 3 (GPLv3).
|
||||
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][gpl3].
|
||||
|
||||
[wikiaa]: https://gitlab.com/ultrasonic/ultrasonic/-/wikis/Using-Ultrasonic-with-Android-Auto
|
||||
[issues]: https://gitlab.com/ultrasonic/ultrasonic/-/issues
|
||||
[newissue]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/new
|
||||
[subsonic]: http://www.subsonic.org/
|
||||
[subapi]: http://www.subsonic.org/pages/api.jsp
|
||||
[airsonic]: https://github.com/airsonic-advanced/airsonic-advanced
|
||||
[supysonic]: https://github.com/spl0k/supysonic
|
||||
[ampache]: https://ampache.org/
|
||||
[gpl3]: https://opensource.org/licenses/gpl-3.0.html
|
||||
Full text of the license is available in the [LICENSE](LICENSE) file and [online](https://opensource.org/licenses/gpl-3.0.html).
|
||||
|
38
build.gradle
38
build.gradle
@ -1,8 +1,6 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
apply from: 'gradle/versions.gradle'
|
||||
apply from: 'dependencies.gradle'
|
||||
|
||||
ext.bootstrap = [
|
||||
kotlinModule : "${project.rootDir}/gradle_scripts/kotlin-module-bootstrap.gradle",
|
||||
@ -12,15 +10,14 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url = "https://plugins.gradle.org/m2/" }
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
}
|
||||
dependencies {
|
||||
classpath libs.gradle
|
||||
classpath libs.kotlin
|
||||
classpath libs.ktlintGradle
|
||||
classpath libs.detekt
|
||||
classpath libs.navigationSafeArgs
|
||||
classpath gradlePlugins.gradle
|
||||
classpath gradlePlugins.kotlin
|
||||
classpath gradlePlugins.ktlintGradle
|
||||
classpath gradlePlugins.detekt
|
||||
classpath gradlePlugins.jacoco
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,32 +26,27 @@ allprojects {
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
// Set Kotlin JVM target to the same for all subprojects
|
||||
tasks.withType(KotlinCompile).configureEach {
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "21"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).tap {
|
||||
configureEach {
|
||||
options.compilerArgs.add("-Xlint:deprecation")
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply from: 'gradle_scripts/jacoco.gradle'
|
||||
|
||||
wrapper {
|
||||
gradleVersion = libs.versions.gradle.get()
|
||||
distributionType = "all"
|
||||
}
|
||||
gradleVersion(versions.gradle)
|
||||
distributionType("all")
|
||||
}
|
||||
|
12
core/cache/build.gradle
vendored
Normal file
12
core/cache/build.gradle
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
apply from: bootstrap.kotlinModule
|
||||
|
||||
dependencies {
|
||||
api project(':core:domain')
|
||||
api other.twitterSerial
|
||||
|
||||
testImplementation testing.kotlinJunit
|
||||
testImplementation testing.mockito
|
||||
testImplementation testing.mockitoInline
|
||||
testImplementation testing.mockitoKotlin
|
||||
testImplementation testing.kluent
|
||||
}
|
14
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt
vendored
Normal file
14
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
package org.moire.ultrasonic.cache
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Provides access to generic directories:
|
||||
* - for temporary caches
|
||||
* - for permanent data storage
|
||||
*/
|
||||
interface Directories {
|
||||
fun getInternalCacheDir(): File
|
||||
fun getInternalDataDir(): File
|
||||
fun getExternalCacheDir(): File?
|
||||
}
|
75
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt
vendored
Normal file
75
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
package org.moire.ultrasonic.cache
|
||||
|
||||
import com.twitter.serial.serializer.SerializationContext
|
||||
import com.twitter.serial.serializer.Serializer
|
||||
import com.twitter.serial.stream.Serial
|
||||
import com.twitter.serial.stream.bytebuffer.ByteBufferSerial
|
||||
import java.io.File
|
||||
|
||||
typealias DomainEntitySerializer<T> = Serializer<T>
|
||||
|
||||
internal const val STORAGE_DIR_NAME = "persistent_storage"
|
||||
|
||||
/**
|
||||
* Provides access to permanent file based storage.
|
||||
*
|
||||
* [serverId] is currently active server. Should be unique per server so stored data will not
|
||||
* interfere with other server data.
|
||||
*
|
||||
* Look at [org.moire.ultrasonic.cache.serializers] package for available [DomainEntitySerializer]s.
|
||||
*/
|
||||
class PermanentFileStorage(
|
||||
private val directories: Directories,
|
||||
private val serverId: String,
|
||||
private val debug: Boolean = false
|
||||
) {
|
||||
private val serializationContext = object : SerializationContext {
|
||||
override fun isDebug(): Boolean = debug
|
||||
override fun isRelease(): Boolean = !debug
|
||||
}
|
||||
|
||||
private val serializer: Serial = ByteBufferSerial(serializationContext)
|
||||
|
||||
/**
|
||||
* Stores given [objectToStore] using [name] as a key and [objectSerializer] as serializer.
|
||||
*/
|
||||
fun <T> store(
|
||||
name: String,
|
||||
objectToStore: T,
|
||||
objectSerializer: DomainEntitySerializer<T>
|
||||
) {
|
||||
val storeFile = getFile(name)
|
||||
if (!storeFile.exists()) storeFile.createNewFile()
|
||||
storeFile.writeBytes(serializer.toByteArray(objectToStore, objectSerializer))
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads object with [name] key using [objectDeserializer] deserializer.
|
||||
*/
|
||||
fun <T> load(
|
||||
name: String,
|
||||
objectDeserializer: DomainEntitySerializer<T>
|
||||
): T? {
|
||||
val storeFile = getFile(name)
|
||||
if (!storeFile.exists()) return null
|
||||
|
||||
return serializer.fromByteArray(storeFile.readBytes(), objectDeserializer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all files in storage.
|
||||
*/
|
||||
fun clearAll() {
|
||||
val storageDir = getStorageDir()
|
||||
storageDir.listFiles().forEach { it.deleteRecursively() }
|
||||
}
|
||||
|
||||
private fun getFile(name: String) = File(getStorageDir(), "$name.ser")
|
||||
|
||||
private fun getStorageDir(): File {
|
||||
val mainDir = File(directories.getInternalDataDir(), STORAGE_DIR_NAME)
|
||||
val serverDir = File(mainDir, serverId)
|
||||
if (!serverDir.exists()) serverDir.mkdirs()
|
||||
return serverDir
|
||||
}
|
||||
}
|
65
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt
vendored
Normal file
65
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("DomainSerializers")
|
||||
package org.moire.ultrasonic.cache.serializers
|
||||
|
||||
import com.twitter.serial.serializer.CollectionSerializers
|
||||
import com.twitter.serial.serializer.ObjectSerializer
|
||||
import com.twitter.serial.serializer.SerializationContext
|
||||
import com.twitter.serial.stream.SerializerDefs
|
||||
import com.twitter.serial.stream.SerializerInput
|
||||
import com.twitter.serial.stream.SerializerOutput
|
||||
import org.moire.ultrasonic.cache.DomainEntitySerializer
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
|
||||
private const val SERIALIZER_VERSION = 1
|
||||
|
||||
private val artistSerializer get() = object : ObjectSerializer<Artist>(SERIALIZER_VERSION) {
|
||||
override fun serializeObject(
|
||||
context: SerializationContext,
|
||||
output: SerializerOutput<out SerializerOutput<*>>,
|
||||
item: Artist
|
||||
) {
|
||||
output.writeString(item.id)
|
||||
.writeString(item.name)
|
||||
.writeString(item.index)
|
||||
.writeString(item.coverArt)
|
||||
.apply {
|
||||
val albumCount = item.albumCount
|
||||
if (albumCount != null) writeLong(albumCount) else writeNull()
|
||||
}
|
||||
.writeInt(item.closeness)
|
||||
}
|
||||
|
||||
override fun deserializeObject(
|
||||
context: SerializationContext,
|
||||
input: SerializerInput,
|
||||
versionNumber: Int
|
||||
): Artist? {
|
||||
if (versionNumber != SERIALIZER_VERSION) return null
|
||||
|
||||
val id = input.readString()
|
||||
val name = input.readString()
|
||||
val index = input.readString()
|
||||
val coverArt = input.readString()
|
||||
val albumCount = if (input.peekType() == SerializerDefs.TYPE_NULL) {
|
||||
input.readNull()
|
||||
null
|
||||
} else {
|
||||
input.readLong()
|
||||
}
|
||||
val closeness = input.readInt()
|
||||
return Artist(id, name, index, coverArt, albumCount, closeness)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializer/deserializer for [Artist] domain entity.
|
||||
*/
|
||||
fun getArtistsSerializer(): DomainEntitySerializer<Artist> = artistSerializer
|
||||
|
||||
private val artistListSerializer = CollectionSerializers.getListSerializer(artistSerializer)
|
||||
|
||||
/**
|
||||
* Serializer/deserializer for list of [Artist] domain entities.
|
||||
*/
|
||||
fun getArtistListSerializer(): DomainEntitySerializer<List<Artist>> = artistListSerializer
|
51
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt
vendored
Normal file
51
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("DomainSerializers")
|
||||
package org.moire.ultrasonic.cache.serializers
|
||||
|
||||
import com.twitter.serial.serializer.ObjectSerializer
|
||||
import com.twitter.serial.serializer.SerializationContext
|
||||
import com.twitter.serial.stream.SerializerInput
|
||||
import com.twitter.serial.stream.SerializerOutput
|
||||
import org.moire.ultrasonic.cache.DomainEntitySerializer
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.Indexes
|
||||
|
||||
private const val SERIALIZATION_VERSION = 1
|
||||
|
||||
private val indexesSerializer get() = object : ObjectSerializer<Indexes>(SERIALIZATION_VERSION) {
|
||||
override fun serializeObject(
|
||||
context: SerializationContext,
|
||||
output: SerializerOutput<out SerializerOutput<*>>,
|
||||
item: Indexes
|
||||
) {
|
||||
val artistListSerializer = getArtistListSerializer()
|
||||
output.writeLong(item.lastModified)
|
||||
.writeString(item.ignoredArticles)
|
||||
.writeObject<MutableList<Artist>>(context, item.shortcuts, artistListSerializer)
|
||||
.writeObject<MutableList<Artist>>(context, item.artists, artistListSerializer)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun deserializeObject(
|
||||
context: SerializationContext,
|
||||
input: SerializerInput,
|
||||
versionNumber: Int
|
||||
): Indexes? {
|
||||
if (versionNumber != SERIALIZATION_VERSION) return null
|
||||
|
||||
val artistListDeserializer = getArtistListSerializer()
|
||||
val lastModified = input.readLong()
|
||||
val ignoredArticles = input.readString() ?: return null
|
||||
val shortcutsList = input.readObject(context, artistListDeserializer) ?: return null
|
||||
val artistsList = input.readObject(context, artistListDeserializer) ?: return null
|
||||
return Indexes(
|
||||
lastModified, ignoredArticles, shortcutsList.toMutableList(),
|
||||
artistsList.toMutableList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get serializer/deserializer for [Indexes] entity.
|
||||
*/
|
||||
fun getIndexesSerializer(): DomainEntitySerializer<Indexes> = indexesSerializer
|
51
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt
vendored
Normal file
51
core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
@file:JvmMultifileClass
|
||||
@file:JvmName("DomainSerializers")
|
||||
package org.moire.ultrasonic.cache.serializers
|
||||
|
||||
import com.twitter.serial.serializer.CollectionSerializers
|
||||
import com.twitter.serial.serializer.ObjectSerializer
|
||||
import com.twitter.serial.serializer.SerializationContext
|
||||
import com.twitter.serial.stream.SerializerInput
|
||||
import com.twitter.serial.stream.SerializerOutput
|
||||
import org.moire.ultrasonic.cache.DomainEntitySerializer
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
|
||||
private const val SERIALIZATION_VERSION = 1
|
||||
|
||||
private val musicFolderSerializer = object : ObjectSerializer<MusicFolder>(SERIALIZATION_VERSION) {
|
||||
|
||||
override fun serializeObject(
|
||||
context: SerializationContext,
|
||||
output: SerializerOutput<out SerializerOutput<*>>,
|
||||
item: MusicFolder
|
||||
) {
|
||||
output.writeString(item.id).writeString(item.name)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun deserializeObject(
|
||||
context: SerializationContext,
|
||||
input: SerializerInput,
|
||||
versionNumber: Int
|
||||
): MusicFolder? {
|
||||
if (versionNumber != SERIALIZATION_VERSION) return null
|
||||
|
||||
val id = input.readString() ?: return null
|
||||
val name = input.readString() ?: return null
|
||||
return MusicFolder(id, name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializer/deserializer for [MusicFolder] domain entity.
|
||||
*/
|
||||
fun getMusicFolderSerializer(): DomainEntitySerializer<MusicFolder> = musicFolderSerializer
|
||||
|
||||
private val musicFolderListSerializer =
|
||||
CollectionSerializers.getListSerializer(musicFolderSerializer)
|
||||
|
||||
/**
|
||||
* Serializer/deserializer for [List] of [MusicFolder] items.
|
||||
*/
|
||||
fun getMusicFolderListSerializer(): DomainEntitySerializer<List<MusicFolder>> =
|
||||
musicFolderListSerializer
|
42
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt
vendored
Normal file
42
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
package org.moire.ultrasonic.cache
|
||||
|
||||
import com.twitter.serial.util.SerializationUtils
|
||||
import java.io.File
|
||||
import org.amshove.kluent.`it returns`
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.mockito.kotlin.mock
|
||||
|
||||
internal const val INTERNAL_DATA_FOLDER = "data"
|
||||
internal const val INTERNAL_CACHE_FOLDER = "cache"
|
||||
internal const val EXTERNAL_CACHE_FOLDER = "external_cache"
|
||||
|
||||
/**
|
||||
* Base test class that inits the storage
|
||||
*/
|
||||
abstract class BaseStorageTest {
|
||||
@get:Rule val tempFileRule = TemporaryFolder()
|
||||
|
||||
protected lateinit var mockDirectories: Directories
|
||||
protected lateinit var storage: PermanentFileStorage
|
||||
|
||||
open val serverId: String = ""
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockDirectories = mock<Directories> {
|
||||
on { getInternalDataDir() } `it returns` tempFileRule.newFolder(INTERNAL_DATA_FOLDER)
|
||||
on { getInternalCacheDir() } `it returns` tempFileRule.newFolder(INTERNAL_CACHE_FOLDER)
|
||||
on { getExternalCacheDir() } `it returns` tempFileRule.newFolder(EXTERNAL_CACHE_FOLDER)
|
||||
}
|
||||
storage = PermanentFileStorage(mockDirectories, serverId, true)
|
||||
}
|
||||
|
||||
protected val storageDir get() = File(mockDirectories.getInternalDataDir(), STORAGE_DIR_NAME)
|
||||
|
||||
protected fun validateSerializedData(index: Int = 0) {
|
||||
val serializedFileBytes = storageDir.listFiles()[index].readBytes()
|
||||
SerializationUtils.validateSerializedData(serializedFileBytes)
|
||||
}
|
||||
}
|
80
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt
vendored
Normal file
80
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
package org.moire.ultrasonic.cache
|
||||
|
||||
import java.io.File
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should contain`
|
||||
import org.junit.Test
|
||||
import org.moire.ultrasonic.cache.serializers.getMusicFolderSerializer
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
|
||||
/**
|
||||
* Integration test for [PermanentFileStorage].
|
||||
*/
|
||||
class PermanentFileStorageTest : BaseStorageTest() {
|
||||
override val serverId: String
|
||||
get() = "some-server-id"
|
||||
|
||||
@Test
|
||||
fun `Should create storage dir if it is not exist`() {
|
||||
val item = MusicFolder("1", "2")
|
||||
storage.store("test", item, getMusicFolderSerializer())
|
||||
|
||||
storageDir.exists() `should be equal to` true
|
||||
getServerStorageDir().exists() `should be equal to` true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should serialize to file`() {
|
||||
val item = MusicFolder("1", "23")
|
||||
val name = "some-name"
|
||||
|
||||
storage.store(name, item, getMusicFolderSerializer())
|
||||
|
||||
val storageFiles = getServerStorageDir().listFiles()
|
||||
storageFiles.size `should be equal to` 1
|
||||
storageFiles[0].name `should contain` name
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should deserialize stored object`() {
|
||||
val item = MusicFolder("some", "nice")
|
||||
val name = "some-name"
|
||||
storage.store(name, item, getMusicFolderSerializer())
|
||||
|
||||
val loadedItem = storage.load(name, getMusicFolderSerializer())
|
||||
|
||||
loadedItem `should be equal to` item
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should overwrite existing stored object`() {
|
||||
val name = "some-nice-name"
|
||||
val item1 = MusicFolder("1", "1")
|
||||
val item2 = MusicFolder("2", "2")
|
||||
storage.store(name, item1, getMusicFolderSerializer())
|
||||
storage.store(name, item2, getMusicFolderSerializer())
|
||||
|
||||
val loadedItem = storage.load(name, getMusicFolderSerializer())
|
||||
|
||||
loadedItem `should be equal to` item2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should clear all files when clearAll is called`() {
|
||||
storage.store("name1", MusicFolder("1", "1"), getMusicFolderSerializer())
|
||||
storage.store("name2", MusicFolder("2", "2"), getMusicFolderSerializer())
|
||||
|
||||
storage.clearAll()
|
||||
|
||||
getServerStorageDir().listFiles().size `should be equal to` 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should return null if serialized file not available`() {
|
||||
val loadedItem = storage.load("some-name", getMusicFolderSerializer())
|
||||
|
||||
loadedItem `should be equal to` null
|
||||
}
|
||||
|
||||
private fun getServerStorageDir() = File(storageDir, serverId)
|
||||
}
|
57
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt
vendored
Normal file
57
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
package org.moire.ultrasonic.cache.serializers
|
||||
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.junit.Test
|
||||
import org.moire.ultrasonic.cache.BaseStorageTest
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
|
||||
/**
|
||||
* [Artist] serializers test.
|
||||
*/
|
||||
class ArtistSerializerTest : BaseStorageTest() {
|
||||
@Test
|
||||
fun `Should correctly serialize Artist object`() {
|
||||
val item = Artist("id", "name", "index", "coverArt", 1, 0)
|
||||
|
||||
storage.store("some-name", item, getArtistsSerializer())
|
||||
|
||||
validateSerializedData()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should correctly deserialize Artist object`() {
|
||||
val itemName = "some-name"
|
||||
val item = Artist("id", "name", "index", "coverArt", null, 0)
|
||||
storage.store(itemName, item, getArtistsSerializer())
|
||||
|
||||
val loadedItem = storage.load(itemName, getArtistsSerializer())
|
||||
|
||||
loadedItem `should be equal to` item
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should correctly serialize list of Artists`() {
|
||||
val itemsList = listOf(
|
||||
Artist(id = "1"),
|
||||
Artist(id = "2", name = "some")
|
||||
)
|
||||
|
||||
storage.store("some-name", itemsList, getArtistListSerializer())
|
||||
|
||||
validateSerializedData()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should correctly deserialize list of Artists`() {
|
||||
val name = "some-name"
|
||||
val itemsList = listOf(
|
||||
Artist(id = "1"),
|
||||
Artist(id = "2", name = "some")
|
||||
)
|
||||
storage.store(name, itemsList, getArtistListSerializer())
|
||||
|
||||
val loadedItems = storage.load(name, getArtistListSerializer())
|
||||
|
||||
loadedItems `should be equal to` itemsList
|
||||
}
|
||||
}
|
38
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt
vendored
Normal file
38
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
package org.moire.ultrasonic.cache.serializers
|
||||
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.junit.Test
|
||||
import org.moire.ultrasonic.cache.BaseStorageTest
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.Indexes
|
||||
|
||||
/**
|
||||
* Test [Indexes] domain entity serializer.
|
||||
*/
|
||||
class IndexesSerializerTest : BaseStorageTest() {
|
||||
@Test
|
||||
fun `Should correctly serialize Indexes object`() {
|
||||
val item = Indexes(
|
||||
220L, "", mutableListOf(Artist("12")),
|
||||
mutableListOf(Artist("233", "some"))
|
||||
)
|
||||
|
||||
storage.store("some-name", item, getIndexesSerializer())
|
||||
|
||||
validateSerializedData()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should correctly deserialize Indexes object`() {
|
||||
val name = "some-name"
|
||||
val item = Indexes(
|
||||
220L, "", mutableListOf(Artist("12")),
|
||||
mutableListOf(Artist("233", "some"))
|
||||
)
|
||||
storage.store(name, item, getIndexesSerializer())
|
||||
|
||||
val loadedItem = storage.load(name, getIndexesSerializer())
|
||||
|
||||
loadedItem `should be equal to` item
|
||||
}
|
||||
}
|
57
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt
vendored
Normal file
57
core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
package org.moire.ultrasonic.cache.serializers
|
||||
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.junit.Test
|
||||
import org.moire.ultrasonic.cache.BaseStorageTest
|
||||
import org.moire.ultrasonic.domain.MusicFolder
|
||||
|
||||
/**
|
||||
* [MusicFolder] serializers test.
|
||||
*/
|
||||
class MusicFolderSerializerTest : BaseStorageTest() {
|
||||
@Test
|
||||
fun `Should correctly serialize MusicFolder object`() {
|
||||
val item = MusicFolder("Music", "Folder")
|
||||
|
||||
storage.store("some-name", item, getMusicFolderSerializer())
|
||||
|
||||
validateSerializedData()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should correctly deserialize MusicFolder object`() {
|
||||
val name = "name"
|
||||
val item = MusicFolder("some", "none")
|
||||
storage.store(name, item, getMusicFolderSerializer())
|
||||
|
||||
val loadedItem = storage.load(name, getMusicFolderSerializer())
|
||||
|
||||
loadedItem `should be equal to` item
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should correctly serialize list of MusicFolders objects`() {
|
||||
val itemsList = listOf(
|
||||
MusicFolder("1", "1"),
|
||||
MusicFolder("2", "2")
|
||||
)
|
||||
|
||||
storage.store("some-name", itemsList, getMusicFolderListSerializer())
|
||||
|
||||
validateSerializedData()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Should correctly deserialize list of MusicFolder objects`() {
|
||||
val name = "some-name"
|
||||
val itemsList = listOf(
|
||||
MusicFolder("1", "1"),
|
||||
MusicFolder("2", "2")
|
||||
)
|
||||
storage.store(name, itemsList, getMusicFolderListSerializer())
|
||||
|
||||
val loadedItem = storage.load(name, getMusicFolderListSerializer())
|
||||
|
||||
loadedItem `should be equal to` itemsList
|
||||
}
|
||||
}
|
@ -1,20 +1,7 @@
|
||||
plugins {
|
||||
alias libs.plugins.ksp
|
||||
}
|
||||
apply from: bootstrap.kotlinModule
|
||||
|
||||
apply from: bootstrap.androidModule
|
||||
|
||||
dependencies {
|
||||
implementation libs.core
|
||||
implementation libs.roomRuntime
|
||||
implementation libs.roomKtx
|
||||
ksp libs.room
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = 'org.moire.ultrasonic.subsonic.domain'
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_21
|
||||
targetCompatibility JavaVersion.VERSION_21
|
||||
}
|
||||
ext {
|
||||
jacocoExclude = [
|
||||
'**/domain/**'
|
||||
]
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Album.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import java.util.Date
|
||||
|
||||
@Entity(tableName = "albums", primaryKeys = ["id", "serverId"])
|
||||
data class Album(
|
||||
override var id: String,
|
||||
@ColumnInfo(defaultValue = "-1")
|
||||
override var serverId: Int = -1,
|
||||
override var parent: String? = null,
|
||||
override var album: String? = null,
|
||||
override var title: String? = null,
|
||||
override val name: String? = null,
|
||||
override var discNumber: Int? = 0,
|
||||
override var coverArt: String? = null,
|
||||
override var songCount: Long? = null,
|
||||
override var created: Date? = null,
|
||||
override var artist: String? = null,
|
||||
override var artistId: String? = null,
|
||||
override var duration: Int? = 0,
|
||||
override var year: Int? = 0,
|
||||
override var genre: String? = null,
|
||||
override var starred: Boolean = false,
|
||||
override var path: String? = null,
|
||||
override var closeness: Int = 0
|
||||
) : MusicDirectory.Child() {
|
||||
override var isDirectory = true
|
||||
override var isVideo = false
|
||||
}
|
@ -1,23 +1,30 @@
|
||||
/*
|
||||
* Artist.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity(tableName = "artists", primaryKeys = ["id", "serverId"])
|
||||
data class Artist(
|
||||
override var id: String,
|
||||
@ColumnInfo(defaultValue = "-1")
|
||||
override var serverId: Int = -1,
|
||||
override var id: String? = null,
|
||||
override var name: String? = null,
|
||||
override var index: String? = null,
|
||||
override var coverArt: String? = null,
|
||||
override var albumCount: Long? = null,
|
||||
override var closeness: Int = 0
|
||||
) : ArtistOrIndex(id, serverId)
|
||||
var index: String? = null,
|
||||
var coverArt: String? = null,
|
||||
var albumCount: Long? = null,
|
||||
var closeness: Int = 0
|
||||
) : Serializable, GenericEntry(), Comparable<Artist> {
|
||||
companion object {
|
||||
private const val serialVersionUID = -5790532593784846982L
|
||||
}
|
||||
|
||||
override fun compareTo(other: Artist): Int {
|
||||
when {
|
||||
this.closeness == other.closeness -> {
|
||||
return 0
|
||||
}
|
||||
this.closeness > other.closeness -> {
|
||||
return -1
|
||||
}
|
||||
else -> {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* ArtistOrIndex.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.Ignore
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
abstract class ArtistOrIndex(
|
||||
@Ignore
|
||||
override var id: String,
|
||||
@Ignore
|
||||
open var serverId: Int,
|
||||
@Ignore
|
||||
override var name: String? = null,
|
||||
@Ignore
|
||||
open var index: String? = null,
|
||||
@Ignore
|
||||
open var coverArt: String? = null,
|
||||
@Ignore
|
||||
open var albumCount: Long? = null,
|
||||
@Ignore
|
||||
open var closeness: Int = 0
|
||||
) : GenericEntry() {
|
||||
|
||||
fun compareTo(other: ArtistOrIndex): Int {
|
||||
return when {
|
||||
this.closeness == other.closeness -> {
|
||||
0
|
||||
}
|
||||
this.closeness > other.closeness -> {
|
||||
-1
|
||||
}
|
||||
else -> {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: Identifiable) = compareTo(other as ArtistOrIndex)
|
||||
}
|
@ -2,6 +2,7 @@ package org.moire.ultrasonic.domain
|
||||
|
||||
import java.io.Serializable
|
||||
import java.util.Date
|
||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
||||
|
||||
data class Bookmark(
|
||||
val position: Int = 0,
|
||||
@ -9,7 +10,7 @@ data class Bookmark(
|
||||
val comment: String,
|
||||
val created: Date? = null,
|
||||
val changed: Date? = null,
|
||||
val track: Track
|
||||
val entry: Entry
|
||||
) : Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID = 8988990025189807803L
|
||||
|
@ -0,0 +1,19 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
abstract class GenericEntry {
|
||||
// TODO: Should be non-null!
|
||||
abstract val id: String?
|
||||
open val name: String? = null
|
||||
|
||||
// These are just a formality and will never be called,
|
||||
// because Kotlin data classes will have autogenerated equals() and hashCode() functions
|
||||
override operator fun equals(other: Any?): Boolean {
|
||||
return this === other
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id?.hashCode() ?: 0
|
||||
result = 31 * result + (name?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
@ -1,13 +1,10 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity
|
||||
data class Genre(
|
||||
@PrimaryKey val index: String,
|
||||
val name: String
|
||||
val name: String,
|
||||
val index: String
|
||||
) : Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID = -3943025175219134028L
|
||||
|
@ -1,19 +0,0 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.Ignore
|
||||
|
||||
abstract class GenericEntry : Identifiable {
|
||||
@Ignore
|
||||
open val name: String? = null
|
||||
}
|
||||
|
||||
interface Identifiable : Comparable<Identifiable> {
|
||||
val id: String
|
||||
|
||||
val longId: Long
|
||||
get() = id.hashCode().toLong()
|
||||
|
||||
override fun compareTo(other: Identifiable): Int {
|
||||
return longId.compareTo(other.longId)
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Index.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
|
||||
@Entity(tableName = "indexes", primaryKeys = ["id", "serverId"])
|
||||
data class Index(
|
||||
override var id: String,
|
||||
@ColumnInfo(defaultValue = "-1")
|
||||
override var serverId: Int = -1,
|
||||
override var name: String? = null,
|
||||
override var index: String? = null,
|
||||
override var coverArt: String? = null,
|
||||
override var albumCount: Long? = null,
|
||||
override var closeness: Int = 0,
|
||||
var musicFolderId: String? = null
|
||||
) : ArtistOrIndex(id, serverId)
|
@ -0,0 +1,14 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class Indexes(
|
||||
val lastModified: Long,
|
||||
val ignoredArticles: String,
|
||||
val shortcuts: MutableList<Artist> = mutableListOf(),
|
||||
val artists: MutableList<Artist> = mutableListOf()
|
||||
) : Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID = 8156117238598414701L
|
||||
}
|
||||
}
|
@ -1,58 +1,92 @@
|
||||
/*
|
||||
* MusicDirectory.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import java.io.Serializable
|
||||
import java.util.Date
|
||||
|
||||
class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
||||
class MusicDirectory {
|
||||
var name: String? = null
|
||||
private val children = mutableListOf<Entry>()
|
||||
|
||||
fun addAll(entries: Collection<Entry>) {
|
||||
children.addAll(entries)
|
||||
}
|
||||
|
||||
fun addFirst(child: Entry) {
|
||||
children.add(0, child)
|
||||
}
|
||||
|
||||
fun addChild(child: Entry) {
|
||||
children.add(child)
|
||||
}
|
||||
|
||||
fun findChild(id: String): Entry? = children.lastOrNull { it.id == id }
|
||||
|
||||
fun getAllChild(): List<Entry> = children.toList()
|
||||
|
||||
@JvmOverloads
|
||||
fun getChildren(includeDirs: Boolean = true, includeFiles: Boolean = true): List<Child> {
|
||||
fun getChildren(
|
||||
includeDirs: Boolean = true,
|
||||
includeFiles: Boolean = true
|
||||
): List<Entry> {
|
||||
if (includeDirs && includeFiles) {
|
||||
return toList()
|
||||
return children
|
||||
}
|
||||
|
||||
return filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
|
||||
return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
|
||||
}
|
||||
|
||||
fun getTracks(): List<Track> {
|
||||
return mapNotNull {
|
||||
it as? Track
|
||||
data class Entry(
|
||||
override var id: String,
|
||||
var parent: String? = null,
|
||||
var isDirectory: Boolean = false,
|
||||
var title: String? = null,
|
||||
var album: String? = null,
|
||||
var albumId: String? = null,
|
||||
var artist: String? = null,
|
||||
var artistId: String? = null,
|
||||
var track: Int? = 0,
|
||||
var year: Int? = 0,
|
||||
var genre: String? = null,
|
||||
var contentType: String? = null,
|
||||
var suffix: String? = null,
|
||||
var transcodedContentType: String? = null,
|
||||
var transcodedSuffix: String? = null,
|
||||
var coverArt: String? = null,
|
||||
var size: Long? = null,
|
||||
var songCount: Long? = null,
|
||||
var duration: Int? = null,
|
||||
var bitRate: Int? = null,
|
||||
var path: String? = null,
|
||||
var isVideo: Boolean = false,
|
||||
var starred: Boolean = false,
|
||||
var discNumber: Int? = null,
|
||||
var type: String? = null,
|
||||
var created: Date? = null,
|
||||
var closeness: Int = 0,
|
||||
var bookmarkPosition: Int = 0,
|
||||
var userRating: Int? = null,
|
||||
var averageRating: Float? = null
|
||||
) : Serializable, GenericEntry(), Comparable<Entry> {
|
||||
fun setDuration(duration: Long) {
|
||||
this.duration = duration.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAlbums(): List<Album> {
|
||||
return mapNotNull {
|
||||
it as? Album
|
||||
companion object {
|
||||
private const val serialVersionUID = -3339106650010798108L
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Child : GenericEntry() {
|
||||
abstract override var id: String
|
||||
abstract var serverId: Int
|
||||
abstract var parent: String?
|
||||
abstract var isDirectory: Boolean
|
||||
abstract var album: String?
|
||||
abstract var title: String?
|
||||
abstract override val name: String?
|
||||
abstract var discNumber: Int?
|
||||
abstract var coverArt: String?
|
||||
abstract var songCount: Long?
|
||||
abstract var created: Date?
|
||||
abstract var artist: String?
|
||||
abstract var artistId: String?
|
||||
abstract var duration: Int?
|
||||
abstract var year: Int?
|
||||
abstract var genre: String?
|
||||
abstract var starred: Boolean
|
||||
abstract var path: String?
|
||||
abstract var closeness: Int
|
||||
abstract var isVideo: Boolean
|
||||
override fun compareTo(other: Entry): Int {
|
||||
when {
|
||||
this.closeness == other.closeness -> {
|
||||
return 0
|
||||
}
|
||||
this.closeness > other.closeness -> {
|
||||
return -1
|
||||
}
|
||||
else -> {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,9 @@
|
||||
/*
|
||||
* MusicFolder.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
|
||||
/**
|
||||
* Represents a top level directory in which music or other media is stored.
|
||||
*/
|
||||
@Entity(tableName = "music_folders", primaryKeys = ["id", "serverId"])
|
||||
data class MusicFolder(
|
||||
override val id: String,
|
||||
override val name: String,
|
||||
@ColumnInfo(defaultValue = "-1")
|
||||
var serverId: Int
|
||||
override val name: String
|
||||
) : GenericEntry()
|
||||
|
@ -0,0 +1,12 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
enum class PlayerState {
|
||||
IDLE,
|
||||
DOWNLOADING,
|
||||
PREPARING,
|
||||
PREPARED,
|
||||
STARTED,
|
||||
STOPPED,
|
||||
PAUSED,
|
||||
COMPLETED
|
||||
}
|
@ -14,8 +14,4 @@ data class Playlist @JvmOverloads constructor(
|
||||
companion object {
|
||||
private const val serialVersionUID = -4160515427075433798L
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,4 @@ data class PodcastsChannel(
|
||||
companion object {
|
||||
private const val serialVersionUID = -4160515427075433798L
|
||||
}
|
||||
|
||||
override fun toString(): String = title.toString()
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
enum class RepeatMode {
|
||||
OFF {
|
||||
override operator fun next(): RepeatMode = ALL
|
||||
},
|
||||
ALL {
|
||||
override operator fun next(): RepeatMode = SINGLE
|
||||
},
|
||||
SINGLE {
|
||||
override operator fun next(): RepeatMode = OFF
|
||||
};
|
||||
|
||||
abstract operator fun next(): RepeatMode
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
||||
|
||||
/**
|
||||
* The result of a search. Contains matching artists, albums and songs.
|
||||
*/
|
||||
data class SearchResult(
|
||||
val artists: List<ArtistOrIndex> = listOf(),
|
||||
val albums: List<Album> = listOf(),
|
||||
val songs: List<Track> = listOf()
|
||||
val artists: List<Artist>,
|
||||
val albums: List<Entry>,
|
||||
val songs: List<Entry>
|
||||
)
|
||||
|
@ -1,9 +1,10 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import java.io.Serializable
|
||||
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
||||
|
||||
data class Share(
|
||||
override var id: String,
|
||||
override var id: String? = null,
|
||||
var url: String? = null,
|
||||
var description: String? = null,
|
||||
var username: String? = null,
|
||||
@ -11,22 +12,17 @@ data class Share(
|
||||
var lastVisited: String? = null,
|
||||
var expires: String? = null,
|
||||
var visitCount: Long? = null,
|
||||
private val tracks: MutableList<Track> = mutableListOf()
|
||||
private val entries: MutableList<Entry> = mutableListOf()
|
||||
) : Serializable, GenericEntry() {
|
||||
override val name: String?
|
||||
get() {
|
||||
if (url != null) {
|
||||
return urlPattern.matcher(url!!).replaceFirst("$1")
|
||||
}
|
||||
return null
|
||||
}
|
||||
get() = url?.let { urlPattern.matcher(url).replaceFirst("$1") }
|
||||
|
||||
fun getEntries(): List<Track> {
|
||||
return tracks.toList()
|
||||
fun getEntries(): List<Entry> {
|
||||
return entries.toList()
|
||||
}
|
||||
|
||||
fun addEntry(track: Track) {
|
||||
tracks.add(track)
|
||||
fun addEntry(entry: Entry) {
|
||||
entries.add(entry)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Track.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import java.io.Serializable
|
||||
import java.util.Date
|
||||
|
||||
@Entity(tableName = "tracks", primaryKeys = ["id", "serverId"])
|
||||
data class Track(
|
||||
override var id: String,
|
||||
@ColumnInfo(defaultValue = "-1")
|
||||
override var serverId: Int = -1,
|
||||
override var parent: String? = null,
|
||||
override var isDirectory: Boolean = false,
|
||||
override var title: String? = null,
|
||||
override var album: String? = null,
|
||||
var albumId: String? = null,
|
||||
override var artist: String? = null,
|
||||
override var artistId: String? = null,
|
||||
var track: Int? = null,
|
||||
override var year: Int? = null,
|
||||
override var genre: String? = null,
|
||||
var contentType: String? = null,
|
||||
var suffix: String? = null,
|
||||
var transcodedContentType: String? = null,
|
||||
var transcodedSuffix: String? = null,
|
||||
override var coverArt: String? = null,
|
||||
var size: Long? = null,
|
||||
override var songCount: Long? = null,
|
||||
override var duration: Int? = null,
|
||||
var bitRate: Int? = null,
|
||||
override var path: String? = null,
|
||||
override var isVideo: Boolean = false,
|
||||
override var starred: Boolean = false,
|
||||
override var discNumber: Int? = null,
|
||||
var type: String? = null,
|
||||
override var created: Date? = null,
|
||||
override var closeness: Int = 0,
|
||||
var bookmarkPosition: Int = 0,
|
||||
var userRating: Int? = null,
|
||||
var averageRating: Float? = null,
|
||||
override var name: String? = null
|
||||
) : Serializable, MusicDirectory.Child() {
|
||||
fun setDuration(duration: Long) {
|
||||
this.duration = duration.toInt()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = -3339106650010798108L
|
||||
}
|
||||
|
||||
fun compareTo(other: Track): Int {
|
||||
return when {
|
||||
this.closeness == other.closeness -> {
|
||||
0
|
||||
}
|
||||
this.closeness > other.closeness -> {
|
||||
-1
|
||||
}
|
||||
else -> {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: Identifiable) = compareTo(other as Track)
|
||||
}
|
@ -1,29 +1,30 @@
|
||||
plugins {
|
||||
alias libs.plugins.ksp
|
||||
}
|
||||
|
||||
apply from: bootstrap.kotlinModule
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api libs.retrofit
|
||||
api libs.jacksonConverter
|
||||
api libs.koinCore
|
||||
api other.retrofit
|
||||
api other.jacksonConverter
|
||||
api other.koinCore
|
||||
|
||||
implementation(libs.jacksonKotlin) {
|
||||
implementation(other.jacksonKotlin) {
|
||||
exclude module: 'kotlin-reflect'
|
||||
}
|
||||
implementation libs.kotlinReflect // for jackson kotlin, but to use the same version
|
||||
implementation libs.okhttpLogging
|
||||
implementation other.kotlinReflect // for jackson kotlin, but to use the same version
|
||||
implementation other.okhttpLogging
|
||||
implementation other.timber
|
||||
|
||||
testImplementation libs.kotlinJunit
|
||||
testImplementation libs.mockito
|
||||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.kluent
|
||||
testImplementation libs.mockWebServer
|
||||
testImplementation libs.apacheCodecs
|
||||
testImplementation testing.kotlinJunit
|
||||
testImplementation testing.mockito
|
||||
testImplementation testing.mockitoInline
|
||||
testImplementation testing.mockitoKotlin
|
||||
testImplementation testing.kluent
|
||||
testImplementation testing.mockWebServer
|
||||
testImplementation testing.apacheCodecs
|
||||
}
|
||||
|
||||
ext {
|
||||
// Excluding data classes
|
||||
jacocoExclude = [
|
||||
'**/models/**',
|
||||
'**/di/**'
|
||||
]
|
||||
}
|
||||
|
@ -8,8 +8,7 @@ import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import okio.Okio
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should contain`
|
||||
import org.amshove.kluent.`should not be`
|
||||
@ -41,12 +40,12 @@ fun MockWebServer.enqueueResponse(resourceName: String) {
|
||||
}
|
||||
|
||||
fun Any.loadJsonResponse(name: String): String {
|
||||
val source = javaClass.classLoader.getResourceAsStream(name)!!.source().buffer()
|
||||
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
|
||||
return source.readString(Charset.forName("UTF-8"))
|
||||
}
|
||||
|
||||
fun Any.loadResourceStream(name: String): InputStream {
|
||||
val source = javaClass.classLoader.getResourceAsStream(name)!!.source().buffer()
|
||||
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
|
||||
return source.inputStream()
|
||||
}
|
||||
|
||||
|
@ -8,8 +8,7 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
||||
* Base class for integration tests for [SubsonicAPIClient] class.
|
||||
*/
|
||||
abstract class SubsonicAPIClientTest {
|
||||
@JvmField @Rule
|
||||
val mockWebServerRule = MockWebServerRule()
|
||||
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
||||
|
||||
protected lateinit var config: SubsonicClientConfiguration
|
||||
protected lateinit var client: SubsonicAPIClient
|
||||
|
@ -2,9 +2,9 @@ package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
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.BY_GENRE
|
||||
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
||||
|
||||
/**
|
||||
* Integration tests for [SubsonicAPIDefinition] for getAlbumList call.
|
||||
@ -28,8 +28,8 @@ class SubsonicApiGetAlbumListRequestTest : SubsonicAPIClientTest() {
|
||||
assertResponseSuccessful(response)
|
||||
with(response.body()!!.albumList) {
|
||||
size `should be equal to` 2
|
||||
this[1] `should be equal to` Album(
|
||||
id = "9997", parent = "9996",
|
||||
this[1] `should be equal to` 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,
|
||||
|
@ -1,8 +1,8 @@
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.junit.Test
|
||||
import org.moire.ultrasonic.api.subsonic.models.MusicDirectory
|
||||
|
@ -3,7 +3,6 @@ package org.moire.ultrasonic.api.subsonic
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
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.SearchTwoResult
|
||||
@ -33,8 +32,9 @@ class SubsonicApiSearchTwoTest : SubsonicAPIClientTest() {
|
||||
artistList.size `should be equal to` 1
|
||||
artistList[0] `should be equal to` Artist(id = "522", name = "The Prodigy")
|
||||
albumList.size `should be equal to` 1
|
||||
albumList[0] `should be equal to` Album(
|
||||
id = "8867", parent = "522", title = "Always Outnumbered, Never Outgunned",
|
||||
albumList[0] `should be equal to` 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")
|
||||
|
@ -1,8 +1,8 @@
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should be`
|
||||
import org.amshove.kluent.`should not be`
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -11,8 +11,7 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
||||
* Base class for testing [okhttp3.Interceptor] implementations.
|
||||
*/
|
||||
abstract class BaseInterceptorTest {
|
||||
@Rule @JvmField
|
||||
val mockWebServerRule = MockWebServerRule()
|
||||
@Rule @JvmField val mockWebServerRule = MockWebServerRule()
|
||||
|
||||
lateinit var client: OkHttpClient
|
||||
|
||||
|
@ -1,10 +1,3 @@
|
||||
/*
|
||||
* ApiVersionCheckWrapper.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
@ -92,13 +85,7 @@ internal class ApiVersionCheckWrapper(
|
||||
checkVersion(V1_4_0)
|
||||
checkParamVersion(musicFolderId, V1_12_0)
|
||||
return api.search2(
|
||||
query,
|
||||
artistCount,
|
||||
artistOffset,
|
||||
albumCount,
|
||||
albumOffset,
|
||||
songCount,
|
||||
musicFolderId
|
||||
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
||||
)
|
||||
}
|
||||
|
||||
@ -114,13 +101,7 @@ internal class ApiVersionCheckWrapper(
|
||||
checkVersion(V1_8_0)
|
||||
checkParamVersion(musicFolderId, V1_12_0)
|
||||
return api.search3(
|
||||
query,
|
||||
artistCount,
|
||||
artistOffset,
|
||||
albumCount,
|
||||
albumOffset,
|
||||
songCount,
|
||||
musicFolderId
|
||||
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
||||
)
|
||||
}
|
||||
|
||||
@ -240,13 +221,7 @@ internal class ApiVersionCheckWrapper(
|
||||
checkParamVersion(estimateContentLength, V1_8_0)
|
||||
checkParamVersion(converted, V1_14_0)
|
||||
return api.stream(
|
||||
id,
|
||||
maxBitRate,
|
||||
format,
|
||||
timeOffset,
|
||||
videoSize,
|
||||
estimateContentLength,
|
||||
converted
|
||||
id, maxBitRate, format, timeOffset, videoSize, estimateContentLength, converted
|
||||
)
|
||||
}
|
||||
|
||||
@ -353,9 +328,8 @@ internal class ApiVersionCheckWrapper(
|
||||
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
||||
// If it is true, it is probably the first call with this server
|
||||
if (!isRealProtocolVersion) return
|
||||
if (currentApiVersion < expectedVersion) {
|
||||
if (currentApiVersion < expectedVersion)
|
||||
throw ApiNotSupportedException(currentApiVersion)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
||||
|
@ -17,8 +17,8 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
|
||||
val contentType = responseBody?.contentType()
|
||||
if (
|
||||
contentType != null &&
|
||||
contentType.type.equals("application", true) &&
|
||||
contentType.subtype.equals("json", true)
|
||||
contentType.type().equals("application", true) &&
|
||||
contentType.subtype().equals("json", true)
|
||||
) {
|
||||
val error = SubsonicAPIClient.jacksonMapper.readValue<SubsonicResponse>(
|
||||
responseBody.byteStream()
|
||||
@ -40,11 +40,11 @@ fun Response<out ResponseBody>.toStreamResponse(): StreamResponse {
|
||||
* It creates Exceptions from the results returned by the Subsonic API
|
||||
*/
|
||||
@Suppress("ThrowsCount")
|
||||
fun <T : SubsonicResponse> Response<T>.throwOnFailure(): Response<T> {
|
||||
fun <T : SubsonicResponse> Response<out T>.throwOnFailure(): Response<out T> {
|
||||
val response = this
|
||||
|
||||
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
|
||||
return this
|
||||
return this as Response<T>
|
||||
}
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Server error, code: " + response.code())
|
||||
|
@ -8,7 +8,6 @@ import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
@ -44,7 +43,7 @@ class SubsonicAPIClient(
|
||||
config.minimalProtocolVersion,
|
||||
PasswordHexInterceptor(config.password),
|
||||
PasswordMD5Interceptor(config.password),
|
||||
config.forcePlainTextPassword
|
||||
config.enableLdapUserSupport
|
||||
)
|
||||
|
||||
var onProtocolChange: (SubsonicAPIVersions) -> Unit = {}
|
||||
@ -64,30 +63,17 @@ class SubsonicAPIClient(
|
||||
}
|
||||
|
||||
val okHttpClient: OkHttpClient = baseOkClient.newBuilder()
|
||||
// Disable HTTP2 because OkHttp with Exoplayer causes a bug. See https://github.com/square/okhttp/issues/6749
|
||||
.readTimeout(READ_TIMEOUT, MILLISECONDS)
|
||||
.apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() }
|
||||
.addInterceptor { chain ->
|
||||
// Adds default request params
|
||||
val originalRequest = chain.request()
|
||||
val newUrl = originalRequest.url.newBuilder()
|
||||
val newUrl = originalRequest.url().newBuilder()
|
||||
.addQueryParameter("u", config.username)
|
||||
.addQueryParameter("c", config.clientID)
|
||||
.addQueryParameter("f", "json")
|
||||
.build()
|
||||
val newRequestBuilder = originalRequest.newBuilder().url(newUrl)
|
||||
if (originalRequest.url.username.isNotEmpty() &&
|
||||
originalRequest.url.password.isNotEmpty()
|
||||
) {
|
||||
newRequestBuilder.addHeader(
|
||||
"Authorization",
|
||||
Credentials.basic(
|
||||
originalRequest.url.username,
|
||||
originalRequest.url.password
|
||||
)
|
||||
)
|
||||
}
|
||||
chain.proceed(newRequestBuilder.build())
|
||||
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
|
||||
}
|
||||
.addInterceptor(versionInterceptor)
|
||||
.addInterceptor(proxyPasswordInterceptor)
|
||||
@ -95,12 +81,10 @@ class SubsonicAPIClient(
|
||||
.apply { if (config.debug) addLogging() }
|
||||
.build()
|
||||
|
||||
val baseUrl = "${config.baseUrl}/rest/"
|
||||
|
||||
// Create the Retrofit instance, and register a special converter factory
|
||||
// It will update our protocol version to the correct version, once we made a successful call
|
||||
private val retrofit: Retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
val retrofit: Retrofit = Retrofit.Builder()
|
||||
.baseUrl("${config.baseUrl}/rest/")
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(
|
||||
VersionAwareJacksonConverterFactory.create(
|
||||
@ -125,19 +109,17 @@ class SubsonicAPIClient(
|
||||
|
||||
private fun OkHttpClient.Builder.addLogging() {
|
||||
val loggingInterceptor = HttpLoggingInterceptor(okLogger)
|
||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.HEADERS
|
||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||
this.addInterceptor(loggingInterceptor)
|
||||
}
|
||||
|
||||
@Suppress("CustomX509TrustManager", "TrustAllX509TrustManager")
|
||||
@SuppressWarnings("TrustAllX509TrustManager", "EmptyFunctionBlock")
|
||||
private fun OkHttpClient.Builder.allowSelfSignedCertificates() {
|
||||
val trustManager =
|
||||
|
||||
object : X509TrustManager {
|
||||
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
|
||||
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
val trustManager = object : X509TrustManager {
|
||||
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
|
||||
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
|
||||
val sslContext = SSLContext.getInstance("SSL")
|
||||
sslContext.init(null, arrayOf(trustManager), SecureRandom())
|
||||
@ -154,17 +136,11 @@ class SubsonicAPIClient(
|
||||
return call.toStreamResponse()
|
||||
}
|
||||
|
||||
val isOffline by lazy {
|
||||
config.baseUrl == OFFLINE_DB_URL
|
||||
}
|
||||
|
||||
companion object {
|
||||
val jacksonMapper: ObjectMapper = ObjectMapper()
|
||||
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
|
||||
.registerModule(KotlinModule.Builder().build())
|
||||
|
||||
const val OFFLINE_DB_URL = "http://localhost"
|
||||
.registerModule(KotlinModule())
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,3 @@
|
||||
/*
|
||||
* SubsonicAPIDefinition.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.api.subsonic
|
||||
|
||||
import okhttp3.ResponseBody
|
||||
@ -54,9 +47,6 @@ interface SubsonicAPIDefinition {
|
||||
@GET("ping.view")
|
||||
fun ping(): Call<SubsonicResponse>
|
||||
|
||||
@GET("ping.view")
|
||||
suspend fun pingSuspend(): SubsonicResponse
|
||||
|
||||
@GET("getLicense.view")
|
||||
fun getLicense(): Call<LicenseResponse>
|
||||
|
||||
@ -90,7 +80,10 @@ interface SubsonicAPIDefinition {
|
||||
): Call<SubsonicResponse>
|
||||
|
||||
@GET("setRating.view")
|
||||
fun setRating(@Query("id") id: String, @Query("rating") rating: Int): Call<SubsonicResponse>
|
||||
fun setRating(
|
||||
@Query("id") id: String,
|
||||
@Query("rating") rating: Int
|
||||
): Call<SubsonicResponse>
|
||||
|
||||
@GET("getArtist.view")
|
||||
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>
|
||||
@ -155,7 +148,8 @@ interface SubsonicAPIDefinition {
|
||||
@Query("public") public: Boolean? = null,
|
||||
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
|
||||
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
|
||||
): Call<SubsonicResponse>
|
||||
):
|
||||
Call<SubsonicResponse>
|
||||
|
||||
@GET("getPodcasts.view")
|
||||
fun getPodcasts(
|
||||
@ -163,12 +157,6 @@ interface SubsonicAPIDefinition {
|
||||
@Query("id") id: String? = null
|
||||
): Call<GetPodcastsResponse>
|
||||
|
||||
@GET("getPodcasts.view")
|
||||
suspend fun getPodcastsSuspend(
|
||||
@Query("includeEpisodes") includeEpisodes: Boolean? = null,
|
||||
@Query("id") id: String? = null
|
||||
): GetPodcastsResponse
|
||||
|
||||
@GET("getLyrics.view")
|
||||
fun getLyrics(
|
||||
@Query("artist") artist: String? = null,
|
||||
@ -223,7 +211,10 @@ interface SubsonicAPIDefinition {
|
||||
|
||||
@Streaming
|
||||
@GET("getCoverArt.view")
|
||||
fun getCoverArt(@Query("id") id: String, @Query("size") size: Long? = null): Call<ResponseBody>
|
||||
fun getCoverArt(
|
||||
@Query("id") id: String,
|
||||
@Query("size") size: Long? = null
|
||||
): Call<ResponseBody>
|
||||
|
||||
@Streaming
|
||||
@GET("stream.view")
|
||||
@ -238,19 +229,6 @@ interface SubsonicAPIDefinition {
|
||||
@Header("Range") offset: Long? = null
|
||||
): Call<ResponseBody>
|
||||
|
||||
@Streaming
|
||||
@GET("download.view")
|
||||
fun download(
|
||||
@Query("id") id: String,
|
||||
@Query("maxBitRate") maxBitRate: Int? = null,
|
||||
@Query("format") format: String? = null,
|
||||
@Query("timeOffset") timeOffset: Int? = null,
|
||||
@Query("size") videoSize: String? = null,
|
||||
@Query("estimateContentLength") estimateContentLength: Boolean? = null,
|
||||
@Query("converted") converted: Boolean? = null,
|
||||
@Header("Range") offset: Long? = null
|
||||
): Call<ResponseBody>
|
||||
|
||||
@GET("jukeboxControl.view")
|
||||
fun jukeboxControl(
|
||||
@Query("action") action: JukeboxAction,
|
||||
@ -263,9 +241,6 @@ interface SubsonicAPIDefinition {
|
||||
@GET("getShares.view")
|
||||
fun getShares(): Call<SharesResponse>
|
||||
|
||||
@GET("getShares.view")
|
||||
suspend fun getSharesSuspend(): SharesResponse
|
||||
|
||||
@GET("createShare.view")
|
||||
fun createShare(
|
||||
@Query("id") idsToShare: List<String>,
|
||||
@ -297,24 +272,15 @@ interface SubsonicAPIDefinition {
|
||||
@GET("getUser.view")
|
||||
fun getUser(@Query("username") username: String): Call<GetUserResponse>
|
||||
|
||||
@GET("getUser.view")
|
||||
suspend fun getUserSuspend(@Query("username") username: String): GetUserResponse
|
||||
|
||||
@GET("getChatMessages.view")
|
||||
fun getChatMessages(@Query("since") since: Long? = null): Call<ChatMessagesResponse>
|
||||
|
||||
@GET("getChatMessages.view")
|
||||
suspend fun getChatMessagesSuspend(@Query("since") since: Long? = null): ChatMessagesResponse
|
||||
|
||||
@GET("addChatMessage.view")
|
||||
fun addChatMessage(@Query("message") message: String): Call<SubsonicResponse>
|
||||
|
||||
@GET("getBookmarks.view")
|
||||
fun getBookmarks(): Call<BookmarksResponse>
|
||||
|
||||
@GET("getBookmarks.view")
|
||||
suspend fun getBookmarksSuspend(): BookmarksResponse
|
||||
|
||||
@GET("createBookmark.view")
|
||||
fun createBookmark(
|
||||
@Query("id") id: String,
|
||||
@ -328,9 +294,6 @@ interface SubsonicAPIDefinition {
|
||||
@GET("getVideos.view")
|
||||
fun getVideos(): Call<VideosResponse>
|
||||
|
||||
@GET("getVideos.view")
|
||||
suspend fun getVideosSuspend(): VideosResponse
|
||||
|
||||
@GET("getAvatar.view")
|
||||
fun getAvatar(@Query("username") username: String): Call<ResponseBody>
|
||||
}
|
||||
|
@ -29,25 +29,20 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
||||
V1_13_0("5.3", "1.13.0"),
|
||||
V1_14_0("6.0", "1.14.0"),
|
||||
V1_15_0("6.1", "1.15.0"),
|
||||
V1_16_0("6.1.2", "1.16.0")
|
||||
;
|
||||
V1_16_0("6.1.2", "1.16.0");
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic @Throws(IllegalArgumentException::class)
|
||||
fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions {
|
||||
val versionComponents = apiVersion.split(".")
|
||||
|
||||
require(versionComponents.size >= 2) { "Unknown api version $apiVersion" }
|
||||
if (versionComponents.size < 2)
|
||||
throw IllegalArgumentException("Unknown api version $apiVersion")
|
||||
|
||||
try {
|
||||
val majorVersion = versionComponents[0].toInt()
|
||||
val minorVersion = versionComponents[1].toInt()
|
||||
val patchVersion = if (versionComponents.size > 2) {
|
||||
versionComponents[2].toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val patchVersion = if (versionComponents.size > 2) versionComponents[2].toInt()
|
||||
else 0
|
||||
|
||||
when (majorVersion) {
|
||||
1 -> when {
|
||||
|
@ -10,7 +10,7 @@ data class SubsonicClientConfiguration(
|
||||
val minimalProtocolVersion: SubsonicAPIVersions,
|
||||
val clientID: String,
|
||||
val allowSelfSignedCertificate: Boolean = false,
|
||||
val forcePlainTextPassword: Boolean = false,
|
||||
val enableLdapUserSupport: Boolean = false,
|
||||
val debug: Boolean = false,
|
||||
val isRealProtocolVersion: Boolean = false
|
||||
)
|
||||
|
@ -35,7 +35,7 @@ class VersionAwareJacksonConverterFactory(
|
||||
type: Type,
|
||||
annotations: Array<Annotation>,
|
||||
retrofit: Retrofit
|
||||
): Converter<ResponseBody, *> {
|
||||
): Converter<ResponseBody, *>? {
|
||||
val javaType: JavaType = mapper!!.typeFactory.constructType(type)
|
||||
val reader: ObjectReader? = mapper!!.readerFor(javaType)
|
||||
return VersionAwareResponseBodyConverter<Any>(notifier, reader!!)
|
||||
@ -48,10 +48,7 @@ class VersionAwareJacksonConverterFactory(
|
||||
retrofit: Retrofit
|
||||
): Converter<*, RequestBody>? {
|
||||
return jacksonConverterFactory?.requestBodyConverter(
|
||||
type,
|
||||
parameterAnnotations,
|
||||
methodAnnotations,
|
||||
retrofit
|
||||
type, parameterAnnotations, methodAnnotations, retrofit
|
||||
)
|
||||
}
|
||||
|
||||
@ -66,7 +63,7 @@ class VersionAwareJacksonConverterFactory(
|
||||
}
|
||||
}
|
||||
|
||||
class VersionAwareResponseBodyConverter<T>(
|
||||
class VersionAwareResponseBodyConverter<T> (
|
||||
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
||||
private val adapter: ObjectReader
|
||||
) : Converter<ResponseBody, T> {
|
||||
|
@ -18,7 +18,7 @@ class PasswordHexInterceptor(private val password: String) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val updatedUrl = originalRequest.url.newBuilder()
|
||||
val updatedUrl = originalRequest.url().newBuilder()
|
||||
.addEncodedQueryParameter("p", passwordHex).build()
|
||||
return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build())
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class PasswordMD5Interceptor(private val password: String) : Interceptor {
|
||||
override fun intercept(chain: Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val salt = getSalt()
|
||||
val updatedUrl = originalRequest.url.newBuilder()
|
||||
val updatedUrl = originalRequest.url().newBuilder()
|
||||
.addQueryParameter("t", getPasswordMD5Hash(salt))
|
||||
.addQueryParameter("s", salt)
|
||||
.build()
|
||||
|
@ -6,7 +6,6 @@ import okhttp3.Interceptor.Chain
|
||||
import okhttp3.Response
|
||||
|
||||
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
|
||||
|
||||
// Allow 20 seconds extra timeout pear MB offset.
|
||||
internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
||||
|
||||
@ -20,7 +19,7 @@ internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
||||
internal class RangeHeaderInterceptor : Interceptor {
|
||||
override fun intercept(chain: Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val headers = originalRequest.headers
|
||||
val headers = originalRequest.headers()
|
||||
return if (headers.names().contains("Range")) {
|
||||
val offsetValue = headers["Range"] ?: "0"
|
||||
val offset = "bytes=$offsetValue-"
|
||||
|
@ -18,7 +18,7 @@ internal class VersionInterceptor(
|
||||
val newRequest = originalRequest.newBuilder()
|
||||
.url(
|
||||
originalRequest
|
||||
.url
|
||||
.url()
|
||||
.newBuilder()
|
||||
.addQueryParameter("v", protocolVersion.restApiVersion)
|
||||
.build()
|
||||
|
@ -5,20 +5,14 @@ import java.util.Calendar
|
||||
|
||||
data class Album(
|
||||
val id: String = "",
|
||||
val parent: String = "",
|
||||
val album: String = "",
|
||||
val title: String? = null,
|
||||
val name: String? = null,
|
||||
val discNumber: Int = 0,
|
||||
val name: String = "",
|
||||
val coverArt: String = "",
|
||||
val songCount: Int = 0,
|
||||
val created: Calendar? = null,
|
||||
val artist: String = "",
|
||||
val artistId: String = "",
|
||||
val songCount: Int = 0,
|
||||
val duration: Int = 0,
|
||||
val created: Calendar? = null,
|
||||
val year: Int = 0,
|
||||
val genre: String = "",
|
||||
val playCount: Int = 0,
|
||||
@JsonProperty("song") val songList: List<MusicDirectoryChild> = emptyList(),
|
||||
@JsonProperty("starred") val starredDate: String = ""
|
||||
@JsonProperty("song") val songList: List<MusicDirectoryChild> = emptyList()
|
||||
)
|
||||
|
@ -1,10 +1,3 @@
|
||||
/*
|
||||
* AlbumListOrderType.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.api.subsonic.models
|
||||
|
||||
/**
|
||||
@ -23,8 +16,7 @@ enum class AlbumListType(val typeName: String) {
|
||||
SORTED_BY_ARTIST("alphabeticalByArtist"),
|
||||
STARRED("starred"),
|
||||
BY_YEAR("byYear"),
|
||||
BY_GENRE("byGenre")
|
||||
;
|
||||
BY_GENRE("byGenre");
|
||||
|
||||
override fun toString(): String {
|
||||
return typeName
|
||||
|
@ -16,8 +16,7 @@ enum class JukeboxAction(val action: String) {
|
||||
CLEAR("clear"),
|
||||
REMOVE("remove"),
|
||||
SHUFFLE("shuffle"),
|
||||
SET_GAIN("setGain")
|
||||
;
|
||||
SET_GAIN("setGain");
|
||||
|
||||
override fun toString(): String {
|
||||
return action
|
||||
|
@ -4,6 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
data class SearchTwoResult(
|
||||
@JsonProperty("artist") val artistList: List<Artist> = emptyList(),
|
||||
@JsonProperty("album") val albumList: List<Album> = emptyList(),
|
||||
@JsonProperty("album") val albumList: List<MusicDirectoryChild> = emptyList(),
|
||||
@JsonProperty("song") val songList: List<MusicDirectoryChild> = emptyList()
|
||||
)
|
||||
|
@ -10,8 +10,7 @@ class BookmarksResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("bookmarks")
|
||||
private val bookmarksWrapper = BookmarkWrapper()
|
||||
@JsonProperty("bookmarks") private val bookmarksWrapper = BookmarkWrapper()
|
||||
|
||||
val bookmarkList: List<Bookmark> get() = bookmarksWrapper.bookmarkList
|
||||
}
|
||||
|
@ -10,8 +10,7 @@ class ChatMessagesResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("chatMessages")
|
||||
private val wrapper = ChatMessagesWrapper()
|
||||
@JsonProperty("chatMessages") private val wrapper = ChatMessagesWrapper()
|
||||
|
||||
val chatMessages: List<ChatMessage> get() = wrapper.messagesList
|
||||
}
|
||||
|
@ -10,8 +10,7 @@ class GenresResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("genres")
|
||||
private val genresWrapper = GenresWrapper()
|
||||
@JsonProperty("genres") private val genresWrapper = GenresWrapper()
|
||||
val genresList: List<Genre> get() = genresWrapper.genresList
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,7 @@ class GetAlbumList2Response(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("albumList2")
|
||||
private val albumWrapper2 = AlbumWrapper2()
|
||||
@JsonProperty("albumList2") private val albumWrapper2 = AlbumWrapper2()
|
||||
|
||||
val albumList: List<Album>
|
||||
get() = albumWrapper2.albumList
|
||||
|
@ -3,20 +3,19 @@ package org.moire.ultrasonic.api.subsonic.response
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicError
|
||||
import org.moire.ultrasonic.api.subsonic.models.Album
|
||||
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
||||
|
||||
class GetAlbumListResponse(
|
||||
status: Status,
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("albumList")
|
||||
private val albumWrapper = AlbumWrapper()
|
||||
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
|
||||
|
||||
val albumList: List<Album>
|
||||
val albumList: List<MusicDirectoryChild>
|
||||
get() = albumWrapper.albumList
|
||||
}
|
||||
|
||||
private class AlbumWrapper(
|
||||
@JsonProperty("album") val albumList: List<Album> = emptyList()
|
||||
@JsonProperty("album") val albumList: List<MusicDirectoryChild> = emptyList()
|
||||
)
|
||||
|
@ -10,8 +10,7 @@ class GetPodcastsResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("podcasts")
|
||||
private val channelsWrapper = PodcastChannelWrapper()
|
||||
@JsonProperty("podcasts") private val channelsWrapper = PodcastChannelWrapper()
|
||||
|
||||
val podcastChannels: List<PodcastChannel>
|
||||
get() = channelsWrapper.channelsList
|
||||
|
@ -10,8 +10,7 @@ class GetRandomSongsResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("randomSongs")
|
||||
private val songsWrapper = RandomSongsWrapper()
|
||||
@JsonProperty("randomSongs") private val songsWrapper = RandomSongsWrapper()
|
||||
|
||||
val songsList
|
||||
get() = songsWrapper.songsList
|
||||
|
@ -10,8 +10,7 @@ class GetSongsByGenreResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("songsByGenre")
|
||||
private val songsByGenreList = SongsByGenreWrapper()
|
||||
@JsonProperty("songsByGenre") private val songsByGenreList = SongsByGenreWrapper()
|
||||
|
||||
val songsList get() = songsByGenreList.songsList
|
||||
}
|
||||
|
@ -11,13 +11,11 @@ class JukeboxResponse(
|
||||
error: SubsonicError?,
|
||||
var jukebox: JukeboxStatus = JukeboxStatus()
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonSetter("jukeboxStatus")
|
||||
fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
||||
@JsonSetter("jukeboxStatus") fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
||||
this.jukebox = jukebox
|
||||
}
|
||||
|
||||
@JsonSetter("jukeboxPlaylist")
|
||||
fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
||||
@JsonSetter("jukeboxPlaylist") fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
||||
this.jukebox = jukebox
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,7 @@ class MusicFoldersResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("musicFolders")
|
||||
private val wrapper = MusicFoldersWrapper()
|
||||
@JsonProperty("musicFolders") private val wrapper = MusicFoldersWrapper()
|
||||
|
||||
val musicFolders get() = wrapper.musicFolders
|
||||
}
|
||||
|
@ -10,8 +10,7 @@ class SharesResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("shares")
|
||||
private val wrappedShares = SharesWrapper()
|
||||
@JsonProperty("shares") private val wrappedShares = SharesWrapper()
|
||||
|
||||
val shares get() = wrappedShares.share
|
||||
}
|
||||
|
@ -20,13 +20,12 @@ open class SubsonicResponse(
|
||||
) {
|
||||
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
|
||||
enum class Status(val jsonValue: String) {
|
||||
OK("ok"),
|
||||
ERROR("failed");
|
||||
OK("ok"), ERROR("failed");
|
||||
|
||||
companion object {
|
||||
fun getStatusFromJson(jsonValue: String) =
|
||||
values().firstOrNull { it.jsonValue == jsonValue }
|
||||
?: throw IllegalArgumentException("Unknown status value: $jsonValue")
|
||||
fun getStatusFromJson(jsonValue: String) = values()
|
||||
.filter { it.jsonValue == jsonValue }.firstOrNull()
|
||||
?: throw IllegalArgumentException("Unknown status value: $jsonValue")
|
||||
|
||||
class StatusJsonDeserializer : JsonDeserializer<Status>() {
|
||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): Status {
|
||||
|
@ -10,8 +10,7 @@ class VideosResponse(
|
||||
version: SubsonicAPIVersions,
|
||||
error: SubsonicError?
|
||||
) : SubsonicResponse(status, version, error) {
|
||||
@JsonProperty("videos")
|
||||
private val videosWrapper = VideosWrapper()
|
||||
@JsonProperty("videos") private val videosWrapper = VideosWrapper()
|
||||
|
||||
val videosList: List<MusicDirectoryChild> get() = videosWrapper.videosList
|
||||
}
|
||||
|
@ -18,9 +18,7 @@ class ProxyPasswordInterceptorTest {
|
||||
|
||||
private val proxyInterceptor = ProxyPasswordInterceptor(
|
||||
V1_12_0,
|
||||
mockPasswordHexInterceptor,
|
||||
mockPasswordMd5Interceptor,
|
||||
false
|
||||
mockPasswordHexInterceptor, mockPasswordMd5Interceptor, false
|
||||
)
|
||||
|
||||
@Test
|
||||
@ -42,10 +40,8 @@ class ProxyPasswordInterceptorTest {
|
||||
@Test
|
||||
fun `Should use hex password if forceHex is true`() {
|
||||
val interceptor = ProxyPasswordInterceptor(
|
||||
V1_16_0,
|
||||
mockPasswordHexInterceptor,
|
||||
mockPasswordMd5Interceptor,
|
||||
true
|
||||
V1_16_0, mockPasswordHexInterceptor,
|
||||
mockPasswordMd5Interceptor, true
|
||||
)
|
||||
|
||||
interceptor.intercept(mockChain)
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.moire.ultrasonic.api.subsonic.models
|
||||
|
||||
import java.util.Locale
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should throw`
|
||||
import org.junit.Test
|
||||
@ -11,7 +10,7 @@ import org.junit.Test
|
||||
class AlbumListTypeTest {
|
||||
@Test
|
||||
fun `Should create type from string ignoring case`() {
|
||||
val type = AlbumListType.SORTED_BY_NAME.typeName.lowercase(Locale.ROOT)
|
||||
val type = AlbumListType.SORTED_BY_NAME.typeName.toLowerCase()
|
||||
|
||||
val albumListType = AlbumListType.fromName(type)
|
||||
|
||||
@ -36,7 +35,6 @@ class AlbumListTypeTest {
|
||||
|
||||
@Test
|
||||
fun `Should return type name for toString call`() {
|
||||
AlbumListType.STARRED.typeName `should be equal to`
|
||||
AlbumListType.STARRED.toString()
|
||||
AlbumListType.STARRED.typeName `should be equal to` AlbumListType.STARRED.toString()
|
||||
}
|
||||
}
|
||||
|
108
dependencies.gradle
Normal file
108
dependencies.gradle
Normal file
@ -0,0 +1,108 @@
|
||||
ext.versions = [
|
||||
minSdk : 14,
|
||||
targetSdk : 29,
|
||||
compileSdk : 29,
|
||||
// You need to run ./gradlew wrapper after updating the version
|
||||
gradle : '7.0',
|
||||
|
||||
navigation : "2.3.5",
|
||||
gradlePlugin : "4.2.0",
|
||||
androidxcore : "1.5.0",
|
||||
ktlint : "0.37.1",
|
||||
ktlintGradle : "9.2.1",
|
||||
detekt : "1.17.1",
|
||||
jacoco : "0.8.7",
|
||||
preferences : "1.1.1",
|
||||
media : "1.3.1",
|
||||
|
||||
androidSupport : "28.0.0",
|
||||
androidLegacySupport : "1.0.0",
|
||||
androidSupportDesign : "1.3.0",
|
||||
constraintLayout : "2.0.4",
|
||||
multidex : "2.0.1",
|
||||
room : "2.3.0",
|
||||
kotlin : "1.5.10",
|
||||
kotlinxCoroutines : "1.5.0-native-mt",
|
||||
viewModelKtx : "2.2.0",
|
||||
|
||||
retrofit : "2.6.4",
|
||||
jackson : "2.9.5",
|
||||
okhttp : "3.12.13",
|
||||
twitterSerial : "0.1.6",
|
||||
koin : "3.0.2",
|
||||
picasso : "2.71828",
|
||||
sortListView : "1.0.1",
|
||||
|
||||
junit4 : "4.13.2",
|
||||
junit5 : "5.7.1",
|
||||
mockito : "3.11.0",
|
||||
mockitoKotlin : "3.2.0",
|
||||
kluent : "1.64",
|
||||
apacheCodecs : "1.15",
|
||||
robolectric : "4.5.1",
|
||||
dexter : "6.2.2",
|
||||
timber : "4.7.1",
|
||||
fastScroll : "2.0.1",
|
||||
]
|
||||
|
||||
ext.gradlePlugins = [
|
||||
gradle : "com.android.tools.build:gradle:$versions.gradlePlugin",
|
||||
kotlin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin",
|
||||
ktlintGradle : "org.jlleitschuh.gradle:ktlint-gradle:$versions.ktlintGradle",
|
||||
detekt : "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$versions.detekt",
|
||||
jacoco : "org.jacoco:org.jacoco.core:$versions.jacoco",
|
||||
]
|
||||
|
||||
ext.androidSupport = [
|
||||
core : "androidx.core:core-ktx:$versions.androidxcore",
|
||||
support : "androidx.legacy:legacy-support-v4:$versions.androidLegacySupport",
|
||||
design : "com.google.android.material:material:$versions.androidSupportDesign",
|
||||
annotations : "com.android.support:support-annotations:$versions.androidSupport",
|
||||
multidex : "androidx.multidex:multidex:$versions.multidex",
|
||||
constraintLayout : "androidx.constraintlayout:constraintlayout:$versions.constraintLayout",
|
||||
room : "androidx.room:room-compiler:$versions.room",
|
||||
roomRuntime : "androidx.room:room-runtime:$versions.room",
|
||||
roomKtx : "androidx.room:room-ktx:$versions.room",
|
||||
viewModelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.viewModelKtx",
|
||||
navigationFragment : "androidx.navigation:navigation-fragment:$versions.navigation",
|
||||
navigationUi : "androidx.navigation:navigation-ui:$versions.navigation",
|
||||
navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:$versions.navigation",
|
||||
navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$versions.navigation",
|
||||
navigationFeature : "androidx.navigation:navigation-dynamic-features-fragment:$versions.navigation",
|
||||
preferences : "androidx.preference:preference:$versions.preferences",
|
||||
media : "androidx.media:media:$versions.media",
|
||||
]
|
||||
|
||||
ext.other = [
|
||||
kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin",
|
||||
kotlinReflect : "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin",
|
||||
kotlinxCoroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.kotlinxCoroutines",
|
||||
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",
|
||||
twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial",
|
||||
koinCore : "io.insert-koin:koin-core:$versions.koin",
|
||||
koinAndroid : "io.insert-koin:koin-android:$versions.koin",
|
||||
koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin",
|
||||
picasso : "com.squareup.picasso:picasso:$versions.picasso",
|
||||
dexter : "com.karumi:dexter:$versions.dexter",
|
||||
timber : "com.jakewharton.timber:timber:$versions.timber",
|
||||
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
|
||||
sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView",
|
||||
]
|
||||
|
||||
ext.testing = [
|
||||
junit : "junit:junit:$versions.junit4",
|
||||
junitVintage : "org.junit.vintage:junit-vintage-engine:$versions.junit5",
|
||||
kotlinJunit : "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin",
|
||||
mockitoKotlin : "org.mockito.kotlin: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",
|
||||
kluentAndroid : "org.amshove.kluent:kluent-android:$versions.kluent",
|
||||
mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp",
|
||||
apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs",
|
||||
robolectric : "org.robolectric:robolectric:$versions.robolectric"
|
||||
]
|
86
detekt-baseline.xml
Normal file
86
detekt-baseline.xml
Normal file
@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues></ManuallySuppressedIssues>
|
||||
<CurrentIssues>
|
||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background</ID>
|
||||
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
|
||||
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
|
||||
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
|
||||
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
|
||||
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
|
||||
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
|
||||
<ID>EmptyFunctionBlock:SongView.kt$SongView${}</ID>
|
||||
<ID>FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent()</ID>
|
||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID>
|
||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID>
|
||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song)</ID>
|
||||
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.<no name provided>$String.format( "%s %s", resources.getString(R.string.settings_connection_failure), getErrorMessage(error) )</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Failed to write log to %s", file)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Log file rotated, logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:FileLoggerTree.kt$FileLoggerTree$String.format("Logging into file %s", file?.name)</ID>
|
||||
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
|
||||
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.<no name provided>$String.format("%s\n\n%s", Util.getShareGreeting(), result.url)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix)</ID>
|
||||
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
||||
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
|
||||
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
|
||||
<ID>LongMethod:MediaPlayerService.kt$MediaPlayerService$private fun updateMediaSession(currentPlaying: DownloadFile?, playerState: PlayerState)</ID>
|
||||
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
|
||||
<ID>LongMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
|
||||
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
|
||||
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateDisplay(refresh: Boolean)</ID>
|
||||
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
|
||||
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) )</ID>
|
||||
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10</ID>
|
||||
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$50L</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$256</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID>
|
||||
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
|
||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$3</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$4</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$60</ID>
|
||||
<ID>MagicNumber:TrackCollectionFragment.kt$TrackCollectionFragment$10</ID>
|
||||
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
||||
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
||||
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
||||
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:FileLoggerTree.kt$FileLoggerTree$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$exception: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception</ID>
|
||||
<ID>TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song))</ID>
|
||||
<ID>TooManyFunctions:LocalMediaPlayer.kt$LocalMediaPlayer</ID>
|
||||
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
@ -2,11 +2,14 @@ build:
|
||||
maxIssues: 0
|
||||
weights:
|
||||
complexity: 2
|
||||
formatting: 1
|
||||
LongParameterList: 1
|
||||
comments: 1
|
||||
|
||||
potential-bugs:
|
||||
active: true
|
||||
DuplicateCaseInWhenExpression:
|
||||
active: true
|
||||
EqualsWithHashCodeExist:
|
||||
active: true
|
||||
ExplicitGarbageCollectionCall:
|
||||
@ -39,39 +42,41 @@ empty-blocks:
|
||||
complexity:
|
||||
active: true
|
||||
TooManyFunctions:
|
||||
thresholdInFiles: 25
|
||||
thresholdInClasses: 25
|
||||
thresholdInFiles: 20
|
||||
thresholdInClasses: 20
|
||||
thresholdInInterfaces: 20
|
||||
thresholdInObjects: 30
|
||||
LabeledExpression:
|
||||
active: false
|
||||
|
||||
|
||||
formatting:
|
||||
autoCorrect: true
|
||||
active: false
|
||||
|
||||
style:
|
||||
active: true
|
||||
NewLineAtEndOfFile:
|
||||
active: true
|
||||
ForbiddenComment:
|
||||
active: true
|
||||
comments:
|
||||
- reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
|
||||
value: 'FIXME:'
|
||||
- reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
|
||||
value: 'STOPSHIP:'
|
||||
values: 'FIXME:,STOPSHIP:'
|
||||
WildcardImport:
|
||||
active: true
|
||||
MaxLineLength:
|
||||
active: false
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
excludePackageStatements: false
|
||||
excludeImportStatements: false
|
||||
MagicNumber:
|
||||
# 100 common in percentage, 1000 in milliseconds
|
||||
ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024', '4096']
|
||||
ignoreNumbers: ['-1', '0', '1', '2', '100', '1000']
|
||||
ignoreEnums: true
|
||||
ignorePropertyDeclaration: true
|
||||
UnnecessaryAbstractClass:
|
||||
active: false
|
||||
ReturnCount:
|
||||
max: 5
|
||||
ForbiddenImport:
|
||||
imports: ['android.app.AlertDialog']
|
||||
max: 3
|
||||
|
||||
comments:
|
||||
active: true
|
@ -1,3 +0,0 @@
|
||||
Others
|
||||
- #671: Bump versions.mockito from 4.1.0 to 4.3.1.
|
||||
- Update translations.
|
@ -1,3 +0,0 @@
|
||||
Enhancements
|
||||
- #683: Rewrite the about and remove the webview.
|
||||
- #685: Server coloring feature.
|
@ -1,2 +0,0 @@
|
||||
Bug fixes
|
||||
- #688: Connection failure.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user