Compare commits
67 Commits
4.0.0-beta
...
develop
Author | SHA1 | Date |
---|---|---|
Óscar García Amor | 90205fe0fb | |
Óscar García Amor | 2165ce75b3 | |
Óscar García Amor | 8b3ee0a8d6 | |
birdbird | 695b2df63f | |
tzugen | 798d795e81 | |
tzugen | ecfce59e0f | |
tzugen | de0cb7713b | |
tzugen | 78bfab3753 | |
tzugen | b955d77152 | |
tzugen | b11694d6a2 | |
tzugen | 31a1fdace1 | |
tzugen | 5b03b632fd | |
tzugen | 152b1d261a | |
tzugen | 53a1a5545a | |
tzugen | ad54db5bcb | |
tzugen | 177329abcf | |
tzugen | 241e51015f | |
tzugen | 60dbe70ca5 | |
tzugen | 8490f7115d | |
tzugen | ee67f4c744 | |
tzugen | 3a3bd10fdb | |
birdbird | 3445576dc9 | |
tzugen | 8c40f662a1 | |
birdbird | 6c6227ce41 | |
tzugen | 240a2fa8f6 | |
tzugen | 7de775dc26 | |
birdbird | d034fc9c71 | |
birdbird | 05ada9297d | |
Maxence G | aa6c037b20 | |
Maxence G | b8c924be27 | |
Maxence G | 0929a6a1bd | |
Maxence G | fefee74a66 | |
Maxence G | 37e3ce09c1 | |
Maxence G | 16b3fcad32 | |
Maxence G | d6aebd9989 | |
Maxence G | 3f408600cb | |
Maxence G | 9014b47b74 | |
tzugen | ac489ae8b9 | |
tzugen | e7f8fa21cb | |
tzugen | b1c3cabfef | |
tzugen | 77865a143d | |
Óscar García Amor | ff9c7b2435 | |
Óscar García Amor | 737563bf6b | |
tzugen | 9a73d72fa4 | |
tzugen | 98ce519014 | |
tzugen | 83fc54d332 | |
Maxence G | a2b9c6b9a3 | |
Maxence G | 5ae56d26c5 | |
Maxence G | 4efb6dcb58 | |
tzugen | 8a90e98989 | |
tzugen | 46a8f4640d | |
tzugen | ab41966943 | |
tzugen | 00d7ce326c | |
Maxence G | bc4b0aa832 | |
Maxence G | 23fd336ffd | |
Maxence G | b57a973510 | |
Maxence G | 8796006ced | |
Maxence G | 545b65921e | |
Maxence G | cf367ead92 | |
Maxence G | 9961213f09 | |
tzugen | 5deb7d4d58 | |
tzugen | 5f31eaaffe | |
tzugen | cad6477cd9 | |
tzugen | b440821ea8 | |
Holger Müller | 8663b9d50e | |
Óscar García Amor | 2bae243be0 | |
tzugen | 66443ba018 |
|
@ -9,7 +9,7 @@ parameters:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/android:2022.03.1
|
- image: cimg/android:2022.06.1
|
||||||
working_directory: ~/ultrasonic
|
working_directory: ~/ultrasonic
|
||||||
environment:
|
environment:
|
||||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||||
|
@ -41,7 +41,6 @@ jobs:
|
||||||
name: unit-tests
|
name: unit-tests
|
||||||
command: |
|
command: |
|
||||||
./gradlew ciTest testDebugUnitTest
|
./gradlew ciTest testDebugUnitTest
|
||||||
./gradlew jacocoFullReport
|
|
||||||
- run:
|
- run:
|
||||||
name: lint
|
name: lint
|
||||||
command: ./gradlew :ultrasonic:lintRelease
|
command: ./gradlew :ultrasonic:lintRelease
|
||||||
|
@ -61,8 +60,6 @@ jobs:
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: subsonic-api/build/reports
|
path: subsonic-api/build/reports
|
||||||
destination: reports
|
destination: reports
|
||||||
- store_artifacts:
|
|
||||||
path: build/reports/jacoco/jacocoFullReport/
|
|
||||||
push_translations:
|
push_translations:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/python:3.6
|
- image: cimg/python:3.6
|
||||||
|
@ -85,7 +82,7 @@ jobs:
|
||||||
tx push -s
|
tx push -s
|
||||||
generate_signed_apk:
|
generate_signed_apk:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/android:2022.03.1
|
- image: cimg/android:2022.06.1
|
||||||
working_directory: ~/ultrasonic
|
working_directory: ~/ultrasonic
|
||||||
environment:
|
environment:
|
||||||
JVM_OPTS: << pipeline.parameters.memory-config >>
|
JVM_OPTS: << pipeline.parameters.memory-config >>
|
||||||
|
|
|
@ -19,17 +19,45 @@ By default Pull Request should be opened against **develop** branch, PR against
|
||||||
|
|
||||||
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
|
1. **License Acceptance:** All contributions must be licensed as [GNU GPLv3](LICENSE) to be accepted.
|
||||||
Use `git commit --signoff` to acknowledge this.
|
Use `git commit --signoff` to acknowledge this.
|
||||||
2. **App is migrating to [Kotlin](https://kotlinlang.org/) programming language:** new Pull Requests
|
2. **No Breakage:** New features or changes to existing ones must not degrade the user experience.
|
||||||
should be written in this programming language.
|
3. **Coding standards:** best-practices should be followed, comment generously, and avoid "clever" algorithms.
|
||||||
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.
|
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
|
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.
|
and test.
|
||||||
|
|
||||||
### Pull Request Process
|
### Pull Request Process
|
||||||
|
On each Pull Request Github runs a number of checks to make sure there are no problems.
|
||||||
|
|
||||||
|
#### Signed commits
|
||||||
|
Commits must be signed. [See here how to set it up](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-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.
|
|
||||||
|
|
62
README.md
62
README.md
|
@ -1,14 +1,25 @@
|
||||||
# Ultrasonic
|
# WE HAVE MOVED
|
||||||
[![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](http://www.subsonic.org/) [API](http://www.subsonic.org/pages/api.jsp) (version 1.7.0 or higher) compatible servers.
|
Ultrasonic code is now hosted in [GitLab][ultrasonic].
|
||||||
|
|
||||||
|
- New Web: https://ultrasonic.gitlab.io
|
||||||
|
- New Git: https://gitlab.com/ultrasonic/ultrasonic
|
||||||
|
- New bugtracker: https://gitlab.com/ultrasonic/ultrasonic/-/issues
|
||||||
|
- New releases: https://gitlab.com/ultrasonic/ultrasonic/-/packages
|
||||||
|
|
||||||
|
[ultrasonic]: https://gitlab.com/ultrasonic/ultrasonic
|
||||||
|
|
||||||
|
# Ultrasonic
|
||||||
|
|
||||||
|
Ultrasonic is free and open-source music streaming Android client for
|
||||||
|
[Subsonic][subsonic] [API][subapi] (version 1.7.0 or higher) compatible
|
||||||
|
servers.
|
||||||
|
|
||||||
## Help wanted
|
## Help wanted
|
||||||
|
|
||||||
We currently don't have that much time to spend developing Subsonic, so any
|
We currently don't have that much time to spend developing Subsonic, so any
|
||||||
contributions or active developers are always welcomed.
|
contributions or active developers are always welcomed.
|
||||||
|
Have a look at [CONTRIBUTING](CONTRIBUTING.md) to get started.
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
|
@ -16,24 +27,26 @@ 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://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://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.github.io/assets/img/get-it-on-github.png" alt="Get it on GitHub" height="70">](https://github.com/ultrasonic/ultrasonic/releases)
|
[<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)
|
||||||
|
|
||||||
**Warning**: All three versions (Google Play, F-Droid and the APKs) are not
|
**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
|
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 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).
|
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
|
## Bugs and issues
|
||||||
|
|
||||||
First, see if your issue haven’t been yet reported [here](https://github.com/ultrasonic/ultrasonic/issues),
|
First, see if your issue haven’t been yet reported [here][issues], otherwise
|
||||||
otherwise open [a new issue](https://github.com/ultrasonic/ultrasonic/issues/new).
|
open [a new issue][newissue].
|
||||||
|
|
||||||
### Known (not our) bugs
|
### Known (not our) bugs
|
||||||
|
|
||||||
If you are using *Madsonic 5.1.X* several sections of Ultrasonic will not
|
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
|
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).
|
more info about this you can read [this bug][madbug].
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -41,16 +54,29 @@ See [CONTRIBUTING](CONTRIBUTING.md).
|
||||||
|
|
||||||
## Supported (tested) Subsonic API implementations
|
## Supported (tested) Subsonic API implementations
|
||||||
|
|
||||||
- [Subsonic](http://www.subsonic.org/pages/index.jsp)
|
- [Subsonic][subsonic]
|
||||||
- [Airsonic-Advanced](https://github.com/airsonic-advanced/airsonic-advanced)
|
- [Airsonic-Advanced][airsonic]
|
||||||
- [Supysonic](https://github.com/spl0k/supysonic)
|
- [Supysonic][supysonic]
|
||||||
- [Ampache](https://ampache.org/)
|
- [Ampache][ampache]
|
||||||
|
|
||||||
Other *Subsonic API* implementations should work as well as long as they follow API
|
Other *Subsonic API* implementations should work as well as long as they
|
||||||
[documentation](http://www.subsonic.org/pages/api.jsp).
|
follow API [documentation][subapi].
|
||||||
|
|
||||||
## License
|
## 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](https://opensource.org/licenses/gpl-3.0.html).
|
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
|
||||||
|
[madbug]: https://gitlab.com/ultrasonic/ultrasonic/-/issues/129
|
||||||
|
[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
|
||||||
|
|
|
@ -17,7 +17,6 @@ buildscript {
|
||||||
classpath libs.kotlin
|
classpath libs.kotlin
|
||||||
classpath libs.ktlintGradle
|
classpath libs.ktlintGradle
|
||||||
classpath libs.detekt
|
classpath libs.detekt
|
||||||
classpath libs.jacoco
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,8 +43,6 @@ allprojects {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: 'gradle_scripts/jacoco.gradle'
|
|
||||||
|
|
||||||
wrapper {
|
wrapper {
|
||||||
gradleVersion(libs.versions.gradle.get())
|
gradleVersion(libs.versions.gradle.get())
|
||||||
distributionType("all")
|
distributionType("all")
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
apply from: bootstrap.androidModule
|
apply from: bootstrap.androidModule
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
ext {
|
|
||||||
jacocoExclude = [
|
|
||||||
'**/domain/**'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation libs.roomRuntime
|
implementation libs.roomRuntime
|
||||||
implementation libs.roomKtx
|
implementation libs.roomKtx
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Album.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@Entity(tableName = "albums", primaryKeys = ["id", "serverId"])
|
||||||
data class Album(
|
data class Album(
|
||||||
@PrimaryKey override var id: String,
|
override var id: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
override var serverId: Int = -1,
|
||||||
override var parent: String? = null,
|
override var parent: String? = null,
|
||||||
override var album: String? = null,
|
override var album: String? = null,
|
||||||
override var title: String? = null,
|
override var title: String? = null,
|
||||||
|
|
|
@ -1,14 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Artist.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
@Entity(tableName = "artists")
|
@Entity(tableName = "artists", primaryKeys = ["id", "serverId"])
|
||||||
data class Artist(
|
data class Artist(
|
||||||
@PrimaryKey override var id: String,
|
override var id: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
override var serverId: Int = -1,
|
||||||
override var name: String? = null,
|
override var name: String? = null,
|
||||||
override var index: String? = null,
|
override var index: String? = null,
|
||||||
override var coverArt: String? = null,
|
override var coverArt: String? = null,
|
||||||
override var albumCount: Long? = null,
|
override var albumCount: Long? = null,
|
||||||
override var closeness: Int = 0
|
override var closeness: Int = 0
|
||||||
) : ArtistOrIndex(id)
|
) : ArtistOrIndex(id, serverId)
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
|
/*
|
||||||
|
* ArtistOrIndex.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import androidx.room.Ignore
|
import androidx.room.Ignore
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
abstract class ArtistOrIndex(
|
abstract class ArtistOrIndex(
|
||||||
@Ignore
|
@Ignore
|
||||||
override var id: String,
|
override var id: String,
|
||||||
@Ignore
|
@Ignore
|
||||||
|
open var serverId: Int,
|
||||||
|
@Ignore
|
||||||
override var name: String? = null,
|
override var name: String? = null,
|
||||||
@Ignore
|
@Ignore
|
||||||
open var index: String? = null,
|
open var index: String? = null,
|
||||||
|
@ -18,15 +28,15 @@ abstract class ArtistOrIndex(
|
||||||
) : GenericEntry() {
|
) : GenericEntry() {
|
||||||
|
|
||||||
fun compareTo(other: ArtistOrIndex): Int {
|
fun compareTo(other: ArtistOrIndex): Int {
|
||||||
when {
|
return when {
|
||||||
this.closeness == other.closeness -> {
|
this.closeness == other.closeness -> {
|
||||||
return 0
|
0
|
||||||
}
|
}
|
||||||
this.closeness > other.closeness -> {
|
this.closeness > other.closeness -> {
|
||||||
return -1
|
-1
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
return 1
|
1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Index.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
@Entity(tableName = "indexes")
|
@Entity(tableName = "indexes", primaryKeys = ["id", "serverId"])
|
||||||
data class Index(
|
data class Index(
|
||||||
@PrimaryKey override var id: String,
|
override var id: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
override var serverId: Int = -1,
|
||||||
override var name: String? = null,
|
override var name: String? = null,
|
||||||
override var index: String? = null,
|
override var index: String? = null,
|
||||||
override var coverArt: String? = null,
|
override var coverArt: String? = null,
|
||||||
override var albumCount: Long? = null,
|
override var albumCount: Long? = null,
|
||||||
override var closeness: Int = 0,
|
override var closeness: Int = 0,
|
||||||
var musicFolderId: String? = null
|
var musicFolderId: String? = null
|
||||||
) : ArtistOrIndex(id)
|
) : ArtistOrIndex(id, serverId)
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
/*
|
||||||
|
* MusicDirectory.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
@ -31,6 +38,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
||||||
|
|
||||||
abstract class Child : GenericEntry() {
|
abstract class Child : GenericEntry() {
|
||||||
abstract override var id: String
|
abstract override var id: String
|
||||||
|
abstract var serverId: Int
|
||||||
abstract var parent: String?
|
abstract var parent: String?
|
||||||
abstract var isDirectory: Boolean
|
abstract var isDirectory: Boolean
|
||||||
abstract var album: String?
|
abstract var album: String?
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
|
/*
|
||||||
|
* MusicFolder.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a top level directory in which music or other media is stored.
|
* Represents a top level directory in which music or other media is stored.
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "music_folders")
|
@Entity(tableName = "music_folders", primaryKeys = ["id", "serverId"])
|
||||||
data class MusicFolder(
|
data class MusicFolder(
|
||||||
@PrimaryKey override val id: String,
|
override val id: String,
|
||||||
override val name: String
|
override val name: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
var serverId: Int
|
||||||
) : GenericEntry()
|
) : GenericEntry()
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Track.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Entity
|
@Entity(tableName = "tracks", primaryKeys = ["id", "serverId"])
|
||||||
data class Track(
|
data class Track(
|
||||||
@PrimaryKey override var id: String,
|
override var id: String,
|
||||||
|
@ColumnInfo(defaultValue = "-1")
|
||||||
|
override var serverId: Int = -1,
|
||||||
override var parent: String? = null,
|
override var parent: String? = null,
|
||||||
override var isDirectory: Boolean = false,
|
override var isDirectory: Boolean = false,
|
||||||
override var title: String? = null,
|
override var title: String? = null,
|
||||||
|
|
|
@ -20,11 +20,3 @@ dependencies {
|
||||||
testImplementation libs.mockWebServer
|
testImplementation libs.mockWebServer
|
||||||
testImplementation libs.apacheCodecs
|
testImplementation libs.apacheCodecs
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
|
||||||
// Excluding data classes
|
|
||||||
jacocoExclude = [
|
|
||||||
'**/models/**',
|
|
||||||
'**/di/**'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ fun <T : SubsonicResponse> Response<T>.throwOnFailure(): Response<T> {
|
||||||
val response = this
|
val response = this
|
||||||
|
|
||||||
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
|
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
|
||||||
return this as Response<T>
|
return this
|
||||||
}
|
}
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw IOException("Server error, code: " + response.code())
|
throw IOException("Server error, code: " + response.code())
|
||||||
|
|
|
@ -64,10 +64,7 @@ style:
|
||||||
WildcardImport:
|
WildcardImport:
|
||||||
active: true
|
active: true
|
||||||
MaxLineLength:
|
MaxLineLength:
|
||||||
active: true
|
active: false
|
||||||
maxLineLength: 120
|
|
||||||
excludePackageStatements: false
|
|
||||||
excludeImportStatements: false
|
|
||||||
MagicNumber:
|
MagicNumber:
|
||||||
# 100 common in percentage, 1000 in milliseconds
|
# 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', '4096']
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
[versions]
|
[versions]
|
||||||
# You need to run ./gradlew wrapper after updating the version
|
# You need to run ./gradlew wrapper after updating the version
|
||||||
gradle = "7.3.2"
|
gradle = "7.3.3"
|
||||||
|
|
||||||
navigation = "2.3.5"
|
navigation = "2.3.5"
|
||||||
gradlePlugin = "7.1.1"
|
gradlePlugin = "7.2.1"
|
||||||
androidxcore = "1.6.0"
|
androidxcore = "1.6.0"
|
||||||
ktlint = "0.43.2"
|
ktlint = "0.43.2"
|
||||||
ktlintGradle = "10.2.0"
|
ktlintGradle = "10.2.0"
|
||||||
detekt = "1.19.0"
|
detekt = "1.19.0"
|
||||||
jacoco = "0.8.7"
|
|
||||||
preferences = "1.1.1"
|
preferences = "1.1.1"
|
||||||
media = "1.3.1"
|
media = "1.3.1"
|
||||||
media3 = "1.0.0-alpha03"
|
media3 = "1.0.0-beta01"
|
||||||
|
|
||||||
androidSupport = "28.0.0"
|
androidSupport = "1.4.0"
|
||||||
androidLegacySupport = "1.0.0"
|
androidLegacySupport = "1.0.0"
|
||||||
androidSupportDesign = "1.4.0"
|
androidSupportDesign = "1.6.1"
|
||||||
constraintLayout = "2.1.1"
|
constraintLayout = "2.1.1"
|
||||||
multidex = "2.0.1"
|
multidex = "2.0.1"
|
||||||
room = "2.4.0"
|
room = "2.4.2"
|
||||||
kotlin = "1.6.10"
|
kotlin = "1.6.10"
|
||||||
kotlinxCoroutines = "1.6.0-native-mt"
|
kotlinxCoroutines = "1.6.0-native-mt"
|
||||||
kotlinxGuava = "1.6.0"
|
kotlinxGuava = "1.6.0"
|
||||||
viewModelKtx = "2.3.0"
|
viewModelKtx = "2.4.1"
|
||||||
|
|
||||||
retrofit = "2.9.0"
|
retrofit = "2.9.0"
|
||||||
jackson = "2.10.1"
|
jackson = "2.10.1"
|
||||||
|
@ -49,12 +48,11 @@ gradle = { module = "com.android.tools.build:gradle", version.r
|
||||||
kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
|
ktlintGradle = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlintGradle" }
|
||||||
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
|
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
|
||||||
jacoco = { module = "org.jacoco:org.jacoco.core", version.ref = "jacoco" }
|
|
||||||
|
|
||||||
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
|
core = { module = "androidx.core:core-ktx", version.ref = "androidxcore" }
|
||||||
support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" }
|
support = { module = "androidx.legacy:legacy-support-v4", version.ref = "androidLegacySupport" }
|
||||||
design = { module = "com.google.android.material:material", version.ref = "androidSupportDesign" }
|
design = { module = "com.google.android.material:material", version.ref = "androidSupportDesign" }
|
||||||
annotations = { module = "com.android.support:support-annotations", version.ref = "androidSupport" }
|
annotations = { module = "androidx.annotation:annotation", version.ref = "androidSupport" }
|
||||||
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
|
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
|
||||||
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" }
|
constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" }
|
||||||
room = { module = "androidx.room:room-compiler", version.ref = "room" }
|
room = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
|
@ -103,4 +101,3 @@ kluentAndroid = { module = "org.amshove.kluent:kluent-android", versio
|
||||||
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
||||||
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
||||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
ext.versions = [
|
ext.versions = [
|
||||||
minSdk : 21,
|
minSdk : 21,
|
||||||
targetSdk : 30,
|
targetSdk : 33,
|
||||||
compileSdk : 31,
|
compileSdk : 31,
|
||||||
]
|
]
|
|
@ -1,5 +1,6 @@
|
||||||
|
#Fri Jun 17 23:13:49 CEST 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-all.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'jacoco'
|
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -48,10 +47,6 @@ android {
|
||||||
|
|
||||||
tasks.withType(Test) {
|
tasks.withType(Test) {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
jacoco {
|
|
||||||
includeNoLocationClasses = true
|
|
||||||
excludes += jacocoExclude
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -61,11 +56,4 @@ dependencies {
|
||||||
testRuntimeOnly libs.junitVintage
|
testRuntimeOnly libs.junitVintage
|
||||||
}
|
}
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
ext {
|
|
||||||
jacocoExclude = ['jdk.internal.*']
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
apply plugin: 'jacoco'
|
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
def mergedJacocoExec = file("${project.buildDir}/jacoco/jacocoMerged.exec")
|
|
||||||
|
|
||||||
def merge = tasks.register('jacocoMergeReports', JacocoMerge) {
|
|
||||||
group = "Reporting"
|
|
||||||
description = "Merge all jacoco reports from projects into one."
|
|
||||||
|
|
||||||
ListProperty<File> jacocoFiles = project.objects.listProperty(File.class)
|
|
||||||
project.subprojects { subproject ->
|
|
||||||
subproject.plugins.withId("jacoco") {
|
|
||||||
project.logger.info("${subproject.name} has Jacoco plugin applied")
|
|
||||||
subproject.tasks.withType(Test) { task ->
|
|
||||||
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
|
|
||||||
if (destFile.exists() && !task.name.contains("Release")) {
|
|
||||||
jacocoFiles.add(destFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executionData(jacocoFiles)
|
|
||||||
destinationFile(mergedJacocoExec)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('jacocoFullReport', JacocoReport) {
|
|
||||||
dependsOn merge
|
|
||||||
group = "Reporting"
|
|
||||||
description = "Generate full Jacoco coverage report including all modules."
|
|
||||||
|
|
||||||
getClassDirectories().setFrom(files())
|
|
||||||
getSourceDirectories().setFrom(files())
|
|
||||||
getExecutionData().setFrom(files())
|
|
||||||
|
|
||||||
reports {
|
|
||||||
xml.enabled = true
|
|
||||||
html.enabled = true
|
|
||||||
csv.enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always run merging, as all input calculation is done in doFirst {}
|
|
||||||
outputs.upToDateWhen { false }
|
|
||||||
// Task will run anyway even if initial inputs are empty
|
|
||||||
onlyIf = { true }
|
|
||||||
|
|
||||||
project.subprojects { subproject ->
|
|
||||||
subproject.plugins.withId("jacoco") {
|
|
||||||
project.logger.info("${subproject.name} has Jacoco plugin applied")
|
|
||||||
subproject.plugins.withId("kotlin-android") {
|
|
||||||
project.logger.info("${subproject.name} is android project")
|
|
||||||
def mainSources = subproject.extensions.findByName("android").sourceSets['main']
|
|
||||||
project.logger.info("Android sources: ${mainSources.java.srcDirs}")
|
|
||||||
mainSources.java.srcDirs.forEach {
|
|
||||||
additionalSourceDirs(it)
|
|
||||||
}
|
|
||||||
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
|
|
||||||
additionalClassDirs(fileTree(
|
|
||||||
dir: "${subproject.buildDir}/tmp/kotlin-classes/debug",
|
|
||||||
excludes: subproject.jacocoExclude
|
|
||||||
))
|
|
||||||
}
|
|
||||||
subproject.plugins.withId("kotlin") { plugin ->
|
|
||||||
project.logger.info("${subproject.name} is common kotlin project")
|
|
||||||
SourceDirectorySet mainSources = subproject.extensions.getByName("kotlin")
|
|
||||||
.sourceSets[SourceSet.MAIN_SOURCE_SET_NAME]
|
|
||||||
.kotlin
|
|
||||||
mainSources.srcDirs.forEach {
|
|
||||||
project.logger.debug("Adding sources: $it")
|
|
||||||
additionalSourceDirs(it)
|
|
||||||
}
|
|
||||||
project.logger.info("Subproject exclude: ${subproject.jacocoExclude}")
|
|
||||||
additionalClassDirs(fileTree(
|
|
||||||
dir: "${subproject.buildDir}/classes/kotlin/main",
|
|
||||||
excludes: subproject.jacocoExclude
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
subproject.tasks.withType(Test) { task ->
|
|
||||||
File destFile = task.extensions.getByType(JacocoTaskExtension.class).destinationFile
|
|
||||||
if (destFile.exists() && !task.name.contains("Release")) {
|
|
||||||
project.logger.info("Adding execution data: $destFile")
|
|
||||||
executionData(destFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'jacoco'
|
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
@ -21,36 +20,8 @@ dependencies {
|
||||||
testRuntimeOnly libs.junitVintage
|
testRuntimeOnly libs.junitVintage
|
||||||
}
|
}
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
ext {
|
|
||||||
// override it in the module
|
|
||||||
jacocoExclude = ['jdk.internal.*']
|
|
||||||
}
|
|
||||||
|
|
||||||
jacocoTestReport {
|
|
||||||
reports {
|
|
||||||
html.required = true
|
|
||||||
xml.required = false
|
|
||||||
csv.required = false
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEvaluate {
|
|
||||||
getClassDirectories().setFrom(files(classDirectories.files.collect {
|
|
||||||
fileTree(dir: it, excludes: jacocoExclude)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("test").configure {
|
tasks.named("test").configure {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
jacoco {
|
|
||||||
excludes += jacocoExclude
|
|
||||||
includeNoLocationClasses = true
|
|
||||||
}
|
|
||||||
finalizedBy jacocoTestReport
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("ciTest") {
|
tasks.register("ciTest") {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'jacoco'
|
|
||||||
apply from: "../gradle_scripts/code_quality.gradle"
|
apply from: "../gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -64,7 +63,7 @@ android {
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
arguments {
|
arguments {
|
||||||
arg("room.schemaLocation", "$buildDir/schemas".toString())
|
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas".toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
|
@ -74,6 +73,7 @@ android {
|
||||||
disable 'IconMissingDensityFolder', 'VectorPath'
|
disable 'IconMissingDensityFolder', 'VectorPath'
|
||||||
ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
||||||
warning 'ImpliedQuantity'
|
warning 'ImpliedQuantity'
|
||||||
|
disable 'ObsoleteLintCustomCheck'
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -135,36 +135,3 @@ dependencies {
|
||||||
|
|
||||||
implementation libs.timber
|
implementation libs.timber
|
||||||
}
|
}
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Excluding all java classes and stuff that should not be covered
|
|
||||||
ext {
|
|
||||||
jacocoExclude = [
|
|
||||||
'**/activity/**',
|
|
||||||
'**/audiofx/**',
|
|
||||||
'**/fragment/**',
|
|
||||||
'**/provider/**',
|
|
||||||
'**/receiver/**',
|
|
||||||
'**/service/**',
|
|
||||||
'**/Test/**',
|
|
||||||
'**/util/**',
|
|
||||||
'**/view/**',
|
|
||||||
'**/R$*.class',
|
|
||||||
'**/R.class',
|
|
||||||
'**/BuildConfig.class',
|
|
||||||
'**/di/**',
|
|
||||||
'jdk.internal.*'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
jacoco {
|
|
||||||
toolVersion(libs.versions.jacoco.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(Test) {
|
|
||||||
jacoco.includeNoLocationClasses = true
|
|
||||||
jacoco.excludes += jacocoExclude
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<issues format="6" by="lint 7.1.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.1.1)" variant="all" version="7.1.1">
|
<issues format="6" by="lint 7.2.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.2.1)" variant="all" version="7.2.1">
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="InflateParams"
|
id="InflateParams"
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
line="151"
|
line="155"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
line="75"
|
line="79"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -77,18 +77,7 @@
|
||||||
errorLine2=" ~~~~~~~">
|
errorLine2=" ~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/AndroidManifest.xml"
|
file="src/main/AndroidManifest.xml"
|
||||||
line="65"
|
line="68"
|
||||||
column="10"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="IntentFilterExportedReceiver"
|
|
||||||
message="As of Android 12, `android:exported` must be set; use `true` to make the activity \
available to other apps, and `false` otherwise. For launcher activities, this should be set to `true`."
|
|
||||||
errorLine1=" <activity android:name=".activity.NavigationActivity""
|
|
||||||
errorLine2=" ~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/AndroidManifest.xml"
|
|
||||||
line="41"
|
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
@ -180,6 +169,61 @@
|
||||||
column="1"/>
|
column="1"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="UnusedResources"
|
||||||
|
message="The resource `R.drawable.media3_notification_pause` appears to be unused"
|
||||||
|
errorLine1="<vector android:height="48dp""
|
||||||
|
errorLine2="^">
|
||||||
|
<location
|
||||||
|
file="src/main/res/drawable/media3_notification_pause.xml"
|
||||||
|
line="1"
|
||||||
|
column="1"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="UnusedResources"
|
||||||
|
message="The resource `R.drawable.media3_notification_play` appears to be unused"
|
||||||
|
errorLine1="<vector android:height="48dp""
|
||||||
|
errorLine2="^">
|
||||||
|
<location
|
||||||
|
file="src/main/res/drawable/media3_notification_play.xml"
|
||||||
|
line="1"
|
||||||
|
column="1"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="UnusedResources"
|
||||||
|
message="The resource `R.drawable.media3_notification_seek_to_next` appears to be unused"
|
||||||
|
errorLine1="<vector android:height="32dp""
|
||||||
|
errorLine2="^">
|
||||||
|
<location
|
||||||
|
file="src/main/res/drawable/media3_notification_seek_to_next.xml"
|
||||||
|
line="1"
|
||||||
|
column="1"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="UnusedResources"
|
||||||
|
message="The resource `R.drawable.media3_notification_seek_to_previous` appears to be unused"
|
||||||
|
errorLine1="<vector android:height="32dp""
|
||||||
|
errorLine2="^">
|
||||||
|
<location
|
||||||
|
file="src/main/res/drawable/media3_notification_seek_to_previous.xml"
|
||||||
|
line="1"
|
||||||
|
column="1"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue
|
||||||
|
id="UnusedResources"
|
||||||
|
message="The resource `R.drawable.media3_notification_small_icon` appears to be unused"
|
||||||
|
errorLine1="<vector xmlns:android="http://schemas.android.com/apk/res/android""
|
||||||
|
errorLine2="^">
|
||||||
|
<location
|
||||||
|
file="src/main/res/drawable/media3_notification_small_icon.xml"
|
||||||
|
line="1"
|
||||||
|
column="1"/>
|
||||||
|
</issue>
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="IconDuplicates"
|
id="IconDuplicates"
|
||||||
message="The following unrelated icon files have identical contents: list_pressed_holo_dark.9.png, list_pressed_holo_light.9.png">
|
message="The following unrelated icon files have identical contents: list_pressed_holo_dark.9.png, list_pressed_holo_light.9.png">
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 5,
|
||||||
|
"identityHash": "4cea788a99b9bc28500948b1cd92e537",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "ServerSetting",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "color",
|
||||||
|
"columnName": "color",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userName",
|
||||||
|
"columnName": "userName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "jukeboxByDefault",
|
||||||
|
"columnName": "jukeboxByDefault",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "allowSelfSignedCertificate",
|
||||||
|
"columnName": "allowSelfSignedCertificate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ldapSupport",
|
||||||
|
"columnName": "ldapSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "musicFolderId",
|
||||||
|
"columnName": "musicFolderId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "minimumApiVersion",
|
||||||
|
"columnName": "minimumApiVersion",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chatSupport",
|
||||||
|
"columnName": "chatSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkSupport",
|
||||||
|
"columnName": "bookmarkSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shareSupport",
|
||||||
|
"columnName": "shareSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "podcastSupport",
|
||||||
|
"columnName": "podcastSupport",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4cea788a99b9bc28500948b1cd92e537')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "0580217b1e87b02d2edaf9b008891cbc",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "artists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "indexes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "musicFolderId",
|
||||||
|
"columnName": "musicFolderId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "music_folders",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0580217b1e87b02d2edaf9b008891cbc')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,474 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "b6ac795e7857eac4fed2dbbd01f80fb8",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "artists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "albums",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parent",
|
||||||
|
"columnName": "parent",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "album",
|
||||||
|
"columnName": "album",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "discNumber",
|
||||||
|
"columnName": "discNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songCount",
|
||||||
|
"columnName": "songCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "created",
|
||||||
|
"columnName": "created",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artist",
|
||||||
|
"columnName": "artist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "year",
|
||||||
|
"columnName": "year",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "genre",
|
||||||
|
"columnName": "genre",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isDirectory",
|
||||||
|
"columnName": "isDirectory",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVideo",
|
||||||
|
"columnName": "isVideo",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tracks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parent",
|
||||||
|
"columnName": "parent",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isDirectory",
|
||||||
|
"columnName": "isDirectory",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "album",
|
||||||
|
"columnName": "album",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumId",
|
||||||
|
"columnName": "albumId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artist",
|
||||||
|
"columnName": "artist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "track",
|
||||||
|
"columnName": "track",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "year",
|
||||||
|
"columnName": "year",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "genre",
|
||||||
|
"columnName": "genre",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentType",
|
||||||
|
"columnName": "contentType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "suffix",
|
||||||
|
"columnName": "suffix",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "transcodedContentType",
|
||||||
|
"columnName": "transcodedContentType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "transcodedSuffix",
|
||||||
|
"columnName": "transcodedSuffix",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songCount",
|
||||||
|
"columnName": "songCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bitRate",
|
||||||
|
"columnName": "bitRate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVideo",
|
||||||
|
"columnName": "isVideo",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "discNumber",
|
||||||
|
"columnName": "discNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "created",
|
||||||
|
"columnName": "created",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkPosition",
|
||||||
|
"columnName": "bookmarkPosition",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userRating",
|
||||||
|
"columnName": "userRating",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "averageRating",
|
||||||
|
"columnName": "averageRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "indexes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "musicFolderId",
|
||||||
|
"columnName": "musicFolderId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "music_folders",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b6ac795e7857eac4fed2dbbd01f80fb8')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,514 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "95e83d6663a862c03ac46f9567453ded",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "artists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "albums",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parent",
|
||||||
|
"columnName": "parent",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "album",
|
||||||
|
"columnName": "album",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "discNumber",
|
||||||
|
"columnName": "discNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songCount",
|
||||||
|
"columnName": "songCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "created",
|
||||||
|
"columnName": "created",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artist",
|
||||||
|
"columnName": "artist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "year",
|
||||||
|
"columnName": "year",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "genre",
|
||||||
|
"columnName": "genre",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isDirectory",
|
||||||
|
"columnName": "isDirectory",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVideo",
|
||||||
|
"columnName": "isVideo",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tracks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parent",
|
||||||
|
"columnName": "parent",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isDirectory",
|
||||||
|
"columnName": "isDirectory",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "album",
|
||||||
|
"columnName": "album",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumId",
|
||||||
|
"columnName": "albumId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artist",
|
||||||
|
"columnName": "artist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "track",
|
||||||
|
"columnName": "track",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "year",
|
||||||
|
"columnName": "year",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "genre",
|
||||||
|
"columnName": "genre",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentType",
|
||||||
|
"columnName": "contentType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "suffix",
|
||||||
|
"columnName": "suffix",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "transcodedContentType",
|
||||||
|
"columnName": "transcodedContentType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "transcodedSuffix",
|
||||||
|
"columnName": "transcodedSuffix",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "size",
|
||||||
|
"columnName": "size",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songCount",
|
||||||
|
"columnName": "songCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bitRate",
|
||||||
|
"columnName": "bitRate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVideo",
|
||||||
|
"columnName": "isVideo",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "starred",
|
||||||
|
"columnName": "starred",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "discNumber",
|
||||||
|
"columnName": "discNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "created",
|
||||||
|
"columnName": "created",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkPosition",
|
||||||
|
"columnName": "bookmarkPosition",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "userRating",
|
||||||
|
"columnName": "userRating",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "averageRating",
|
||||||
|
"columnName": "averageRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "indexes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "index",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverArt",
|
||||||
|
"columnName": "coverArt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumCount",
|
||||||
|
"columnName": "albumCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "closeness",
|
||||||
|
"columnName": "closeness",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "musicFolderId",
|
||||||
|
"columnName": "musicFolderId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "music_folders",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT NOT NULL, PRIMARY KEY(`id`, `serverId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '95e83d6663a862c03ac46f9567453ded')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/NoActionBar"
|
android:theme="@style/NoActionBar"
|
||||||
|
@ -40,7 +42,8 @@
|
||||||
|
|
||||||
<activity android:name=".activity.NavigationActivity"
|
<activity android:name=".activity.NavigationActivity"
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="orientation|keyboardHidden"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.SEARCH"/>
|
<action android:name="android.intent.action.SEARCH"/>
|
||||||
|
@ -61,9 +64,10 @@
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<!-- TODO: Check if it works with exported=false as well -->
|
<!-- Needs to be exported: https://android.googlesource.com/platform/developers/build/+/4de32d4/prebuilts/gradle/MediaBrowserService/README.md -->
|
||||||
<service android:name=".playback.PlaybackService"
|
<service android:name=".playback.PlaybackService"
|
||||||
android:label="@string/common.appname"
|
android:label="@string/common.appname"
|
||||||
|
android:foregroundServiceType="mediaPlayback"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -150,7 +154,8 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
<provider
|
<provider
|
||||||
android:name=".provider.SearchSuggestionProvider"
|
android:name=".provider.SearchSuggestionProvider"
|
||||||
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"/>
|
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
|
@ -392,6 +392,8 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
if (!infoDialogDisplayed) {
|
if (!infoDialogDisplayed) {
|
||||||
infoDialogDisplayed = true
|
infoDialogDisplayed = true
|
||||||
|
|
||||||
|
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
||||||
|
|
||||||
InfoDialog.Builder(this)
|
InfoDialog.Builder(this)
|
||||||
.setTitle(R.string.main_welcome_title)
|
.setTitle(R.string.main_welcome_title)
|
||||||
.setMessage(R.string.main_welcome_text_demo)
|
.setMessage(R.string.main_welcome_text_demo)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* ArtistRowAdapter.kt
|
* ArtistRowBinder.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
@ -19,6 +19,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.drakeet.multitype.ItemViewBinder
|
import com.drakeet.multitype.ItemViewBinder
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||||
|
@ -57,7 +58,7 @@ class ArtistRowBinder(
|
||||||
|
|
||||||
holder.coverArtId = item.coverArt
|
holder.coverArtId = item.coverArt
|
||||||
|
|
||||||
if (Settings.shouldShowArtistPicture) {
|
if (showArtistPicture()) {
|
||||||
holder.coverArt.visibility = View.VISIBLE
|
holder.coverArt.visibility = View.VISIBLE
|
||||||
val key = FileUtil.getArtistArtKey(item.name, false)
|
val key = FileUtil.getArtistArtKey(item.name, false)
|
||||||
imageLoader.loadImage(
|
imageLoader.loadImage(
|
||||||
|
@ -102,11 +103,16 @@ class ArtistRowBinder(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSectionFromName(name: String): String {
|
private fun getSectionFromName(name: String): String {
|
||||||
var section = name.first().uppercaseChar()
|
if (name.isEmpty()) return SECTION_KEY_DEFAULT
|
||||||
if (!section.isLetter()) section = '#'
|
val section = name.first().uppercaseChar()
|
||||||
|
if (!section.isLetter()) return SECTION_KEY_DEFAULT
|
||||||
return section.toString()
|
return section.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showArtistPicture(): Boolean {
|
||||||
|
return ActiveServerProvider.isID3Enabled() && Settings.shouldShowArtistPicture
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of our ViewHolder class
|
* Creates an instance of our ViewHolder class
|
||||||
*/
|
*/
|
||||||
|
@ -123,4 +129,8 @@ class ArtistRowBinder(
|
||||||
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
|
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
|
||||||
return ViewHolder(inflater.inflate(layout, parent, false))
|
return ViewHolder(inflater.inflate(layout, parent, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SECTION_KEY_DEFAULT = "#"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
|
||||||
throw IllegalAccessException("You must use submitList() to add data to the Adapter")
|
throw IllegalAccessException("You must use submitList() to add data to the Adapter")
|
||||||
}
|
}
|
||||||
|
|
||||||
var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
|
private var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
|
||||||
AdapterListUpdateCallback(this),
|
AdapterListUpdateCallback(this),
|
||||||
AsyncDifferConfig.Builder(diffCallback).build()
|
AsyncDifferConfig.Builder(diffCallback).build()
|
||||||
)
|
)
|
||||||
|
@ -182,12 +182,11 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
|
||||||
|
|
||||||
// Select them all
|
// Select them all
|
||||||
getCurrentList().mapNotNullTo(
|
getCurrentList().mapNotNullTo(
|
||||||
selectedSet,
|
selectedSet
|
||||||
{ entry ->
|
) { entry ->
|
||||||
// Exclude any -1 ids, eg. headers and other UI elements
|
// Exclude any -1 ids, eg. headers and other UI elements
|
||||||
entry.longId.takeIf { it != -1L }
|
entry.longId.takeIf { it != -1L }
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
return selectedSet.count()
|
return selectedSet.count()
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,6 +153,8 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
|
||||||
star.setImageDrawable(imageHelper.starHollowDrawable)
|
star.setImageDrawable(imageHelper.starHollowDrawable)
|
||||||
song.starred = false
|
song.starred = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should this be done here ?
|
||||||
Thread {
|
Thread {
|
||||||
val musicService = MusicServiceFactory.getMusicService()
|
val musicService = MusicServiceFactory.getMusicService()
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
/*
|
||||||
|
* ActiveServerProvider.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
|
@ -110,20 +117,23 @@ class ActiveServerProvider(
|
||||||
|
|
||||||
Timber.i("Switching to new database, id:$activeServer")
|
Timber.i("Switching to new database, id:$activeServer")
|
||||||
cachedServerId = activeServer
|
cachedServerId = activeServer
|
||||||
return buildDatabase(cachedServerId)
|
cachedDatabase = initDatabase(activeServer)
|
||||||
|
|
||||||
|
return cachedDatabase!!
|
||||||
}
|
}
|
||||||
|
|
||||||
val offlineMetaDatabase: MetaDatabase by lazy {
|
val offlineMetaDatabase: MetaDatabase by lazy {
|
||||||
buildDatabase(OFFLINE_DB_ID)
|
initDatabase(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDatabase(id: Int?): MetaDatabase {
|
private fun initDatabase(serverId: Int): MetaDatabase {
|
||||||
return Room.databaseBuilder(
|
return Room.databaseBuilder(
|
||||||
UApp.applicationContext(),
|
UApp.applicationContext(),
|
||||||
MetaDatabase::class.java,
|
MetaDatabase::class.java,
|
||||||
METADATA_DB + id
|
METADATA_DB + serverId
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.addMigrations(META_MIGRATION_2_3)
|
||||||
|
.fallbackToDestructiveMigrationOnDowngrade()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +249,13 @@ class ActiveServerProvider(
|
||||||
return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false)
|
return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries if ID3 tags should be used
|
||||||
|
*/
|
||||||
|
fun isID3Enabled(): Boolean {
|
||||||
|
return Settings.shouldUseId3Tags && (!isOffline() || Settings.useId3TagsOffline)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries if Server Scaling is enabled
|
* Queries if Server Scaling is enabled
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import org.moire.ultrasonic.domain.Album
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface AlbumDao : GenericDao<Album> {
|
||||||
|
/**
|
||||||
|
* Clear the whole database
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM albums")
|
||||||
|
fun clear()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all albums
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM albums")
|
||||||
|
fun get(): List<Album>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all albums in a specific range
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM albums LIMIT :offset,:size")
|
||||||
|
fun get(size: Int, offset: Int = 0): List<Album>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get album by id
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM albums where id LIKE :albumId LIMIT 1")
|
||||||
|
fun get(albumId: String): Album
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get albums by artist
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM albums WHERE artistId LIKE :id")
|
||||||
|
fun byArtist(id: String): List<Album>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear albums by artist
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM albums WHERE artistId LIKE :id")
|
||||||
|
fun clearByArtist(id: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Make generic
|
||||||
|
* Upserts (insert or update) an object to the database
|
||||||
|
*
|
||||||
|
* @param obj the object to upsert
|
||||||
|
*/
|
||||||
|
@Transaction
|
||||||
|
@JvmSuppressWildcards
|
||||||
|
fun upsert(obj: Album) {
|
||||||
|
val id = insertIgnoring(obj)
|
||||||
|
if (id == -1L) {
|
||||||
|
update(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upserts (insert or update) a list of objects
|
||||||
|
*
|
||||||
|
* @param objList the object to be upserted
|
||||||
|
*/
|
||||||
|
@Transaction
|
||||||
|
@JvmSuppressWildcards
|
||||||
|
fun upsert(objList: List<Album>) {
|
||||||
|
val insertResult = insertIgnoring(objList)
|
||||||
|
val updateList: MutableList<Album> = ArrayList()
|
||||||
|
for (i in insertResult.indices) {
|
||||||
|
if (insertResult[i] == -1L) {
|
||||||
|
updateList.add(objList[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateList.isNotEmpty()) {
|
||||||
|
update(updateList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import androidx.room.Query
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ArtistsDao {
|
interface ArtistDao {
|
||||||
/**
|
/**
|
||||||
* Insert a list in the database. If the item already exists, replace it.
|
* Insert a list in the database. If the item already exists, replace it.
|
||||||
*
|
*
|
||||||
|
@ -43,5 +43,5 @@ interface ArtistsDao {
|
||||||
* Get artist by id
|
* Get artist by id
|
||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM artists WHERE id LIKE :id")
|
@Query("SELECT * FROM artists WHERE id LIKE :id")
|
||||||
fun get(id: String): Artist
|
fun get(id: String): Artist?
|
||||||
}
|
}
|
|
@ -53,6 +53,7 @@ interface IndexDao : GenericDao<Index> {
|
||||||
fun get(musicFolderId: String): List<Index>
|
fun get(musicFolderId: String): List<Index>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO: Make generic
|
||||||
* Upserts (insert or update) an object to the database
|
* Upserts (insert or update) an object to the database
|
||||||
*
|
*
|
||||||
* @param obj the object to upsert
|
* @param obj the object to upsert
|
||||||
|
|
|
@ -1,23 +1,85 @@
|
||||||
|
/*
|
||||||
|
* MetaDatabase.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import java.util.Date
|
||||||
|
import org.moire.ultrasonic.domain.Album
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.Index
|
import org.moire.ultrasonic.domain.Index
|
||||||
import org.moire.ultrasonic.domain.MusicFolder
|
import org.moire.ultrasonic.domain.MusicFolder
|
||||||
|
import org.moire.ultrasonic.domain.Track
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This database is used to store and cache the ID3 metadata
|
* This database is used to store and cache the ID3 metadata
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Artist::class, Index::class, MusicFolder::class],
|
entities = [
|
||||||
version = 1
|
Artist::class,
|
||||||
|
Album::class,
|
||||||
|
Track::class,
|
||||||
|
Index::class,
|
||||||
|
MusicFolder::class
|
||||||
|
],
|
||||||
|
autoMigrations = [
|
||||||
|
AutoMigration(
|
||||||
|
from = 1,
|
||||||
|
to = 2
|
||||||
|
),
|
||||||
|
],
|
||||||
|
exportSchema = true,
|
||||||
|
version = 3
|
||||||
)
|
)
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
abstract class MetaDatabase : RoomDatabase() {
|
abstract class MetaDatabase : RoomDatabase() {
|
||||||
abstract fun artistsDao(): ArtistsDao
|
abstract fun artistDao(): ArtistDao
|
||||||
|
|
||||||
|
abstract fun albumDao(): AlbumDao
|
||||||
|
|
||||||
|
abstract fun trackDao(): TrackDao
|
||||||
|
|
||||||
abstract fun musicFoldersDao(): MusicFoldersDao
|
abstract fun musicFoldersDao(): MusicFoldersDao
|
||||||
|
|
||||||
abstract fun indexDao(): IndexDao
|
abstract fun indexDao(): IndexDao
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
@TypeConverter
|
||||||
|
fun fromTimestamp(value: Long?): Date? {
|
||||||
|
return value?.let { Date(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun dateToTimestamp(date: Date?): Long? {
|
||||||
|
return date?.time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ktlint-disable max-line-length */
|
||||||
|
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("DROP TABLE `albums`")
|
||||||
|
database.execSQL("DROP TABLE `indexes`")
|
||||||
|
database.execSQL("DROP TABLE `artists`")
|
||||||
|
database.execSQL("DROP TABLE `tracks`")
|
||||||
|
database.execSQL("DROP TABLE `music_folders`")
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))")
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))")
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ktlint-enable max-line-length */
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Query
|
||||||
|
import org.moire.ultrasonic.domain.Track
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
@Entity(tableName = "tracks")
|
||||||
|
interface TrackDao : GenericDao<Track> {
|
||||||
|
/**
|
||||||
|
* Clear the whole database
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM tracks")
|
||||||
|
fun clear()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all albums
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM tracks")
|
||||||
|
fun get(): List<Track>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get albums by artist
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM tracks WHERE albumId LIKE :id")
|
||||||
|
fun byAlbum(id: String): List<Track>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get albums by artist
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM tracks WHERE artistId LIKE :id")
|
||||||
|
fun byArtist(id: String): List<Track>
|
||||||
|
}
|
|
@ -1,3 +1,10 @@
|
||||||
|
/*
|
||||||
|
* APIAlbumConverter.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIAlbumConverter")
|
@file:JvmName("APIAlbumConverter")
|
||||||
|
@ -6,8 +13,9 @@ package org.moire.ultrasonic.domain
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Album
|
import org.moire.ultrasonic.api.subsonic.models.Album
|
||||||
typealias DomainAlbum = org.moire.ultrasonic.domain.Album
|
typealias DomainAlbum = org.moire.ultrasonic.domain.Album
|
||||||
|
|
||||||
fun Album.toDomainEntity(): DomainAlbum = Album(
|
fun Album.toDomainEntity(serverId: Int): DomainAlbum = Album(
|
||||||
id = this@toDomainEntity.id,
|
id = this@toDomainEntity.id,
|
||||||
|
serverId = serverId,
|
||||||
title = this@toDomainEntity.name ?: this@toDomainEntity.title,
|
title = this@toDomainEntity.name ?: this@toDomainEntity.title,
|
||||||
album = this@toDomainEntity.album,
|
album = this@toDomainEntity.album,
|
||||||
coverArt = this@toDomainEntity.coverArt,
|
coverArt = this@toDomainEntity.coverArt,
|
||||||
|
@ -21,8 +29,10 @@ fun Album.toDomainEntity(): DomainAlbum = Album(
|
||||||
starred = this@toDomainEntity.starredDate.isNotEmpty()
|
starred = this@toDomainEntity.starredDate.isNotEmpty()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Album.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
|
fun Album.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory = MusicDirectory().apply {
|
||||||
addAll(this@toMusicDirectoryDomainEntity.songList.map { it.toTrackEntity() })
|
addAll(this@toMusicDirectoryDomainEntity.songList.map { it.toTrackEntity(serverId) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<Album>.toDomainEntityList(): List<DomainAlbum> = this.map { it.toDomainEntity() }
|
fun List<Album>.toDomainEntityList(serverId: Int): List<DomainAlbum> = this.map {
|
||||||
|
it.toDomainEntity(serverId)
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
/*
|
||||||
|
* APIArtistConverter.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIArtistConverter")
|
@file:JvmName("APIArtistConverter")
|
||||||
|
@ -6,24 +13,26 @@ package org.moire.ultrasonic.domain
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
||||||
|
|
||||||
// When we like to convert to an Artist
|
// When we like to convert to an Artist
|
||||||
fun APIArtist.toDomainEntity(): Artist = Artist(
|
fun APIArtist.toDomainEntity(serverId: Int): Artist = Artist(
|
||||||
id = this@toDomainEntity.id,
|
id = this@toDomainEntity.id,
|
||||||
|
serverId = serverId,
|
||||||
coverArt = this@toDomainEntity.coverArt,
|
coverArt = this@toDomainEntity.coverArt,
|
||||||
name = this@toDomainEntity.name
|
name = this@toDomainEntity.name
|
||||||
)
|
)
|
||||||
|
|
||||||
// When we like to convert to an index (eg. a single directory).
|
// When we like to convert to an index (eg. a single directory).
|
||||||
fun APIArtist.toIndexEntity(): Index = Index(
|
fun APIArtist.toIndexEntity(serverId: Int): Index = Index(
|
||||||
id = this@toIndexEntity.id,
|
id = this@toIndexEntity.id,
|
||||||
|
serverId = serverId,
|
||||||
coverArt = this@toIndexEntity.coverArt,
|
coverArt = this@toIndexEntity.coverArt,
|
||||||
name = this@toIndexEntity.name
|
name = this@toIndexEntity.name
|
||||||
)
|
)
|
||||||
|
|
||||||
fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
|
fun APIArtist.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory = MusicDirectory().apply {
|
||||||
name = this@toMusicDirectoryDomainEntity.name
|
name = this@toMusicDirectoryDomainEntity.name
|
||||||
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() })
|
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity(serverId) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun APIArtist.toDomainEntityList(): List<Album> {
|
fun APIArtist.toDomainEntityList(serverId: Int): List<Album> {
|
||||||
return this.albumsList.map { it.toDomainEntity() }
|
return this.albumsList.map { it.toDomainEntity(serverId) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
|
/*
|
||||||
|
* APIBookmarkConverter.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
// Contains helper functions to convert api Bookmark entity to domain entity
|
// Contains helper functions to convert api Bookmark entity to domain entity
|
||||||
@file:JvmName("APIBookmarkConverter")
|
@file:JvmName("APIBookmarkConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Bookmark as ApiBookmark
|
import org.moire.ultrasonic.api.subsonic.models.Bookmark as ApiBookmark
|
||||||
|
|
||||||
fun ApiBookmark.toDomainEntity(): Bookmark = Bookmark(
|
fun ApiBookmark.toDomainEntity(serverId: Int): Bookmark = Bookmark(
|
||||||
position = this@toDomainEntity.position.toInt(),
|
position = this@toDomainEntity.position.toInt(),
|
||||||
username = this@toDomainEntity.username,
|
username = this@toDomainEntity.username,
|
||||||
comment = this@toDomainEntity.comment,
|
comment = this@toDomainEntity.comment,
|
||||||
created = this@toDomainEntity.created?.time,
|
created = this@toDomainEntity.created?.time,
|
||||||
changed = this@toDomainEntity.changed?.time,
|
changed = this@toDomainEntity.changed?.time,
|
||||||
track = this@toDomainEntity.entry.toTrackEntity()
|
track = this@toDomainEntity.entry.toTrackEntity(serverId)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<ApiBookmark>.toDomainEntitiesList(): List<Bookmark> = map { it.toDomainEntity() }
|
fun List<ApiBookmark>.toDomainEntitiesList(serverId: Int): List<Bookmark> =
|
||||||
|
map { it.toDomainEntity(serverId) }
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
|
/*
|
||||||
|
* APIIndexesConverter.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
// Converts Indexes entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Indexes entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIIndexesConverter")
|
@file:JvmName("APIIndexesConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Index as APIIndex
|
import org.moire.ultrasonic.api.subsonic.models.Index as APIIndex
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes
|
import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes
|
||||||
|
|
||||||
fun APIIndexes.toArtistList(): List<Artist> {
|
fun APIIndexes.toArtistList(serverId: Int): List<Artist> {
|
||||||
val shortcuts = this.shortcutList.map { it.toDomainEntity() }.toMutableList()
|
val shortcuts = this.shortcutList.map { it.toDomainEntity(serverId) }.toMutableList()
|
||||||
val indexes = this.indexList.foldIndexToArtistList()
|
val indexes = this.indexList.foldIndexToArtistList(serverId)
|
||||||
|
|
||||||
indexes.forEach {
|
indexes.forEach {
|
||||||
if (!shortcuts.contains(it)) {
|
if (!shortcuts.contains(it)) {
|
||||||
|
@ -19,9 +27,9 @@ fun APIIndexes.toArtistList(): List<Artist> {
|
||||||
return shortcuts
|
return shortcuts
|
||||||
}
|
}
|
||||||
|
|
||||||
fun APIIndexes.toIndexList(musicFolderId: String?): List<Index> {
|
fun APIIndexes.toIndexList(serverId: Int, musicFolderId: String?): List<Index> {
|
||||||
val shortcuts = this.shortcutList.map { it.toIndexEntity() }.toMutableList()
|
val shortcuts = this.shortcutList.map { it.toIndexEntity(serverId) }.toMutableList()
|
||||||
val indexes = this.indexList.foldIndexToIndexList(musicFolderId)
|
val indexes = this.indexList.foldIndexToIndexList(musicFolderId, serverId)
|
||||||
|
|
||||||
indexes.forEach {
|
indexes.forEach {
|
||||||
if (!shortcuts.contains(it)) {
|
if (!shortcuts.contains(it)) {
|
||||||
|
@ -32,22 +40,23 @@ fun APIIndexes.toIndexList(musicFolderId: String?): List<Index> {
|
||||||
return shortcuts
|
return shortcuts
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<APIIndex>.foldIndexToArtistList(): List<Artist> = this.fold(
|
private fun List<APIIndex>.foldIndexToArtistList(serverId: Int): List<Artist> = this.fold(
|
||||||
listOf(),
|
listOf()
|
||||||
{ acc, index ->
|
) { acc, index ->
|
||||||
acc + index.artists.map {
|
acc + index.artists.map {
|
||||||
it.toDomainEntity()
|
it.toDomainEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
private fun List<APIIndex>.foldIndexToIndexList(musicFolderId: String?): List<Index> = this.fold(
|
private fun List<APIIndex>.foldIndexToIndexList(
|
||||||
listOf(),
|
musicFolderId: String?,
|
||||||
{ acc, index ->
|
serverId: Int
|
||||||
|
): List<Index> = this.fold(
|
||||||
|
listOf()
|
||||||
|
) { acc, index ->
|
||||||
acc + index.artists.map {
|
acc + index.artists.map {
|
||||||
val ret = it.toIndexEntity()
|
val ret = it.toIndexEntity(serverId)
|
||||||
ret.musicFolderId = musicFolderId
|
ret.musicFolderId = musicFolderId
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* APIMusicDirectoryConverter.kt
|
* APIMusicDirectoryConverter.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
@ -26,12 +26,12 @@ internal val dateFormat: DateFormat by lazy {
|
||||||
SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MusicDirectoryChild.toTrackEntity(): Track = Track(id).apply {
|
fun MusicDirectoryChild.toTrackEntity(serverId: Int): Track = Track(id, serverId).apply {
|
||||||
populateCommonProps(this, this@toTrackEntity)
|
populateCommonProps(this, this@toTrackEntity)
|
||||||
populateTrackProps(this, this@toTrackEntity)
|
populateTrackProps(this, this@toTrackEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MusicDirectoryChild.toAlbumEntity(): Album = Album(id).apply {
|
fun MusicDirectoryChild.toAlbumEntity(serverId: Int): Album = Album(id, serverId).apply {
|
||||||
populateCommonProps(this, this@toAlbumEntity)
|
populateCommonProps(this, this@toAlbumEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,24 +80,24 @@ private fun populateTrackProps(
|
||||||
track.averageRating = source.averageRating
|
track.averageRating = source.averageRating
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<MusicDirectoryChild>.toDomainEntityList(): List<MusicDirectory.Child> {
|
fun List<MusicDirectoryChild>.toDomainEntityList(serverId: Int): List<MusicDirectory.Child> {
|
||||||
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
|
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
|
||||||
|
|
||||||
forEach {
|
forEach {
|
||||||
if (it.isDir)
|
if (it.isDir)
|
||||||
newList.add(it.toAlbumEntity())
|
newList.add(it.toAlbumEntity(serverId))
|
||||||
else
|
else
|
||||||
newList.add(it.toTrackEntity())
|
newList.add(it.toTrackEntity(serverId))
|
||||||
}
|
}
|
||||||
|
|
||||||
return newList
|
return newList
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<MusicDirectoryChild>.toTrackList(): List<Track> = this.map {
|
fun List<MusicDirectoryChild>.toTrackList(serverId: Int): List<Track> = this.map {
|
||||||
it.toTrackEntity()
|
it.toTrackEntity(serverId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun APIMusicDirectory.toDomainEntity(): MusicDirectory = MusicDirectory().apply {
|
fun APIMusicDirectory.toDomainEntity(serverId: Int): MusicDirectory = MusicDirectory().apply {
|
||||||
name = this@toDomainEntity.name
|
name = this@toDomainEntity.name
|
||||||
addAll(this@toDomainEntity.childList.toDomainEntityList())
|
addAll(this@toDomainEntity.childList.toDomainEntityList(serverId))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
/*
|
||||||
|
* APIMusicFolderConverter.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIMusicFolderConverter")
|
@file:JvmName("APIMusicFolderConverter")
|
||||||
|
@ -5,7 +12,15 @@ package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
|
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
|
||||||
|
|
||||||
fun APIMusicFolder.toDomainEntity(): MusicFolder = MusicFolder(this.id, this.name)
|
fun APIMusicFolder.toDomainEntity(serverId: Int): MusicFolder = MusicFolder(
|
||||||
|
id = this.id,
|
||||||
|
serverId = serverId,
|
||||||
|
name = this.name
|
||||||
|
)
|
||||||
|
|
||||||
fun List<APIMusicFolder>.toDomainEntityList(): List<MusicFolder> =
|
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> =
|
||||||
this.map { it.toDomainEntity() }
|
this.map {
|
||||||
|
val item = it.toDomainEntity(serverId)
|
||||||
|
item.serverId = serverId
|
||||||
|
item
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
|
/*
|
||||||
|
* APIPlaylistConverter.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
// Converts Playlist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Playlist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIPlaylistConverter")
|
@file:JvmName("APIPlaylistConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -10,9 +18,16 @@ import org.moire.ultrasonic.util.Util.ifNotNull
|
||||||
|
|
||||||
internal val playlistDateFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
|
internal val playlistDateFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
|
||||||
|
|
||||||
fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
|
fun APIPlaylist.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory =
|
||||||
|
MusicDirectory().apply {
|
||||||
name = this@toMusicDirectoryDomainEntity.name
|
name = this@toMusicDirectoryDomainEntity.name
|
||||||
addAll(this@toMusicDirectoryDomainEntity.entriesList.map { it.toTrackEntity() })
|
addAll(
|
||||||
|
this@toMusicDirectoryDomainEntity.entriesList.map {
|
||||||
|
val item = it.toTrackEntity(serverId)
|
||||||
|
item.serverId = serverId
|
||||||
|
item
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
|
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
/*
|
||||||
|
* APISearchConverter.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APISearchConverter")
|
@file:JvmName("APISearchConverter")
|
||||||
|
@ -7,19 +14,19 @@ import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
|
||||||
import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult
|
import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult
|
||||||
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
|
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
|
||||||
|
|
||||||
fun APISearchResult.toDomainEntity(): SearchResult = SearchResult(
|
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
|
||||||
emptyList(), emptyList(),
|
emptyList(), emptyList(),
|
||||||
this.matchList.map { it.toTrackEntity() }
|
this.matchList.map { it.toTrackEntity(serverId) }
|
||||||
)
|
)
|
||||||
|
|
||||||
fun SearchTwoResult.toDomainEntity(): SearchResult = SearchResult(
|
fun SearchTwoResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
|
||||||
this.artistList.map { it.toIndexEntity() },
|
this.artistList.map { it.toIndexEntity(serverId) },
|
||||||
this.albumList.map { it.toDomainEntity() },
|
this.albumList.map { it.toDomainEntity(serverId) },
|
||||||
this.songList.map { it.toTrackEntity() }
|
this.songList.map { it.toTrackEntity(serverId) }
|
||||||
)
|
)
|
||||||
|
|
||||||
fun SearchThreeResult.toDomainEntity(): SearchResult = SearchResult(
|
fun SearchThreeResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
|
||||||
this.artistList.map { it.toDomainEntity() },
|
this.artistList.map { it.toDomainEntity(serverId) },
|
||||||
this.albumList.map { it.toDomainEntity() },
|
this.albumList.map { it.toDomainEntity(serverId) },
|
||||||
this.songList.map { it.toTrackEntity() }
|
this.songList.map { it.toTrackEntity(serverId) }
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
/*
|
||||||
|
* APIShareConverter.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
// Contains helper method to convert subsonic api share to domain model
|
// Contains helper method to convert subsonic api share to domain model
|
||||||
@file:JvmName("APIShareConverter")
|
@file:JvmName("APIShareConverter")
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
@ -9,11 +16,11 @@ import org.moire.ultrasonic.util.Util.ifNotNull
|
||||||
|
|
||||||
internal val shareTimeFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
|
internal val shareTimeFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
|
||||||
|
|
||||||
fun List<APIShare>.toDomainEntitiesList(): List<Share> = this.map {
|
fun List<APIShare>.toDomainEntitiesList(serverId: Int): List<Share> = this.map {
|
||||||
it.toDomainEntity()
|
it.toDomainEntity(serverId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun APIShare.toDomainEntity(): Share = Share(
|
fun APIShare.toDomainEntity(serverId: Int): Share = Share(
|
||||||
created = this@toDomainEntity.created.ifNotNull { shareTimeFormat.format(it.time) },
|
created = this@toDomainEntity.created.ifNotNull { shareTimeFormat.format(it.time) },
|
||||||
description = this@toDomainEntity.description,
|
description = this@toDomainEntity.description,
|
||||||
expires = this@toDomainEntity.expires.ifNotNull { shareTimeFormat.format(it.time) },
|
expires = this@toDomainEntity.expires.ifNotNull { shareTimeFormat.format(it.time) },
|
||||||
|
@ -22,5 +29,5 @@ fun APIShare.toDomainEntity(): Share = Share(
|
||||||
url = this@toDomainEntity.url,
|
url = this@toDomainEntity.url,
|
||||||
username = this@toDomainEntity.username,
|
username = this@toDomainEntity.username,
|
||||||
visitCount = this@toDomainEntity.visitCount.toLong(),
|
visitCount = this@toDomainEntity.visitCount.toLong(),
|
||||||
tracks = this@toDomainEntity.items.toTrackList().toMutableList()
|
tracks = this@toDomainEntity.items.toTrackList(serverId).toMutableList()
|
||||||
)
|
)
|
||||||
|
|
|
@ -66,17 +66,17 @@ class MainFragment : Fragment(), KoinComponent {
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
var shouldRestart = false
|
var shouldRelayout = false
|
||||||
val currentId3Setting = Settings.shouldUseId3Tags
|
val currentId3Setting = Settings.shouldUseId3Tags
|
||||||
|
|
||||||
// If setting has changed...
|
// If setting has changed...
|
||||||
if (currentId3Setting != cachedId3Setting) {
|
if (currentId3Setting != useId3) {
|
||||||
cachedId3Setting = currentId3Setting
|
useId3 = currentId3Setting
|
||||||
shouldRestart = true
|
shouldRelayout = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// then setup the list anew.
|
// then setup the list anew.
|
||||||
if (shouldRestart) {
|
if (shouldRelayout) {
|
||||||
setupItemVisibility()
|
setupItemVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,17 +109,19 @@ class MainFragment : Fragment(), KoinComponent {
|
||||||
|
|
||||||
private fun setupItemVisibility() {
|
private fun setupItemVisibility() {
|
||||||
// Cache some values
|
// Cache some values
|
||||||
cachedId3Setting = Settings.shouldUseId3Tags
|
useId3 = Settings.shouldUseId3Tags
|
||||||
|
useId3Offline = Settings.useId3TagsOffline
|
||||||
|
|
||||||
val isOnline = !isOffline()
|
val isOnline = !isOffline()
|
||||||
|
|
||||||
// Music
|
// Music
|
||||||
musicTitle.isVisible = true
|
musicTitle.isVisible = true
|
||||||
artistsButton.isVisible = true
|
artistsButton.isVisible = true
|
||||||
albumsButton.isVisible = isOnline
|
albumsButton.isVisible = isOnline || useId3Offline
|
||||||
genresButton.isVisible = true
|
genresButton.isVisible = true
|
||||||
|
|
||||||
// Songs
|
// Songs
|
||||||
songsTitle.isVisible = isOnline
|
songsTitle.isVisible = true
|
||||||
randomSongsButton.isVisible = true
|
randomSongsButton.isVisible = true
|
||||||
songsStarredButton.isVisible = isOnline
|
songsStarredButton.isVisible = isOnline
|
||||||
|
|
||||||
|
@ -128,7 +130,7 @@ class MainFragment : Fragment(), KoinComponent {
|
||||||
albumsNewestButton.isVisible = isOnline
|
albumsNewestButton.isVisible = isOnline
|
||||||
albumsRecentButton.isVisible = isOnline
|
albumsRecentButton.isVisible = isOnline
|
||||||
albumsFrequentButton.isVisible = isOnline
|
albumsFrequentButton.isVisible = isOnline
|
||||||
albumsHighestButton.isVisible = isOnline && !cachedId3Setting
|
albumsHighestButton.isVisible = isOnline && !useId3
|
||||||
albumsRandomButton.isVisible = isOnline
|
albumsRandomButton.isVisible = isOnline
|
||||||
albumsStarredButton.isVisible = isOnline
|
albumsStarredButton.isVisible = isOnline
|
||||||
albumsAlphaByNameButton.isVisible = isOnline
|
albumsAlphaByNameButton.isVisible = isOnline
|
||||||
|
@ -240,6 +242,7 @@ class MainFragment : Fragment(), KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var cachedId3Setting = false
|
private var useId3 = false
|
||||||
|
private var useId3Offline = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,17 +33,22 @@ import android.widget.LinearLayout
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import android.widget.ViewFlipper
|
import android.widget.ViewFlipper
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Timeline
|
import androidx.media3.common.Timeline
|
||||||
|
import androidx.media3.session.SessionResult
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
|
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.common.util.concurrent.FutureCallback
|
||||||
|
import com.google.common.util.concurrent.Futures
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -747,7 +752,14 @@ class PlayerFragment :
|
||||||
if (currentSong == null) return true
|
if (currentSong == null) return true
|
||||||
|
|
||||||
val isStarred = currentSong!!.starred
|
val isStarred = currentSong!!.starred
|
||||||
val id = currentSong!!.id
|
|
||||||
|
mediaPlayerController.controller?.setRating(
|
||||||
|
HeartRating(!isStarred)
|
||||||
|
)?.let {
|
||||||
|
Futures.addCallback(
|
||||||
|
it,
|
||||||
|
object : FutureCallback<SessionResult> {
|
||||||
|
override fun onSuccess(result: SessionResult?) {
|
||||||
if (isStarred) {
|
if (isStarred) {
|
||||||
starMenuItem.icon = hollowStar
|
starMenuItem.icon = hollowStar
|
||||||
currentSong!!.starred = false
|
currentSong!!.starred = false
|
||||||
|
@ -755,18 +767,17 @@ class PlayerFragment :
|
||||||
starMenuItem.icon = fullStar
|
starMenuItem.icon = fullStar
|
||||||
currentSong!!.starred = true
|
currentSong!!.starred = true
|
||||||
}
|
}
|
||||||
Thread {
|
|
||||||
val musicService = getMusicService()
|
|
||||||
try {
|
|
||||||
if (isStarred) {
|
|
||||||
musicService.unstar(id, null, null)
|
|
||||||
} else {
|
|
||||||
musicService.star(id, null, null)
|
|
||||||
}
|
}
|
||||||
} catch (all: Exception) {
|
|
||||||
Timber.e(all)
|
override fun onFailure(t: Throwable) {
|
||||||
|
Toast.makeText(context, "SetRating failed", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}.start()
|
},
|
||||||
|
this.executorService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_item_bookmark_set -> {
|
R.id.menu_item_bookmark_set -> {
|
||||||
|
|
|
@ -6,11 +6,16 @@ import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.SearchRecentSuggestions
|
import android.provider.SearchRecentSuggestions
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
@ -76,6 +81,7 @@ class SettingsFragment :
|
||||||
private var directoryCacheTime: ListPreference? = null
|
private var directoryCacheTime: ListPreference? = null
|
||||||
private var mediaButtonsEnabled: CheckBoxPreference? = null
|
private var mediaButtonsEnabled: CheckBoxPreference? = null
|
||||||
private var showArtistPicture: CheckBoxPreference? = null
|
private var showArtistPicture: CheckBoxPreference? = null
|
||||||
|
private var useId3TagsOffline: CheckBoxPreference? = null
|
||||||
private var sharingDefaultDescription: EditTextPreference? = null
|
private var sharingDefaultDescription: EditTextPreference? = null
|
||||||
private var sharingDefaultGreeting: EditTextPreference? = null
|
private var sharingDefaultGreeting: EditTextPreference? = null
|
||||||
private var sharingDefaultExpiration: TimeSpanPreference? = null
|
private var sharingDefaultExpiration: TimeSpanPreference? = null
|
||||||
|
@ -121,14 +127,39 @@ class SettingsFragment :
|
||||||
pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE)
|
pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE)
|
||||||
debugLogToFile = findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE)
|
debugLogToFile = findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE)
|
||||||
showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE)
|
showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE)
|
||||||
|
useId3TagsOffline = findPreference(Constants.PREFERENCES_KEY_ID3_TAGS_OFFLINE)
|
||||||
customCacheLocation = findPreference(Constants.PREFERENCES_KEY_CUSTOM_CACHE_LOCATION)
|
customCacheLocation = findPreference(Constants.PREFERENCES_KEY_CUSTOM_CACHE_LOCATION)
|
||||||
|
|
||||||
sharingDefaultGreeting?.text = shareGreeting
|
sharingDefaultGreeting?.text = shareGreeting
|
||||||
|
|
||||||
|
setupTextColors()
|
||||||
setupClearSearchPreference()
|
setupClearSearchPreference()
|
||||||
setupCacheLocationPreference()
|
setupCacheLocationPreference()
|
||||||
setupBluetoothDevicePreferences()
|
setupBluetoothDevicePreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupTextColors(enabled: Boolean = shouldUseId3Tags) {
|
||||||
|
val firstPart = getString(R.string.settings_use_id3_offline_warning)
|
||||||
|
var secondPart = getString(R.string.settings_use_id3_offline_summary)
|
||||||
|
|
||||||
|
// Little hack to circumvent a bug in Android. If we just change the color,
|
||||||
|
// the text is not refreshed. If we also change the string, it is refreshed.
|
||||||
|
if (enabled) secondPart += " "
|
||||||
|
|
||||||
|
val color = if (enabled) "#bd5164" else "#813b48"
|
||||||
|
|
||||||
|
Timber.i(color)
|
||||||
|
|
||||||
|
val warning = SpannableString(firstPart + "\n" + secondPart)
|
||||||
|
warning.setSpan(
|
||||||
|
ForegroundColorSpan(Color.parseColor(color)), 0, firstPart.length, 0
|
||||||
|
)
|
||||||
|
warning.setSpan(
|
||||||
|
StyleSpan(Typeface.BOLD), 0, firstPart.length, 0
|
||||||
|
)
|
||||||
|
useId3TagsOffline?.summary = warning
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
update()
|
update()
|
||||||
|
@ -196,7 +227,10 @@ class SettingsFragment :
|
||||||
setDebugLogToFile(sharedPreferences.getBoolean(key, false))
|
setDebugLogToFile(sharedPreferences.getBoolean(key, false))
|
||||||
}
|
}
|
||||||
Constants.PREFERENCES_KEY_ID3_TAGS -> {
|
Constants.PREFERENCES_KEY_ID3_TAGS -> {
|
||||||
showArtistPicture!!.isEnabled = sharedPreferences.getBoolean(key, false)
|
val enabled = sharedPreferences.getBoolean(key, false)
|
||||||
|
showArtistPicture?.isEnabled = enabled
|
||||||
|
useId3TagsOffline?.isEnabled = enabled
|
||||||
|
setupTextColors(enabled)
|
||||||
}
|
}
|
||||||
Constants.PREFERENCES_KEY_THEME -> {
|
Constants.PREFERENCES_KEY_THEME -> {
|
||||||
RxBus.themeChangedEventPublisher.onNext(Unit)
|
RxBus.themeChangedEventPublisher.onNext(Unit)
|
||||||
|
@ -372,6 +406,7 @@ class SettingsFragment :
|
||||||
debugLogToFile?.summary = ""
|
debugLogToFile?.summary = ""
|
||||||
}
|
}
|
||||||
showArtistPicture?.isEnabled = shouldUseId3Tags
|
showArtistPicture?.isEnabled = shouldUseId3Tags
|
||||||
|
useId3TagsOffline?.isEnabled = shouldUseId3Tags
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setHideMedia(hide: Boolean) {
|
private fun setHideMedia(hide: Boolean) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* TrackCollectionFragment.kt
|
* TrackCollectionFragment.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
@ -31,6 +31,7 @@ import org.moire.ultrasonic.adapters.AlbumHeader
|
||||||
import org.moire.ultrasonic.adapters.AlbumRowBinder
|
import org.moire.ultrasonic.adapters.AlbumRowBinder
|
||||||
import org.moire.ultrasonic.adapters.HeaderViewBinder
|
import org.moire.ultrasonic.adapters.HeaderViewBinder
|
||||||
import org.moire.ultrasonic.adapters.TrackViewBinder
|
import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
@ -47,6 +48,7 @@ import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
|
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
|
||||||
|
@ -61,11 +63,11 @@ import org.moire.ultrasonic.util.Util
|
||||||
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
|
|
||||||
private var albumButtons: View? = null
|
private var albumButtons: View? = null
|
||||||
internal var selectButton: ImageView? = null
|
private var selectButton: ImageView? = null
|
||||||
internal var playNowButton: ImageView? = null
|
internal var playNowButton: ImageView? = null
|
||||||
private var playNextButton: ImageView? = null
|
private var playNextButton: ImageView? = null
|
||||||
private var playLastButton: ImageView? = null
|
private var playLastButton: ImageView? = null
|
||||||
internal var pinButton: ImageView? = null
|
private var pinButton: ImageView? = null
|
||||||
private var unpinButton: ImageView? = null
|
private var unpinButton: ImageView? = null
|
||||||
private var downloadButton: ImageView? = null
|
private var downloadButton: ImageView? = null
|
||||||
private var deleteButton: ImageView? = null
|
private var deleteButton: ImageView? = null
|
||||||
|
@ -144,11 +146,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
|
|
||||||
// Update the buttons when the selection has changed
|
// Update the buttons when the selection has changed
|
||||||
viewAdapter.selectionRevision.observe(
|
viewAdapter.selectionRevision.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
enableButtons()
|
enableButtons()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal open fun setupButtons(view: View) {
|
internal open fun setupButtons(view: View) {
|
||||||
|
@ -267,10 +268,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
private val childCount: Int
|
private val childCount: Int
|
||||||
get() {
|
get() {
|
||||||
val count = viewAdapter.getCurrentList().count()
|
val count = viewAdapter.getCurrentList().count()
|
||||||
if (listModel.showHeader) {
|
return if (listModel.showHeader) {
|
||||||
return count - 1
|
count - 1
|
||||||
} else {
|
} else {
|
||||||
return count
|
count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,13 +321,13 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
} as List<Track>
|
} as List<Track>
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun selectAllOrNone() {
|
private fun selectAllOrNone() {
|
||||||
val someUnselected = viewAdapter.selectedSet.size < childCount
|
val someUnselected = viewAdapter.selectedSet.size < childCount
|
||||||
|
|
||||||
selectAll(someUnselected, true)
|
selectAll(someUnselected, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun selectAll(selected: Boolean, toast: Boolean) {
|
private fun selectAll(selected: Boolean, toast: Boolean) {
|
||||||
var selectedCount = viewAdapter.selectedSet.size * -1
|
var selectedCount = viewAdapter.selectedSet.size * -1
|
||||||
|
|
||||||
selectedCount += viewAdapter.setSelectionStatusOfAll(selected)
|
selectedCount += viewAdapter.setSelectionStatusOfAll(selected)
|
||||||
|
@ -366,7 +367,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
deleteButton?.isVisible = (enabled && deleteEnabled)
|
deleteButton?.isVisible = (enabled && deleteEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun downloadBackground(save: Boolean) {
|
private fun downloadBackground(save: Boolean) {
|
||||||
var songs = getSelectedSongs()
|
var songs = getSelectedSongs()
|
||||||
|
|
||||||
if (songs.isEmpty()) {
|
if (songs.isEmpty()) {
|
||||||
|
@ -426,6 +427,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
|
|
||||||
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
|
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
|
||||||
|
|
||||||
|
Timber.i("Received list")
|
||||||
val entryList: MutableList<MusicDirectory.Child> = it.toMutableList()
|
val entryList: MutableList<MusicDirectory.Child> = it.toMutableList()
|
||||||
|
|
||||||
if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
|
if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
|
||||||
|
@ -454,9 +456,9 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
moreButton!!.visibility = View.GONE
|
moreButton!!.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
moreButton!!.visibility = View.VISIBLE
|
moreButton!!.visibility = View.VISIBLE
|
||||||
if (arguments?.getInt(Constants.INTENT_RANDOM, 0) ?: 0 > 0) {
|
if ((arguments?.getInt(Constants.INTENT_RANDOM, 0) ?: 0) > 0) {
|
||||||
moreRandomTracks()
|
moreRandomTracks()
|
||||||
} else if (arguments?.getString(Constants.INTENT_GENRE_NAME, "") ?: "" != "") {
|
} else if ((arguments?.getString(Constants.INTENT_GENRE_NAME, "") ?: "") != "") {
|
||||||
moreSongsForGenre()
|
moreSongsForGenre()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -497,6 +499,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
listModel.currentListIsSortable = true
|
listModel.currentListIsSortable = true
|
||||||
|
|
||||||
|
Timber.i("Processed list")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun moreSongsForGenre(args: Bundle = requireArguments()) {
|
private fun moreSongsForGenre(args: Bundle = requireArguments()) {
|
||||||
|
@ -556,6 +560,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
args: Bundle?,
|
args: Bundle?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): LiveData<List<MusicDirectory.Child>> {
|
): LiveData<List<MusicDirectory.Child>> {
|
||||||
|
Timber.i("Starting gathering track collection data...")
|
||||||
if (args == null) return listModel.currentList
|
if (args == null) return listModel.currentList
|
||||||
val id = args.getString(Constants.INTENT_ID)
|
val id = args.getString(Constants.INTENT_ID)
|
||||||
val isAlbum = args.getBoolean(Constants.INTENT_IS_ALBUM, false)
|
val isAlbum = args.getBoolean(Constants.INTENT_IS_ALBUM, false)
|
||||||
|
@ -600,7 +605,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
listModel.getRandom(albumListSize)
|
listModel.getRandom(albumListSize)
|
||||||
} else {
|
} else {
|
||||||
setTitle(name)
|
setTitle(name)
|
||||||
if (!isOffline() && Settings.shouldUseId3Tags) {
|
if (ActiveServerProvider.isID3Enabled()) {
|
||||||
if (isAlbum) {
|
if (isAlbum) {
|
||||||
listModel.getAlbum(refresh2, id!!, name)
|
listModel.getAlbum(refresh2, id!!, name)
|
||||||
} else {
|
} else {
|
||||||
|
@ -669,7 +674,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun getClickedSong(item: MusicDirectory.Child): List<Track> {
|
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
|
||||||
// This can probably be done better
|
// This can probably be done better
|
||||||
return viewAdapter.getCurrentList().mapNotNull {
|
return viewAdapter.getCurrentList().mapNotNull {
|
||||||
if (it is Track && (it.id == item.id))
|
if (it is Track && (it.id == item.id))
|
||||||
|
|
|
@ -39,7 +39,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||||
id: String,
|
id: String,
|
||||||
name: String?
|
name: String?
|
||||||
) {
|
) {
|
||||||
list.postValue(musicService.getArtist(id, name, refresh))
|
list.postValue(musicService.getAlbumsOfArtist(id, name, refresh))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun load(
|
override fun load(
|
||||||
|
|
|
@ -1,20 +1,8 @@
|
||||||
/*
|
/*
|
||||||
This file is part of Subsonic.
|
* ArtistListModel.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
Subsonic is free software: you can redistribute it and/or modify
|
*
|
||||||
it under the terms of the GNU General Public License as published by
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Subsonic is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Copyright 2020 (C) Jozsef Varga
|
|
||||||
*/
|
*/
|
||||||
package org.moire.ultrasonic.model
|
package org.moire.ultrasonic.model
|
||||||
|
|
||||||
|
@ -24,6 +12,7 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
|
|
||||||
|
@ -56,12 +45,10 @@ class ArtistListModel(application: Application) : GenericListModel(application)
|
||||||
|
|
||||||
val musicFolderId = activeServer.musicFolderId
|
val musicFolderId = activeServer.musicFolderId
|
||||||
|
|
||||||
val result: List<ArtistOrIndex>
|
val result = if (ActiveServerProvider.isID3Enabled()) {
|
||||||
|
musicService.getArtists(refresh)
|
||||||
if (!isOffline && useId3Tags) {
|
|
||||||
result = musicService.getArtists(refresh)
|
|
||||||
} else {
|
} else {
|
||||||
result = musicService.getIndexes(musicFolderId, refresh)
|
musicService.getIndexes(musicFolderId, refresh)
|
||||||
}
|
}
|
||||||
|
|
||||||
artists.postValue(result.toMutableList().sortedWith(comparator))
|
artists.postValue(result.toMutableList().sortedWith(comparator))
|
||||||
|
|
|
@ -251,15 +251,15 @@ open class APIDataSource private constructor(
|
||||||
@Suppress("ThrowsCount")
|
@Suppress("ThrowsCount")
|
||||||
@Throws(HttpDataSourceException::class)
|
@Throws(HttpDataSourceException::class)
|
||||||
private fun skipFully(bytesToSkip: Long, dataSpec: DataSpec) {
|
private fun skipFully(bytesToSkip: Long, dataSpec: DataSpec) {
|
||||||
var bytesToSkip = bytesToSkip
|
var bytesToSkipCpy = bytesToSkip
|
||||||
if (bytesToSkip == 0L) {
|
if (bytesToSkipCpy == 0L) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val skipBuffer = ByteArray(4096)
|
val skipBuffer = ByteArray(4096)
|
||||||
try {
|
try {
|
||||||
while (bytesToSkip > 0) {
|
while (bytesToSkipCpy > 0) {
|
||||||
val readLength =
|
val readLength =
|
||||||
bytesToSkip.coerceAtMost(skipBuffer.size.toLong()).toInt()
|
bytesToSkipCpy.coerceAtMost(skipBuffer.size.toLong()).toInt()
|
||||||
val read = Util.castNonNull(responseByteStream).read(skipBuffer, 0, readLength)
|
val read = Util.castNonNull(responseByteStream).read(skipBuffer, 0, readLength)
|
||||||
if (Thread.currentThread().isInterrupted) {
|
if (Thread.currentThread().isInterrupted) {
|
||||||
throw InterruptedIOException()
|
throw InterruptedIOException()
|
||||||
|
@ -271,7 +271,7 @@ open class APIDataSource private constructor(
|
||||||
HttpDataSourceException.TYPE_OPEN
|
HttpDataSourceException.TYPE_OPEN
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bytesToSkip -= read.toLong()
|
bytesToSkipCpy -= read.toLong()
|
||||||
bytesTransferred(read)
|
bytesTransferred(read)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -305,8 +305,8 @@ open class APIDataSource private constructor(
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun readInternal(buffer: ByteArray, offset: Int, readLength: Int): Int {
|
private fun readInternal(buffer: ByteArray, offset: Int, readLength: Int): Int {
|
||||||
var readLength = readLength
|
var readLengthCpy = readLength
|
||||||
if (readLength == 0) {
|
if (readLengthCpy == 0) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if (bytesToRead != C.LENGTH_UNSET.toLong()) {
|
if (bytesToRead != C.LENGTH_UNSET.toLong()) {
|
||||||
|
@ -314,9 +314,9 @@ open class APIDataSource private constructor(
|
||||||
if (bytesRemaining == 0L) {
|
if (bytesRemaining == 0L) {
|
||||||
return C.RESULT_END_OF_INPUT
|
return C.RESULT_END_OF_INPUT
|
||||||
}
|
}
|
||||||
readLength = readLength.toLong().coerceAtMost(bytesRemaining).toInt()
|
readLengthCpy = readLengthCpy.toLong().coerceAtMost(bytesRemaining).toInt()
|
||||||
}
|
}
|
||||||
val read = Util.castNonNull(responseByteStream).read(buffer, offset, readLength)
|
val read = Util.castNonNull(responseByteStream).read(buffer, offset, readLengthCpy)
|
||||||
if (read == -1) {
|
if (read == -1) {
|
||||||
return C.RESULT_END_OF_INPUT
|
return C.RESULT_END_OF_INPUT
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,9 @@ package org.moire.ultrasonic.playback
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import android.widget.Toast.LENGTH_SHORT
|
||||||
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS
|
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS
|
||||||
|
@ -18,11 +21,17 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
|
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.Rating
|
||||||
import androidx.media3.session.LibraryResult
|
import androidx.media3.session.LibraryResult
|
||||||
import androidx.media3.session.MediaLibraryService
|
import androidx.media3.session.MediaLibraryService
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
|
import androidx.media3.session.SessionCommand
|
||||||
import androidx.media3.session.SessionResult
|
import androidx.media3.session.SessionResult
|
||||||
|
import androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE
|
||||||
|
import androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN
|
||||||
|
import androidx.media3.session.SessionResult.RESULT_SUCCESS
|
||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
|
import com.google.common.util.concurrent.FutureCallback
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -42,6 +51,7 @@ import org.moire.ultrasonic.domain.SearchResult
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
|
import org.moire.ultrasonic.util.MainThreadExecutor
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -81,19 +91,18 @@ private const val MEDIA_SEARCH_SONG_ITEM = "MEDIA_SEARCH_SONG_ITEM"
|
||||||
private const val DISPLAY_LIMIT = 100
|
private const val DISPLAY_LIMIT = 100
|
||||||
private const val SEARCH_LIMIT = 10
|
private const val SEARCH_LIMIT = 10
|
||||||
|
|
||||||
private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch"
|
// List of available custom SessionCommands
|
||||||
private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri"
|
const val SESSION_CUSTOM_SET_RATING = "SESSION_CUSTOM_SET_RATING"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MediaBrowserService implementation for e.g. Android Auto
|
* MediaBrowserService implementation for e.g. Android Auto
|
||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
||||||
class AutoMediaBrowserCallback(var player: Player) :
|
class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibraryService) :
|
||||||
MediaLibraryService.MediaLibrarySession.MediaLibrarySessionCallback, KoinComponent {
|
MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
|
||||||
|
|
||||||
private val mediaPlayerController by inject<MediaPlayerController>()
|
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
private val musicService = MusicServiceFactory.getMusicService()
|
|
||||||
|
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||||
|
@ -103,6 +112,7 @@ class AutoMediaBrowserCallback(var player: Player) :
|
||||||
private var randomSongsCache: List<Track>? = null
|
private var randomSongsCache: List<Track>? = null
|
||||||
private var searchSongsCache: List<Track>? = null
|
private var searchSongsCache: List<Track>? = null
|
||||||
|
|
||||||
|
private val musicService get() = MusicServiceFactory.getMusicService()
|
||||||
private val isOffline get() = ActiveServerProvider.isOffline()
|
private val isOffline get() = ActiveServerProvider.isOffline()
|
||||||
private val useId3Tags get() = Settings.shouldUseId3Tags
|
private val useId3Tags get() = Settings.shouldUseId3Tags
|
||||||
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
||||||
|
@ -154,6 +164,25 @@ class AutoMediaBrowserCallback(var player: Player) :
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConnect(
|
||||||
|
session: MediaSession,
|
||||||
|
controller: MediaSession.ControllerInfo
|
||||||
|
): MediaSession.ConnectionResult {
|
||||||
|
val connectionResult = super.onConnect(session, controller)
|
||||||
|
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Currently we need to create a custom session command, see https://github.com/androidx/media/issues/107
|
||||||
|
* When this issue is fixed we should be able to remove this method again
|
||||||
|
*/
|
||||||
|
availableSessionCommands.add(SessionCommand(SESSION_CUSTOM_SET_RATING, Bundle()))
|
||||||
|
|
||||||
|
return MediaSession.ConnectionResult.accept(
|
||||||
|
availableSessionCommands.build(),
|
||||||
|
connectionResult.availablePlayerCommands
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onGetItem(
|
override fun onGetItem(
|
||||||
session: MediaLibraryService.MediaLibrarySession,
|
session: MediaLibraryService.MediaLibrarySession,
|
||||||
browser: MediaSession.ControllerInfo,
|
browser: MediaSession.ControllerInfo,
|
||||||
|
@ -181,39 +210,120 @@ class AutoMediaBrowserCallback(var player: Player) :
|
||||||
return onLoadChildren(parentId)
|
return onLoadChildren(parentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setMediaItemFromSearchQuery(query: String) {
|
override fun onCustomCommand(
|
||||||
// Only accept query with pattern "play [Title]" or "[Title]"
|
|
||||||
// Where [Title]: must be exactly matched
|
|
||||||
// If no media with exact name found, play a random media instead
|
|
||||||
val mediaTitle =
|
|
||||||
if (query.startsWith("play ", ignoreCase = true)) {
|
|
||||||
query.drop(5)
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}
|
|
||||||
|
|
||||||
playFromMediaId(mediaTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSetMediaUri(
|
|
||||||
session: MediaSession,
|
session: MediaSession,
|
||||||
controller: MediaSession.ControllerInfo,
|
controller: MediaSession.ControllerInfo,
|
||||||
uri: Uri,
|
customCommand: SessionCommand,
|
||||||
extras: Bundle
|
args: Bundle
|
||||||
): Int {
|
): ListenableFuture<SessionResult> {
|
||||||
|
|
||||||
if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) ||
|
var customCommandFuture: ListenableFuture<SessionResult>? = null
|
||||||
uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT)
|
|
||||||
) {
|
|
||||||
val searchQuery =
|
|
||||||
uri.getQueryParameter("query")
|
|
||||||
?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED
|
|
||||||
setMediaItemFromSearchQuery(searchQuery)
|
|
||||||
|
|
||||||
return SessionResult.RESULT_SUCCESS
|
when (customCommand.customAction) {
|
||||||
} else {
|
SESSION_CUSTOM_SET_RATING -> {
|
||||||
return SessionResult.RESULT_ERROR_NOT_SUPPORTED
|
/*
|
||||||
|
* It is currently not possible to edit a MediaItem after creation so the isRated value
|
||||||
|
* is stored in the track.starred value
|
||||||
|
* See https://github.com/androidx/media/issues/33
|
||||||
|
*/
|
||||||
|
val track = mediaPlayerController.currentPlayingLegacy?.track
|
||||||
|
if (track != null) {
|
||||||
|
customCommandFuture = onSetRating(
|
||||||
|
session,
|
||||||
|
controller,
|
||||||
|
HeartRating(!track.starred)
|
||||||
|
)
|
||||||
|
Futures.addCallback(
|
||||||
|
customCommandFuture,
|
||||||
|
object : FutureCallback<SessionResult> {
|
||||||
|
override fun onSuccess(result: SessionResult) {
|
||||||
|
track.starred = !track.starred
|
||||||
|
// This needs to be called on the main Thread
|
||||||
|
libraryService.onUpdateNotification(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFailure(t: Throwable) {
|
||||||
|
Toast.makeText(
|
||||||
|
mediaPlayerController.context,
|
||||||
|
"There was an error updating the rating",
|
||||||
|
LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MainThreadExecutor()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.d(
|
||||||
|
"CustomCommand not recognized %s with extra %s",
|
||||||
|
customCommand.customAction,
|
||||||
|
customCommand.customExtras.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (customCommandFuture != null)
|
||||||
|
return customCommandFuture
|
||||||
|
return super.onCustomCommand(session, controller, customCommand, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetRating(
|
||||||
|
session: MediaSession,
|
||||||
|
controller: MediaSession.ControllerInfo,
|
||||||
|
rating: Rating
|
||||||
|
): ListenableFuture<SessionResult> {
|
||||||
|
if (session.player.currentMediaItem != null)
|
||||||
|
return onSetRating(
|
||||||
|
session,
|
||||||
|
controller,
|
||||||
|
session.player.currentMediaItem!!.mediaId,
|
||||||
|
rating
|
||||||
|
)
|
||||||
|
return super.onSetRating(session, controller, rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetRating(
|
||||||
|
session: MediaSession,
|
||||||
|
controller: MediaSession.ControllerInfo,
|
||||||
|
mediaId: String,
|
||||||
|
rating: Rating
|
||||||
|
): ListenableFuture<SessionResult> {
|
||||||
|
return serviceScope.future {
|
||||||
|
if (rating is HeartRating) {
|
||||||
|
try {
|
||||||
|
if (rating.isHeart) {
|
||||||
|
musicService.star(mediaId, null, null)
|
||||||
|
} else {
|
||||||
|
musicService.unstar(mediaId, null, null)
|
||||||
|
}
|
||||||
|
} catch (all: Exception) {
|
||||||
|
Timber.e(all)
|
||||||
|
// TODO: Better handle exception
|
||||||
|
return@future SessionResult(RESULT_ERROR_UNKNOWN)
|
||||||
|
}
|
||||||
|
return@future SessionResult(RESULT_SUCCESS)
|
||||||
|
}
|
||||||
|
return@future SessionResult(RESULT_ERROR_BAD_VALUE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
|
||||||
|
* and thereby customarily it is required to rebuild it..
|
||||||
|
* See also: https://stackoverflow.com/questions/70096715/adding-mediaitem-when-using-the-media3-library-caused-an-error
|
||||||
|
*/
|
||||||
|
override fun onAddMediaItems(
|
||||||
|
mediaSession: MediaSession,
|
||||||
|
controller: MediaSession.ControllerInfo,
|
||||||
|
mediaItems: MutableList<MediaItem>
|
||||||
|
): ListenableFuture<MutableList<MediaItem>> {
|
||||||
|
|
||||||
|
val updatedMediaItems = mediaItems.map { mediaItem ->
|
||||||
|
mediaItem.buildUpon()
|
||||||
|
.setUri(mediaItem.requestMetadata.mediaUri)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
return Futures.immediateFuture(updatedMediaItems.toMutableList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ReturnCount", "ComplexMethod")
|
@Suppress("ReturnCount", "ComplexMethod")
|
||||||
|
@ -525,7 +635,7 @@ class AutoMediaBrowserCallback(var player: Player) :
|
||||||
|
|
||||||
return serviceScope.future {
|
return serviceScope.future {
|
||||||
val albums = if (!isOffline && useId3Tags) {
|
val albums = if (!isOffline && useId3Tags) {
|
||||||
callWithErrorHandling { musicService.getArtist(id, name, false) }
|
callWithErrorHandling { musicService.getAlbumsOfArtist(id, name, false) }
|
||||||
} else {
|
} else {
|
||||||
callWithErrorHandling {
|
callWithErrorHandling {
|
||||||
musicService.getMusicDirectory(id, name, false).getAlbums()
|
musicService.getMusicDirectory(id, name, false).getAlbums()
|
||||||
|
@ -554,8 +664,8 @@ class AutoMediaBrowserCallback(var player: Player) :
|
||||||
val songs = listSongsInMusicService(id, name)
|
val songs = listSongsInMusicService(id, name)
|
||||||
|
|
||||||
if (songs != null) {
|
if (songs != null) {
|
||||||
if (songs.getChildren(includeDirs = true, includeFiles = false).count() == 0 &&
|
if (songs.getChildren(includeDirs = true, includeFiles = false).isEmpty() &&
|
||||||
songs.getChildren(includeDirs = false, includeFiles = true).count() > 0
|
songs.getChildren(includeDirs = false, includeFiles = true).isNotEmpty()
|
||||||
)
|
)
|
||||||
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
|
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
|
||||||
|
|
||||||
|
@ -1096,6 +1206,7 @@ class AutoMediaBrowserCallback(var player: Player) :
|
||||||
album = track.album,
|
album = track.album,
|
||||||
artist = track.artist,
|
artist = track.artist,
|
||||||
genre = track.genre,
|
genre = track.genre,
|
||||||
|
starred = track.starred
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1110,6 +1221,7 @@ class AutoMediaBrowserCallback(var player: Player) :
|
||||||
genre: String? = null,
|
genre: String? = null,
|
||||||
sourceUri: Uri? = null,
|
sourceUri: Uri? = null,
|
||||||
imageUri: Uri? = null,
|
imageUri: Uri? = null,
|
||||||
|
starred: Boolean = false
|
||||||
): MediaItem {
|
): MediaItem {
|
||||||
val metadata =
|
val metadata =
|
||||||
MediaMetadata.Builder()
|
MediaMetadata.Builder()
|
||||||
|
@ -1117,6 +1229,7 @@ class AutoMediaBrowserCallback(var player: Player) :
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setArtist(artist)
|
.setArtist(artist)
|
||||||
.setGenre(genre)
|
.setGenre(genre)
|
||||||
|
.setUserRating(HeartRating(starred))
|
||||||
.setFolderType(folderType)
|
.setFolderType(folderType)
|
||||||
.setIsPlayable(isPlayable)
|
.setIsPlayable(isPlayable)
|
||||||
.setArtworkUri(imageUri)
|
.setArtworkUri(imageUri)
|
||||||
|
|
|
@ -101,8 +101,8 @@ class CachedDataSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readInternal(buffer: ByteArray, offset: Int, readLength: Int): Int {
|
private fun readInternal(buffer: ByteArray, offset: Int, readLength: Int): Int {
|
||||||
var readLength = readLength
|
var readLengthCpy = readLength
|
||||||
if (readLength == 0) {
|
if (readLengthCpy == 0) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if (bytesToRead != C.LENGTH_UNSET.toLong()) {
|
if (bytesToRead != C.LENGTH_UNSET.toLong()) {
|
||||||
|
@ -110,9 +110,9 @@ class CachedDataSource(
|
||||||
if (bytesRemaining == 0L) {
|
if (bytesRemaining == 0L) {
|
||||||
return C.RESULT_END_OF_INPUT
|
return C.RESULT_END_OF_INPUT
|
||||||
}
|
}
|
||||||
readLength = readLength.toLong().coerceAtMost(bytesRemaining).toInt()
|
readLengthCpy = readLengthCpy.toLong().coerceAtMost(bytesRemaining).toInt()
|
||||||
}
|
}
|
||||||
val read = Util.castNonNull(responseByteStream).read(buffer, offset, readLength)
|
val read = Util.castNonNull(responseByteStream).read(buffer, offset, readLengthCpy)
|
||||||
if (read == -1) {
|
if (read == -1) {
|
||||||
Timber.i("CachedDatasource: EndOfInput")
|
Timber.i("CachedDatasource: EndOfInput")
|
||||||
return C.RESULT_END_OF_INPUT
|
return C.RESULT_END_OF_INPUT
|
||||||
|
@ -134,15 +134,15 @@ class CachedDataSource(
|
||||||
@Suppress("ThrowsCount")
|
@Suppress("ThrowsCount")
|
||||||
@Throws(HttpDataSourceException::class)
|
@Throws(HttpDataSourceException::class)
|
||||||
private fun skipFully(bytesToSkip: Long, dataSpec: DataSpec) {
|
private fun skipFully(bytesToSkip: Long, dataSpec: DataSpec) {
|
||||||
var bytesToSkip = bytesToSkip
|
var bytesToSkipCpy = bytesToSkip
|
||||||
if (bytesToSkip == 0L) {
|
if (bytesToSkipCpy == 0L) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val skipBuffer = ByteArray(4096)
|
val skipBuffer = ByteArray(4096)
|
||||||
try {
|
try {
|
||||||
while (bytesToSkip > 0) {
|
while (bytesToSkipCpy > 0) {
|
||||||
val readLength =
|
val readLength =
|
||||||
bytesToSkip.coerceAtMost(skipBuffer.size.toLong()).toInt()
|
bytesToSkipCpy.coerceAtMost(skipBuffer.size.toLong()).toInt()
|
||||||
val read = Util.castNonNull(responseByteStream).read(skipBuffer, 0, readLength)
|
val read = Util.castNonNull(responseByteStream).read(skipBuffer, 0, readLength)
|
||||||
if (Thread.currentThread().isInterrupted) {
|
if (Thread.currentThread().isInterrupted) {
|
||||||
throw InterruptedIOException()
|
throw InterruptedIOException()
|
||||||
|
@ -154,7 +154,7 @@ class CachedDataSource(
|
||||||
HttpDataSourceException.TYPE_OPEN
|
HttpDataSourceException.TYPE_OPEN
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bytesToSkip -= read.toLong()
|
bytesToSkipCpy -= read.toLong()
|
||||||
bytesTransferred(read)
|
bytesTransferred(read)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
|
@ -50,7 +50,7 @@ class LegacyPlaylistManager : KoinComponent {
|
||||||
|
|
||||||
for (i in 0 until n) {
|
for (i in 0 until n) {
|
||||||
val item = controller.getMediaItemAt(i)
|
val item = controller.getMediaItemAt(i)
|
||||||
val file = mediaItemCache[item.mediaMetadata.mediaUri.toString()]
|
val file = mediaItemCache[item.requestMetadata.toString()]
|
||||||
if (file != null)
|
if (file != null)
|
||||||
_playlist.add(file)
|
_playlist.add(file)
|
||||||
}
|
}
|
||||||
|
@ -59,11 +59,11 @@ class LegacyPlaylistManager : KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addToCache(item: MediaItem, file: DownloadFile) {
|
fun addToCache(item: MediaItem, file: DownloadFile) {
|
||||||
mediaItemCache.put(item.mediaMetadata.mediaUri.toString(), file)
|
mediaItemCache.put(item.requestMetadata.toString(), file)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCurrentPlaying(item: MediaItem?) {
|
fun updateCurrentPlaying(item: MediaItem?) {
|
||||||
currentPlaying = mediaItemCache[item?.mediaMetadata?.mediaUri.toString()]
|
currentPlaying = mediaItemCache[item?.requestMetadata.toString()]
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
|
|
@ -7,144 +7,88 @@
|
||||||
|
|
||||||
package org.moire.ultrasonic.playback
|
package org.moire.ultrasonic.playback
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.Assertions
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.common.util.Util
|
import androidx.media3.session.CommandButton
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||||
import androidx.media3.session.MediaNotification
|
import androidx.media3.session.MediaNotification
|
||||||
import androidx.media3.session.MediaNotification.ActionFactory
|
import androidx.media3.session.MediaSession
|
||||||
|
import androidx.media3.session.SessionCommand
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class MediaNotificationProvider(context: Context) :
|
||||||
|
DefaultMediaNotificationProvider(context), KoinComponent {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This is a copy of DefaultMediaNotificationProvider.java with some small changes
|
* It is currently not possible to edit a MediaItem after creation so the isRated value
|
||||||
* I have opened a bug https://github.com/androidx/media/issues/65 to make it easier to customize
|
* is stored in the track.starred value. See https://github.com/androidx/media/issues/33
|
||||||
* the icons and actions without creating our own copy of this class..
|
* TODO: Once the bug is fixed remove this circular reference!
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||||
/* package */
|
|
||||||
internal class MediaNotificationProvider(context: Context) :
|
|
||||||
MediaNotification.Provider {
|
|
||||||
private val context: Context = context.applicationContext
|
|
||||||
private val notificationManager: NotificationManager = Assertions.checkStateNotNull(
|
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
)
|
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
override fun addNotificationActions(
|
||||||
override fun createNotification(
|
mediaSession: MediaSession,
|
||||||
mediaController: MediaController,
|
mediaButtons: MutableList<CommandButton>,
|
||||||
actionFactory: ActionFactory,
|
builder: NotificationCompat.Builder,
|
||||||
onNotificationChangedCallback: MediaNotification.Provider.Callback
|
actionFactory: MediaNotification.ActionFactory
|
||||||
): MediaNotification {
|
): IntArray {
|
||||||
ensureNotificationChannel()
|
val tmp: MutableList<CommandButton> = mutableListOf()
|
||||||
val builder: NotificationCompat.Builder = NotificationCompat.Builder(
|
/*
|
||||||
context,
|
* TODO:
|
||||||
NOTIFICATION_CHANNEL_ID
|
* It is currently not possible to edit a MediaItem after creation so the isRated value
|
||||||
)
|
* is stored in the track.starred value
|
||||||
// Skip to previous action.
|
* See https://github.com/androidx/media/issues/33
|
||||||
builder.addAction(
|
*/
|
||||||
actionFactory.createMediaAction(
|
val rating = mediaPlayerController.currentPlayingLegacy?.track?.starred?.let {
|
||||||
IconCompat.createWithResource(
|
HeartRating(
|
||||||
context,
|
it
|
||||||
R.drawable.media3_notification_seek_to_previous
|
|
||||||
),
|
|
||||||
context.getString(R.string.media3_controls_seek_to_previous_description),
|
|
||||||
ActionFactory.COMMAND_SKIP_TO_PREVIOUS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (mediaController.playbackState == Player.STATE_ENDED ||
|
|
||||||
!mediaController.playWhenReady
|
|
||||||
) {
|
|
||||||
// Play action.
|
|
||||||
builder.addAction(
|
|
||||||
actionFactory.createMediaAction(
|
|
||||||
IconCompat.createWithResource(context, R.drawable.media3_notification_play),
|
|
||||||
context.getString(R.string.media3_controls_play_description),
|
|
||||||
ActionFactory.COMMAND_PLAY
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Pause action.
|
|
||||||
builder.addAction(
|
|
||||||
actionFactory.createMediaAction(
|
|
||||||
IconCompat.createWithResource(context, R.drawable.media3_notification_pause),
|
|
||||||
context.getString(R.string.media3_controls_pause_description),
|
|
||||||
ActionFactory.COMMAND_PAUSE
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Skip to next action.
|
if (rating is HeartRating) {
|
||||||
builder.addAction(
|
tmp.add(
|
||||||
actionFactory.createMediaAction(
|
CommandButton.Builder()
|
||||||
IconCompat.createWithResource(context, R.drawable.media3_notification_seek_to_next),
|
.setDisplayName("Love")
|
||||||
context.getString(R.string.media3_controls_seek_to_next_description),
|
.setIconResId(
|
||||||
ActionFactory.COMMAND_SKIP_TO_NEXT
|
if (rating.isHeart) R.drawable.ic_star_full_dark
|
||||||
|
else R.drawable.ic_star_hollow_dark
|
||||||
|
)
|
||||||
|
.setSessionCommand(
|
||||||
|
SessionCommand(
|
||||||
|
SESSION_CUSTOM_SET_RATING,
|
||||||
|
HeartRating(rating.isHeart).toBundle()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.setExtras(HeartRating(rating.isHeart).toBundle())
|
||||||
// Set metadata info in the notification.
|
.setEnabled(true)
|
||||||
val metadata = mediaController.mediaMetadata
|
|
||||||
builder.setContentTitle(metadata.title).setContentText(metadata.artist)
|
|
||||||
if (metadata.artworkData != null) {
|
|
||||||
val artworkBitmap =
|
|
||||||
BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData!!.size)
|
|
||||||
builder.setLargeIcon(artworkBitmap)
|
|
||||||
}
|
|
||||||
val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
|
|
||||||
.setShowActionsInCompactView(0, 1, 2)
|
|
||||||
val notification: Notification = builder
|
|
||||||
.setContentIntent(mediaController.sessionActivity)
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setSmallIcon(getSmallIconResId())
|
|
||||||
.setStyle(mediaStyle)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
.setOngoing(false)
|
|
||||||
.build()
|
.build()
|
||||||
return MediaNotification(
|
)
|
||||||
NOTIFICATION_ID,
|
}
|
||||||
notification
|
return super.addNotificationActions(
|
||||||
|
mediaSession,
|
||||||
|
mediaButtons + tmp,
|
||||||
|
builder,
|
||||||
|
actionFactory
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleCustomAction(
|
override fun getMediaButtons(
|
||||||
mediaController: MediaController,
|
playerCommands: Player.Commands,
|
||||||
action: String,
|
customLayout: MutableList<CommandButton>,
|
||||||
extras: Bundle
|
playWhenReady: Boolean
|
||||||
) {
|
): MutableList<CommandButton> {
|
||||||
// We don't handle custom commands.
|
val commands = super.getMediaButtons(playerCommands, customLayout, playWhenReady)
|
||||||
|
|
||||||
|
commands.forEachIndexed { index, command ->
|
||||||
|
command.extras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureNotificationChannel() {
|
return commands
|
||||||
if (Util.SDK_INT < Build.VERSION_CODES.O ||
|
|
||||||
notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
NOTIFICATION_CHANNEL_ID,
|
|
||||||
NOTIFICATION_CHANNEL_NAME,
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
)
|
|
||||||
channel.setShowBadge(false)
|
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"
|
|
||||||
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"
|
|
||||||
private const val NOTIFICATION_ID = 3032
|
|
||||||
private fun getSmallIconResId(): Int {
|
|
||||||
return R.drawable.ic_stat_ultrasonic
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,7 @@ import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.media3.common.AudioAttributes
|
import androidx.media3.common.AudioAttributes
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.C.CONTENT_TYPE_MUSIC
|
|
||||||
import androidx.media3.common.C.USAGE_MEDIA
|
import androidx.media3.common.C.USAGE_MEDIA
|
||||||
import androidx.media3.common.MediaItem
|
|
||||||
import androidx.media3.datasource.DataSource
|
import androidx.media3.datasource.DataSource
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
@ -38,29 +36,12 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||||
private lateinit var apiDataSource: APIDataSource.Factory
|
private lateinit var apiDataSource: APIDataSource.Factory
|
||||||
|
|
||||||
private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback
|
private lateinit var librarySessionCallback: MediaLibrarySession.Callback
|
||||||
|
|
||||||
private var rxBusSubscription = CompositeDisposable()
|
private var rxBusSubscription = CompositeDisposable()
|
||||||
|
|
||||||
private var isStarted = false
|
private var isStarted = false
|
||||||
|
|
||||||
/*
|
|
||||||
* For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
|
|
||||||
* and thereby customarily it is required to rebuild it..
|
|
||||||
*/
|
|
||||||
private class CustomMediaItemFiller : MediaSession.MediaItemFiller {
|
|
||||||
override fun fillInLocalConfiguration(
|
|
||||||
session: MediaSession,
|
|
||||||
controller: MediaSession.ControllerInfo,
|
|
||||||
mediaItem: MediaItem
|
|
||||||
): MediaItem {
|
|
||||||
// Again, set the Uri, so that it will get a LocalConfiguration
|
|
||||||
return mediaItem.buildUpon()
|
|
||||||
.setUri(mediaItem.mediaMetadata.mediaUri)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
Timber.i("onCreate called")
|
Timber.i("onCreate called")
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
@ -130,11 +111,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||||
player.experimentalSetOffloadSchedulingEnabled(true)
|
player.experimentalSetOffloadSchedulingEnabled(true)
|
||||||
|
|
||||||
// Create browser interface
|
// Create browser interface
|
||||||
librarySessionCallback = AutoMediaBrowserCallback(player)
|
librarySessionCallback = AutoMediaBrowserCallback(player, this)
|
||||||
|
|
||||||
// This will need to use the AutoCalls
|
// This will need to use the AutoCalls
|
||||||
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||||
.setMediaItemFiller(CustomMediaItemFiller())
|
|
||||||
.setSessionActivity(getPendingIntentForContent())
|
.setSessionActivity(getPendingIntentForContent())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -171,7 +151,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||||
private fun getAudioAttributes(): AudioAttributes {
|
private fun getAudioAttributes(): AudioAttributes {
|
||||||
return AudioAttributes.Builder()
|
return AudioAttributes.Builder()
|
||||||
.setUsage(USAGE_MEDIA)
|
.setUsage(USAGE_MEDIA)
|
||||||
.setContentType(CONTENT_TYPE_MUSIC)
|
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||||
|
|
||||||
// Old style TimeLimitedCache
|
// Old style TimeLimitedCache
|
||||||
private val cachedMusicDirectories: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
|
private val cachedMusicDirectories: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
|
||||||
private val cachedArtist: LRUCache<String, TimeLimitedCache<List<Album>>>
|
|
||||||
private val cachedAlbum: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
|
private val cachedAlbum: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
|
||||||
private val cachedUserInfo: LRUCache<String, TimeLimitedCache<UserInfo?>>
|
private val cachedUserInfo: LRUCache<String, TimeLimitedCache<UserInfo?>>
|
||||||
private val cachedLicenseValid = TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS)
|
private val cachedLicenseValid = TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS)
|
||||||
|
@ -53,7 +52,8 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||||
private val cachedGenres = TimeLimitedCache<List<Genre>>(10 * 3600, TimeUnit.SECONDS)
|
private val cachedGenres = TimeLimitedCache<List<Genre>>(10 * 3600, TimeUnit.SECONDS)
|
||||||
|
|
||||||
// New Room Database
|
// New Room Database
|
||||||
private var cachedArtists = metaDatabase.artistsDao()
|
private var cachedArtists = metaDatabase.artistDao()
|
||||||
|
private var cachedAlbums = metaDatabase.albumDao()
|
||||||
private var cachedIndexes = metaDatabase.indexDao()
|
private var cachedIndexes = metaDatabase.indexDao()
|
||||||
private val cachedMusicFolders = metaDatabase.musicFoldersDao()
|
private val cachedMusicFolders = metaDatabase.musicFoldersDao()
|
||||||
|
|
||||||
|
@ -103,10 +103,10 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||||
|
|
||||||
var indexes: List<Index>
|
var indexes: List<Index>
|
||||||
|
|
||||||
if (musicFolderId == null) {
|
indexes = if (musicFolderId == null) {
|
||||||
indexes = cachedIndexes.get()
|
cachedIndexes.get()
|
||||||
} else {
|
} else {
|
||||||
indexes = cachedIndexes.get(musicFolderId)
|
cachedIndexes.get(musicFolderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (indexes.isEmpty()) {
|
if (indexes.isEmpty()) {
|
||||||
|
@ -120,14 +120,15 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getArtists(refresh: Boolean): List<Artist> {
|
override fun getArtists(refresh: Boolean): List<Artist> {
|
||||||
checkSettingsChanged()
|
checkSettingsChanged()
|
||||||
|
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
cachedArtists.clear()
|
cachedArtists.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = cachedArtists.get()
|
var result = cachedArtists.get()
|
||||||
|
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
result = musicService.getArtists(refresh)
|
result = musicService.getArtists(refresh)
|
||||||
cachedArtist.clear()
|
|
||||||
cachedArtists.set(result)
|
cachedArtists.set(result)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
@ -149,21 +150,29 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||||
return dir
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Retrieves all albums of the provided artist.
|
||||||
|
* Cached in the RoomDB
|
||||||
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getArtist(id: String, name: String?, refresh: Boolean):
|
override fun getAlbumsOfArtist(id: String, name: String?, refresh: Boolean):
|
||||||
List<Album> {
|
List<Album> {
|
||||||
checkSettingsChanged()
|
checkSettingsChanged()
|
||||||
var cache = if (refresh) null else cachedArtist[id]
|
|
||||||
var dir = cache?.get()
|
var result: List<Album>
|
||||||
if (dir == null) {
|
|
||||||
dir = musicService.getArtist(id, name, refresh)
|
result = if (refresh) {
|
||||||
cache = TimeLimitedCache(
|
cachedAlbums.clearByArtist(id)
|
||||||
Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS
|
listOf()
|
||||||
)
|
} else {
|
||||||
cache.set(dir)
|
cachedAlbums.byArtist(id)
|
||||||
cachedArtist.put(id, cache)
|
|
||||||
}
|
}
|
||||||
return dir
|
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
result = musicService.getAlbumsOfArtist(id, name, refresh)
|
||||||
|
cachedAlbums.upsert(result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -326,7 +335,8 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||||
if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) {
|
if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) {
|
||||||
// Switch database
|
// Switch database
|
||||||
metaDatabase = activeServerProvider.getActiveMetaDatabase()
|
metaDatabase = activeServerProvider.getActiveMetaDatabase()
|
||||||
cachedArtists = metaDatabase.artistsDao()
|
cachedArtists = metaDatabase.artistDao()
|
||||||
|
cachedAlbums = metaDatabase.albumDao()
|
||||||
cachedIndexes = metaDatabase.indexDao()
|
cachedIndexes = metaDatabase.indexDao()
|
||||||
|
|
||||||
// Clear in memory caches
|
// Clear in memory caches
|
||||||
|
@ -335,7 +345,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||||
cachedPlaylists.clear()
|
cachedPlaylists.clear()
|
||||||
cachedGenres.clear()
|
cachedGenres.clear()
|
||||||
cachedAlbum.clear()
|
cachedAlbum.clear()
|
||||||
cachedArtist.clear()
|
|
||||||
cachedUserInfo.clear()
|
cachedUserInfo.clear()
|
||||||
|
|
||||||
// Set the cache keys
|
// Set the cache keys
|
||||||
|
@ -472,7 +481,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||||
|
|
||||||
init {
|
init {
|
||||||
cachedMusicDirectories = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
cachedMusicDirectories = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
||||||
cachedArtist = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
|
||||||
cachedAlbum = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
cachedAlbum = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
||||||
cachedUserInfo = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
cachedUserInfo = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import java.util.PriorityQueue
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
|
import org.moire.ultrasonic.data.MetaDatabase
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.playback.LegacyPlaylistManager
|
import org.moire.ultrasonic.playback.LegacyPlaylistManager
|
||||||
|
@ -402,6 +403,14 @@ class Downloader(
|
||||||
downloadFile.completeFile
|
downloadFile.completeFile
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hidden feature: If track is toggled between pinned/saved, refresh the metadata..
|
||||||
|
try {
|
||||||
|
downloadFile.track.cacheMetadata()
|
||||||
|
} catch (ignore: Exception) {
|
||||||
|
Timber.w(ignore)
|
||||||
|
}
|
||||||
|
|
||||||
downloadFile.status.postValue(newStatus)
|
downloadFile.status.postValue(newStatus)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -457,8 +466,10 @@ class Downloader(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadFile.track.artistId != null) {
|
try {
|
||||||
cacheMetadata(downloadFile.track.artistId!!)
|
downloadFile.track.cacheMetadata()
|
||||||
|
} catch (ignore: Exception) {
|
||||||
|
Timber.w(ignore)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadAndSaveCoverArt()
|
downloadAndSaveCoverArt()
|
||||||
|
@ -510,13 +521,35 @@ class Downloader(
|
||||||
return String.format(Locale.ROOT, "DownloadTask (%s)", downloadFile.track)
|
return String.format(Locale.ROOT, "DownloadTask (%s)", downloadFile.track)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cacheMetadata(artistId: String) {
|
private fun Track.cacheMetadata() {
|
||||||
// TODO: Right now it's caching the track artist.
|
if (artistId.isNullOrEmpty()) return
|
||||||
// Once the albums are cached in db, we should retrieve the album,
|
|
||||||
// and then cache the album artist.
|
val onlineDB = activeServerProvider.getActiveMetaDatabase()
|
||||||
if (artistId.isEmpty()) return
|
val offlineDB = activeServerProvider.offlineMetaDatabase
|
||||||
var artist: Artist? =
|
|
||||||
activeServerProvider.getActiveMetaDatabase().artistsDao().get(artistId)
|
cacheArtist(onlineDB, offlineDB, artistId!!)
|
||||||
|
|
||||||
|
// Now cache the album
|
||||||
|
if (albumId?.isNotEmpty() == true) {
|
||||||
|
// This is a cached call
|
||||||
|
val albums = musicService.getAlbumsOfArtist(artistId!!, null, false)
|
||||||
|
val album = albums.find { it.id == albumId }
|
||||||
|
|
||||||
|
if (album != null) {
|
||||||
|
offlineDB.albumDao().insert(album)
|
||||||
|
|
||||||
|
// If the album is a Compilation, also cache the Album artist
|
||||||
|
if (album.artistId != null && album.artistId != artistId)
|
||||||
|
cacheArtist(onlineDB, offlineDB, album.artistId!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now cache the track data
|
||||||
|
offlineDB.trackDao().insert(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cacheArtist(onlineDB: MetaDatabase, offlineDB: MetaDatabase, artistId: String) {
|
||||||
|
var artist: Artist? = onlineDB.artistDao().get(artistId)
|
||||||
|
|
||||||
// If we are downloading a new album, and the user has not visited the Artists list
|
// If we are downloading a new album, and the user has not visited the Artists list
|
||||||
// recently, then the artist won't be in the database.
|
// recently, then the artist won't be in the database.
|
||||||
|
@ -527,9 +560,9 @@ class Downloader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have found an artist, catch it.
|
// If we have found an artist, cache it.
|
||||||
if (artist != null) {
|
if (artist != null) {
|
||||||
activeServerProvider.offlineMetaDatabase.artistsDao().insert(artist)
|
offlineDB.artistDao().insert(artist)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,18 @@ package org.moire.ultrasonic.service
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Timeline
|
import androidx.media3.common.Timeline
|
||||||
import androidx.media3.session.MediaController
|
import androidx.media3.session.MediaController
|
||||||
|
import androidx.media3.session.SessionResult
|
||||||
import androidx.media3.session.SessionToken
|
import androidx.media3.session.SessionToken
|
||||||
|
import com.google.common.util.concurrent.FutureCallback
|
||||||
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -34,6 +39,7 @@ import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3
|
||||||
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4
|
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
|
import org.moire.ultrasonic.util.MainThreadExecutor
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ -579,25 +585,32 @@ class MediaPlayerController(
|
||||||
if (legacyPlaylistManager.currentPlaying == null) return
|
if (legacyPlaylistManager.currentPlaying == null) return
|
||||||
val song = legacyPlaylistManager.currentPlaying!!.track
|
val song = legacyPlaylistManager.currentPlaying!!.track
|
||||||
|
|
||||||
Thread {
|
controller?.setRating(
|
||||||
val musicService = getMusicService()
|
HeartRating(!song.starred)
|
||||||
try {
|
).let {
|
||||||
if (song.starred) {
|
Futures.addCallback(
|
||||||
musicService.unstar(song.id, null, null)
|
it,
|
||||||
} else {
|
object : FutureCallback<SessionResult> {
|
||||||
musicService.star(song.id, null, null)
|
override fun onSuccess(result: SessionResult?) {
|
||||||
}
|
|
||||||
} catch (all: Exception) {
|
|
||||||
Timber.e(all)
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
|
|
||||||
// Trigger an update
|
// Trigger an update
|
||||||
// TODO Update Metadata of MediaItem...
|
// TODO Update Metadata of MediaItem...
|
||||||
// localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying)
|
// localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying)
|
||||||
song.starred = !song.starred
|
song.starred = !song.starred
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFailure(t: Throwable) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"There was an error updating the rating",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MainThreadExecutor()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
|
@Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
|
||||||
fun setSongRating(rating: Int) {
|
fun setSongRating(rating: Int) {
|
||||||
if (!Settings.useFiveStarRating) return
|
if (!Settings.useFiveStarRating) return
|
||||||
|
@ -659,16 +672,22 @@ fun Track.toMediaItem(): MediaItem {
|
||||||
val bitrate = Settings.maxBitRate
|
val bitrate = Settings.maxBitRate
|
||||||
val uri = "$id|$bitrate|$filePath"
|
val uri = "$id|$bitrate|$filePath"
|
||||||
|
|
||||||
|
val rmd = MediaItem.RequestMetadata.Builder()
|
||||||
|
.setMediaUri(uri.toUri())
|
||||||
|
.build()
|
||||||
|
|
||||||
val metadata = MediaMetadata.Builder()
|
val metadata = MediaMetadata.Builder()
|
||||||
metadata.setTitle(title)
|
metadata.setTitle(title)
|
||||||
.setArtist(artist)
|
.setArtist(artist)
|
||||||
.setAlbumTitle(album)
|
.setAlbumTitle(album)
|
||||||
.setMediaUri(uri.toUri())
|
|
||||||
.setAlbumArtist(artist)
|
.setAlbumArtist(artist)
|
||||||
|
.setUserRating(HeartRating(starred))
|
||||||
|
.build()
|
||||||
|
|
||||||
val mediaItem = MediaItem.Builder()
|
val mediaItem = MediaItem.Builder()
|
||||||
.setUri(uri)
|
.setUri(uri)
|
||||||
.setMediaId(id)
|
.setMediaId(id)
|
||||||
|
.setRequestMetadata(rmd)
|
||||||
.setMediaMetadata(metadata.build())
|
.setMediaMetadata(metadata.build())
|
||||||
|
|
||||||
return mediaItem.build()
|
return mediaItem.build()
|
||||||
|
|
|
@ -59,7 +59,7 @@ interface MusicService {
|
||||||
fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory
|
fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getArtist(id: String, name: String?, refresh: Boolean): List<Album>
|
fun getAlbumsOfArtist(id: String, name: String?, refresh: Boolean): List<Album>
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory
|
fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* OfflineMusicService.kt
|
* OfflineMusicService.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
@ -23,6 +23,7 @@ import java.util.regex.Pattern
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
|
import org.moire.ultrasonic.data.MetaDatabase
|
||||||
import org.moire.ultrasonic.domain.Album
|
import org.moire.ultrasonic.domain.Album
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
|
@ -43,6 +44,7 @@ import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.domain.UserInfo
|
import org.moire.ultrasonic.domain.UserInfo
|
||||||
import org.moire.ultrasonic.util.AbstractFile
|
import org.moire.ultrasonic.util.AbstractFile
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
import org.moire.ultrasonic.util.Storage
|
import org.moire.ultrasonic.util.Storage
|
||||||
import org.moire.ultrasonic.util.Util.safeClose
|
import org.moire.ultrasonic.util.Util.safeClose
|
||||||
|
@ -52,12 +54,19 @@ import timber.log.Timber
|
||||||
class OfflineMusicService : MusicService, KoinComponent {
|
class OfflineMusicService : MusicService, KoinComponent {
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
|
||||||
|
private var metaDatabase: MetaDatabase = activeServerProvider.getActiveMetaDatabase()
|
||||||
|
|
||||||
|
// New Room Database
|
||||||
|
private var cachedArtists = metaDatabase.artistDao()
|
||||||
|
private var cachedAlbums = metaDatabase.albumDao()
|
||||||
|
private var cachedTracks = metaDatabase.trackDao()
|
||||||
|
|
||||||
override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
|
override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
|
||||||
val indexes: MutableList<Index> = ArrayList()
|
val indexes: MutableList<Index> = ArrayList()
|
||||||
val root = FileUtil.musicDirectory
|
val root = FileUtil.musicDirectory
|
||||||
for (file in FileUtil.listFiles(root)) {
|
for (file in FileUtil.listFiles(root)) {
|
||||||
if (file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
val index = Index(file.path)
|
val index = Index(id = file.path)
|
||||||
index.id = file.path
|
index.id = file.path
|
||||||
index.index = file.name.substring(0, 1)
|
index.index = file.name.substring(0, 1)
|
||||||
index.name = file.name
|
index.name = file.name
|
||||||
|
@ -97,6 +106,13 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||||
return indexes
|
return indexes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getArtists(refresh: Boolean): List<Artist> {
|
||||||
|
val result = cachedArtists.get()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
|
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
|
||||||
*/
|
*/
|
||||||
|
@ -312,7 +328,8 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||||
offset: Int,
|
offset: Int,
|
||||||
musicFolderId: String?
|
musicFolderId: String?
|
||||||
): List<Album> {
|
): List<Album> {
|
||||||
throw OfflineException("getAlbumList2 isn't available in offline mode")
|
// TODO: Implement filtering by musicFolder?
|
||||||
|
return cachedAlbums.get(size, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -450,20 +467,39 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||||
|
|
||||||
override fun isLicenseValid(): Boolean = true
|
override fun isLicenseValid(): Boolean = true
|
||||||
|
|
||||||
@Throws(OfflineException::class)
|
@Throws(Exception::class)
|
||||||
override fun getArtists(refresh: Boolean): List<Artist> {
|
override fun getAlbumsOfArtist(id: String, name: String?, refresh: Boolean):
|
||||||
throw OfflineException("getArtists isn't available in offline mode")
|
List<Album> {
|
||||||
|
val directAlbums = cachedAlbums.byArtist(id)
|
||||||
|
|
||||||
|
// The direct albums won't contain any compilations that the artist has participated in
|
||||||
|
// We need to fetch the tracks of the artist and then gather the compilation albums from that.
|
||||||
|
val tracks = cachedTracks.byArtist(id)
|
||||||
|
val albumIds = tracks.map {
|
||||||
|
it.albumId
|
||||||
|
}.distinct().filterNotNull()
|
||||||
|
|
||||||
|
val compilationAlbums = albumIds.map {
|
||||||
|
cachedAlbums.get(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(OfflineException::class)
|
return directAlbums.plus(compilationAlbums).distinct()
|
||||||
override fun getArtist(id: String, name: String?, refresh: Boolean):
|
|
||||||
List<Album> {
|
|
||||||
throw OfflineException("getArtist isn't available in offline mode")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(OfflineException::class)
|
@Throws(OfflineException::class)
|
||||||
override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
throw OfflineException("getAlbum isn't available in offline mode")
|
|
||||||
|
Timber.i("Starting album query...")
|
||||||
|
|
||||||
|
val list = cachedTracks
|
||||||
|
.byAlbum(id)
|
||||||
|
.sortedWith(EntryByDiscAndTrackComparator())
|
||||||
|
|
||||||
|
val dir = MusicDirectory()
|
||||||
|
dir.addAll(list)
|
||||||
|
|
||||||
|
Timber.i("Returning query.")
|
||||||
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(OfflineException::class)
|
@Throws(OfflineException::class)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* RestMusicService.kt
|
* RESTMusicService.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
@ -78,7 +78,7 @@ open class RESTMusicService(
|
||||||
): List<MusicFolder> {
|
): List<MusicFolder> {
|
||||||
val response = API.getMusicFolders().execute().throwOnFailure()
|
val response = API.getMusicFolders().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.musicFolders.toDomainEntityList()
|
return response.body()!!.musicFolders.toDomainEntityList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,7 +91,10 @@ open class RESTMusicService(
|
||||||
): List<Index> {
|
): List<Index> {
|
||||||
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
|
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.indexes.toIndexList(musicFolderId)
|
return response.body()!!.indexes.toIndexList(
|
||||||
|
ActiveServerProvider.getActiveServerId(),
|
||||||
|
musicFolderId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -100,7 +103,7 @@ open class RESTMusicService(
|
||||||
): List<Artist> {
|
): List<Artist> {
|
||||||
val response = API.getArtists(null).execute().throwOnFailure()
|
val response = API.getArtists(null).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.indexes.toArtistList()
|
return response.body()!!.indexes.toArtistList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -137,18 +140,18 @@ open class RESTMusicService(
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = API.getMusicDirectory(id).execute().throwOnFailure()
|
val response = API.getMusicDirectory(id).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.musicDirectory.toDomainEntity()
|
return response.body()!!.musicDirectory.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getArtist(
|
override fun getAlbumsOfArtist(
|
||||||
id: String,
|
id: String,
|
||||||
name: String?,
|
name: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): List<Album> {
|
): List<Album> {
|
||||||
val response = API.getArtist(id).execute().throwOnFailure()
|
val response = API.getArtist(id).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.artist.toDomainEntityList()
|
return response.body()!!.artist.toDomainEntityList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -159,7 +162,7 @@ open class RESTMusicService(
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = API.getAlbum(id).execute().throwOnFailure()
|
val response = API.getAlbum(id).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.album.toMusicDirectoryDomainEntity()
|
return response.body()!!.album.toMusicDirectoryDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -189,7 +192,7 @@ open class RESTMusicService(
|
||||||
API.search(null, null, null, criteria.query, criteria.songCount, null, null)
|
API.search(null, null, null, criteria.query, criteria.songCount, null, null)
|
||||||
.execute().throwOnFailure()
|
.execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.searchResult.toDomainEntity()
|
return response.body()!!.searchResult.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -205,7 +208,7 @@ open class RESTMusicService(
|
||||||
criteria.songCount, null
|
criteria.songCount, null
|
||||||
).execute().throwOnFailure()
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.searchResult.toDomainEntity()
|
return response.body()!!.searchResult.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -218,7 +221,7 @@ open class RESTMusicService(
|
||||||
criteria.songCount, null
|
criteria.songCount, null
|
||||||
).execute().throwOnFailure()
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.searchResult.toDomainEntity()
|
return response.body()!!.searchResult.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -228,7 +231,7 @@ open class RESTMusicService(
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = API.getPlaylist(id).execute().throwOnFailure()
|
val response = API.getPlaylist(id).execute().throwOnFailure()
|
||||||
|
|
||||||
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity()
|
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity(activeServerId)
|
||||||
savePlaylist(name, playlist)
|
savePlaylist(name, playlist)
|
||||||
|
|
||||||
return playlist
|
return playlist
|
||||||
|
@ -319,7 +322,7 @@ open class RESTMusicService(
|
||||||
"skipped" != podcastEntry.status &&
|
"skipped" != podcastEntry.status &&
|
||||||
"error" != podcastEntry.status
|
"error" != podcastEntry.status
|
||||||
) {
|
) {
|
||||||
val entry = podcastEntry.toTrackEntity()
|
val entry = podcastEntry.toTrackEntity(activeServerId)
|
||||||
entry.track = null
|
entry.track = null
|
||||||
musicDirectory.add(entry)
|
musicDirectory.add(entry)
|
||||||
}
|
}
|
||||||
|
@ -363,7 +366,7 @@ open class RESTMusicService(
|
||||||
musicFolderId
|
musicFolderId
|
||||||
).execute().throwOnFailure()
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.albumList.toDomainEntityList()
|
return response.body()!!.albumList.toDomainEntityList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -383,7 +386,7 @@ open class RESTMusicService(
|
||||||
musicFolderId
|
musicFolderId
|
||||||
).execute().throwOnFailure()
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.albumList.toDomainEntityList()
|
return response.body()!!.albumList.toDomainEntityList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -399,7 +402,7 @@ open class RESTMusicService(
|
||||||
).execute().throwOnFailure()
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
result.addAll(response.body()!!.songsList.toDomainEntityList())
|
result.addAll(response.body()!!.songsList.toDomainEntityList(activeServerId))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -408,14 +411,14 @@ open class RESTMusicService(
|
||||||
override fun getStarred(): SearchResult {
|
override fun getStarred(): SearchResult {
|
||||||
val response = API.getStarred(null).execute().throwOnFailure()
|
val response = API.getStarred(null).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.starred.toDomainEntity()
|
return response.body()!!.starred.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getStarred2(): SearchResult {
|
override fun getStarred2(): SearchResult {
|
||||||
val response = API.getStarred2(null).execute().throwOnFailure()
|
val response = API.getStarred2(null).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.starred2.toDomainEntity()
|
return response.body()!!.starred2.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -546,7 +549,7 @@ open class RESTMusicService(
|
||||||
): List<Share> {
|
): List<Share> {
|
||||||
val response = API.getShares().execute().throwOnFailure()
|
val response = API.getShares().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.shares.toDomainEntitiesList()
|
return response.body()!!.shares.toDomainEntitiesList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -567,7 +570,7 @@ open class RESTMusicService(
|
||||||
val response = API.getSongsByGenre(genre, count, offset, null).execute().throwOnFailure()
|
val response = API.getSongsByGenre(genre, count, offset, null).execute().throwOnFailure()
|
||||||
|
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
result.addAll(response.body()!!.songsList.toDomainEntityList())
|
result.addAll(response.body()!!.songsList.toDomainEntityList(activeServerId))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -601,7 +604,7 @@ open class RESTMusicService(
|
||||||
override fun getBookmarks(): List<Bookmark> {
|
override fun getBookmarks(): List<Bookmark> {
|
||||||
val response = API.getBookmarks().execute().throwOnFailure()
|
val response = API.getBookmarks().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.bookmarkList.toDomainEntitiesList()
|
return response.body()!!.bookmarkList.toDomainEntitiesList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -626,7 +629,7 @@ open class RESTMusicService(
|
||||||
val response = API.getVideos().execute().throwOnFailure()
|
val response = API.getVideos().execute().throwOnFailure()
|
||||||
|
|
||||||
val musicDirectory = MusicDirectory()
|
val musicDirectory = MusicDirectory()
|
||||||
musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList())
|
musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList(activeServerId))
|
||||||
|
|
||||||
return musicDirectory
|
return musicDirectory
|
||||||
}
|
}
|
||||||
|
@ -639,7 +642,7 @@ open class RESTMusicService(
|
||||||
): List<Share> {
|
): List<Share> {
|
||||||
val response = API.createShare(ids, description, expires).execute().throwOnFailure()
|
val response = API.createShare(ids, description, expires).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.shares.toDomainEntitiesList()
|
return response.body()!!.shares.toDomainEntitiesList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -663,6 +666,9 @@ open class RESTMusicService(
|
||||||
API.updateShare(id, description, expiresValue).execute().throwOnFailure()
|
API.updateShare(id, description, expiresValue).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val activeServerId: Int
|
||||||
|
get() = ActiveServerProvider.getActiveServerId()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// The client will notice if the minimum supported API version has changed
|
// The client will notice if the minimum supported API version has changed
|
||||||
// By registering a callback we ensure this info is saved in the database as well
|
// By registering a callback we ensure this info is saved in the database as well
|
||||||
|
|
|
@ -269,7 +269,7 @@ class DownloadHandler(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val musicService = getMusicService()
|
val musicService = getMusicService()
|
||||||
val artist = musicService.getArtist(id, "", false)
|
val artist = musicService.getAlbumsOfArtist(id, "", false)
|
||||||
for ((id1) in artist) {
|
for ((id1) in artist) {
|
||||||
val albumDirectory = musicService.getAlbum(
|
val albumDirectory = musicService.getAlbum(
|
||||||
id1,
|
id1,
|
||||||
|
|
|
@ -86,6 +86,7 @@ object Constants {
|
||||||
const val PREFERENCES_KEY_INCREMENT_TIME = "incrementTime"
|
const val PREFERENCES_KEY_INCREMENT_TIME = "incrementTime"
|
||||||
const val PREFERENCES_KEY_SHOW_NOW_PLAYING_DETAILS = "showNowPlayingDetails"
|
const val PREFERENCES_KEY_SHOW_NOW_PLAYING_DETAILS = "showNowPlayingDetails"
|
||||||
const val PREFERENCES_KEY_ID3_TAGS = "useId3Tags"
|
const val PREFERENCES_KEY_ID3_TAGS = "useId3Tags"
|
||||||
|
const val PREFERENCES_KEY_ID3_TAGS_OFFLINE = "useId3TagsOffline"
|
||||||
const val PREFERENCES_KEY_SHOW_ARTIST_PICTURE = "showArtistPicture"
|
const val PREFERENCES_KEY_SHOW_ARTIST_PICTURE = "showArtistPicture"
|
||||||
const val PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval"
|
const val PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval"
|
||||||
const val PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime"
|
const val PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime"
|
||||||
|
@ -104,6 +105,7 @@ object Constants {
|
||||||
const val PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE = "pauseOnBluetoothDevice"
|
const val PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE = "pauseOnBluetoothDevice"
|
||||||
const val PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile"
|
const val PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile"
|
||||||
const val PREFERENCES_KEY_OVERRIDE_LANGUAGE = "overrideLanguage"
|
const val PREFERENCES_KEY_OVERRIDE_LANGUAGE = "overrideLanguage"
|
||||||
|
const val PREFERENCES_FIRST_INSTALLED_VERSION = "firstInstalledVersion"
|
||||||
const val PREFERENCE_VALUE_ALL = 0
|
const val PREFERENCE_VALUE_ALL = 0
|
||||||
const val PREFERENCE_VALUE_A2DP = 1
|
const val PREFERENCE_VALUE_A2DP = 1
|
||||||
const val PREFERENCE_VALUE_DISABLED = 2
|
const val PREFERENCE_VALUE_DISABLED = 2
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* MainThreadExecutor.java
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Executor for running Futures on the main thread
|
||||||
|
* See https://stackoverflow.com/questions/52642246/how-to-get-executor-for-main-thread-on-api-level-28
|
||||||
|
*/
|
||||||
|
class MainThreadExecutor : Executor {
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
override fun execute(r: Runnable) {
|
||||||
|
handler.post(r)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Settings.kt
|
* Settings.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
@ -13,7 +13,6 @@ import androidx.preference.PreferenceManager
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains convenience functions for reading and writing preferences
|
* Contains convenience functions for reading and writing preferences
|
||||||
|
@ -131,6 +130,7 @@ object Settings {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
var mediaButtonsEnabled
|
var mediaButtonsEnabled
|
||||||
by BooleanSetting(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true)
|
by BooleanSetting(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true)
|
||||||
|
|
||||||
var resumePlayOnHeadphonePlug
|
var resumePlayOnHeadphonePlug
|
||||||
by BooleanSetting(R.string.setting_keys_resume_play_on_headphones_plug, true)
|
by BooleanSetting(R.string.setting_keys_resume_play_on_headphones_plug, true)
|
||||||
|
|
||||||
|
@ -160,9 +160,14 @@ object Settings {
|
||||||
var showNowPlayingDetails
|
var showNowPlayingDetails
|
||||||
by BooleanSetting(Constants.PREFERENCES_KEY_SHOW_NOW_PLAYING_DETAILS, false)
|
by BooleanSetting(Constants.PREFERENCES_KEY_SHOW_NOW_PLAYING_DETAILS, false)
|
||||||
|
|
||||||
|
// Normally you don't need to use these Settings directly,
|
||||||
|
// use ActiveServerProvider.isID3Enabled() instead
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
var shouldUseId3Tags
|
var shouldUseId3Tags by BooleanSetting(Constants.PREFERENCES_KEY_ID3_TAGS, false)
|
||||||
by BooleanSetting(Constants.PREFERENCES_KEY_ID3_TAGS, false)
|
|
||||||
|
// See comment above.
|
||||||
|
@JvmStatic
|
||||||
|
var useId3TagsOffline by BooleanSetting(Constants.PREFERENCES_KEY_ID3_TAGS_OFFLINE, false)
|
||||||
|
|
||||||
var activeServer by IntSetting(Constants.PREFERENCES_KEY_SERVER_INSTANCE, -1)
|
var activeServer by IntSetting(Constants.PREFERENCES_KEY_SERVER_INSTANCE, -1)
|
||||||
|
|
||||||
|
@ -170,15 +175,8 @@ object Settings {
|
||||||
|
|
||||||
var firstRunExecuted by BooleanSetting(Constants.PREFERENCES_KEY_FIRST_RUN_EXECUTED, false)
|
var firstRunExecuted by BooleanSetting(Constants.PREFERENCES_KEY_FIRST_RUN_EXECUTED, false)
|
||||||
|
|
||||||
val shouldShowArtistPicture: Boolean
|
val shouldShowArtistPicture
|
||||||
get() {
|
by BooleanSetting(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE, false)
|
||||||
val preferences = preferences
|
|
||||||
val isOffline = ActiveServerProvider.isOffline()
|
|
||||||
val isId3Enabled = preferences.getBoolean(Constants.PREFERENCES_KEY_ID3_TAGS, false)
|
|
||||||
val shouldShowArtistPicture =
|
|
||||||
preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE, false)
|
|
||||||
return !isOffline && isId3Enabled && shouldShowArtistPicture
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
var chatRefreshInterval by StringIntSetting(
|
var chatRefreshInterval by StringIntSetting(
|
||||||
|
@ -253,6 +251,9 @@ object Settings {
|
||||||
|
|
||||||
var useHwOffload by BooleanSetting(Constants.PREFERENCES_KEY_HARDWARE_OFFLOAD, false)
|
var useHwOffload by BooleanSetting(Constants.PREFERENCES_KEY_HARDWARE_OFFLOAD, false)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
var firstInstalledVersion by IntSetting(Constants.PREFERENCES_FIRST_INSTALLED_VERSION, 0)
|
||||||
|
|
||||||
// TODO: Remove in December 2022
|
// TODO: Remove in December 2022
|
||||||
fun migrateFeatureStorage() {
|
fun migrateFeatureStorage() {
|
||||||
val sp = appContext.getSharedPreferences("feature_flags", Context.MODE_PRIVATE)
|
val sp = appContext.getSharedPreferences("feature_flags", Context.MODE_PRIVATE)
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ripple
|
<ripple
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
android:color="?android:colorControlHighlight">
|
||||||
android:color="?android:colorControlHighlight"
|
|
||||||
tools:targetApi="lollipop">
|
|
||||||
|
|
||||||
<item android:id="@android:id/mask">
|
<item android:id="@android:id/mask">
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFF"
|
||||||
|
android:pathData="m12,3.8438c-4.9703,0 -9,4.0292 -9,9l0,5.0625c0,1.2426 1.0074,2.25 2.25,2.25 1.2426,0 2.25,-1.0074 2.25,-2.25l0,-3.375c0,-1.2426 -1.0074,-2.25 -2.25,-2.25 -0.4067,0 -0.783,0.1164 -1.1121,0.3049C4.2752,8.3573 7.7379,4.9688 12,4.9688 16.2621,4.9688 19.7242,8.3573 19.8621,12.5861 19.5336,12.3977 19.1567,12.2813 18.75,12.2813c-1.2426,0 -2.25,1.0074 -2.25,2.25l0,3.375c0,1.2426 1.0074,2.25 2.25,2.25 1.2426,0 2.25,-1.0074 2.25,-2.25L21,12.8438C21,7.8729 16.9708,3.8438 12,3.8438ZM5.25,13.4063c0.621,0 1.125,0.504 1.125,1.125l0,3.375c0,0.621 -0.504,1.125 -1.125,1.125 -0.621,0 -1.125,-0.504 -1.125,-1.125l0,-3.375c0,-0.621 0.504,-1.125 1.125,-1.125zM19.875,17.9063c0,0.621 -0.504,1.125 -1.125,1.125 -0.621,0 -1.125,-0.504 -1.125,-1.125l0,-3.375c0,-0.621 0.504,-1.125 1.125,-1.125 0.621,0 1.125,0.504 1.125,1.125z"/>
|
||||||
|
</vector>
|
|
@ -1,9 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ripple
|
<ripple
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
android:color="?android:colorControlHighlight">
|
||||||
android:color="?android:colorControlHighlight"
|
|
||||||
tools:targetApi="lollipop">
|
|
||||||
|
|
||||||
<item android:id="@android:id/mask">
|
<item android:id="@android:id/mask">
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ripple
|
<ripple
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
android:color="?android:colorControlHighlight">
|
||||||
android:color="?android:colorControlHighlight"
|
|
||||||
tools:targetApi="lollipop">
|
|
||||||
|
|
||||||
<item android:id="@android:id/mask">
|
<item android:id="@android:id/mask">
|
||||||
<shape android:shape="oval">
|
<shape android:shape="oval">
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
<item>3</item>
|
<item>3</item>
|
||||||
<item>5</item>
|
<item>5</item>
|
||||||
<item>10</item>
|
<item>10</item>
|
||||||
|
<item>50</item>
|
||||||
|
<item>100</item>
|
||||||
|
<item>500</item>
|
||||||
|
<item>1000</item>
|
||||||
<item>-1</item>
|
<item>-1</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="preloadCountNames" translatable="false">
|
<string-array name="preloadCountNames" translatable="false">
|
||||||
|
|
|
@ -316,6 +316,9 @@
|
||||||
<string name="settings.show_now_playing_details">Show details in Now Playing</string>
|
<string name="settings.show_now_playing_details">Show details in Now Playing</string>
|
||||||
<string name="settings.use_id3">Browse Using ID3 Tags</string>
|
<string name="settings.use_id3">Browse Using ID3 Tags</string>
|
||||||
<string name="settings.use_id3_summary">Use ID3 tag methods instead of file system based methods</string>
|
<string name="settings.use_id3_summary">Use ID3 tag methods instead of file system based methods</string>
|
||||||
|
<string name="settings.use_id3_offline">Use ID3 method also when offline</string>
|
||||||
|
<string name="settings.use_id3_offline_warning">Experimental: If you enable this Setting it will only show the music that you have downloaded with Ultrasonic 4.0 or later.</string>
|
||||||
|
<string name="settings.use_id3_offline_summary">Earlier downloads don\'t have the necessary metadata downloaded. You can toggle between Pin and Save mode to trigger the download of the missing metadata.</string>
|
||||||
<string name="settings.show_artist_picture">Show artist picture in artist list</string>
|
<string name="settings.show_artist_picture">Show artist picture in artist list</string>
|
||||||
<string name="settings.show_artist_picture_summary">Displays the artist picture in the artist list if available</string>
|
<string name="settings.show_artist_picture_summary">Displays the artist picture in the artist list if available</string>
|
||||||
<string name="main.video" tools:ignore="UnusedResources">Video</string>
|
<string name="main.video" tools:ignore="UnusedResources">Video</string>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Targeting Android 11 or lower -->
|
||||||
|
<full-backup-content>
|
||||||
|
<!-- The following "exclude" elements are not part of the auto backup -->
|
||||||
|
<!--exclude domain="database" path="name.db" /-->
|
||||||
|
<exclude domain="root"/>
|
||||||
|
<!-- Exclude specific shared preferences that contain GCM registration Id -->
|
||||||
|
|
||||||
|
<!-- The following "include" elements are part of the auto backup -->
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<include domain="database" path="."/>
|
||||||
|
</full-backup-content>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Targeting Android 12 or higher -->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup disableIfNoEncryptionCapabilities="true">
|
||||||
|
<!--include domain=["file" | "database" | "sharedpref" | "external" | "root"] path="string"/-->
|
||||||
|
<!--exclude domain=["file" | "database" | "sharedpref" | "external" | "root"] path="string"/-->
|
||||||
|
<exclude domain="root"/>
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<include domain="database" path="."/>
|
||||||
|
</cloud-backup>
|
||||||
|
<device-transfer>
|
||||||
|
<!-- Specifying nothing here means: Include all: https://developer.android.com/guide/topics/data/autobackup#include-exclude-android-11 -->
|
||||||
|
</device-transfer>
|
||||||
|
</data-extraction-rules>
|
|
@ -59,6 +59,12 @@
|
||||||
a:summary="@string/settings.use_id3_summary"
|
a:summary="@string/settings.use_id3_summary"
|
||||||
a:title="@string/settings.use_id3"
|
a:title="@string/settings.use_id3"
|
||||||
app:iconSpaceReserved="false"/>
|
app:iconSpaceReserved="false"/>
|
||||||
|
<CheckBoxPreference
|
||||||
|
a:defaultValue="true"
|
||||||
|
a:key="useId3TagsOffline"
|
||||||
|
a:summary="@string/settings.use_id3_offline_summary"
|
||||||
|
a:title="@string/settings.use_id3_offline"
|
||||||
|
app:iconSpaceReserved="false"/>
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
a:defaultValue="true"
|
a:defaultValue="true"
|
||||||
a:key="showArtistPicture"
|
a:key="showArtistPicture"
|
||||||
|
|
|
@ -12,6 +12,8 @@ import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
||||||
* Unit test for extension functions in [APIAlbumConverter.kt] file.
|
* Unit test for extension functions in [APIAlbumConverter.kt] file.
|
||||||
*/
|
*/
|
||||||
class APIAlbumConverterTest {
|
class APIAlbumConverterTest {
|
||||||
|
private val serverId = -1
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert Album to domain entity`() {
|
fun `Should convert Album to domain entity`() {
|
||||||
val entity = Album(
|
val entity = Album(
|
||||||
|
@ -20,7 +22,7 @@ class APIAlbumConverterTest {
|
||||||
created = Calendar.getInstance(), year = 2017, genre = "some-genre"
|
created = Calendar.getInstance(), year = 2017, genre = "some-genre"
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toDomainEntity()
|
val convertedEntity = entity.toDomainEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
id `should be equal to` entity.id
|
id `should be equal to` entity.id
|
||||||
|
@ -46,12 +48,12 @@ class APIAlbumConverterTest {
|
||||||
songList = listOf(MusicDirectoryChild())
|
songList = listOf(MusicDirectoryChild())
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toMusicDirectoryDomainEntity()
|
val convertedEntity = entity.toMusicDirectoryDomainEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
name `should be equal to` null
|
name `should be equal to` null
|
||||||
size `should be equal to` entity.songList.size
|
size `should be equal to` entity.songList.size
|
||||||
this[0] `should be equal to` entity.songList[0].toTrackEntity()
|
this[0] `should be equal to` entity.songList[0].toTrackEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,12 +61,12 @@ class APIAlbumConverterTest {
|
||||||
fun `Should convert list of Album entities to domain list entities`() {
|
fun `Should convert list of Album entities to domain list entities`() {
|
||||||
val entityList = listOf(Album(id = "455"), Album(id = "1"), Album(id = "1000"))
|
val entityList = listOf(Album(id = "455"), Album(id = "1"), Album(id = "1000"))
|
||||||
|
|
||||||
val convertedList = entityList.toDomainEntityList()
|
val convertedList = entityList.toDomainEntityList(serverId)
|
||||||
|
|
||||||
with(convertedList) {
|
with(convertedList) {
|
||||||
size `should be equal to` entityList.size
|
size `should be equal to` entityList.size
|
||||||
forEachIndexed { index, entry ->
|
forEachIndexed { index, entry ->
|
||||||
entry `should be equal to` entityList[index].toDomainEntity()
|
entry `should be equal to` entityList[index].toDomainEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,12 @@ import org.moire.ultrasonic.api.subsonic.models.Artist
|
||||||
/**
|
/**
|
||||||
* Unit test for extension functions in APIArtistConverter.kt file.
|
* Unit test for extension functions in APIArtistConverter.kt file.
|
||||||
*/
|
*/
|
||||||
class APIArtistConverterTest {
|
class APIArtistConverterTest : BaseTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert artist entity`() {
|
fun `Should convert artist entity`() {
|
||||||
val entity = Artist(id = "10", name = "artist-name", starred = Calendar.getInstance())
|
val entity = Artist(id = "10", name = "artist-name", starred = Calendar.getInstance())
|
||||||
|
|
||||||
val convertedEntity = entity.toDomainEntity()
|
val convertedEntity = entity.toDomainEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
id `should be equal to` entity.id
|
id `should be equal to` entity.id
|
||||||
|
@ -38,12 +38,12 @@ class APIArtistConverterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toMusicDirectoryDomainEntity()
|
val convertedEntity = entity.toMusicDirectoryDomainEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
name `should be equal to` entity.name
|
name `should be equal to` entity.name
|
||||||
getChildren() `should be equal to` entity.albumsList
|
getChildren() `should be equal to` entity.albumsList
|
||||||
.map { it.toDomainEntity() }.toMutableList()
|
.map { it.toDomainEntity(serverId) }.toMutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
||||||
/**
|
/**
|
||||||
* Unit test for function that converts [Bookmark] api entity to domain.
|
* Unit test for function that converts [Bookmark] api entity to domain.
|
||||||
*/
|
*/
|
||||||
class APIBookmarkConverterTest {
|
class APIBookmarkConverterTest : BaseTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert to domain entity`() {
|
fun `Should convert to domain entity`() {
|
||||||
val entity = Bookmark(
|
val entity = Bookmark(
|
||||||
|
@ -19,7 +20,7 @@ class APIBookmarkConverterTest {
|
||||||
Calendar.getInstance(), MusicDirectoryChild(id = "12333")
|
Calendar.getInstance(), MusicDirectoryChild(id = "12333")
|
||||||
)
|
)
|
||||||
|
|
||||||
val domainEntity = entity.toDomainEntity()
|
val domainEntity = entity.toDomainEntity(serverId)
|
||||||
|
|
||||||
with(domainEntity) {
|
with(domainEntity) {
|
||||||
position `should be equal to` entity.position.toInt()
|
position `should be equal to` entity.position.toInt()
|
||||||
|
@ -27,7 +28,7 @@ class APIBookmarkConverterTest {
|
||||||
comment `should be equal to` entity.comment
|
comment `should be equal to` entity.comment
|
||||||
created `should be equal to` entity.created?.time
|
created `should be equal to` entity.created?.time
|
||||||
changed `should be equal to` entity.changed?.time
|
changed `should be equal to` entity.changed?.time
|
||||||
track `should be equal to` entity.entry.toTrackEntity()
|
track `should be equal to` entity.entry.toTrackEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,11 +36,11 @@ class APIBookmarkConverterTest {
|
||||||
fun `Should convert list of entities to domain entities`() {
|
fun `Should convert list of entities to domain entities`() {
|
||||||
val entitiesList = listOf(Bookmark(443L), Bookmark(444L))
|
val entitiesList = listOf(Bookmark(443L), Bookmark(444L))
|
||||||
|
|
||||||
val domainEntitiesList = entitiesList.toDomainEntitiesList()
|
val domainEntitiesList = entitiesList.toDomainEntitiesList(serverId)
|
||||||
|
|
||||||
domainEntitiesList.size `should be equal to` entitiesList.size
|
domainEntitiesList.size `should be equal to` entitiesList.size
|
||||||
domainEntitiesList.forEachIndexed({ index, bookmark ->
|
domainEntitiesList.forEachIndexed { index, bookmark ->
|
||||||
bookmark `should be equal to` entitiesList[index].toDomainEntity()
|
bookmark `should be equal to` entitiesList[index].toDomainEntity(serverId)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.Indexes
|
||||||
/**
|
/**
|
||||||
* Unit tests for extension functions in [APIIndexesConverter.kt].
|
* Unit tests for extension functions in [APIIndexesConverter.kt].
|
||||||
*/
|
*/
|
||||||
class APIIndexConverterTest {
|
class APIIndexConverterTest : BaseTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert Indexes entity`() {
|
fun `Should convert Indexes entity`() {
|
||||||
val artistsA = listOf(
|
val artistsA = listOf(
|
||||||
|
@ -31,9 +31,12 @@ class APIIndexConverterTest {
|
||||||
shortcutList = artistsA
|
shortcutList = artistsA
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toArtistList()
|
val convertedEntity = entity.toArtistList(serverId)
|
||||||
|
|
||||||
|
val expectedArtists = (artistsA + artistsT).map {
|
||||||
|
it.toDomainEntity(serverId)
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
val expectedArtists = (artistsA + artistsT).map { it.toDomainEntity() }.toMutableList()
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
size `should be equal to` expectedArtists.size
|
size `should be equal to` expectedArtists.size
|
||||||
this `should be equal to` expectedArtists
|
this `should be equal to` expectedArtists
|
||||||
|
|
|
@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
||||||
/**
|
/**
|
||||||
* Unit test for extension functions in APIMusicDirectoryConverter.kt file.
|
* Unit test for extension functions in APIMusicDirectoryConverter.kt file.
|
||||||
*/
|
*/
|
||||||
class APIMusicDirectoryConverterTest {
|
class APIMusicDirectoryConverterTest : BaseTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert MusicDirectory entity`() {
|
fun `Should convert MusicDirectory entity`() {
|
||||||
val entity = MusicDirectory(
|
val entity = MusicDirectory(
|
||||||
|
@ -20,13 +20,13 @@ class APIMusicDirectoryConverterTest {
|
||||||
childList = listOf(MusicDirectoryChild("1"), MusicDirectoryChild("2"))
|
childList = listOf(MusicDirectoryChild("1"), MusicDirectoryChild("2"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toDomainEntity()
|
val convertedEntity = entity.toDomainEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
name `should be equal to` entity.name
|
name `should be equal to` entity.name
|
||||||
size `should be equal to` entity.childList.size
|
size `should be equal to` entity.childList.size
|
||||||
getChildren() `should be equal to` entity.childList
|
getChildren() `should be equal to` entity.childList
|
||||||
.map { it.toTrackEntity() }.toMutableList()
|
.map { it.toTrackEntity(serverId) }.toMutableList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class APIMusicDirectoryConverterTest {
|
||||||
starred = Calendar.getInstance(), userRating = 3, averageRating = 2.99F
|
starred = Calendar.getInstance(), userRating = 3, averageRating = 2.99F
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toTrackEntity()
|
val convertedEntity = entity.toTrackEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
id `should be equal to` entity.id
|
id `should be equal to` entity.id
|
||||||
|
@ -84,7 +84,7 @@ class APIMusicDirectoryConverterTest {
|
||||||
artist = "some-artist", publishDate = Calendar.getInstance()
|
artist = "some-artist", publishDate = Calendar.getInstance()
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toTrackEntity()
|
val convertedEntity = entity.toTrackEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
id `should be equal to` entity.streamId
|
id `should be equal to` entity.streamId
|
||||||
|
@ -96,11 +96,11 @@ class APIMusicDirectoryConverterTest {
|
||||||
fun `Should convert list of MusicDirectoryChild to domain entity list`() {
|
fun `Should convert list of MusicDirectoryChild to domain entity list`() {
|
||||||
val entitiesList = listOf(MusicDirectoryChild(id = "45"), MusicDirectoryChild(id = "34"))
|
val entitiesList = listOf(MusicDirectoryChild(id = "45"), MusicDirectoryChild(id = "34"))
|
||||||
|
|
||||||
val domainList = entitiesList.toDomainEntityList()
|
val domainList = entitiesList.toDomainEntityList(serverId)
|
||||||
|
|
||||||
domainList.size `should be equal to` entitiesList.size
|
domainList.size `should be equal to` entitiesList.size
|
||||||
domainList.forEachIndexed { index, entry ->
|
domainList.forEachIndexed { index, entry ->
|
||||||
entry `should be equal to` entitiesList[index].toTrackEntity()
|
entry `should be equal to` entitiesList[index].toTrackEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,12 @@ import org.moire.ultrasonic.api.subsonic.models.MusicFolder
|
||||||
/**
|
/**
|
||||||
* Unit test for extension functions in file APIMusicFolderConverter.kt.
|
* Unit test for extension functions in file APIMusicFolderConverter.kt.
|
||||||
*/
|
*/
|
||||||
class APIMusicFolderConverterTest {
|
class APIMusicFolderConverterTest : BaseTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert MusicFolder entity`() {
|
fun `Should convert MusicFolder entity`() {
|
||||||
val entity = MusicFolder(id = "10", name = "some-name")
|
val entity = MusicFolder(id = "10", name = "some-name")
|
||||||
|
|
||||||
val convertedEntity = entity.toDomainEntity()
|
val convertedEntity = entity.toDomainEntity(serverId)
|
||||||
|
|
||||||
convertedEntity.name `should be equal to` entity.name
|
convertedEntity.name `should be equal to` entity.name
|
||||||
convertedEntity.id `should be equal to` entity.id
|
convertedEntity.id `should be equal to` entity.id
|
||||||
|
@ -27,7 +27,7 @@ class APIMusicFolderConverterTest {
|
||||||
MusicFolder(id = "4", name = "some-name-4")
|
MusicFolder(id = "4", name = "some-name-4")
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedList = entityList.toDomainEntityList()
|
val convertedList = entityList.toDomainEntityList(serverId)
|
||||||
|
|
||||||
with(convertedList) {
|
with(convertedList) {
|
||||||
size `should be equal to` entityList.size
|
size `should be equal to` entityList.size
|
||||||
|
|
|
@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.Playlist
|
||||||
/**
|
/**
|
||||||
* Unit test for extension functions that converts api playlist entity to domain.
|
* Unit test for extension functions that converts api playlist entity to domain.
|
||||||
*/
|
*/
|
||||||
class APIPlaylistConverterTest {
|
class APIPlaylistConverterTest : BaseTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert Playlist to MusicDirectory domain entity`() {
|
fun `Should convert Playlist to MusicDirectory domain entity`() {
|
||||||
val entity = Playlist(
|
val entity = Playlist(
|
||||||
|
@ -22,13 +22,13 @@ class APIPlaylistConverterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toMusicDirectoryDomainEntity()
|
val convertedEntity = entity.toMusicDirectoryDomainEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
name `should be equal to` entity.name
|
name `should be equal to` entity.name
|
||||||
size `should be equal to` entity.entriesList.size
|
size `should be equal to` entity.entriesList.size
|
||||||
this[0] `should be equal to` entity.entriesList[0].toTrackEntity()
|
this[0] `should be equal to` entity.entriesList[0].toTrackEntity(serverId)
|
||||||
this[1] `should be equal to` entity.entriesList[1].toTrackEntity()
|
this[1] `should be equal to` entity.entriesList[1].toTrackEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
|
||||||
/**
|
/**
|
||||||
* Unit test for extension function in APISearchConverter.kt file.
|
* Unit test for extension function in APISearchConverter.kt file.
|
||||||
*/
|
*/
|
||||||
class APISearchConverterTest {
|
class APISearchConverterTest : BaseTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert SearchResult to domain entity`() {
|
fun `Should convert SearchResult to domain entity`() {
|
||||||
val entity = SearchResult(
|
val entity = SearchResult(
|
||||||
|
@ -26,7 +26,7 @@ class APISearchConverterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toDomainEntity()
|
val convertedEntity = entity.toDomainEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
albums `should not be equal to` null
|
albums `should not be equal to` null
|
||||||
|
@ -34,7 +34,7 @@ class APISearchConverterTest {
|
||||||
artists `should not be equal to` null
|
artists `should not be equal to` null
|
||||||
artists.size `should be equal to` 0
|
artists.size `should be equal to` 0
|
||||||
songs.size `should be equal to` entity.matchList.size
|
songs.size `should be equal to` entity.matchList.size
|
||||||
songs[0] `should be equal to` entity.matchList[0].toTrackEntity()
|
songs[0] `should be equal to` entity.matchList[0].toTrackEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,15 +46,15 @@ class APISearchConverterTest {
|
||||||
listOf(MusicDirectoryChild(id = "9118", parent = "112"))
|
listOf(MusicDirectoryChild(id = "9118", parent = "112"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toDomainEntity()
|
val convertedEntity = entity.toDomainEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
artists.size `should be equal to` entity.artistList.size
|
artists.size `should be equal to` entity.artistList.size
|
||||||
artists[0] `should be equal to` entity.artistList[0].toIndexEntity()
|
artists[0] `should be equal to` entity.artistList[0].toIndexEntity(serverId)
|
||||||
albums.size `should be equal to` entity.albumList.size
|
albums.size `should be equal to` entity.albumList.size
|
||||||
albums[0] `should be equal to` entity.albumList[0].toDomainEntity()
|
albums[0] `should be equal to` entity.albumList[0].toDomainEntity(serverId)
|
||||||
songs.size `should be equal to` entity.songList.size
|
songs.size `should be equal to` entity.songList.size
|
||||||
songs[0] `should be equal to` entity.songList[0].toTrackEntity()
|
songs[0] `should be equal to` entity.songList[0].toTrackEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,15 +66,15 @@ class APISearchConverterTest {
|
||||||
songList = listOf(MusicDirectoryChild(id = "7123", title = "song1"))
|
songList = listOf(MusicDirectoryChild(id = "7123", title = "song1"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toDomainEntity()
|
val convertedEntity = entity.toDomainEntity(serverId)
|
||||||
|
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
artists.size `should be equal to` entity.artistList.size
|
artists.size `should be equal to` entity.artistList.size
|
||||||
artists[0] `should be equal to` entity.artistList[0].toDomainEntity()
|
artists[0] `should be equal to` entity.artistList[0].toDomainEntity(serverId)
|
||||||
albums.size `should be equal to` entity.albumList.size
|
albums.size `should be equal to` entity.albumList.size
|
||||||
albums[0] `should be equal to` entity.albumList[0].toDomainEntity()
|
albums[0] `should be equal to` entity.albumList[0].toDomainEntity(serverId)
|
||||||
songs.size `should be equal to` entity.songList.size
|
songs.size `should be equal to` entity.songList.size
|
||||||
songs[0] `should be equal to` entity.songList[0].toTrackEntity()
|
songs[0] `should be equal to` entity.songList[0].toTrackEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,12 @@ import org.moire.ultrasonic.api.subsonic.models.Share
|
||||||
/**
|
/**
|
||||||
* Unit test for api to domain share entity converter functions.
|
* Unit test for api to domain share entity converter functions.
|
||||||
*/
|
*/
|
||||||
class APIShareConverterTest {
|
class APIShareConverterTest : BaseTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert share entity to domain`() {
|
fun `Should convert share entity to domain`() {
|
||||||
val entity = createFakeShare()
|
val entity = createFakeShare()
|
||||||
|
|
||||||
val domainEntity = entity.toDomainEntity()
|
val domainEntity = entity.toDomainEntity(serverId)
|
||||||
|
|
||||||
with(domainEntity) {
|
with(domainEntity) {
|
||||||
id `should be equal to` entity.id
|
id `should be equal to` entity.id
|
||||||
|
@ -27,7 +27,7 @@ class APIShareConverterTest {
|
||||||
lastVisited `should be equal to` shareTimeFormat.format(entity.lastVisited!!.time)
|
lastVisited `should be equal to` shareTimeFormat.format(entity.lastVisited!!.time)
|
||||||
expires `should be equal to` shareTimeFormat.format(entity.expires!!.time)
|
expires `should be equal to` shareTimeFormat.format(entity.expires!!.time)
|
||||||
visitCount `should be equal to` entity.visitCount.toLong()
|
visitCount `should be equal to` entity.visitCount.toLong()
|
||||||
this.getEntries() `should be equal to` entity.items.toDomainEntityList()
|
this.getEntries() `should be equal to` entity.items.toDomainEntityList(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,10 +47,10 @@ class APIShareConverterTest {
|
||||||
createFakeShare().copy(id = "554", lastVisited = null)
|
createFakeShare().copy(id = "554", lastVisited = null)
|
||||||
)
|
)
|
||||||
|
|
||||||
val domainEntityList = entityList.toDomainEntitiesList()
|
val domainEntityList = entityList.toDomainEntitiesList(serverId)
|
||||||
|
|
||||||
domainEntityList.size `should be equal to` entityList.size
|
domainEntityList.size `should be equal to` entityList.size
|
||||||
domainEntityList[0] `should be equal to` entityList[0].toDomainEntity()
|
domainEntityList[0] `should be equal to` entityList[0].toDomainEntity(serverId)
|
||||||
domainEntityList[1] `should be equal to` entityList[1].toDomainEntity()
|
domainEntityList[1] `should be equal to` entityList[1].toDomainEntity(serverId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* BaseTest.kt
|
||||||
|
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
open class BaseTest {
|
||||||
|
internal val serverId = -1
|
||||||
|
}
|
Loading…
Reference in New Issue