Compare commits

..

No commits in common. "develop" and "3.2.0" have entirely different histories.

636 changed files with 22657 additions and 23715 deletions

157
.circleci/config.yml Normal file
View File

@ -0,0 +1,157 @@
version: 2.1
parameters:
memory-config:
type: string
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g"
memory-config-debug:
type: string
default: "-Xmx3200m -Xms256m -XX:MaxMetaspaceSize=1g -verbose:gc -Xlog:gc*"
jobs:
build:
docker:
- image: cimg/android:2022.03.1
working_directory: ~/ultrasonic
environment:
JVM_OPTS: << pipeline.parameters.memory-config >>
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
GRADLE_OPTS: << pipeline.parameters.memory-config >>
steps:
- checkout
- restore_cache:
keys:
- v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
- v2-ultrasonic-{{ .Branch }}
- v2-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
cat gradle.properties
- run:
name: checkstyle
command: ./gradlew -Pqc ktlintCheck
- run:
name: static analysis
command: ./gradlew -Pqc detekt
- run:
name: build debug
command: ./gradlew assembleDebug
- run:
name: unit-tests
command: |
./gradlew ciTest testDebugUnitTest
./gradlew jacocoFullReport
- run:
name: lint
command: ./gradlew :ultrasonic:lintRelease
- run:
name: build
command: ./gradlew buildRelease
- run:
name: assemble release
command: ./gradlew assembleRelease
- save_cache:
paths:
- ~/.gradle
key: v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
- 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: cimg/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: cimg/android:2022.03.1
working_directory: ~/ultrasonic
environment:
JVM_OPTS: << pipeline.parameters.memory-config >>
JAVA_TOOL_OPTIONS: << pipeline.parameters.memory-config >>
GRADLE_OPTS: << pipeline.parameters.memory-config >>
steps:
- checkout
- restore_cache:
keys:
- v2-ultrasonic-{{ .Branch }}-{{ checksum "gradle/libs.versions.toml" }}
- v2-ultrasonic-{{ .Branch }}
- v2-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: |
export PATH="${JAVA_HOME}/bin:${PATH}"
mkdir -p /tmp/ultrasonic-release
${ANDROID_HOME}/build-tools/32.0.0/zipalign -v 4 ultrasonic/build/outputs/apk/release/ultrasonic-release-unsigned.apk /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
${ANDROID_HOME}/build-tools/32.0.0/apksigner sign --verbose --ks ~/ultrasonic/ultrasonic-keystore --ks-pass pass:${ULTRASONIC_KEYSTORE_STOREPASS} --key-pass pass:${ULTRASONIC_KEYSTORE_KEYPASS} /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
${ANDROID_HOME}/build-tools/32.0.0/apksigner verify --verbose /tmp/ultrasonic-release/ultrasonic-${CIRCLE_TAG}.apk
- persist_to_workspace:
root: /tmp/ultrasonic-release
paths:
- ultrasonic-*.apk*
publish_github_signed_apk:
docker:
- image: cimg/go:1.18
steps:
- attach_workspace:
at: /tmp/ultrasonic-release
- run:
name: install ghr
command: go install -v github.com/tcnksm/ghr@latest
- 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
- 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: /.*/

View File

@ -1,2 +0,0 @@
[*.{kt,kts}]
ktlint_code_style = android_studio

15
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,15 @@
# 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: "monthly"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-patch"]

1
.gitignore vendored
View File

@ -18,7 +18,6 @@ out/
# Gradle files
.gradle/
.kotlin/
build/
# Local configuration file (sdk path, etc)

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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``

View File

@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"baseBranches": [ "develop" ],
"labels": [ "housekeeping", "dependencies" ]
}

View File

@ -1,6 +1,11 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value />
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">

2
.idea/compiler.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

@ -1,7 +1,7 @@
<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">
<inspection_tool class="Reformat" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="processChangedFilesOnly" value="true" />
</inspection_tool>
</profile>

4
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK" />
</project>

10
.tx/config Normal file
View 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

View File

@ -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 havent 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 havent been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
then, please, first discuss the change you wish to make via [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
## 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.

View File

@ -2,20 +2,13 @@
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.
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!
### 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

View File

@ -1,14 +1,14 @@
# Ultrasonic
[![Build Status](https://circleci.com/gh/ultrasonic/ultrasonic/tree/develop.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/ultrasonic)
[![Codecov branch](https://img.shields.io/codecov/c/github/ultrasonic/ultrasonic/develop.svg)]()
[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)
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,24 @@ 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.
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].
If you want to use the version downloaded from F-Droid or form Github with **Android Auto**, you must enable Unknown Sources as it is described in [this wiki page](https://github.com/ultrasonic/ultrasonic/wiki/Using-Ultrasonic-with-Android-Auto).
## Bugs and issues
First, see if your issue havent been yet reported [here][issues], otherwise
open [a new issue][newissue].
First, see if your issue havent been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
otherwise open [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
### 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 +41,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-Advanced](https://github.com/airsonic-advanced/airsonic-advanced)
- [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).

View File

@ -1,5 +1,3 @@
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'
@ -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 libs.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(libs.versions.gradle.get())
distributionType("all")
}

View File

@ -1,20 +1,14 @@
plugins {
alias libs.plugins.ksp
}
apply from: bootstrap.androidModule
apply plugin: 'kotlin-kapt'
ext {
jacocoExclude = [
'**/domain/**'
]
}
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
}
kapt libs.room
}

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.moire.ultrasonic.subsonic.domain">
</manifest>

View File

@ -1,21 +1,10 @@
/*
* 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 androidx.room.PrimaryKey
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,
@PrimaryKey override var id: String,
override var parent: String? = null,
override var album: String? = null,
override var title: String? = null,
@ -31,7 +20,7 @@ data class Album(
override var genre: String? = null,
override var starred: Boolean = false,
override var path: String? = null,
override var closeness: Int = 0
override var closeness: Int = 0,
) : MusicDirectory.Child() {
override var isDirectory = true
override var isVideo = false

View File

@ -1,23 +1,14 @@
/*
* 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 androidx.room.PrimaryKey
@Entity(tableName = "artists", primaryKeys = ["id", "serverId"])
@Entity(tableName = "artists")
data class Artist(
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
@PrimaryKey override var id: String,
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)
) : ArtistOrIndex(id)

View File

@ -1,21 +1,11 @@
/*
* 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,
@ -28,15 +18,15 @@ abstract class ArtistOrIndex(
) : GenericEntry() {
fun compareTo(other: ArtistOrIndex): Int {
return when {
when {
this.closeness == other.closeness -> {
0
return 0
}
this.closeness > other.closeness -> {
-1
return -1
}
else -> {
1
return 1
}
}
}

View File

@ -1,24 +1,15 @@
/*
* 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
import androidx.room.PrimaryKey
@Entity(tableName = "indexes", primaryKeys = ["id", "serverId"])
@Entity(tableName = "indexes")
data class Index(
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
@PrimaryKey override var id: String,
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)
) : ArtistOrIndex(id)

View File

@ -1,10 +1,3 @@
/*
* MusicDirectory.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import java.util.Date
@ -13,7 +6,10 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
var name: String? = null
@JvmOverloads
fun getChildren(includeDirs: Boolean = true, includeFiles: Boolean = true): List<Child> {
fun getChildren(
includeDirs: Boolean = true,
includeFiles: Boolean = true
): List<Child> {
if (includeDirs && includeFiles) {
return toList()
}
@ -35,7 +31,6 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
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?

View File

@ -1,22 +1,13 @@
/*
* 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
import androidx.room.PrimaryKey
/**
* Represents a top level directory in which music or other media is stored.
*/
@Entity(tableName = "music_folders", primaryKeys = ["id", "serverId"])
@Entity(tableName = "music_folders")
data class MusicFolder(
override val id: String,
override val name: String,
@ColumnInfo(defaultValue = "-1")
var serverId: Int
@PrimaryKey override val id: String,
override val name: String
) : GenericEntry()

View File

@ -0,0 +1,12 @@
package org.moire.ultrasonic.domain
enum class PlayerState {
IDLE,
DOWNLOADING,
PREPARING,
PREPARED,
STARTED,
STOPPED,
PAUSED,
COMPLETED
}

View File

@ -14,8 +14,4 @@ data class Playlist @JvmOverloads constructor(
companion object {
private const val serialVersionUID = -4160515427075433798L
}
override fun toString(): String {
return name
}
}

View File

@ -12,6 +12,4 @@ data class PodcastsChannel(
companion object {
private const val serialVersionUID = -4160515427075433798L
}
override fun toString(): String = title.toString()
}

View File

@ -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
}

View File

@ -1,22 +1,13 @@
/*
* 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 androidx.room.PrimaryKey
import java.io.Serializable
import java.util.Date
@Entity(tableName = "tracks", primaryKeys = ["id", "serverId"])
@Entity
data class Track(
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
@PrimaryKey override var id: String,
override var parent: String? = null,
override var isDirectory: Boolean = false,
override var title: String? = null,
@ -57,15 +48,15 @@ data class Track(
}
fun compareTo(other: Track): Int {
return when {
when {
this.closeness == other.closeness -> {
0
return 0
}
this.closeness > other.closeness -> {
-1
return -1
}
else -> {
1
return 1
}
}
}

View File

@ -1,14 +1,5 @@
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
@ -19,11 +10,21 @@ dependencies {
}
implementation libs.kotlinReflect // for jackson kotlin, but to use the same version
implementation libs.okhttpLogging
implementation libs.timber
testImplementation libs.kotlinJunit
testImplementation libs.mockito
testImplementation libs.mockitoInline
testImplementation libs.mockitoKotlin
testImplementation libs.kluent
testImplementation libs.mockWebServer
testImplementation libs.apacheCodecs
}
ext {
// Excluding data classes
jacocoExclude = [
'**/models/**',
'**/di/**'
]
}

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -44,7 +44,7 @@ fun <T : SubsonicResponse> Response<T>.throwOnFailure(): Response<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())

View File

@ -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,7 +63,6 @@ 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 ->
@ -75,19 +73,7 @@ class SubsonicAPIClient(
.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)
.baseUrl("${config.baseUrl}/rest/")
.client(okHttpClient)
.addConverterFactory(
VersionAwareJacksonConverterFactory.create(
@ -125,16 +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")
private fun OkHttpClient.Builder.allowSelfSignedCertificates() {
val trustManager =
@Suppress("CustomX509TrustManager")
object : X509TrustManager {
@Suppress("TrustAllX509TrustManager")
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
@Suppress("TrustAllX509TrustManager")
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}
@ -154,17 +139,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())
}
}

View File

@ -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")
@ -263,9 +254,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 +285,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 +307,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>
}

View File

@ -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 {

View File

@ -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
)

View File

@ -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> {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -10,8 +10,7 @@ class GetAlbumListResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("albumList")
private val albumWrapper = AlbumWrapper()
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
val albumList: List<Album>
get() = albumWrapper.albumList

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)

View File

@ -36,7 +36,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()
}
}

43
detekt-baseline.xml Normal file
View File

@ -0,0 +1,43 @@
<?xml version='1.0' encoding='UTF-8'?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided>$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !background</ID>
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>ImplicitDefaultLocale:EditServerFragment.kt$EditServerFragment.&lt;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>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</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>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array&lt;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:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided>$60000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</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:MediaPlayerService.kt$MediaPlayerService$3</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</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>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>TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw RuntimeException( String.format(Locale.ROOT, "Download of '%s' was cancelled", track) )</ID>
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
</CurrentIssues>
</SmellBaseline>

View File

@ -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:
@ -46,32 +49,34 @@ complexity:
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', '5', '10', '100', '256', '512', '1000', '1024']
ignoreEnums: true
ignorePropertyDeclaration: true
UnnecessaryAbstractClass:
active: false
ReturnCount:
max: 5
ForbiddenImport:
imports: ['android.app.AlertDialog']
comments:
active: true

View File

@ -1,37 +0,0 @@
Release notes
Ultrasonic 4.0.0 brings along a complete overhaul of how media is being played back. We have switched to a modern library to handle the playback and fixed wuite a few bugs in the process.
Additionally the Offline mode now supports browsing based on ID3 tags. This requires additional metadata which will be missing for tracks downloaded with previous versions. If you enable this setting, the app will therefore show only the music that you have downloaded with 4.0.0 or later.
Bug fixes
- #621: Empty notification shouldn't be visible.
- #664: Weird behaviour and system crash.
- #680: Now playing list should be able to distinguish duplicate items.
- #696: Ultrasonic is activated with BT even when all media play functions are disabled.
- #697: No sound after one track.
- #717: Insert after current song not fully working.
- #735: No longer showing any track data via bluetooth.
- #738: App no longer appears as running or in notifications when resuming playback after wake.
- #742: Pinned/downloaded music gets wiped/deleted on exit & start.
- #759: Crash browsing by folder (unchecking browsing by ID3 label in config).
- #767: Shuffle setting does not persist after reboot/resume.
- #768: Album art doesn't appear in the media notification.
- #769: The setting "settings.show_artist_picture" do nothing.
- #776: Strange behavior in device rotation.
- #785: Buttons below track list don't update anymore
- #787: The down arrow is no longer animated when downloading new tracks.
Enhancements
- #426: Start service only when playback is started by the user.
- #432: Ultrasonic notification should be dismissible by swipe.
- #553: Duration not read from metadata.
- #581: Bluetooth not updating Now Playing information anymore.
- #718: Add ID3 support when offline.
- #727: Disable LoggingInterceptor for Streaming calls.
- #739: Add more preload values.
- #752: Target SDK 31.
- #756: Android Auto needs work.
- #775: Use Picasso to load images for the notification.
Others
- #751: Remove Jacoco.
- #786: Update to MediaBeta02.

View File

@ -1,27 +0,0 @@
Bug fixes
- #765: Bad performance in Track scrolling.
- #779: Starting playback from Bluetooth should display the Media
notification.
- #780: Album art isn't displayed on first play of a non-cached track.
- #790: Items shouldn't be stuck in the ActivelyDownloading queue.
- #792: LyricsFragment unaccessible.
- #794: Equalizer broken.
- #795: Backups not allowed.
- #797: Server setting 'Jukebox By Default' broken.
- #798: Jukebox mode in 4.0.0 seems mostly broken.
- #799: Trying to open settings crashes the app.
- #801: Last.FM scrobbling broken on v4.0.0.
- #817: Crash when switching to Offline mode.
Enhancements
- #734: Migrate Downloader.
- #778: Do not show badge on Ultrasonic icon during playback.
- #784: Redesign Album Art download and cache handling.
- #796: Make the app more material.
- #815: Download buttons don't show when a track is downloading.
- #826: Reword the force plain text password setting.
Others
- #510: Use SafeArgs for remaining fragments.
- #793: Remove Visualizer.
- #818: Add Renovatebot.

View File

@ -1,6 +0,0 @@
Bug fixes
- #821: Multiple podcast selected = Never-ending buffering.
- !847: Fix Download bug in Custom Locations.
Enhancements
- !820: Updated widget handling and layouts, add Day&Night theme.

View File

@ -1,8 +0,0 @@
Bug fixes
- #823: Version 4 caches incomplete files (poor download error handling?).
- #832: Version 4.1.1 and develop: back(-navigation) no longer possible.
- #835: Crashing when play all songs in offline mode.
- #837: Saving playlist fails.
Enhancements
- #816: Accented letters should not be added seperately in the Artists list.

View File

@ -1,8 +0,0 @@
Bug fixes
- #833: Widget height is always a minimum of 2 rows instead of previous 1 row.
- #840: DownloadTask shouldn't be started multiple times for a track.
- !849: Bunch of fixes 💐.
Enhancements
- #841: Remove whitespace as default text in server settings.
- !865: Fix a small deprecation notice.

View File

@ -1 +0,0 @@
See 110

View File

@ -1,7 +0,0 @@
Bug fixes
- #849: NoSuchElementException.
- !874: Fix a crash.
Features
- #806: Display albums sorted chronologically.
- #843: Add Album cover view (Rework UI).

View File

@ -1,5 +0,0 @@
Bug fixes
- #848: ConcurrentModification crash.
Enhancements
- #859: "Download on Wi-Fi only" setting doesn't detect Wi-Fi when VPN is enabled.

View File

@ -1,14 +0,0 @@
Bug fixes
- #831: Version 4.1.1 and develop: 'jukebox on/off' no longer shown in 'Now Playing'.
- #850: ArrayIndexOutOfBoundsException.
- #858: Playlist is empty on relauch.
- #861: Podcast titles all shown as "null".
- #863: Ultrasonic crashes on startup when pointed to subsonic demo server.
- #864: Ultrasonic sends invalid URLs when Server Address ends in '/'.
- #867: Ultrasonic shouldn't crash when deleting a playlist.
- #872: Ultrasonic should handle custom cache location without creating multiple copies of a track.
- #886: Jukebox crashes since 4.2.1.
Enhancements
- #829: isJukeBoxAvailable is called on every visit of PlayerFragment.
- #854: Remove Videos menu option for servers which don't support it.

View File

@ -1,7 +0,0 @@
Bug fixes
- #831: Version 4.1.1 and develop: 'jukebox on/off' no longer shown in 'Now Playing'.
Enhancements
- #827: Make app full compliant Android Auto to publish in Play Store.
- #878: "Play shuffled" option for playlists always begins with the first track.
- #891: Dump config to log file when logging is enabled.

View File

@ -1,4 +0,0 @@
Bug fixes
- Fix a crash when a ID3 tag date is in a wrong format.
- Fix a crash on API 31 (newest Android).
- Fix empty search results.

View File

@ -1,2 +0,0 @@
Bug fixes
- Fix a crash when downloading the album art.

View File

@ -1,8 +0,0 @@
Bug fixes
- Fix various crashes
Changes since 4.2.0
- #827: Make app full compliant Android Auto to publish in Play Store.
- #878: "Play shuffled" option for playlists always begins with the first track.
- #891: Dump config to log file when logging is enabled.
- #854: Remove Videos menu option for servers which don't support it.

View File

@ -1,8 +0,0 @@
Bug fixes
- Fix more exceptions
Changes since 4.2.0
- #827: Make app full compliant Android Auto to publish in Play Store.
- #878: "Play shuffled" option for playlists always begins with the first track.
- #891: Dump config to log file when logging is enabled.
- #854: Remove Videos menu option for servers which don't support it.

View File

@ -1,10 +0,0 @@
Features:
- This releases focuses on shuffled playback. The view of the playlist will now present itself in the order it will actually play. You can toggle the shuffle mode to create a new order, while the past playback history will be preserved.
- Use Coroutines for triggering the download or playback of music through the context menus
- Enable Artists pictures by Default
Bug fixes:
- Remove an unhelpful popup that "ID must be set"
- Shuffle mode doesn't always play all tracks
- Shuffle mode starts with the first track most of the time

View File

@ -1,10 +0,0 @@
Features:
- This releases focuses on shuffled playback. The view of the playlist will now present itself in the order it will actually play. You can toggle the shuffle mode to create a new order, while the past playback history will be preserved.
- Use Coroutines for triggering the download or playback of music through the context menus
- Enable Artists pictures by Default
Bug fixes:
- Remove an unhelpful popup that "ID must be set"
- Shuffle mode doesn't always play all tracks
- Shuffle mode starts with the first track most of the time

View File

@ -1,10 +0,0 @@
Features:
- Revamp management of ratings. Tracks can be starred from the notification in Android 13, and the changes will show up everywhere immediately.
- Add a setting to control the maximum bitrate when pinning music (can be used to avoid downloading lossless files like flac).
- Modernize the Jukebox player.
- The hardware keys can be used to set the Jukebox volume.
- The current playlist shows a spinner when loading takes some time
Bug fixes:
- Request correct bluetooth permission on Android 13 (needed to pause/play on connect)
- Update dependencies (OkHttp, Material)

View File

@ -1,12 +0,0 @@
Features:
- Search is accesible through a new icon on the main screen
- Modernize Back Handling
- Reenable R8 Code minification
- Add a "Play Random Songs" shortcut
Bug fixes:
- Tracks buttons flash a scrollbar sometimes in Android 13
- Fix EndlessScrolling in genre listing
- Couldn't delete a track when shuffle was active
- Upgrade material to 1.9.0

View File

@ -1,15 +0,0 @@
Features:
- Search is accessible through a new icon on the main screen
- Modernize Back Handling
- Reenable R8 Code minification
- Add a "Play Random Songs" shortcut
Bug fixes:
- Readd the "Star" button to the Now Playing screen
- Fix a rare crash when shuffling playlists with duplicate entries
- Fix a crash when choosing "Play next" on an empty playlist.
- Tracks buttons flash a scrollbar sometimes in Android 13
- Fix EndlessScrolling in genre listing
- Couldn't delete a track when shuffle was active
- Upgrade material to 1.9.0

View File

@ -1,15 +0,0 @@
Features:
- Search is accessible through a new icon on the main screen
- Modernize Back Handling
- Reenable R8 Code minification
- Add a "Play Random Songs" shortcut
Bug fixes:
- Avoid triggering a bug in Supysonic
- Readd the "Star" button to the Now Playing screen
- Fix a rare crash when shuffling playlists with duplicate entries
- Fix a crash when choosing "Play next" on an empty playlist.
- Tracks buttons flash a scrollbar sometimes in Android 13
- Fix EndlessScrolling in genre listing
- Couldn't delete a track when shuffle was active

View File

@ -1,15 +0,0 @@
Features:
- Search is accessible through a new icon on the main screen
- Modernize Back Handling
- Reenable R8 Code minification
- Add a "Play Random Songs" shortcut
Bug fixes:
- Fix a few crashes
- Avoid triggering a bug in Supysonic
- Readd the "Star" button to the Now Playing screen
- Fix a rare crash when shuffling playlists with duplicate entries
- Fix a crash when choosing "Play next" on an empty playlist.
- Tracks buttons flash a scrollbar sometimes in Android 13
- Fix EndlessScrolling in genre listing
- Couldn't delete a track when shuffle was active

View File

@ -1,9 +0,0 @@
### Features
- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto)
- Properly handling nested directory structures (Android Auto)
- Add a toast when adding tracks to the playlist
- Allow pinning when offline
### Dependencies
- Update koin
- Update media3 to v1.1.0

View File

@ -1,12 +0,0 @@
### Fixes
- Fix a bug in 4.7.0 that repeat mode was activated by default.
### Features
- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto)
- Properly handling nested directory structures (Android Auto)
- Add a toast when adding tracks to the playlist
- Allow pinning when offline
### Dependencies
- Update koin
- Update media3 to v1.1.0

View File

@ -1,5 +0,0 @@
### Features
- Improved display of rating stars
- Completely modernize all older code parts
- Updates for Android 14
- Update dependencies

View File

@ -1,18 +1,18 @@
Ultrasonic is a Subsonic (and compatible servers) client to Android. You can use Ultrasonic to connect with your server and listen music.
Main features:
* Small size & fast
* Material You theme with dark and light variants
* Thin
* Fast
* Dark and light theme
* Multiple server support
* Download tracks for offline playback
* Offline Mode
* Bookmarks
* Playlists on server
* Shuffled playback
* Ramdom play
* Jukebox mode
* And much more!!
* Server chat
* And much more!!!
Note: Ultrasonic uses semantic release versions. Releases with a zero in the last digit introduce new features or significant changes, all other releases focus on fixing bugs.
The source code is available with GPL license in GitLab: https://gitlab.com/ultrasonic/ultrasonic
If you have any issue, please post in: https://gitlab.com/ultrasonic/ultrasonic/issues
Play store icon designed by: http://www.flaticon.com/authors/sebastien-gabriel
The source code is available with GPL license in Github: https://github.com/ultrasonic/ultrasonic
If you have any issue, please post in: https://github.com/ultrasonic/ultrasonic/issues
Play store icon designed by: http://www.flaticon.com/authors/sebastien-gabriel

View File

@ -1,37 +0,0 @@
Notas de la versión
Ultrasonic 4.0.0 trae consigo una revisión completa de cómo se reproducen los medios. Hemos cambiado a una biblioteca moderna para manejar la reproducción y hemos corregido algunos errores en el proceso.
Además, el modo Offline soporta ahora la navegación basada en las etiquetas ID3. Esto requiere metadatos adicionales que faltarán para las pistas descargadas con versiones anteriores. Si activas esta opción, la aplicación sólo mostrará la música que hayas descargado con la versión 4.0.0 o posterior.
Corrección de errores
- #621: Las notificaciones vacías no deberían ser visibles.
- #664: Comportamiento extraño y caída del sistema.
- #680: La lista de reproducción debería poder distinguir los elementos duplicados.
- #696: Ultrasonic se activan con BT incluso cuando todas las funciones de reproducción de medios están desactivadas.
- #697: No hay sonido después de una pista.
- #717: Insertar después de la canción actual no funciona del todo.
- #735: Ya no se muestran los datos de las pistas por bluetooth.
- #738: La aplicación ya no aparece como en ejecución o en las notificaciones al reanudar la reproducción.
- #742: La música anclada/descargada se borra al salir e iniciar.
- #759: Se bloquea la navegación por carpetas (desmarcando la navegación por etiquetas ID3 en la configuración).
- #767: La configuración de la reproducción aleatoria no persiste después de reiniciar/reanudar.
- #768: La carátula del álbum no aparece en la notificación de medios.
- #769: El ajuste "settings.show_artist_picture" no hace nada.
- #776: Comportamiento extraño en la rotación del dispositivo.
- #785: Los botones debajo de la lista de canciones ya no se actualizan.
- #787: La flecha hacia abajo ya no se anima cuando se descargan nuevas pistas.
Mejoras
- #426: Iniciar el servicio sólo cuando la reproducción es iniciada por el usuario.
- #432: Las notificaciones de Ultrasonic deberían poder desactivarse al deslizar el dedo.
- #553: La duración de las pistas no se lee de los metadatos.
- #581: Bluetooth ya no actualiza la información de Reproduciendo ahora.
- #718: Añadir soporte de ID3 cuando se está desconectado.
- #727: Desactivar LoggingInterceptor para las llamadas de Streaming.
- #739: Añadir más valores de precarga.
- #752: Apuntar al SDK 31.
- #756: Android Auto necesita trabajo.
- #775: Usar Picasso para cargar imágenes para la notificación.
Otros
- #751: Eliminar Jacoco.
- #786: Actualizar a MediaBeta02.

View File

@ -1,33 +0,0 @@
Corrección de errores
- #765: Mal rendimiento en el desplazamiento de pistas.
- #779: El inicio de la reproducción desde Bluetooth debería mostrar la
notificación de medios.
- #780: La carátula del álbum no se muestra en la primera reproducción de
una pista no almacenada.
- #790: Los elementos no deberían quedarse en la cola de
ActivelyDownloading.
- #792: LyricsFragment no es accesible.
- #794: Ecualizador roto.
- #795: Copias de seguridad no permitidas.
- #797: Configuración del servidor 'Jukebox por defecto' esta rota.
- #798: El modo Jukebox en la versión 4.0.0 parece estar roto en su mayor
parte.
- #799: Al intentar abrir la configuración se bloquea la aplicación.
- #801: El scrobbling de Last.FM no funciona en la versión 4.0.0.
- #817: Se bloquea al cambiar al modo Offline.
Mejoras
- #734: Migración del descargador.
- #778: No mostrar la insignia en el icono de Ultrasonic durante la
reproducción.
- #784: Rediseño de la descarga de la carátula del álbum y manejo de la
caché.
- #796: Hacer la aplicación más material.
- #815: Los botones de descarga no se muestran cuando una pista se está
descargando.
- #826: Reformular el ajuste de forzar la contraseña en texto plano.
Otros
- #510: Usar SafeArgs para los fragmentos restantes.
- #793: Eliminar el visualizador.
- #818: Añadir Renovatebot.

View File

@ -1,7 +0,0 @@
Corrección de errores
- #821: Múltiples podcasts seleccionados = Buffering interminable.
- !847: Corrección del error de descarga en las ubicaciones personalizadas.
Mejoras
- !820: Actualización del manejo de los widgets y de los diseños, adición
del tema Day&Night.

View File

@ -1,10 +0,0 @@
Corrección de errores
- #823: La versión 4 almacena en caché los archivos incompletos (¿mal manejo
de errores de descarga?).
- #832: Versión 4.1.1 y desarrollo: ya no es posible retroceder(-navegar).
- #835: Crash al reproducir todas las canciones en modo offline.
- #837: No se puede guardar la lista de reproducción.
Mejoras
- #816: Las letras acentuadas no deben añadirse por separado en la lista de
artistas.

View File

@ -1,8 +0,0 @@
Corrección de errores
- #833: La altura del widget es siempre un mínimo de 2 filas en lugar de la anterior de 1 fila.
- #840: DownloadTask no debería iniciarse varias veces para una canción.
- !849: Un montón de correcciones 💐.
Mejoras
- #841: Eliminar los espacios en blanco como texto por defecto en la configuración del servidor.
- !865: Corregir un pequeño aviso de código obsoleto.

View File

@ -1 +0,0 @@
Ver 110

View File

@ -1,8 +0,0 @@
Corrección de errores
- #849: NoSuchElementException.
- !874: Corrección de un fallo.
Características
- #806: Mostrar los álbumes ordenados cronológicamente.
- #843: Añadir la vista de la portada del álbum (reconstrucción de la
interfaz de usuario).

View File

@ -1,5 +0,0 @@
Corrección de errores
- #848: Fallo de ConcurrentModification.
Mejoras
- #859: El ajuste "Descargar sólo en Wi-Fi" no detecta la Wi-Fi cuando la VPN está activada.

View File

@ -1,14 +0,0 @@
Corrección de errores
- #831: Versión 4.1.1 y desarrollo: 'jukebox encender/apagar' ya no se muestra en 'Reproduciendo ahora'.
- #850: ArrayIndexOutOfBoundsException.
- #858: La lista de reproducción está vacía.
- #861: Los títulos de los podcasts se muestran como "null".
- #863: Ultrasonic se bloquea al inicio cuando se apunta al servidor de demostración subsonic.
- #864: Ultrasonic envía URLs inválidas cuando la dirección del servidor termina en '/'.
- #867: Ultrasonic no debería bloquearse al borrar una lista de reproducción.
- #872: Ultrasonic debería manejar la ubicación de caché personalizada sin crear múltiples copias de una pista.
- #886: Jukebox se bloquea desde 4.2.1.
Mejoras
- #829: isJukeBoxAvailable se llama en cada visita de PlayerFragment.
- #854: Eliminar la opción de menú Videos para servidores que no la soportan.

View File

@ -1,7 +0,0 @@
Corrección de errores
- #831: Versión 4.1.1 y desarrollo: 'jukebox on/off' ya no se muestra en 'Reproduciendo ahora'.
Mejoras
- #827: Hacer aplicación completamente compatible con Android Auto para publicar en Play Store.
- #878: "Reproducir aleatoriamente" ya no siempre comienza con la primera pista.
- #891: Volcado de configuración al archivo de registro cuando el registro está habilitado.

View File

@ -1,4 +0,0 @@
Corrección de errores
- Corrección de un fallo cuando la fecha de una etiqueta ID3 tiene un formato incorrecto.
- Corrección de un fallo en la API 31 (versión de Android más reciente).
- Corrección de resultados de búsqueda vacíos.

View File

@ -1,2 +0,0 @@
Corrección de errores
- Corrección de un fallo al descargar la carátula del álbum.

View File

@ -13,6 +13,6 @@ Características principales:
* Chat del servidor
* Y muchas mas!!!
El código esta disponible con licencia GPL en GitLab: https://gitlab.com/ultrasonic/ultrasonic
Si tienes problemas puedes dejar tu petición en: https://gitlab.com/ultrasonic/ultrasonic/issues
Icono diseñado por Sebastien Gabriel: http://www.flaticon.com/authors/sebastien-gabriel
El código esta disponible con licencia GPL en Github: https://github.com/ultrasonic/ultrasonic
Si tienes problemas puedes dejar tu petición en: https://github.com/ultrasonic/ultrasonic/issues
Icono diseñado por Sebastien Gabriel: http://www.flaticon.com/authors/sebastien-gabriel

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