Merge remote-tracking branch 'base/develop' into check-server-features
This commit is contained in:
commit
d8e7b991cd
|
@ -35,6 +35,13 @@ allprojects {
|
||||||
google()
|
google()
|
||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set Kotlin JVM target to the same for all subprojects
|
||||||
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: 'gradle_scripts/jacoco.gradle'
|
apply from: 'gradle_scripts/jacoco.gradle'
|
||||||
|
|
|
@ -9,8 +9,22 @@ data class Artist(
|
||||||
var coverArt: String? = null,
|
var coverArt: String? = null,
|
||||||
var albumCount: Long? = null,
|
var albumCount: Long? = null,
|
||||||
var closeness: Int = 0
|
var closeness: Int = 0
|
||||||
) : Serializable, GenericEntry() {
|
) : Serializable, GenericEntry(), Comparable<Artist> {
|
||||||
companion object {
|
companion object {
|
||||||
private const val serialVersionUID = -5790532593784846982L
|
private const val serialVersionUID = -5790532593784846982L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: Artist): Int {
|
||||||
|
when {
|
||||||
|
this.closeness == other.closeness -> {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
this.closeness > other.closeness -> {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ class MusicDirectory {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Entry(
|
data class Entry(
|
||||||
override var id: String? = null,
|
override var id: String,
|
||||||
var parent: String? = null,
|
var parent: String? = null,
|
||||||
var isDirectory: Boolean = false,
|
var isDirectory: Boolean = false,
|
||||||
var title: String? = null,
|
var title: String? = null,
|
||||||
|
@ -66,7 +66,7 @@ class MusicDirectory {
|
||||||
var bookmarkPosition: Int = 0,
|
var bookmarkPosition: Int = 0,
|
||||||
var userRating: Int? = null,
|
var userRating: Int? = null,
|
||||||
var averageRating: Float? = null
|
var averageRating: Float? = null
|
||||||
) : Serializable, GenericEntry() {
|
) : Serializable, GenericEntry(), Comparable<Entry> {
|
||||||
fun setDuration(duration: Long) {
|
fun setDuration(duration: Long) {
|
||||||
this.duration = duration.toInt()
|
this.duration = duration.toInt()
|
||||||
}
|
}
|
||||||
|
@ -74,5 +74,19 @@ class MusicDirectory {
|
||||||
companion object {
|
companion object {
|
||||||
private const val serialVersionUID = -3339106650010798108L
|
private const val serialVersionUID = -3339106650010798108L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: Entry): Int {
|
||||||
|
when {
|
||||||
|
this.closeness == other.closeness -> {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
this.closeness > other.closeness -> {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,10 @@ class AvatarRequestHandler(
|
||||||
?: throw IllegalArgumentException("Nullable username")
|
?: throw IllegalArgumentException("Nullable username")
|
||||||
|
|
||||||
val response = apiClient.getAvatar(username)
|
val response = apiClient.getAvatar(username)
|
||||||
if (response.hasError()) {
|
if (response.hasError() || response.stream == null) {
|
||||||
throw IOException("${response.apiError}")
|
throw IOException("${response.apiError}")
|
||||||
} else {
|
} else {
|
||||||
return Result(Okio.source(response.stream), Picasso.LoadedFrom.NETWORK)
|
return Result(Okio.source(response.stream!!), Picasso.LoadedFrom.NETWORK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,10 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request
|
||||||
?: throw IllegalArgumentException("Nullable id")
|
?: throw IllegalArgumentException("Nullable id")
|
||||||
|
|
||||||
val response = apiClient.getCoverArt(id)
|
val response = apiClient.getCoverArt(id)
|
||||||
if (response.hasError()) {
|
if (response.hasError() || response.stream == null) {
|
||||||
throw IOException("${response.apiError}")
|
throw IOException("${response.apiError}")
|
||||||
} else {
|
} else {
|
||||||
return Result(Okio.source(response.stream), NETWORK)
|
return Result(Okio.source(response.stream!!), NETWORK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ class PasswordMD5Interceptor(private val password: String) : Interceptor {
|
||||||
val md5Digest = MessageDigest.getInstance("MD5")
|
val md5Digest = MessageDigest.getInstance("MD5")
|
||||||
return md5Digest.digest(
|
return md5Digest.digest(
|
||||||
"$password$salt".toByteArray()
|
"$password$salt".toByteArray()
|
||||||
).toHexBytes().toLowerCase(Locale.getDefault())
|
).toHexBytes().lowercase(Locale.getDefault())
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
throw IllegalStateException(e)
|
throw IllegalStateException(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ ext.versions = [
|
||||||
androidxcore : "1.5.0",
|
androidxcore : "1.5.0",
|
||||||
ktlint : "0.37.1",
|
ktlint : "0.37.1",
|
||||||
ktlintGradle : "9.2.1",
|
ktlintGradle : "9.2.1",
|
||||||
detekt : "1.17.0",
|
detekt : "1.17.1",
|
||||||
jacoco : "0.8.7",
|
jacoco : "0.8.7",
|
||||||
preferences : "1.1.1",
|
preferences : "1.1.1",
|
||||||
media : "1.3.1",
|
media : "1.3.1",
|
||||||
|
@ -20,16 +20,16 @@ ext.versions = [
|
||||||
androidSupportDesign : "1.3.0",
|
androidSupportDesign : "1.3.0",
|
||||||
constraintLayout : "2.0.4",
|
constraintLayout : "2.0.4",
|
||||||
multidex : "2.0.1",
|
multidex : "2.0.1",
|
||||||
room : "2.2.6",
|
room : "2.3.0",
|
||||||
kotlin : "1.4.32",
|
kotlin : "1.5.10",
|
||||||
kotlinxCoroutines : "1.4.3-native-mt",
|
kotlinxCoroutines : "1.5.0-native-mt",
|
||||||
viewModelKtx : "2.2.0",
|
viewModelKtx : "2.2.0",
|
||||||
|
|
||||||
retrofit : "2.6.4",
|
retrofit : "2.6.4",
|
||||||
jackson : "2.9.5",
|
jackson : "2.9.5",
|
||||||
okhttp : "3.12.13",
|
okhttp : "3.12.13",
|
||||||
twitterSerial : "0.1.6",
|
twitterSerial : "0.1.6",
|
||||||
koin : "2.2.2",
|
koin : "3.0.2",
|
||||||
picasso : "2.71828",
|
picasso : "2.71828",
|
||||||
sortListView : "1.0.1",
|
sortListView : "1.0.1",
|
||||||
|
|
||||||
|
|
|
@ -67,18 +67,10 @@
|
||||||
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||||
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
||||||
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||||
<ID>ReturnCount:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</ID>
|
|
||||||
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
||||||
<ID>ReturnCount:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
|
|
||||||
<ID>ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
|
|
||||||
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
|
||||||
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||||
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
||||||
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||||
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
|
|
||||||
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { // Froyo or lower }</ID>
|
|
||||||
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { }</ID>
|
|
||||||
<ID>SwallowedException:MediaPlayerService.kt$MediaPlayerService$catch (x: IndexOutOfBoundsException) { // Ignored }</ID>
|
|
||||||
<ID>SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() }</ID>
|
<ID>SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() }</ID>
|
||||||
<ID>ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>)</ID>
|
<ID>ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>)</ID>
|
||||||
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
|
||||||
|
@ -89,7 +81,6 @@
|
||||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
|
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$x: IndexOutOfBoundsException</ID>
|
|
||||||
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
|
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
|
||||||
<ID>TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception</ID>
|
||||||
|
@ -98,7 +89,6 @@
|
||||||
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
||||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||||
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
||||||
<ID>UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"</ID>
|
|
||||||
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
|
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
|
||||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||||
</CurrentIssues>
|
</CurrentIssues>
|
||||||
|
|
|
@ -69,6 +69,8 @@ style:
|
||||||
ignorePropertyDeclaration: true
|
ignorePropertyDeclaration: true
|
||||||
UnnecessaryAbstractClass:
|
UnnecessaryAbstractClass:
|
||||||
active: false
|
active: false
|
||||||
|
ReturnCount:
|
||||||
|
max: 3
|
||||||
|
|
||||||
comments:
|
comments:
|
||||||
active: true
|
active: true
|
||||||
|
|
|
@ -13,10 +13,6 @@ android {
|
||||||
targetSdkVersion versions.targetSdk
|
targetSdkVersion versions.targetSdk
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
// Sets Java compatibility to Java 8
|
// Sets Java compatibility to Java 8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
|
|
@ -56,7 +56,6 @@ android {
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
freeCompilerArgs += "-Xopt-in=org.koin.core.component.KoinApiExtension"
|
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
@ -94,7 +93,6 @@ dependencies {
|
||||||
implementation other.kotlinStdlib
|
implementation other.kotlinStdlib
|
||||||
implementation other.kotlinxCoroutines
|
implementation other.kotlinxCoroutines
|
||||||
implementation other.koinAndroid
|
implementation other.koinAndroid
|
||||||
implementation other.koinViewModel
|
|
||||||
implementation other.okhttpLogging
|
implementation other.okhttpLogging
|
||||||
implementation other.fastScroll
|
implementation other.fastScroll
|
||||||
implementation other.sortListView
|
implementation other.sortListView
|
||||||
|
|
|
@ -23,72 +23,6 @@
|
||||||
column="55"/>
|
column="55"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" String lhs = lhsArtist.getName().toLowerCase();"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="97"
|
|
||||||
column="37"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" String rhs = rhsArtist.getName().toLowerCase();"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="98"
|
|
||||||
column="37"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" int index = lhs.indexOf(String.format("%s ", article.toLowerCase()));"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="115"
|
|
||||||
column="58"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" index = rhs.indexOf(String.format("%s ", article.toLowerCase()));"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="122"
|
|
||||||
column="54"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" String query = criteria.getQuery().toLowerCase();"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="466"
|
|
||||||
column="38"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" String[] nameParts = COMPILE.split(name.toLowerCase());"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="468"
|
|
||||||
column="43"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="InlinedApi"
|
id="InlinedApi"
|
||||||
message="Field requires API level 16 (current min is 14): `android.Manifest.permission#READ_EXTERNAL_STORAGE`"
|
message="Field requires API level 16 (current min is 14): `android.Manifest.permission#READ_EXTERNAL_STORAGE`"
|
||||||
|
@ -484,17 +418,6 @@
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="TrulyRandom"
|
|
||||||
message="Potentially insecure random numbers on Android 4.3 and older. Read https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html for more info."
|
|
||||||
errorLine1=" Random random = new java.security.SecureRandom();"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="633"
|
|
||||||
column="37"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="AllowAllHostnameVerifier"
|
id="AllowAllHostnameVerifier"
|
||||||
message="Using the `AllowAllHostnameVerifier` HostnameVerifier is unsafe because it always returns true, which could cause insecure network traffic due to trusting TLS/SSL server certificates for wrong hostnames"
|
message="Using the `AllowAllHostnameVerifier` HostnameVerifier is unsafe because it always returns true, which could cause insecure network traffic due to trusting TLS/SSL server certificates for wrong hostnames"
|
||||||
|
|
|
@ -162,7 +162,8 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
|
||||||
|
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING);
|
FeatureStorage features = KoinJavaComponent.get(FeatureStorage.class);
|
||||||
|
useFiveStarRating = features.isFeatureEnabled(Feature.FIVE_STAR_RATING);
|
||||||
|
|
||||||
swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
|
swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
|
||||||
swipeVelocity = swipeDistance;
|
swipeVelocity = swipeDistance;
|
||||||
|
|
|
@ -1,539 +0,0 @@
|
||||||
/*
|
|
||||||
This file is part of Subsonic.
|
|
||||||
|
|
||||||
Subsonic is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
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 2009 (C) Sindre Mehus
|
|
||||||
*/
|
|
||||||
package org.moire.ultrasonic.service;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
|
||||||
import org.moire.ultrasonic.domain.Bookmark;
|
|
||||||
import org.moire.ultrasonic.domain.ChatMessage;
|
|
||||||
import org.moire.ultrasonic.domain.Genre;
|
|
||||||
import org.moire.ultrasonic.domain.Indexes;
|
|
||||||
import org.moire.ultrasonic.domain.JukeboxStatus;
|
|
||||||
import org.moire.ultrasonic.domain.Lyrics;
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
|
||||||
import org.moire.ultrasonic.domain.MusicFolder;
|
|
||||||
import org.moire.ultrasonic.domain.Playlist;
|
|
||||||
import org.moire.ultrasonic.domain.PodcastsChannel;
|
|
||||||
import org.moire.ultrasonic.domain.SearchCriteria;
|
|
||||||
import org.moire.ultrasonic.domain.SearchResult;
|
|
||||||
import org.moire.ultrasonic.domain.Share;
|
|
||||||
import org.moire.ultrasonic.domain.UserInfo;
|
|
||||||
import org.moire.ultrasonic.util.Constants;
|
|
||||||
import org.moire.ultrasonic.util.LRUCache;
|
|
||||||
import org.moire.ultrasonic.util.TimeLimitedCache;
|
|
||||||
import org.moire.ultrasonic.util.Util;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import kotlin.Lazy;
|
|
||||||
import kotlin.Pair;
|
|
||||||
|
|
||||||
import static org.koin.java.KoinJavaComponent.inject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Sindre Mehus
|
|
||||||
*/
|
|
||||||
public class CachedMusicService implements MusicService
|
|
||||||
{
|
|
||||||
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
|
||||||
|
|
||||||
private static final int MUSIC_DIR_CACHE_SIZE = 100;
|
|
||||||
|
|
||||||
private final MusicService musicService;
|
|
||||||
private final LRUCache<String, TimeLimitedCache<MusicDirectory>> cachedMusicDirectories;
|
|
||||||
private final LRUCache<String, TimeLimitedCache<MusicDirectory>> cachedArtist;
|
|
||||||
private final LRUCache<String, TimeLimitedCache<MusicDirectory>> cachedAlbum;
|
|
||||||
private final LRUCache<String, TimeLimitedCache<UserInfo>> cachedUserInfo;
|
|
||||||
private final TimeLimitedCache<Boolean> cachedLicenseValid = new TimeLimitedCache<>(120, TimeUnit.SECONDS);
|
|
||||||
private final TimeLimitedCache<Indexes> cachedIndexes = new TimeLimitedCache<>(60 * 60, TimeUnit.SECONDS);
|
|
||||||
private final TimeLimitedCache<Indexes> cachedArtists = new TimeLimitedCache<>(60 * 60, TimeUnit.SECONDS);
|
|
||||||
private final TimeLimitedCache<List<Playlist>> cachedPlaylists = new TimeLimitedCache<>(3600, TimeUnit.SECONDS);
|
|
||||||
private final TimeLimitedCache<List<PodcastsChannel>> cachedPodcastsChannels = new TimeLimitedCache<>(3600, TimeUnit.SECONDS);
|
|
||||||
private final TimeLimitedCache<List<MusicFolder>> cachedMusicFolders = new TimeLimitedCache<>(10 * 3600, TimeUnit.SECONDS);
|
|
||||||
private final TimeLimitedCache<List<Genre>> cachedGenres = new TimeLimitedCache<>(10 * 3600, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
private String restUrl;
|
|
||||||
private String cachedMusicFolderId;
|
|
||||||
|
|
||||||
public CachedMusicService(MusicService musicService)
|
|
||||||
{
|
|
||||||
this.musicService = musicService;
|
|
||||||
cachedMusicDirectories = new LRUCache<>(MUSIC_DIR_CACHE_SIZE);
|
|
||||||
cachedArtist = new LRUCache<>(MUSIC_DIR_CACHE_SIZE);
|
|
||||||
cachedAlbum = new LRUCache<>(MUSIC_DIR_CACHE_SIZE);
|
|
||||||
cachedUserInfo = new LRUCache<>(MUSIC_DIR_CACHE_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void ping() throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
musicService.ping();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLicenseValid() throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
Boolean result = cachedLicenseValid.get();
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
result = musicService.isLicenseValid();
|
|
||||||
cachedLicenseValid.set(result, result ? 30L * 60L : 2L * 60L, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<MusicFolder> getMusicFolders(boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
if (refresh)
|
|
||||||
{
|
|
||||||
cachedMusicFolders.clear();
|
|
||||||
}
|
|
||||||
List<MusicFolder> result = cachedMusicFolders.get();
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
result = musicService.getMusicFolders(refresh);
|
|
||||||
cachedMusicFolders.set(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Indexes getIndexes(String musicFolderId, boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
if (refresh)
|
|
||||||
{
|
|
||||||
cachedIndexes.clear();
|
|
||||||
cachedMusicFolders.clear();
|
|
||||||
cachedMusicDirectories.clear();
|
|
||||||
}
|
|
||||||
Indexes result = cachedIndexes.get();
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
result = musicService.getIndexes(musicFolderId, refresh);
|
|
||||||
cachedIndexes.set(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Indexes getArtists(boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
if (refresh)
|
|
||||||
{
|
|
||||||
cachedArtists.clear();
|
|
||||||
}
|
|
||||||
Indexes result = cachedArtists.get();
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
result = musicService.getArtists(refresh);
|
|
||||||
cachedArtists.set(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getMusicDirectory(String id, String name, boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedMusicDirectories.get(id);
|
|
||||||
|
|
||||||
MusicDirectory dir = cache == null ? null : cache.get();
|
|
||||||
|
|
||||||
if (dir == null)
|
|
||||||
{
|
|
||||||
dir = musicService.getMusicDirectory(id, name, refresh);
|
|
||||||
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(), TimeUnit.SECONDS);
|
|
||||||
cache.set(dir);
|
|
||||||
cachedMusicDirectories.put(id, cache);
|
|
||||||
}
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getArtist(String id, String name, boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedArtist.get(id);
|
|
||||||
MusicDirectory dir = cache == null ? null : cache.get();
|
|
||||||
if (dir == null)
|
|
||||||
{
|
|
||||||
dir = musicService.getArtist(id, name, refresh);
|
|
||||||
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(), TimeUnit.SECONDS);
|
|
||||||
cache.set(dir);
|
|
||||||
cachedArtist.put(id, cache);
|
|
||||||
}
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getAlbum(String id, String name, boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedAlbum.get(id);
|
|
||||||
MusicDirectory dir = cache == null ? null : cache.get();
|
|
||||||
if (dir == null)
|
|
||||||
{
|
|
||||||
dir = musicService.getAlbum(id, name, refresh);
|
|
||||||
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(), TimeUnit.SECONDS);
|
|
||||||
cache.set(dir);
|
|
||||||
cachedAlbum.put(id, cache);
|
|
||||||
}
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SearchResult search(SearchCriteria criteria) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.search(criteria);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getPlaylist(String id, String name) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getPlaylist(id, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<PodcastsChannel> getPodcastsChannels(boolean refresh) throws Exception {
|
|
||||||
checkSettingsChanged();
|
|
||||||
List<PodcastsChannel> result = refresh ? null : cachedPodcastsChannels.get();
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
result = musicService.getPodcastsChannels(refresh);
|
|
||||||
cachedPodcastsChannels.set(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getPodcastEpisodes(String podcastChannelId) throws Exception {
|
|
||||||
return musicService.getPodcastEpisodes(podcastChannelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Playlist> getPlaylists(boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
List<Playlist> result = refresh ? null : cachedPlaylists.get();
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
result = musicService.getPlaylists(refresh);
|
|
||||||
cachedPlaylists.set(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries) throws Exception
|
|
||||||
{
|
|
||||||
cachedPlaylists.clear();
|
|
||||||
musicService.createPlaylist(id, name, entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deletePlaylist(String id) throws Exception
|
|
||||||
{
|
|
||||||
musicService.deletePlaylist(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updatePlaylist(String id, String name, String comment, boolean pub) throws Exception
|
|
||||||
{
|
|
||||||
musicService.updatePlaylist(id, name, comment, pub);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Lyrics getLyrics(String artist, String title) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getLyrics(artist, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void scrobble(String id, boolean submission) throws Exception
|
|
||||||
{
|
|
||||||
musicService.scrobble(id, submission);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getAlbumList(String type, int size, int offset, String musicFolderId) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getAlbumList(type, size, offset, musicFolderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getAlbumList2(String type, int size, int offset, String musicFolderId) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getAlbumList2(type, size, offset, musicFolderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getRandomSongs(int size) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getRandomSongs(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SearchResult getStarred() throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getStarred();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SearchResult getStarred2() throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getStarred2();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bitmap getCoverArt(MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getCoverArt(entry, size, saveToFile, highQuality);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Pair<InputStream, Boolean> getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getDownloadInputStream(song, offset, maxBitrate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getVideoUrl(String id, boolean useFlash) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getVideoUrl(id, useFlash);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus updateJukeboxPlaylist(List<String> ids) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.updateJukeboxPlaylist(ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus skipJukebox(int index, int offsetSeconds) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.skipJukebox(index, offsetSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus stopJukebox() throws Exception
|
|
||||||
{
|
|
||||||
return musicService.stopJukebox();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus startJukebox() throws Exception
|
|
||||||
{
|
|
||||||
return musicService.startJukebox();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus getJukeboxStatus() throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getJukeboxStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus setJukeboxGain(float gain) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.setJukeboxGain(gain);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkSettingsChanged()
|
|
||||||
{
|
|
||||||
String newUrl = activeServerProvider.getValue().getRestUrl(null);
|
|
||||||
String newFolderId = activeServerProvider.getValue().getActiveServer().getMusicFolderId();
|
|
||||||
if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId,newFolderId))
|
|
||||||
{
|
|
||||||
cachedMusicFolders.clear();
|
|
||||||
cachedMusicDirectories.clear();
|
|
||||||
cachedLicenseValid.clear();
|
|
||||||
cachedIndexes.clear();
|
|
||||||
cachedPlaylists.clear();
|
|
||||||
cachedGenres.clear();
|
|
||||||
cachedAlbum.clear();
|
|
||||||
cachedArtist.clear();
|
|
||||||
cachedUserInfo.clear();
|
|
||||||
restUrl = newUrl;
|
|
||||||
cachedMusicFolderId = newFolderId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void star(String id, String albumId, String artistId) throws Exception
|
|
||||||
{
|
|
||||||
musicService.star(id, albumId, artistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unstar(String id, String albumId, String artistId) throws Exception
|
|
||||||
{
|
|
||||||
musicService.unstar(id, albumId, artistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setRating(String id, int rating) throws Exception
|
|
||||||
{
|
|
||||||
musicService.setRating(id, rating);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Genre> getGenres(boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
if (refresh)
|
|
||||||
{
|
|
||||||
cachedGenres.clear();
|
|
||||||
}
|
|
||||||
List<Genre> result = cachedGenres.get();
|
|
||||||
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
result = musicService.getGenres(refresh);
|
|
||||||
cachedGenres.set(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
Collections.sort(result, new Comparator<Genre>()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public int compare(Genre genre, Genre genre2)
|
|
||||||
{
|
|
||||||
return genre.getName().compareToIgnoreCase(genre2.getName());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getSongsByGenre(String genre, int count, int offset) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getSongsByGenre(genre, count, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Share> getShares(boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getShares(refresh);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ChatMessage> getChatMessages(Long since) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getChatMessages(since);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addChatMessage(String message) throws Exception
|
|
||||||
{
|
|
||||||
musicService.addChatMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Bookmark> getBookmarks() throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getBookmarks();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteBookmark(String id) throws Exception
|
|
||||||
{
|
|
||||||
musicService.deleteBookmark(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void createBookmark(String id, int position) throws Exception
|
|
||||||
{
|
|
||||||
musicService.createBookmark(id, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getVideos(boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
TimeLimitedCache<MusicDirectory> cache = refresh ? null : cachedMusicDirectories.get(Constants.INTENT_EXTRA_NAME_VIDEOS);
|
|
||||||
|
|
||||||
MusicDirectory dir = cache == null ? null : cache.get();
|
|
||||||
|
|
||||||
if (dir == null)
|
|
||||||
{
|
|
||||||
dir = musicService.getVideos(refresh);
|
|
||||||
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(), TimeUnit.SECONDS);
|
|
||||||
cache.set(dir);
|
|
||||||
cachedMusicDirectories.put(Constants.INTENT_EXTRA_NAME_VIDEOS, cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserInfo getUser(String username) throws Exception
|
|
||||||
{
|
|
||||||
checkSettingsChanged();
|
|
||||||
|
|
||||||
TimeLimitedCache<UserInfo> cache = cachedUserInfo.get(username);
|
|
||||||
|
|
||||||
UserInfo userInfo = cache == null ? null : cache.get();
|
|
||||||
|
|
||||||
if (userInfo == null)
|
|
||||||
{
|
|
||||||
userInfo = musicService.getUser(username);
|
|
||||||
cache = new TimeLimitedCache<>(Util.getDirectoryCacheTime(), TimeUnit.SECONDS);
|
|
||||||
cache.set(userInfo);
|
|
||||||
cachedUserInfo.put(username, cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
return userInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Share> createShare(List<String> ids, String description, Long expires) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.createShare(ids, description, expires);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteShare(String id) throws Exception
|
|
||||||
{
|
|
||||||
musicService.deleteShare(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateShare(String id, String description, Long expires) throws Exception
|
|
||||||
{
|
|
||||||
musicService.updateShare(id, description, expires);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bitmap getAvatar(String username, int size, boolean saveToFile, boolean highQuality) throws Exception
|
|
||||||
{
|
|
||||||
return musicService.getAvatar(username, size, saveToFile, highQuality);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
/*
|
|
||||||
This file is part of Subsonic.
|
|
||||||
|
|
||||||
Subsonic is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
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 2009 (C) Sindre Mehus
|
|
||||||
*/
|
|
||||||
package org.moire.ultrasonic.service;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.domain.Bookmark;
|
|
||||||
import org.moire.ultrasonic.domain.ChatMessage;
|
|
||||||
import org.moire.ultrasonic.domain.Genre;
|
|
||||||
import org.moire.ultrasonic.domain.Indexes;
|
|
||||||
import org.moire.ultrasonic.domain.JukeboxStatus;
|
|
||||||
import org.moire.ultrasonic.domain.Lyrics;
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
|
||||||
import org.moire.ultrasonic.domain.MusicFolder;
|
|
||||||
import org.moire.ultrasonic.domain.Playlist;
|
|
||||||
import org.moire.ultrasonic.domain.PodcastsChannel;
|
|
||||||
import org.moire.ultrasonic.domain.SearchCriteria;
|
|
||||||
import org.moire.ultrasonic.domain.SearchResult;
|
|
||||||
import org.moire.ultrasonic.domain.Share;
|
|
||||||
import org.moire.ultrasonic.domain.UserInfo;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import kotlin.Pair;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Sindre Mehus
|
|
||||||
*/
|
|
||||||
public interface MusicService
|
|
||||||
{
|
|
||||||
|
|
||||||
void ping() throws Exception;
|
|
||||||
|
|
||||||
boolean isLicenseValid() throws Exception;
|
|
||||||
|
|
||||||
List<Genre> getGenres(boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
void star(String id, String albumId, String artistId) throws Exception;
|
|
||||||
|
|
||||||
void unstar(String id, String albumId, String artistId) throws Exception;
|
|
||||||
|
|
||||||
void setRating(String id, int rating) throws Exception;
|
|
||||||
|
|
||||||
List<MusicFolder> getMusicFolders(boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
Indexes getIndexes(String musicFolderId, boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
Indexes getArtists(boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getMusicDirectory(String id, String name, boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getArtist(String id, String name, boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getAlbum(String id, String name, boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
SearchResult search(SearchCriteria criteria) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getPlaylist(String id, String name) throws Exception;
|
|
||||||
|
|
||||||
List<PodcastsChannel> getPodcastsChannels(boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
List<Playlist> getPlaylists(boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries) throws Exception;
|
|
||||||
|
|
||||||
void deletePlaylist(String id) throws Exception;
|
|
||||||
|
|
||||||
void updatePlaylist(String id, String name, String comment, boolean pub) throws Exception;
|
|
||||||
|
|
||||||
Lyrics getLyrics(String artist, String title) throws Exception;
|
|
||||||
|
|
||||||
void scrobble(String id, boolean submission) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getAlbumList(String type, int size, int offset, String musicFolderId) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getAlbumList2(String type, int size, int offset, String musicFolderId) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getRandomSongs(int size) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getSongsByGenre(String genre, int count, int offset) throws Exception;
|
|
||||||
|
|
||||||
SearchResult getStarred() throws Exception;
|
|
||||||
|
|
||||||
SearchResult getStarred2() throws Exception;
|
|
||||||
|
|
||||||
Bitmap getCoverArt(MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality) throws Exception;
|
|
||||||
|
|
||||||
Bitmap getAvatar(String username, int size, boolean saveToFile, boolean highQuality) throws Exception;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return response {@link InputStream} and a {@link Boolean} that indicates if this response is
|
|
||||||
* partial.
|
|
||||||
*/
|
|
||||||
Pair<InputStream, Boolean> getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) throws Exception;
|
|
||||||
|
|
||||||
// TODO: Refactor and remove this call (see RestMusicService implementation)
|
|
||||||
String getVideoUrl(String id, boolean useFlash) throws Exception;
|
|
||||||
|
|
||||||
JukeboxStatus updateJukeboxPlaylist(List<String> ids) throws Exception;
|
|
||||||
|
|
||||||
JukeboxStatus skipJukebox(int index, int offsetSeconds) throws Exception;
|
|
||||||
|
|
||||||
JukeboxStatus stopJukebox() throws Exception;
|
|
||||||
|
|
||||||
JukeboxStatus startJukebox() throws Exception;
|
|
||||||
|
|
||||||
JukeboxStatus getJukeboxStatus() throws Exception;
|
|
||||||
|
|
||||||
JukeboxStatus setJukeboxGain(float gain) throws Exception;
|
|
||||||
|
|
||||||
List<Share> getShares(boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
List<ChatMessage> getChatMessages(Long since) throws Exception;
|
|
||||||
|
|
||||||
void addChatMessage(String message) throws Exception;
|
|
||||||
|
|
||||||
List<Bookmark> getBookmarks() throws Exception;
|
|
||||||
|
|
||||||
void deleteBookmark(String id) throws Exception;
|
|
||||||
|
|
||||||
void createBookmark(String id, int position) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getVideos(boolean refresh) throws Exception;
|
|
||||||
|
|
||||||
UserInfo getUser(String username) throws Exception;
|
|
||||||
|
|
||||||
List<Share> createShare(List<String> ids, String description, Long expires) throws Exception;
|
|
||||||
|
|
||||||
void deleteShare(String id) throws Exception;
|
|
||||||
|
|
||||||
void updateShare(String id, String description, Long expires) throws Exception;
|
|
||||||
|
|
||||||
MusicDirectory getPodcastEpisodes(String podcastChannelId) throws Exception;
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
This file is part of Subsonic.
|
|
||||||
|
|
||||||
Subsonic is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
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 2009 (C) Sindre Mehus
|
|
||||||
*/
|
|
||||||
package org.moire.ultrasonic.service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thrown by service methods that are not available in offline mode.
|
|
||||||
*
|
|
||||||
* @author Sindre Mehus
|
|
||||||
* @version $Id$
|
|
||||||
*/
|
|
||||||
public class OfflineException extends Exception
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = -4479642294747429444L;
|
|
||||||
|
|
||||||
public OfflineException(String message)
|
|
||||||
{
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,889 +0,0 @@
|
||||||
/*
|
|
||||||
This file is part of Subsonic.
|
|
||||||
|
|
||||||
Subsonic is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
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 2009 (C) Sindre Mehus
|
|
||||||
*/
|
|
||||||
package org.moire.ultrasonic.service;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.media.MediaMetadataRetriever;
|
|
||||||
|
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider;
|
|
||||||
import org.moire.ultrasonic.domain.Artist;
|
|
||||||
import org.moire.ultrasonic.domain.Bookmark;
|
|
||||||
import org.moire.ultrasonic.domain.ChatMessage;
|
|
||||||
import org.moire.ultrasonic.domain.Genre;
|
|
||||||
import org.moire.ultrasonic.domain.Indexes;
|
|
||||||
import org.moire.ultrasonic.domain.JukeboxStatus;
|
|
||||||
import org.moire.ultrasonic.domain.Lyrics;
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
|
||||||
import org.moire.ultrasonic.domain.MusicFolder;
|
|
||||||
import org.moire.ultrasonic.domain.Playlist;
|
|
||||||
import org.moire.ultrasonic.domain.PodcastsChannel;
|
|
||||||
import org.moire.ultrasonic.domain.SearchCriteria;
|
|
||||||
import org.moire.ultrasonic.domain.SearchResult;
|
|
||||||
import org.moire.ultrasonic.domain.Share;
|
|
||||||
import org.moire.ultrasonic.domain.UserInfo;
|
|
||||||
import org.moire.ultrasonic.util.Constants;
|
|
||||||
import org.moire.ultrasonic.util.FileUtil;
|
|
||||||
import org.moire.ultrasonic.util.Util;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.FileWriter;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.SortedSet;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import kotlin.Lazy;
|
|
||||||
import kotlin.Pair;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static org.koin.java.KoinJavaComponent.inject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Sindre Mehus
|
|
||||||
*/
|
|
||||||
public class OfflineMusicService implements MusicService
|
|
||||||
{
|
|
||||||
private static final Pattern COMPILE = Pattern.compile(" ");
|
|
||||||
private final Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Indexes getIndexes(String musicFolderId, boolean refresh)
|
|
||||||
{
|
|
||||||
List<Artist> artists = new ArrayList<>();
|
|
||||||
File root = FileUtil.getMusicDirectory();
|
|
||||||
for (File file : FileUtil.listFiles(root))
|
|
||||||
{
|
|
||||||
if (file.isDirectory())
|
|
||||||
{
|
|
||||||
Artist artist = new Artist();
|
|
||||||
artist.setId(file.getPath());
|
|
||||||
artist.setIndex(file.getName().substring(0, 1));
|
|
||||||
artist.setName(file.getName());
|
|
||||||
artists.add(artist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String ignoredArticlesString = "The El La Los Las Le Les";
|
|
||||||
final String[] ignoredArticles = COMPILE.split(ignoredArticlesString);
|
|
||||||
|
|
||||||
Collections.sort(artists, (lhsArtist, rhsArtist) -> {
|
|
||||||
String lhs = lhsArtist.getName().toLowerCase();
|
|
||||||
String rhs = rhsArtist.getName().toLowerCase();
|
|
||||||
|
|
||||||
char lhs1 = lhs.charAt(0);
|
|
||||||
char rhs1 = rhs.charAt(0);
|
|
||||||
|
|
||||||
if (Character.isDigit(lhs1) && !Character.isDigit(rhs1))
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Character.isDigit(rhs1) && !Character.isDigit(lhs1))
|
|
||||||
{
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String article : ignoredArticles)
|
|
||||||
{
|
|
||||||
int index = lhs.indexOf(String.format("%s ", article.toLowerCase()));
|
|
||||||
|
|
||||||
if (index == 0)
|
|
||||||
{
|
|
||||||
lhs = lhs.substring(article.length() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
index = rhs.indexOf(String.format("%s ", article.toLowerCase()));
|
|
||||||
|
|
||||||
if (index == 0)
|
|
||||||
{
|
|
||||||
rhs = rhs.substring(article.length() + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lhs.compareTo(rhs);
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Indexes(0L, ignoredArticlesString, Collections.emptyList(), artists);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh)
|
|
||||||
{
|
|
||||||
File dir = new File(id);
|
|
||||||
MusicDirectory result = new MusicDirectory();
|
|
||||||
result.setName(dir.getName());
|
|
||||||
|
|
||||||
Collection<String> names = new HashSet<>();
|
|
||||||
|
|
||||||
for (File file : FileUtil.listMediaFiles(dir))
|
|
||||||
{
|
|
||||||
String name = getName(file);
|
|
||||||
if (name != null & !names.contains(name))
|
|
||||||
{
|
|
||||||
names.add(name);
|
|
||||||
result.addChild(createEntry(file, name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getName(File file)
|
|
||||||
{
|
|
||||||
String name = file.getName();
|
|
||||||
|
|
||||||
if (file.isDirectory())
|
|
||||||
{
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
name = name.replace(".complete", "");
|
|
||||||
return FileUtil.getBaseName(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MusicDirectory.Entry createEntry(File file, String name)
|
|
||||||
{
|
|
||||||
MusicDirectory.Entry entry = new MusicDirectory.Entry();
|
|
||||||
entry.setDirectory(file.isDirectory());
|
|
||||||
entry.setId(file.getPath());
|
|
||||||
entry.setParent(file.getParent());
|
|
||||||
entry.setSize(file.length());
|
|
||||||
String root = FileUtil.getMusicDirectory().getPath();
|
|
||||||
entry.setPath(file.getPath().replaceFirst(String.format("^%s/", root), ""));
|
|
||||||
entry.setTitle(name);
|
|
||||||
|
|
||||||
if (file.isFile())
|
|
||||||
{
|
|
||||||
String artist = null;
|
|
||||||
String album = null;
|
|
||||||
String title = null;
|
|
||||||
String track = null;
|
|
||||||
String disc = null;
|
|
||||||
String year = null;
|
|
||||||
String genre = null;
|
|
||||||
String duration = null;
|
|
||||||
String hasVideo = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
|
|
||||||
mmr.setDataSource(file.getPath());
|
|
||||||
artist = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
|
|
||||||
album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
|
|
||||||
title = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
|
|
||||||
track = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER);
|
|
||||||
disc = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER);
|
|
||||||
year = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR);
|
|
||||||
genre = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE);
|
|
||||||
duration = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
|
||||||
hasVideo = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
|
|
||||||
mmr.release();
|
|
||||||
}
|
|
||||||
catch (Exception ignored)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.setArtist(artist != null ? artist : file.getParentFile().getParentFile().getName());
|
|
||||||
entry.setAlbum(album != null ? album : file.getParentFile().getName());
|
|
||||||
|
|
||||||
if (title != null)
|
|
||||||
{
|
|
||||||
entry.setTitle(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.setVideo(hasVideo != null);
|
|
||||||
|
|
||||||
Timber.i("Offline Stuff: %s", track);
|
|
||||||
|
|
||||||
if (track != null)
|
|
||||||
{
|
|
||||||
|
|
||||||
int trackValue = 0;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int slashIndex = track.indexOf('/');
|
|
||||||
|
|
||||||
if (slashIndex > 0)
|
|
||||||
{
|
|
||||||
track = track.substring(0, slashIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackValue = Integer.parseInt(track);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Timber.e(ex,"Offline Stuff");
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.i("Offline Stuff: Setting Track: %d", trackValue);
|
|
||||||
|
|
||||||
entry.setTrack(trackValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disc != null)
|
|
||||||
{
|
|
||||||
int discValue = 0;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int slashIndex = disc.indexOf('/');
|
|
||||||
|
|
||||||
if (slashIndex > 0)
|
|
||||||
{
|
|
||||||
disc = disc.substring(0, slashIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
discValue = Integer.parseInt(disc);
|
|
||||||
}
|
|
||||||
catch (Exception ignored)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.setDiscNumber(discValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (year != null)
|
|
||||||
{
|
|
||||||
int yearValue = 0;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
yearValue = Integer.parseInt(year);
|
|
||||||
}
|
|
||||||
catch (Exception ignored)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.setYear(yearValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (genre != null)
|
|
||||||
{
|
|
||||||
entry.setGenre(genre);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration != null)
|
|
||||||
{
|
|
||||||
long durationValue = 0;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
durationValue = Long.parseLong(duration);
|
|
||||||
durationValue = TimeUnit.MILLISECONDS.toSeconds(durationValue);
|
|
||||||
}
|
|
||||||
catch (Exception ignored)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.setDuration(durationValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", "")));
|
|
||||||
|
|
||||||
File albumArt = FileUtil.getAlbumArtFile(entry);
|
|
||||||
|
|
||||||
if (albumArt.exists())
|
|
||||||
{
|
|
||||||
entry.setCoverArt(albumArt.getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bitmap getAvatar(String username, int size, boolean saveToFile, boolean highQuality)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Bitmap bitmap = FileUtil.getAvatarBitmap(username, size, highQuality);
|
|
||||||
return Util.scaleBitmap(bitmap, size);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Bitmap getCoverArt(MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Bitmap bitmap = FileUtil.getAlbumArtBitmap(entry, size, highQuality);
|
|
||||||
return Util.scaleBitmap(bitmap, size);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SearchResult search(SearchCriteria criteria)
|
|
||||||
{
|
|
||||||
List<Artist> artists = new ArrayList<>();
|
|
||||||
List<MusicDirectory.Entry> albums = new ArrayList<>();
|
|
||||||
List<MusicDirectory.Entry> songs = new ArrayList<>();
|
|
||||||
File root = FileUtil.getMusicDirectory();
|
|
||||||
int closeness;
|
|
||||||
|
|
||||||
for (File artistFile : FileUtil.listFiles(root))
|
|
||||||
{
|
|
||||||
String artistName = artistFile.getName();
|
|
||||||
if (artistFile.isDirectory())
|
|
||||||
{
|
|
||||||
if ((closeness = matchCriteria(criteria, artistName)) > 0)
|
|
||||||
{
|
|
||||||
Artist artist = new Artist();
|
|
||||||
artist.setId(artistFile.getPath());
|
|
||||||
artist.setIndex(artistFile.getName().substring(0, 1));
|
|
||||||
artist.setName(artistName);
|
|
||||||
artist.setCloseness(closeness);
|
|
||||||
artists.add(artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
recursiveAlbumSearch(artistName, artistFile, criteria, albums, songs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Collections.sort(artists, (lhs, rhs) -> {
|
|
||||||
if (lhs.getCloseness() == rhs.getCloseness())
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
else return lhs.getCloseness() > rhs.getCloseness() ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
Collections.sort(albums, (lhs, rhs) -> {
|
|
||||||
if (lhs.getCloseness() == rhs.getCloseness())
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
else return lhs.getCloseness() > rhs.getCloseness() ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
Collections.sort(songs, (lhs, rhs) -> {
|
|
||||||
if (lhs.getCloseness() == rhs.getCloseness())
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
else return lhs.getCloseness() > rhs.getCloseness() ? -1 : 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new SearchResult(artists, albums, songs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void recursiveAlbumSearch(String artistName, File file, SearchCriteria criteria, List<MusicDirectory.Entry> albums, List<MusicDirectory.Entry> songs)
|
|
||||||
{
|
|
||||||
int closeness;
|
|
||||||
|
|
||||||
for (File albumFile : FileUtil.listMediaFiles(file))
|
|
||||||
{
|
|
||||||
if (albumFile.isDirectory())
|
|
||||||
{
|
|
||||||
String albumName = getName(albumFile);
|
|
||||||
if ((closeness = matchCriteria(criteria, albumName)) > 0)
|
|
||||||
{
|
|
||||||
MusicDirectory.Entry album = createEntry(albumFile, albumName);
|
|
||||||
album.setArtist(artistName);
|
|
||||||
album.setCloseness(closeness);
|
|
||||||
albums.add(album);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (File songFile : FileUtil.listMediaFiles(albumFile))
|
|
||||||
{
|
|
||||||
String songName = getName(songFile);
|
|
||||||
|
|
||||||
if (songFile.isDirectory())
|
|
||||||
{
|
|
||||||
recursiveAlbumSearch(artistName, songFile, criteria, albums, songs);
|
|
||||||
}
|
|
||||||
else if ((closeness = matchCriteria(criteria, songName)) > 0)
|
|
||||||
{
|
|
||||||
MusicDirectory.Entry song = createEntry(albumFile, songName);
|
|
||||||
song.setArtist(artistName);
|
|
||||||
song.setAlbum(albumName);
|
|
||||||
song.setCloseness(closeness);
|
|
||||||
songs.add(song);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
String songName = getName(albumFile);
|
|
||||||
|
|
||||||
if ((closeness = matchCriteria(criteria, songName)) > 0)
|
|
||||||
{
|
|
||||||
MusicDirectory.Entry song = createEntry(albumFile, songName);
|
|
||||||
song.setArtist(artistName);
|
|
||||||
song.setAlbum(songName);
|
|
||||||
song.setCloseness(closeness);
|
|
||||||
songs.add(song);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int matchCriteria(SearchCriteria criteria, String name)
|
|
||||||
{
|
|
||||||
String query = criteria.getQuery().toLowerCase();
|
|
||||||
String[] queryParts = COMPILE.split(query);
|
|
||||||
String[] nameParts = COMPILE.split(name.toLowerCase());
|
|
||||||
|
|
||||||
int closeness = 0;
|
|
||||||
|
|
||||||
for (String queryPart : queryParts)
|
|
||||||
{
|
|
||||||
for (String namePart : nameParts)
|
|
||||||
{
|
|
||||||
if (namePart.equals(queryPart))
|
|
||||||
{
|
|
||||||
closeness++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return closeness;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Playlist> getPlaylists(boolean refresh)
|
|
||||||
{
|
|
||||||
List<Playlist> playlists = new ArrayList<>();
|
|
||||||
File root = FileUtil.getPlaylistDirectory();
|
|
||||||
String lastServer = null;
|
|
||||||
boolean removeServer = true;
|
|
||||||
for (File folder : FileUtil.listFiles(root))
|
|
||||||
{
|
|
||||||
if (folder.isDirectory())
|
|
||||||
{
|
|
||||||
String server = folder.getName();
|
|
||||||
SortedSet<File> fileList = FileUtil.listFiles(folder);
|
|
||||||
for (File file : fileList)
|
|
||||||
{
|
|
||||||
if (FileUtil.isPlaylistFile(file))
|
|
||||||
{
|
|
||||||
String id = file.getName();
|
|
||||||
String filename = server + ": " + FileUtil.getBaseName(id);
|
|
||||||
Playlist playlist = new Playlist(server, filename);
|
|
||||||
playlists.add(playlist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!server.equals(lastServer) && !fileList.isEmpty())
|
|
||||||
{
|
|
||||||
if (lastServer != null)
|
|
||||||
{
|
|
||||||
removeServer = false;
|
|
||||||
}
|
|
||||||
lastServer = server;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Delete legacy playlist files
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!folder.delete()) {
|
|
||||||
Timber.w("Failed to delete old playlist file: %s", folder.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Timber.w(e, "Failed to delete old playlist file: %s", folder.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removeServer)
|
|
||||||
{
|
|
||||||
for (Playlist playlist : playlists)
|
|
||||||
{
|
|
||||||
playlist.setName(playlist.getName().substring(playlist.getId().length() + 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return playlists;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getPlaylist(String id, String name) throws Exception
|
|
||||||
{
|
|
||||||
Reader reader = null;
|
|
||||||
BufferedReader buffer = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int firstIndex = name.indexOf(id);
|
|
||||||
|
|
||||||
if (firstIndex != -1)
|
|
||||||
{
|
|
||||||
name = name.substring(id.length() + 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
File playlistFile = FileUtil.getPlaylistFile(id, name);
|
|
||||||
reader = new FileReader(playlistFile);
|
|
||||||
buffer = new BufferedReader(reader);
|
|
||||||
|
|
||||||
MusicDirectory playlist = new MusicDirectory();
|
|
||||||
String line = buffer.readLine();
|
|
||||||
if (!"#EXTM3U".equals(line)) return playlist;
|
|
||||||
|
|
||||||
while ((line = buffer.readLine()) != null)
|
|
||||||
{
|
|
||||||
File entryFile = new File(line);
|
|
||||||
String entryName = getName(entryFile);
|
|
||||||
|
|
||||||
if (entryFile.exists() && entryName != null)
|
|
||||||
{
|
|
||||||
playlist.addChild(createEntry(entryFile, entryName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return playlist;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Util.close(buffer);
|
|
||||||
Util.close(reader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void createPlaylist(String id, String name, List<MusicDirectory.Entry> entries) throws Exception
|
|
||||||
{
|
|
||||||
File playlistFile = FileUtil.getPlaylistFile(activeServerProvider.getValue().getActiveServer().getName(), name);
|
|
||||||
FileWriter fw = new FileWriter(playlistFile);
|
|
||||||
BufferedWriter bw = new BufferedWriter(fw);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
fw.write("#EXTM3U\n");
|
|
||||||
for (MusicDirectory.Entry e : entries)
|
|
||||||
{
|
|
||||||
String filePath = FileUtil.getSongFile(e).getAbsolutePath();
|
|
||||||
if (!new File(filePath).exists())
|
|
||||||
{
|
|
||||||
String ext = FileUtil.getExtension(filePath);
|
|
||||||
String base = FileUtil.getBaseName(filePath);
|
|
||||||
filePath = base + ".complete." + ext;
|
|
||||||
}
|
|
||||||
fw.write(filePath + '\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Timber.w("Failed to save playlist: %s", name);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
bw.close();
|
|
||||||
fw.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getRandomSongs(int size)
|
|
||||||
{
|
|
||||||
File root = FileUtil.getMusicDirectory();
|
|
||||||
List<File> children = new LinkedList<>();
|
|
||||||
listFilesRecursively(root, children);
|
|
||||||
MusicDirectory result = new MusicDirectory();
|
|
||||||
|
|
||||||
if (children.isEmpty())
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Random random = new java.security.SecureRandom();
|
|
||||||
for (int i = 0; i < size; i++)
|
|
||||||
{
|
|
||||||
File file = children.get(random.nextInt(children.size()));
|
|
||||||
result.addChild(createEntry(file, getName(file)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void listFilesRecursively(File parent, List<File> children)
|
|
||||||
{
|
|
||||||
for (File file : FileUtil.listMediaFiles(parent))
|
|
||||||
{
|
|
||||||
if (file.isFile())
|
|
||||||
{
|
|
||||||
children.add(file);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
listFilesRecursively(file, children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deletePlaylist(String id) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Playlists not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updatePlaylist(String id, String name, String comment, boolean pub) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Updating playlist not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Lyrics getLyrics(String artist, String title) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Lyrics not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void scrobble(String id, boolean submission) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Scrobbling not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getAlbumList(String type, int size, int offset, String musicFolderId) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Album lists not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus updateJukeboxPlaylist(List<String> ids) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Jukebox not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus skipJukebox(int index, int offsetSeconds) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Jukebox not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus stopJukebox() throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Jukebox not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus startJukebox() throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Jukebox not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus getJukeboxStatus() throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Jukebox not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public JukeboxStatus setJukeboxGain(float gain) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Jukebox not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SearchResult getStarred() throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Starred not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getSongsByGenre(String genre, int count, int offset) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Getting Songs By Genre not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Genre> getGenres(boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Getting Genres not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public UserInfo getUser(String username) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Getting user info not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Share> createShare(List<String> ids, String description, Long expires) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Creating shares not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Share> getShares(boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Getting shares not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteShare(String id) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Deleting shares not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateShare(String id, String description, Long expires) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Updating shares not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void star(String id, String albumId, String artistId) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Star not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unstar(String id, String albumId, String artistId) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("UnStar not available in offline mode");
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public List<MusicFolder> getMusicFolders(boolean refresh) throws Exception
|
|
||||||
{
|
|
||||||
throw new OfflineException("Music folders not available in offline mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getAlbumList2(String type, int size, int offset, String musicFolderId) {
|
|
||||||
Timber.w("OfflineMusicService.getAlbumList2 was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getVideoUrl(String id, boolean useFlash) {
|
|
||||||
Timber.w("OfflineMusicService.getVideoUrl was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<ChatMessage> getChatMessages(Long since) {
|
|
||||||
Timber.w("OfflineMusicService.getChatMessages was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addChatMessage(String message) {
|
|
||||||
Timber.w("OfflineMusicService.addChatMessage was called but it isn't available");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Bookmark> getBookmarks() {
|
|
||||||
Timber.w("OfflineMusicService.getBookmarks was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteBookmark(String id) {
|
|
||||||
Timber.w("OfflineMusicService.deleteBookmark was called but it isn't available");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void createBookmark(String id, int position) {
|
|
||||||
Timber.w("OfflineMusicService.createBookmark was called but it isn't available");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getVideos(boolean refresh) {
|
|
||||||
Timber.w("OfflineMusicService.getVideos was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SearchResult getStarred2() {
|
|
||||||
Timber.w("OfflineMusicService.getStarred2 was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void ping() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLicenseValid() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Indexes getArtists(boolean refresh) {
|
|
||||||
Timber.w("OfflineMusicService.getArtists was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getArtist(String id, String name, boolean refresh) {
|
|
||||||
Timber.w("OfflineMusicService.getArtist was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getAlbum(String id, String name, boolean refresh) {
|
|
||||||
Timber.w("OfflineMusicService.getAlbum was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MusicDirectory getPodcastEpisodes(String podcastChannelId) {
|
|
||||||
Timber.w("OfflineMusicService.getPodcastEpisodes was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Pair<InputStream, Boolean> getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) {
|
|
||||||
Timber.w("OfflineMusicService.getDownloadInputStream was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setRating(String id, int rating) {
|
|
||||||
Timber.w("OfflineMusicService.setRating was called but it isn't available");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<PodcastsChannel> getPodcastsChannels(boolean refresh) {
|
|
||||||
Timber.w("OfflineMusicService.getPodcastsChannels was called but it isn't available");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,7 +23,7 @@ import kotlin.Lazy;
|
||||||
import static org.koin.java.KoinJavaComponent.inject;
|
import static org.koin.java.KoinJavaComponent.inject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for cleaning up files from the offline download cache on the filesystem
|
* Responsible for cleaning up files from the offline download cache on the filesystem.
|
||||||
*/
|
*/
|
||||||
public class CacheCleaner
|
public class CacheCleaner
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
This file is part of Subsonic.
|
|
||||||
|
|
||||||
Subsonic is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
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 2009 (C) Sindre Mehus
|
|
||||||
*/
|
|
||||||
package org.moire.ultrasonic.util;
|
|
||||||
|
|
||||||
import java.lang.ref.SoftReference;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Sindre Mehus
|
|
||||||
* @version $Id$
|
|
||||||
*/
|
|
||||||
public class TimeLimitedCache<T>
|
|
||||||
{
|
|
||||||
|
|
||||||
private SoftReference<T> value;
|
|
||||||
private final long ttlMillis;
|
|
||||||
private long expires;
|
|
||||||
|
|
||||||
public TimeLimitedCache(long ttl, TimeUnit timeUnit)
|
|
||||||
{
|
|
||||||
this.ttlMillis = TimeUnit.MILLISECONDS.convert(ttl, timeUnit);
|
|
||||||
}
|
|
||||||
|
|
||||||
public T get()
|
|
||||||
{
|
|
||||||
return System.currentTimeMillis() < expires ? value.get() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void set(T value)
|
|
||||||
{
|
|
||||||
set(value, ttlMillis, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void set(T value, long ttl, TimeUnit timeUnit)
|
|
||||||
{
|
|
||||||
this.value = new SoftReference<T>(value);
|
|
||||||
expires = System.currentTimeMillis() + timeUnit.toMillis(ttl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear()
|
|
||||||
{
|
|
||||||
expires = 0L;
|
|
||||||
value = null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -28,7 +28,7 @@ import androidx.navigation.ui.setupWithNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
|
@ -126,7 +126,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||||
val dest: String = try {
|
val dest: String = try {
|
||||||
resources.getResourceName(destination.id)
|
resources.getResourceName(destination.id)
|
||||||
} catch (e: Resources.NotFoundException) {
|
} catch (ignored: Resources.NotFoundException) {
|
||||||
destination.id.toString()
|
destination.id.toString()
|
||||||
}
|
}
|
||||||
Timber.d("Navigated to $dest")
|
Timber.d("Navigated to $dest")
|
||||||
|
|
|
@ -2,7 +2,7 @@ package org.moire.ultrasonic.di
|
||||||
|
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.viewmodel.dsl.viewModel
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.data.AppDatabase
|
import org.moire.ultrasonic.data.AppDatabase
|
||||||
|
|
|
@ -13,8 +13,7 @@ internal val dateFormat: DateFormat by lazy {
|
||||||
SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry().apply {
|
fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry(id).apply {
|
||||||
id = this@toDomainEntity.id
|
|
||||||
parent = this@toDomainEntity.parent
|
parent = this@toDomainEntity.parent
|
||||||
isDirectory = this@toDomainEntity.isDir
|
isDirectory = this@toDomainEntity.isDir
|
||||||
title = this@toDomainEntity.title
|
title = this@toDomainEntity.title
|
||||||
|
|
|
@ -6,7 +6,6 @@ import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
@ -15,7 +14,6 @@ import org.moire.ultrasonic.util.Constants
|
||||||
* Displays a list of Albums from the media library
|
* Displays a list of Albums from the media library
|
||||||
* TODO: Check refresh is working
|
* TODO: Check refresh is working
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
class AlbumListFragment : GenericListFragment<MusicDirectory.Entry, AlbumRowAdapter>() {
|
class AlbumListFragment : GenericListFragment<MusicDirectory.Entry, AlbumRowAdapter>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,14 +5,12 @@ import android.os.Bundle
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
@KoinApiExtension
|
|
||||||
class AlbumListModel(application: Application) : GenericListModel(application) {
|
class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||||
|
|
||||||
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
|
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
|
||||||
|
|
|
@ -57,7 +57,7 @@ class AlbumRowAdapter(
|
||||||
|
|
||||||
imageLoader.loadImage(
|
imageLoader.loadImage(
|
||||||
holder.coverArt,
|
holder.coverArt,
|
||||||
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
|
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
||||||
false, 0, false, true, R.drawable.unknown_album
|
false, 0, false, true, R.drawable.unknown_album
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package org.moire.ultrasonic.fragment
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
@ -11,7 +10,6 @@ import org.moire.ultrasonic.util.Constants
|
||||||
/**
|
/**
|
||||||
* Displays the list of Artists from the media library
|
* Displays the list of Artists from the media library
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
|
class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -23,14 +23,12 @@ import android.os.Bundle
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides ViewModel which contains the list of available Artists
|
* Provides ViewModel which contains the list of available Artists
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
class ArtistListModel(application: Application) : GenericListModel(application) {
|
class ArtistListModel(application: Application) : GenericListModel(application) {
|
||||||
private val artists: MutableLiveData<List<Artist>> = MutableLiveData()
|
private val artists: MutableLiveData<List<Artist>> = MutableLiveData()
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ class ArtistRowAdapter(
|
||||||
holder.coverArt.visibility = View.VISIBLE
|
holder.coverArt.visibility = View.VISIBLE
|
||||||
imageLoader.loadImage(
|
imageLoader.loadImage(
|
||||||
holder.coverArt,
|
holder.coverArt,
|
||||||
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
|
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
||||||
false, 0, false, true, R.drawable.ic_contact_picture
|
false, 0, false, true, R.drawable.ic_contact_picture
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -96,7 +96,7 @@ class ArtistRowAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSectionFromName(name: String): String {
|
private fun getSectionFromName(name: String): String {
|
||||||
var section = name.first().toUpperCase()
|
var section = name.first().uppercaseChar()
|
||||||
if (!section.isLetter()) section = '#'
|
if (!section.isLetter()) section = '#'
|
||||||
return section.toString()
|
return section.toString()
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.moire.ultrasonic.BuildConfig
|
import org.moire.ultrasonic.BuildConfig
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
|
|
|
@ -13,8 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.Artist
|
||||||
|
@ -31,7 +30,6 @@ import org.moire.ultrasonic.view.SelectMusicFolderView
|
||||||
* @param T: The type of data which will be used (must extend GenericEntry)
|
* @param T: The type of data which will be used (must extend GenericEntry)
|
||||||
* @param TA: The Adapter to use (must extend GenericRowAdapter)
|
* @param TA: The Adapter to use (must extend GenericRowAdapter)
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> : Fragment() {
|
abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> : Fragment() {
|
||||||
internal val activeServerProvider: ActiveServerProvider by inject()
|
internal val activeServerProvider: ActiveServerProvider by inject()
|
||||||
internal val serverSettingsModel: ServerSettingsModel by viewModel()
|
internal val serverSettingsModel: ServerSettingsModel by viewModel()
|
||||||
|
|
|
@ -15,7 +15,6 @@ import java.net.UnknownHostException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
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
|
||||||
|
@ -29,7 +28,6 @@ import org.moire.ultrasonic.util.Util
|
||||||
/**
|
/**
|
||||||
* An abstract Model, which can be extended to retrieve a list of items from the API
|
* An abstract Model, which can be extended to retrieve a list of items from the API
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
open class GenericListModel(application: Application) :
|
open class GenericListModel(application: Application) :
|
||||||
AndroidViewModel(application), KoinComponent {
|
AndroidViewModel(application), KoinComponent {
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.android.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX
|
import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX
|
||||||
|
|
|
@ -28,13 +28,11 @@ import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Random
|
import java.util.Random
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
@ -61,7 +59,6 @@ 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.
|
||||||
* TODO: Refactor this fragment and model to extend the GenericListFragment
|
* TODO: Refactor this fragment and model to extend the GenericListFragment
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
class TrackCollectionFragment : Fragment() {
|
class TrackCollectionFragment : Fragment() {
|
||||||
|
|
||||||
private var refreshAlbumListView: SwipeRefreshLayout? = null
|
private var refreshAlbumListView: SwipeRefreshLayout? = null
|
||||||
|
@ -92,7 +89,7 @@ class TrackCollectionFragment : Fragment() {
|
||||||
private var cancellationToken: CancellationToken? = null
|
private var cancellationToken: CancellationToken? = null
|
||||||
|
|
||||||
private val model: TrackCollectionModel by viewModels()
|
private val model: TrackCollectionModel by viewModels()
|
||||||
private val random: Random = SecureRandom()
|
private val random: Random = Random()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Util.applyTheme(this.context)
|
Util.applyTheme(this.context)
|
||||||
|
@ -258,7 +255,7 @@ class TrackCollectionFragment : Fragment() {
|
||||||
model.getMusicFolders(refresh)
|
model.getMusicFolders(refresh)
|
||||||
|
|
||||||
if (playlistId != null) {
|
if (playlistId != null) {
|
||||||
setTitle(playlistName)
|
setTitle(playlistName!!)
|
||||||
model.getPlaylist(playlistId, playlistName)
|
model.getPlaylist(playlistId, playlistName)
|
||||||
} else if (podcastChannelId != null) {
|
} else if (podcastChannelId != null) {
|
||||||
setTitle(getString(R.string.podcasts_label))
|
setTitle(getString(R.string.podcasts_label))
|
||||||
|
@ -282,12 +279,12 @@ class TrackCollectionFragment : Fragment() {
|
||||||
setTitle(name)
|
setTitle(name)
|
||||||
if (!isOffline() && Util.getShouldUseId3Tags()) {
|
if (!isOffline() && Util.getShouldUseId3Tags()) {
|
||||||
if (isAlbum) {
|
if (isAlbum) {
|
||||||
model.getAlbum(refresh, id, name, parentId)
|
model.getAlbum(refresh, id!!, name, parentId)
|
||||||
} else {
|
} else {
|
||||||
model.getArtist(refresh, id, name)
|
model.getArtist(refresh, id!!, name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
model.getMusicDirectory(refresh, id, name, parentId)
|
model.getMusicDirectory(refresh, id!!, name, parentId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
|
@ -24,7 +23,6 @@ import org.moire.ultrasonic.util.Util
|
||||||
* Model for retrieving different collections of tracks from the API
|
* Model for retrieving different collections of tracks from the API
|
||||||
* TODO: Refactor this model to extend the GenericListModel
|
* TODO: Refactor this model to extend the GenericListModel
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
class TrackCollectionModel(application: Application) : GenericListModel(application) {
|
class TrackCollectionModel(application: Application) : GenericListModel(application) {
|
||||||
|
|
||||||
private val allSongsId = "-1"
|
private val allSongsId = "-1"
|
||||||
|
@ -43,7 +41,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
|
|
||||||
suspend fun getMusicDirectory(
|
suspend fun getMusicDirectory(
|
||||||
refresh: Boolean,
|
refresh: Boolean,
|
||||||
id: String?,
|
id: String,
|
||||||
name: String?,
|
name: String?,
|
||||||
parentId: String?
|
parentId: String?
|
||||||
) {
|
) {
|
||||||
|
@ -53,7 +51,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
|
|
||||||
var root = MusicDirectory()
|
var root = MusicDirectory()
|
||||||
|
|
||||||
if (allSongsId == id) {
|
if (allSongsId == id && parentId != null) {
|
||||||
val musicDirectory = service.getMusicDirectory(
|
val musicDirectory = service.getMusicDirectory(
|
||||||
parentId, name, refresh
|
parentId, name, refresh
|
||||||
)
|
)
|
||||||
|
@ -73,12 +71,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
musicDirectory.findChild(allSongsId) == null &&
|
musicDirectory.findChild(allSongsId) == null &&
|
||||||
hasOnlyFolders(musicDirectory)
|
hasOnlyFolders(musicDirectory)
|
||||||
) {
|
) {
|
||||||
val allSongs = MusicDirectory.Entry()
|
val allSongs = MusicDirectory.Entry(allSongsId)
|
||||||
|
|
||||||
allSongs.isDirectory = true
|
allSongs.isDirectory = true
|
||||||
allSongs.artist = name
|
allSongs.artist = name
|
||||||
allSongs.parent = id
|
allSongs.parent = id
|
||||||
allSongs.id = allSongsId
|
|
||||||
allSongs.title = String.format(
|
allSongs.title = String.format(
|
||||||
context.resources.getString(R.string.select_album_all_songs), name
|
context.resources.getString(R.string.select_album_all_songs), name
|
||||||
)
|
)
|
||||||
|
@ -122,7 +119,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
* TODO: This method should be moved to AlbumListModel,
|
* TODO: This method should be moved to AlbumListModel,
|
||||||
* since it displays a list of albums by a specified artist.
|
* since it displays a list of albums by a specified artist.
|
||||||
*/
|
*/
|
||||||
suspend fun getArtist(refresh: Boolean, id: String?, name: String?) {
|
suspend fun getArtist(refresh: Boolean, id: String, name: String?) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
|
@ -135,12 +132,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
musicDirectory.findChild(allSongsId) == null &&
|
musicDirectory.findChild(allSongsId) == null &&
|
||||||
hasOnlyFolders(musicDirectory)
|
hasOnlyFolders(musicDirectory)
|
||||||
) {
|
) {
|
||||||
val allSongs = MusicDirectory.Entry()
|
val allSongs = MusicDirectory.Entry(allSongsId)
|
||||||
|
|
||||||
allSongs.isDirectory = true
|
allSongs.isDirectory = true
|
||||||
allSongs.artist = name
|
allSongs.artist = name
|
||||||
allSongs.parent = id
|
allSongs.parent = id
|
||||||
allSongs.id = allSongsId
|
|
||||||
allSongs.title = String.format(
|
allSongs.title = String.format(
|
||||||
context.resources.getString(R.string.select_album_all_songs), name
|
context.resources.getString(R.string.select_album_all_songs), name
|
||||||
)
|
)
|
||||||
|
@ -154,7 +150,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?) {
|
suspend fun getAlbum(refresh: Boolean, id: String, name: String?, parentId: String?) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
|
@ -162,7 +158,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
|
|
||||||
val musicDirectory: MusicDirectory
|
val musicDirectory: MusicDirectory
|
||||||
|
|
||||||
if (allSongsId == id) {
|
if (allSongsId == id && parentId != null) {
|
||||||
val root = MusicDirectory()
|
val root = MusicDirectory()
|
||||||
|
|
||||||
val songs: MutableCollection<MusicDirectory.Entry> = LinkedList()
|
val songs: MutableCollection<MusicDirectory.Entry> = LinkedList()
|
||||||
|
@ -212,9 +208,9 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
val musicDirectory: MusicDirectory
|
val musicDirectory: MusicDirectory
|
||||||
|
|
||||||
if (Util.getShouldUseId3Tags()) {
|
if (Util.getShouldUseId3Tags()) {
|
||||||
musicDirectory = Util.getSongsFromSearchResult(service.starred2)
|
musicDirectory = Util.getSongsFromSearchResult(service.getStarred2())
|
||||||
} else {
|
} else {
|
||||||
musicDirectory = Util.getSongsFromSearchResult(service.starred)
|
musicDirectory = Util.getSongsFromSearchResult(service.getStarred())
|
||||||
}
|
}
|
||||||
|
|
||||||
currentDirectory.postValue(musicDirectory)
|
currentDirectory.postValue(musicDirectory)
|
||||||
|
@ -241,7 +237,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPlaylist(playlistId: String, playlistName: String?) {
|
suspend fun getPlaylist(playlistId: String, playlistName: String) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
|
|
|
@ -161,7 +161,7 @@ class FileLoggerTree : Timber.DebugTree() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLogFileList(): Array<File> {
|
private fun getLogFileList(): Array<out File>? {
|
||||||
val directory = FileUtil.getUltrasonicDirectory()
|
val directory = FileUtil.getUltrasonicDirectory()
|
||||||
return directory.listFiles { t -> t.name.matches(fileNameRegex) }
|
return directory.listFiles { t -> t.name.matches(fileNameRegex) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ import timber.log.Timber
|
||||||
class AudioFocusHandler(private val context: Context) {
|
class AudioFocusHandler(private val context: Context) {
|
||||||
// TODO: This is a circular reference, try to remove it
|
// TODO: This is a circular reference, try to remove it
|
||||||
// This should be doable by using the native MediaController framework
|
// This should be doable by using the native MediaController framework
|
||||||
private val mediaPlayerControllerLazy = inject(MediaPlayerController::class.java)
|
private val mediaPlayerControllerLazy =
|
||||||
|
inject<MediaPlayerController>(MediaPlayerController::class.java)
|
||||||
|
|
||||||
private val audioManager by lazy {
|
private val audioManager by lazy {
|
||||||
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
|
|
@ -0,0 +1,470 @@
|
||||||
|
/*
|
||||||
|
* CachedMusicService.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
|
import org.moire.ultrasonic.domain.Bookmark
|
||||||
|
import org.moire.ultrasonic.domain.ChatMessage
|
||||||
|
import org.moire.ultrasonic.domain.Genre
|
||||||
|
import org.moire.ultrasonic.domain.Indexes
|
||||||
|
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||||
|
import org.moire.ultrasonic.domain.Lyrics
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
import org.moire.ultrasonic.domain.MusicFolder
|
||||||
|
import org.moire.ultrasonic.domain.Playlist
|
||||||
|
import org.moire.ultrasonic.domain.PodcastsChannel
|
||||||
|
import org.moire.ultrasonic.domain.SearchCriteria
|
||||||
|
import org.moire.ultrasonic.domain.SearchResult
|
||||||
|
import org.moire.ultrasonic.domain.Share
|
||||||
|
import org.moire.ultrasonic.domain.UserInfo
|
||||||
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
import org.moire.ultrasonic.util.LRUCache
|
||||||
|
import org.moire.ultrasonic.util.TimeLimitedCache
|
||||||
|
import org.moire.ultrasonic.util.Util
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
|
class CachedMusicService(private val musicService: MusicService) : MusicService, KoinComponent {
|
||||||
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
|
||||||
|
private val cachedMusicDirectories: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
|
||||||
|
private val cachedArtist: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
|
||||||
|
private val cachedAlbum: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
|
||||||
|
private val cachedUserInfo: LRUCache<String?, TimeLimitedCache<UserInfo?>>
|
||||||
|
private val cachedLicenseValid = TimeLimitedCache<Boolean>(expiresAfter = 10, TimeUnit.MINUTES)
|
||||||
|
private val cachedIndexes = TimeLimitedCache<Indexes?>()
|
||||||
|
private val cachedArtists = TimeLimitedCache<Indexes?>()
|
||||||
|
private val cachedPlaylists = TimeLimitedCache<List<Playlist>?>()
|
||||||
|
private val cachedPodcastsChannels = TimeLimitedCache<List<PodcastsChannel>>()
|
||||||
|
private val cachedMusicFolders =
|
||||||
|
TimeLimitedCache<List<MusicFolder>?>(10, TimeUnit.HOURS)
|
||||||
|
private val cachedGenres = TimeLimitedCache<List<Genre>?>(10, TimeUnit.HOURS)
|
||||||
|
private var restUrl: String? = null
|
||||||
|
private var cachedMusicFolderId: String? = null
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun ping() {
|
||||||
|
checkSettingsChanged()
|
||||||
|
musicService.ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun isLicenseValid(): Boolean {
|
||||||
|
checkSettingsChanged()
|
||||||
|
var isValid = cachedLicenseValid.get()
|
||||||
|
if (isValid == null) {
|
||||||
|
isValid = musicService.isLicenseValid()
|
||||||
|
cachedLicenseValid.set(isValid)
|
||||||
|
}
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getMusicFolders(refresh: Boolean): List<MusicFolder> {
|
||||||
|
checkSettingsChanged()
|
||||||
|
if (refresh) {
|
||||||
|
cachedMusicFolders.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
val cache = cachedMusicFolders.get()
|
||||||
|
if (cache != null) return cache
|
||||||
|
|
||||||
|
val result = musicService.getMusicFolders(refresh)
|
||||||
|
cachedMusicFolders.set(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes {
|
||||||
|
checkSettingsChanged()
|
||||||
|
if (refresh) {
|
||||||
|
cachedIndexes.clear()
|
||||||
|
cachedMusicFolders.clear()
|
||||||
|
cachedMusicDirectories.clear()
|
||||||
|
}
|
||||||
|
var result = cachedIndexes.get()
|
||||||
|
if (result == null) {
|
||||||
|
result = musicService.getIndexes(musicFolderId, refresh)
|
||||||
|
cachedIndexes.set(result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getArtists(refresh: Boolean): Indexes {
|
||||||
|
checkSettingsChanged()
|
||||||
|
if (refresh) {
|
||||||
|
cachedArtists.clear()
|
||||||
|
}
|
||||||
|
var result = cachedArtists.get()
|
||||||
|
if (result == null) {
|
||||||
|
result = musicService.getArtists(refresh)
|
||||||
|
cachedArtists.set(result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
|
checkSettingsChanged()
|
||||||
|
var cache = if (refresh) null else cachedMusicDirectories[id]
|
||||||
|
var dir = cache?.get()
|
||||||
|
if (dir == null) {
|
||||||
|
dir = musicService.getMusicDirectory(id, name, refresh)
|
||||||
|
cache = TimeLimitedCache(
|
||||||
|
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
cache.set(dir)
|
||||||
|
cachedMusicDirectories.put(id, cache)
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
|
checkSettingsChanged()
|
||||||
|
var cache = if (refresh) null else cachedArtist[id]
|
||||||
|
var dir = cache?.get()
|
||||||
|
if (dir == null) {
|
||||||
|
dir = musicService.getArtist(id, name, refresh)
|
||||||
|
cache = TimeLimitedCache(
|
||||||
|
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
cache.set(dir)
|
||||||
|
cachedArtist.put(id, cache)
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
|
checkSettingsChanged()
|
||||||
|
var cache = if (refresh) null else cachedAlbum[id]
|
||||||
|
var dir = cache?.get()
|
||||||
|
if (dir == null) {
|
||||||
|
dir = musicService.getAlbum(id, name, refresh)
|
||||||
|
cache = TimeLimitedCache(
|
||||||
|
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
cache.set(dir)
|
||||||
|
cachedAlbum.put(id, cache)
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun search(criteria: SearchCriteria): SearchResult? {
|
||||||
|
return musicService.search(criteria)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getPlaylist(id: String, name: String): MusicDirectory {
|
||||||
|
return musicService.getPlaylist(id, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getPodcastsChannels(refresh: Boolean): List<PodcastsChannel> {
|
||||||
|
checkSettingsChanged()
|
||||||
|
var result = if (refresh) null else cachedPodcastsChannels.get()
|
||||||
|
if (result == null) {
|
||||||
|
result = musicService.getPodcastsChannels(refresh)
|
||||||
|
cachedPodcastsChannels.set(result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory? {
|
||||||
|
return musicService.getPodcastEpisodes(podcastChannelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getPlaylists(refresh: Boolean): List<Playlist> {
|
||||||
|
checkSettingsChanged()
|
||||||
|
var result = if (refresh) null else cachedPlaylists.get()
|
||||||
|
if (result == null) {
|
||||||
|
result = musicService.getPlaylists(refresh)
|
||||||
|
cachedPlaylists.set(result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>) {
|
||||||
|
cachedPlaylists.clear()
|
||||||
|
musicService.createPlaylist(id, name, entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun deletePlaylist(id: String) {
|
||||||
|
musicService.deletePlaylist(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun updatePlaylist(id: String, name: String?, comment: String?, pub: Boolean) {
|
||||||
|
musicService.updatePlaylist(id, name, comment, pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getLyrics(artist: String, title: String): Lyrics? {
|
||||||
|
return musicService.getLyrics(artist, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun scrobble(id: String, submission: Boolean) {
|
||||||
|
musicService.scrobble(id, submission)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getAlbumList(
|
||||||
|
type: String,
|
||||||
|
size: Int,
|
||||||
|
offset: Int,
|
||||||
|
musicFolderId: String?
|
||||||
|
): MusicDirectory {
|
||||||
|
return musicService.getAlbumList(type, size, offset, musicFolderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getAlbumList2(
|
||||||
|
type: String,
|
||||||
|
size: Int,
|
||||||
|
offset: Int,
|
||||||
|
musicFolderId: String?
|
||||||
|
): MusicDirectory {
|
||||||
|
return musicService.getAlbumList2(type, size, offset, musicFolderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getRandomSongs(size: Int): MusicDirectory {
|
||||||
|
return musicService.getRandomSongs(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getStarred(): SearchResult = musicService.getStarred()
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getStarred2(): SearchResult = musicService.getStarred2()
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getCoverArt(
|
||||||
|
entry: MusicDirectory.Entry?,
|
||||||
|
size: Int,
|
||||||
|
saveToFile: Boolean,
|
||||||
|
highQuality: Boolean
|
||||||
|
): Bitmap? {
|
||||||
|
return musicService.getCoverArt(entry, size, saveToFile, highQuality)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getDownloadInputStream(
|
||||||
|
song: MusicDirectory.Entry,
|
||||||
|
offset: Long,
|
||||||
|
maxBitrate: Int
|
||||||
|
): Pair<InputStream, Boolean> {
|
||||||
|
return musicService.getDownloadInputStream(song, offset, maxBitrate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getVideoUrl(id: String, useFlash: Boolean): String? {
|
||||||
|
return musicService.getVideoUrl(id, useFlash)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
||||||
|
return musicService.updateJukeboxPlaylist(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun skipJukebox(index: Int, offsetSeconds: Int): JukeboxStatus {
|
||||||
|
return musicService.skipJukebox(index, offsetSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun stopJukebox(): JukeboxStatus {
|
||||||
|
return musicService.stopJukebox()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun startJukebox(): JukeboxStatus {
|
||||||
|
return musicService.startJukebox()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getJukeboxStatus(): JukeboxStatus = musicService.getJukeboxStatus()
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun setJukeboxGain(gain: Float): JukeboxStatus {
|
||||||
|
return musicService.setJukeboxGain(gain)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkSettingsChanged() {
|
||||||
|
val newUrl = activeServerProvider.getRestUrl(null)
|
||||||
|
val newFolderId = activeServerProvider.getActiveServer().musicFolderId
|
||||||
|
if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) {
|
||||||
|
cachedMusicFolders.clear()
|
||||||
|
cachedMusicDirectories.clear()
|
||||||
|
cachedLicenseValid.clear()
|
||||||
|
cachedIndexes.clear()
|
||||||
|
cachedPlaylists.clear()
|
||||||
|
cachedGenres.clear()
|
||||||
|
cachedAlbum.clear()
|
||||||
|
cachedArtist.clear()
|
||||||
|
cachedUserInfo.clear()
|
||||||
|
restUrl = newUrl
|
||||||
|
cachedMusicFolderId = newFolderId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun star(id: String?, albumId: String?, artistId: String?) {
|
||||||
|
musicService.star(id, albumId, artistId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun unstar(id: String?, albumId: String?, artistId: String?) {
|
||||||
|
musicService.unstar(id, albumId, artistId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun setRating(id: String, rating: Int) {
|
||||||
|
musicService.setRating(id, rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getGenres(refresh: Boolean): List<Genre>? {
|
||||||
|
checkSettingsChanged()
|
||||||
|
if (refresh) {
|
||||||
|
cachedGenres.clear()
|
||||||
|
}
|
||||||
|
var result = cachedGenres.get()
|
||||||
|
if (result == null) {
|
||||||
|
result = musicService.getGenres(refresh)
|
||||||
|
cachedGenres.set(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sorted = result?.toMutableList()
|
||||||
|
sorted?.sortWith { genre, genre2 ->
|
||||||
|
genre.name.compareTo(
|
||||||
|
genre2.name,
|
||||||
|
ignoreCase = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return sorted
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getSongsByGenre(genre: String, count: Int, offset: Int): MusicDirectory {
|
||||||
|
return musicService.getSongsByGenre(genre, count, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getShares(refresh: Boolean): List<Share> {
|
||||||
|
return musicService.getShares(refresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getChatMessages(since: Long?): List<ChatMessage?>? {
|
||||||
|
return musicService.getChatMessages(since)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun addChatMessage(message: String) {
|
||||||
|
musicService.addChatMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getBookmarks(): List<Bookmark?>? = musicService.getBookmarks()
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun deleteBookmark(id: String) {
|
||||||
|
musicService.deleteBookmark(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun createBookmark(id: String, position: Int) {
|
||||||
|
musicService.createBookmark(id, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getVideos(refresh: Boolean): MusicDirectory? {
|
||||||
|
checkSettingsChanged()
|
||||||
|
var cache =
|
||||||
|
if (refresh) null else cachedMusicDirectories[Constants.INTENT_EXTRA_NAME_VIDEOS]
|
||||||
|
var dir = cache?.get()
|
||||||
|
if (dir == null) {
|
||||||
|
dir = musicService.getVideos(refresh)
|
||||||
|
cache = TimeLimitedCache(
|
||||||
|
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
cache.set(dir)
|
||||||
|
cachedMusicDirectories.put(Constants.INTENT_EXTRA_NAME_VIDEOS, cache)
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getUser(username: String): UserInfo {
|
||||||
|
checkSettingsChanged()
|
||||||
|
var cache = cachedUserInfo[username]
|
||||||
|
var userInfo = cache?.get()
|
||||||
|
if (userInfo == null) {
|
||||||
|
userInfo = musicService.getUser(username)
|
||||||
|
cache = TimeLimitedCache(
|
||||||
|
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
cache.set(userInfo)
|
||||||
|
cachedUserInfo.put(username, cache)
|
||||||
|
}
|
||||||
|
return userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun createShare(
|
||||||
|
ids: List<String>,
|
||||||
|
description: String?,
|
||||||
|
expires: Long?
|
||||||
|
): List<Share> {
|
||||||
|
return musicService.createShare(ids, description, expires)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun deleteShare(id: String) {
|
||||||
|
musicService.deleteShare(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun updateShare(id: String, description: String?, expires: Long?) {
|
||||||
|
musicService.updateShare(id, description, expires)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getAvatar(
|
||||||
|
username: String?,
|
||||||
|
size: Int,
|
||||||
|
saveToFile: Boolean,
|
||||||
|
highQuality: Boolean
|
||||||
|
): Bitmap? {
|
||||||
|
return musicService.getAvatar(username, size, saveToFile, highQuality)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MUSIC_DIR_CACHE_SIZE = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
cachedMusicDirectories = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
||||||
|
cachedArtist = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
||||||
|
cachedAlbum = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
||||||
|
cachedUserInfo = LRUCache(MUSIC_DIR_CACHE_SIZE)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,8 +19,8 @@ import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.io.RandomAccessFile
|
import java.io.RandomAccessFile
|
||||||
import org.koin.core.component.KoinApiExtension
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||||
|
@ -36,11 +36,10 @@ import timber.log.Timber
|
||||||
* @author Sindre Mehus
|
* @author Sindre Mehus
|
||||||
* @version $Id$
|
* @version $Id$
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
class DownloadFile(
|
class DownloadFile(
|
||||||
val song: MusicDirectory.Entry,
|
val song: MusicDirectory.Entry,
|
||||||
private val save: Boolean
|
private val save: Boolean
|
||||||
) {
|
) : KoinComponent {
|
||||||
val partialFile: File
|
val partialFile: File
|
||||||
val completeFile: File
|
val completeFile: File
|
||||||
private val saveFile: File = FileUtil.getSongFile(song)
|
private val saveFile: File = FileUtil.getSongFile(song)
|
||||||
|
@ -59,7 +58,7 @@ class DownloadFile(
|
||||||
@Volatile
|
@Volatile
|
||||||
private var completeWhenDone = false
|
private var completeWhenDone = false
|
||||||
|
|
||||||
private val downloader = inject(Downloader::class.java)
|
private val downloader: Downloader by inject()
|
||||||
|
|
||||||
val progress: MutableLiveData<Int> = MutableLiveData(0)
|
val progress: MutableLiveData<Int> = MutableLiveData(0)
|
||||||
|
|
||||||
|
@ -201,7 +200,6 @@ class DownloadFile(
|
||||||
return String.format("DownloadFile (%s)", song)
|
return String.format("DownloadFile (%s)", song)
|
||||||
}
|
}
|
||||||
|
|
||||||
@KoinApiExtension
|
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
private inner class DownloadTask : CancellableTask() {
|
private inner class DownloadTask : CancellableTask() {
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
|
@ -310,7 +308,7 @@ class DownloadFile(
|
||||||
}
|
}
|
||||||
wifiLock?.release()
|
wifiLock?.release()
|
||||||
CacheCleaner().cleanSpace()
|
CacheCleaner().cleanSpace()
|
||||||
downloader.value.checkDownloads()
|
downloader.checkDownloads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ import java.net.URLEncoder
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||||
import org.moire.ultrasonic.audiofx.VisualizerController
|
import org.moire.ultrasonic.audiofx.VisualizerController
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
|
@ -40,7 +39,6 @@ import timber.log.Timber
|
||||||
/**
|
/**
|
||||||
* Represents a Media Player which uses the mobile's resources for playback
|
* Represents a Media Player which uses the mobile's resources for playback
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
class LocalMediaPlayer(
|
class LocalMediaPlayer(
|
||||||
private val audioFocusHandler: AudioFocusHandler,
|
private val audioFocusHandler: AudioFocusHandler,
|
||||||
private val context: Context
|
private val context: Context
|
||||||
|
@ -106,7 +104,7 @@ class LocalMediaPlayer(
|
||||||
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId)
|
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId)
|
||||||
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
||||||
context.sendBroadcast(i)
|
context.sendBroadcast(i)
|
||||||
} catch (e: Throwable) {
|
} catch (ignored: Throwable) {
|
||||||
// Froyo or lower
|
// Froyo or lower
|
||||||
}
|
}
|
||||||
mediaPlayerLooper = Looper.myLooper()
|
mediaPlayerLooper = Looper.myLooper()
|
||||||
|
@ -466,7 +464,7 @@ class LocalMediaPlayer(
|
||||||
// the equalizer or visualizer with the player
|
// the equalizer or visualizer with the player
|
||||||
try {
|
try {
|
||||||
nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId
|
nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId
|
||||||
} catch (e: Throwable) {
|
} catch (ignored: Throwable) {
|
||||||
}
|
}
|
||||||
|
|
||||||
nextMediaPlayer!!.setDataSource(file.path)
|
nextMediaPlayer!!.setDataSource(file.path)
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import org.koin.core.component.KoinApiExtension
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.java.KoinJavaComponent.get
|
import org.koin.core.component.get
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
@ -30,7 +30,6 @@ import timber.log.Timber
|
||||||
* This class contains everything that is necessary for the Application UI
|
* This class contains everything that is necessary for the Application UI
|
||||||
* to control the Media Player implementation.
|
* to control the Media Player implementation.
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class MediaPlayerController(
|
class MediaPlayerController(
|
||||||
private val downloadQueueSerializer: DownloadQueueSerializer,
|
private val downloadQueueSerializer: DownloadQueueSerializer,
|
||||||
|
@ -38,7 +37,7 @@ class MediaPlayerController(
|
||||||
private val downloader: Downloader,
|
private val downloader: Downloader,
|
||||||
private val shufflePlayBuffer: ShufflePlayBuffer,
|
private val shufflePlayBuffer: ShufflePlayBuffer,
|
||||||
private val localMediaPlayer: LocalMediaPlayer
|
private val localMediaPlayer: LocalMediaPlayer
|
||||||
) {
|
) : KoinComponent {
|
||||||
|
|
||||||
private var created = false
|
private var created = false
|
||||||
var suggestedPlaylistName: String? = null
|
var suggestedPlaylistName: String? = null
|
||||||
|
@ -46,8 +45,8 @@ class MediaPlayerController(
|
||||||
var showVisualization = false
|
var showVisualization = false
|
||||||
private var autoPlayStart = false
|
private var autoPlayStart = false
|
||||||
|
|
||||||
private val jukeboxMediaPlayer = inject(JukeboxMediaPlayer::class.java).value
|
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
|
||||||
private val activeServerProvider = inject(ActiveServerProvider::class.java).value
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
|
||||||
fun onCreate() {
|
fun onCreate() {
|
||||||
if (created) return
|
if (created) return
|
||||||
|
@ -429,10 +428,7 @@ class MediaPlayerController(
|
||||||
get() {
|
get() {
|
||||||
try {
|
try {
|
||||||
val username = activeServerProvider.getActiveServer().userName
|
val username = activeServerProvider.getActiveServer().userName
|
||||||
val (_, _, _, _, _, _, _, _, _, _, _, _, jukeboxRole) = getMusicService().getUser(
|
return getMusicService().getUser(username).jukeboxRole
|
||||||
username
|
|
||||||
)
|
|
||||||
return jukeboxRole
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.w(e, "Error getting user information")
|
Timber.w(e, "Error getting user information")
|
||||||
}
|
}
|
||||||
|
@ -465,7 +461,8 @@ class MediaPlayerController(
|
||||||
|
|
||||||
@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 (!get(FeatureStorage::class.java).isFeatureEnabled(Feature.FIVE_STAR_RATING)) return
|
val features: FeatureStorage = get()
|
||||||
|
if (!features.isFeatureEnabled(Feature.FIVE_STAR_RATING)) return
|
||||||
if (localMediaPlayer.currentPlaying == null) return
|
if (localMediaPlayer.currentPlaying == null) return
|
||||||
val song = localMediaPlayer.currentPlaying!!.song
|
val song = localMediaPlayer.currentPlaying!!.song
|
||||||
song.userRating = rating
|
song.userRating = rating
|
||||||
|
|
|
@ -24,7 +24,6 @@ import android.view.KeyEvent
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.activity.NavigationActivity
|
import org.moire.ultrasonic.activity.NavigationActivity
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
|
@ -49,7 +48,6 @@ import timber.log.Timber
|
||||||
* Android Foreground Service for playing music
|
* Android Foreground Service for playing music
|
||||||
* while the rest of the Ultrasonic App is in the background.
|
* while the rest of the Ultrasonic App is in the background.
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
class MediaPlayerService : Service() {
|
class MediaPlayerService : Service() {
|
||||||
private val binder: IBinder = SimpleServiceBinder(this)
|
private val binder: IBinder = SimpleServiceBinder(this)
|
||||||
|
@ -173,8 +171,7 @@ class MediaPlayerService : Service() {
|
||||||
fun setCurrentPlaying(currentPlayingIndex: Int) {
|
fun setCurrentPlaying(currentPlayingIndex: Int) {
|
||||||
try {
|
try {
|
||||||
localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex])
|
localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex])
|
||||||
} catch (x: IndexOutOfBoundsException) {
|
} catch (ignored: IndexOutOfBoundsException) {
|
||||||
// Ignored
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
/*
|
||||||
|
* MusicService.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import java.io.InputStream
|
||||||
|
import org.moire.ultrasonic.domain.Bookmark
|
||||||
|
import org.moire.ultrasonic.domain.ChatMessage
|
||||||
|
import org.moire.ultrasonic.domain.Genre
|
||||||
|
import org.moire.ultrasonic.domain.Indexes
|
||||||
|
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||||
|
import org.moire.ultrasonic.domain.Lyrics
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
import org.moire.ultrasonic.domain.MusicFolder
|
||||||
|
import org.moire.ultrasonic.domain.Playlist
|
||||||
|
import org.moire.ultrasonic.domain.PodcastsChannel
|
||||||
|
import org.moire.ultrasonic.domain.SearchCriteria
|
||||||
|
import org.moire.ultrasonic.domain.SearchResult
|
||||||
|
import org.moire.ultrasonic.domain.Share
|
||||||
|
import org.moire.ultrasonic.domain.UserInfo
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
|
interface MusicService {
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun ping()
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun isLicenseValid(): Boolean
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getGenres(refresh: Boolean): List<Genre>?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun star(id: String?, albumId: String?, artistId: String?)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun unstar(id: String?, albumId: String?, artistId: String?)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun setRating(id: String, rating: Int)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getMusicFolders(refresh: Boolean): List<MusicFolder>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getArtists(refresh: Boolean): Indexes
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun search(criteria: SearchCriteria): SearchResult?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getPlaylist(id: String, name: String): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getPodcastsChannels(refresh: Boolean): List<PodcastsChannel>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getPlaylists(refresh: Boolean): List<Playlist>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun deletePlaylist(id: String)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun updatePlaylist(id: String, name: String?, comment: String?, pub: Boolean)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getLyrics(artist: String, title: String): Lyrics?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun scrobble(id: String, submission: Boolean)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getAlbumList(type: String, size: Int, offset: Int, musicFolderId: String?): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getAlbumList2(
|
||||||
|
type: String,
|
||||||
|
size: Int,
|
||||||
|
offset: Int,
|
||||||
|
musicFolderId: String?
|
||||||
|
): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getRandomSongs(size: Int): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getSongsByGenre(genre: String, count: Int, offset: Int): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getStarred(): SearchResult
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getStarred2(): SearchResult
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getCoverArt(
|
||||||
|
entry: MusicDirectory.Entry?,
|
||||||
|
size: Int,
|
||||||
|
saveToFile: Boolean,
|
||||||
|
highQuality: Boolean
|
||||||
|
): Bitmap?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getAvatar(username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean): Bitmap?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return response [InputStream] and a [Boolean] that indicates if this response is
|
||||||
|
* partial.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getDownloadInputStream(
|
||||||
|
song: MusicDirectory.Entry,
|
||||||
|
offset: Long,
|
||||||
|
maxBitrate: Int
|
||||||
|
): Pair<InputStream, Boolean>
|
||||||
|
|
||||||
|
// TODO: Refactor and remove this call (see RestMusicService implementation)
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getVideoUrl(id: String, useFlash: Boolean): String?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun skipJukebox(index: Int, offsetSeconds: Int): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun stopJukebox(): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun startJukebox(): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getJukeboxStatus(): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun setJukeboxGain(gain: Float): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getShares(refresh: Boolean): List<Share>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getChatMessages(since: Long?): List<ChatMessage?>?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun addChatMessage(message: String)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getBookmarks(): List<Bookmark?>?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun deleteBookmark(id: String)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun createBookmark(id: String, position: Int)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getVideos(refresh: Boolean): MusicDirectory?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getUser(username: String): UserInfo
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun createShare(ids: List<String>, description: String?, expires: Long?): List<Share>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun deleteShare(id: String)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun updateShare(id: String, description: String?, expires: Long?)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory?
|
||||||
|
}
|
|
@ -18,7 +18,6 @@
|
||||||
*/
|
*/
|
||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import org.koin.core.context.loadKoinModules
|
import org.koin.core.context.loadKoinModules
|
||||||
|
@ -30,7 +29,6 @@ import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE
|
||||||
import org.moire.ultrasonic.di.musicServiceModule
|
import org.moire.ultrasonic.di.musicServiceModule
|
||||||
|
|
||||||
// TODO Refactor everywhere to use DI way to get MusicService, and then remove this class
|
// TODO Refactor everywhere to use DI way to get MusicService, and then remove this class
|
||||||
@KoinApiExtension
|
|
||||||
object MusicServiceFactory : KoinComponent {
|
object MusicServiceFactory : KoinComponent {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getMusicService(): MusicService {
|
fun getMusicService(): MusicService {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* OfflineException.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown by service methods that are not available in offline mode.
|
||||||
|
*/
|
||||||
|
class OfflineException(message: String?) : Exception(message) {
|
||||||
|
companion object {
|
||||||
|
private const val serialVersionUID = -4479642294747429444L
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,696 @@
|
||||||
|
/*
|
||||||
|
* OfflineMusicService.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.BufferedWriter
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileReader
|
||||||
|
import java.io.FileWriter
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.Reader
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.HashSet
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Random
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
|
import org.moire.ultrasonic.domain.Artist
|
||||||
|
import org.moire.ultrasonic.domain.Bookmark
|
||||||
|
import org.moire.ultrasonic.domain.ChatMessage
|
||||||
|
import org.moire.ultrasonic.domain.Genre
|
||||||
|
import org.moire.ultrasonic.domain.Indexes
|
||||||
|
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||||
|
import org.moire.ultrasonic.domain.Lyrics
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
import org.moire.ultrasonic.domain.MusicFolder
|
||||||
|
import org.moire.ultrasonic.domain.Playlist
|
||||||
|
import org.moire.ultrasonic.domain.PodcastsChannel
|
||||||
|
import org.moire.ultrasonic.domain.SearchCriteria
|
||||||
|
import org.moire.ultrasonic.domain.SearchResult
|
||||||
|
import org.moire.ultrasonic.domain.Share
|
||||||
|
import org.moire.ultrasonic.domain.UserInfo
|
||||||
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
|
import org.moire.ultrasonic.util.Util
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
// TODO: There are quite a number of deeply nested and complicated functions in this class..
|
||||||
|
// Simplify them :)
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
|
class OfflineMusicService : MusicService, KoinComponent {
|
||||||
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
|
||||||
|
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes {
|
||||||
|
val artists: MutableList<Artist> = ArrayList()
|
||||||
|
val root = FileUtil.getMusicDirectory()
|
||||||
|
for (file in FileUtil.listFiles(root)) {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
val artist = Artist()
|
||||||
|
artist.id = file.path
|
||||||
|
artist.index = file.name.substring(0, 1)
|
||||||
|
artist.name = file.name
|
||||||
|
artists.add(artist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val ignoredArticlesString = "The El La Los Las Le Les"
|
||||||
|
val ignoredArticles = COMPILE.split(ignoredArticlesString)
|
||||||
|
artists.sortWith { lhsArtist, rhsArtist ->
|
||||||
|
var lhs = lhsArtist.name!!.lowercase(Locale.ROOT)
|
||||||
|
var rhs = rhsArtist.name!!.lowercase(Locale.ROOT)
|
||||||
|
val lhs1 = lhs[0]
|
||||||
|
val rhs1 = rhs[0]
|
||||||
|
if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) {
|
||||||
|
return@sortWith 1
|
||||||
|
}
|
||||||
|
if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) {
|
||||||
|
return@sortWith -1
|
||||||
|
}
|
||||||
|
for (article in ignoredArticles) {
|
||||||
|
var index = lhs.indexOf(
|
||||||
|
String.format(Locale.ROOT, "%s ", article.lowercase(Locale.ROOT))
|
||||||
|
)
|
||||||
|
if (index == 0) {
|
||||||
|
lhs = lhs.substring(article.length + 1)
|
||||||
|
}
|
||||||
|
index = rhs.indexOf(
|
||||||
|
String.format(Locale.ROOT, "%s ", article.lowercase(Locale.ROOT))
|
||||||
|
)
|
||||||
|
if (index == 0) {
|
||||||
|
rhs = rhs.substring(article.length + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lhs.compareTo(rhs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Indexes(0L, ignoredArticlesString, artists = artists)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMusicDirectory(
|
||||||
|
id: String,
|
||||||
|
name: String?,
|
||||||
|
refresh: Boolean
|
||||||
|
): MusicDirectory {
|
||||||
|
val dir = File(id)
|
||||||
|
val result = MusicDirectory()
|
||||||
|
result.name = dir.name
|
||||||
|
|
||||||
|
val seen: MutableCollection<String?> = HashSet()
|
||||||
|
|
||||||
|
for (file in FileUtil.listMediaFiles(dir)) {
|
||||||
|
val filename = getName(file)
|
||||||
|
if (filename != null && !seen.contains(filename)) {
|
||||||
|
seen.add(filename)
|
||||||
|
result.addChild(createEntry(file, filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAvatar(
|
||||||
|
username: String?,
|
||||||
|
size: Int,
|
||||||
|
saveToFile: Boolean,
|
||||||
|
highQuality: Boolean
|
||||||
|
): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val bitmap = FileUtil.getAvatarBitmap(username, size, highQuality)
|
||||||
|
Util.scaleBitmap(bitmap, size)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCoverArt(
|
||||||
|
entry: MusicDirectory.Entry?,
|
||||||
|
size: Int,
|
||||||
|
saveToFile: Boolean,
|
||||||
|
highQuality: Boolean
|
||||||
|
): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val bitmap = FileUtil.getAlbumArtBitmap(entry, size, highQuality)
|
||||||
|
Util.scaleBitmap(bitmap, size)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun search(criteria: SearchCriteria): SearchResult {
|
||||||
|
val artists: MutableList<Artist> = ArrayList()
|
||||||
|
val albums: MutableList<MusicDirectory.Entry> = ArrayList()
|
||||||
|
val songs: MutableList<MusicDirectory.Entry> = ArrayList()
|
||||||
|
val root = FileUtil.getMusicDirectory()
|
||||||
|
var closeness: Int
|
||||||
|
for (artistFile in FileUtil.listFiles(root)) {
|
||||||
|
val artistName = artistFile.name
|
||||||
|
if (artistFile.isDirectory) {
|
||||||
|
if (matchCriteria(criteria, artistName).also { closeness = it } > 0) {
|
||||||
|
val artist = Artist()
|
||||||
|
artist.id = artistFile.path
|
||||||
|
artist.index = artistFile.name.substring(0, 1)
|
||||||
|
artist.name = artistName
|
||||||
|
artist.closeness = closeness
|
||||||
|
artists.add(artist)
|
||||||
|
}
|
||||||
|
recursiveAlbumSearch(artistName, artistFile, criteria, albums, songs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
artists.sort()
|
||||||
|
albums.sort()
|
||||||
|
songs.sort()
|
||||||
|
|
||||||
|
return SearchResult(artists, albums, songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NestedBlockDepth", "TooGenericExceptionCaught")
|
||||||
|
override fun getPlaylists(refresh: Boolean): List<Playlist> {
|
||||||
|
val playlists: MutableList<Playlist> = ArrayList()
|
||||||
|
val root = FileUtil.getPlaylistDirectory()
|
||||||
|
var lastServer: String? = null
|
||||||
|
var removeServer = true
|
||||||
|
for (folder in FileUtil.listFiles(root)) {
|
||||||
|
if (folder.isDirectory) {
|
||||||
|
val server = folder.name
|
||||||
|
val fileList = FileUtil.listFiles(folder)
|
||||||
|
for (file in fileList) {
|
||||||
|
if (FileUtil.isPlaylistFile(file)) {
|
||||||
|
val id = file.name
|
||||||
|
val filename = server + ": " + FileUtil.getBaseName(id)
|
||||||
|
val playlist = Playlist(server, filename)
|
||||||
|
playlists.add(playlist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (server != lastServer && !fileList.isEmpty()) {
|
||||||
|
if (lastServer != null) {
|
||||||
|
removeServer = false
|
||||||
|
}
|
||||||
|
lastServer = server
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Delete legacy playlist files
|
||||||
|
try {
|
||||||
|
if (!folder.delete()) {
|
||||||
|
Timber.w("Failed to delete old playlist file: %s", folder.name)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e, "Failed to delete old playlist file: %s", folder.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeServer) {
|
||||||
|
for (playlist in playlists) {
|
||||||
|
playlist.name = playlist.name.substring(playlist.id.length + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return playlists
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getPlaylist(id: String, name: String): MusicDirectory {
|
||||||
|
var playlistName = name
|
||||||
|
var reader: Reader? = null
|
||||||
|
var buffer: BufferedReader? = null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val firstIndex = playlistName.indexOf(id)
|
||||||
|
if (firstIndex != -1) {
|
||||||
|
playlistName = playlistName.substring(id.length + 2)
|
||||||
|
}
|
||||||
|
val playlistFile = FileUtil.getPlaylistFile(id, playlistName)
|
||||||
|
reader = FileReader(playlistFile)
|
||||||
|
buffer = BufferedReader(reader)
|
||||||
|
val playlist = MusicDirectory()
|
||||||
|
var line = buffer.readLine()
|
||||||
|
if ("#EXTM3U" != line) return playlist
|
||||||
|
while (buffer.readLine().also { line = it } != null) {
|
||||||
|
val entryFile = File(line)
|
||||||
|
val entryName = getName(entryFile)
|
||||||
|
if (entryFile.exists() && entryName != null) {
|
||||||
|
playlist.addChild(createEntry(entryFile, entryName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playlist
|
||||||
|
} finally {
|
||||||
|
Util.close(buffer)
|
||||||
|
Util.close(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>) {
|
||||||
|
val playlistFile =
|
||||||
|
FileUtil.getPlaylistFile(activeServerProvider.getActiveServer().name, name)
|
||||||
|
val fw = FileWriter(playlistFile)
|
||||||
|
val bw = BufferedWriter(fw)
|
||||||
|
try {
|
||||||
|
fw.write("#EXTM3U\n")
|
||||||
|
for (e in entries) {
|
||||||
|
var filePath = FileUtil.getSongFile(e).absolutePath
|
||||||
|
if (!File(filePath).exists()) {
|
||||||
|
val ext = FileUtil.getExtension(filePath)
|
||||||
|
val base = FileUtil.getBaseName(filePath)
|
||||||
|
filePath = "$base.complete.$ext"
|
||||||
|
}
|
||||||
|
fw.write(
|
||||||
|
"""
|
||||||
|
$filePath
|
||||||
|
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
Timber.w("Failed to save playlist: %s", name)
|
||||||
|
} finally {
|
||||||
|
bw.close()
|
||||||
|
fw.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRandomSongs(size: Int): MusicDirectory {
|
||||||
|
val root = FileUtil.getMusicDirectory()
|
||||||
|
val children: MutableList<File> = LinkedList()
|
||||||
|
listFilesRecursively(root, children)
|
||||||
|
val result = MusicDirectory()
|
||||||
|
if (children.isEmpty()) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
val random = Random()
|
||||||
|
for (i in 0 until size) {
|
||||||
|
val file = children[random.nextInt(children.size)]
|
||||||
|
result.addChild(createEntry(file, getName(file)))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun deletePlaylist(id: String) {
|
||||||
|
throw OfflineException("Playlists not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun updatePlaylist(id: String, name: String?, comment: String?, pub: Boolean) {
|
||||||
|
throw OfflineException("Updating playlist not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getLyrics(artist: String, title: String): Lyrics? {
|
||||||
|
throw OfflineException("Lyrics not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun scrobble(id: String, submission: Boolean) {
|
||||||
|
throw OfflineException("Scrobbling not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getAlbumList(
|
||||||
|
type: String,
|
||||||
|
size: Int,
|
||||||
|
offset: Int,
|
||||||
|
musicFolderId: String?
|
||||||
|
): MusicDirectory {
|
||||||
|
throw OfflineException("Album lists not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
|
||||||
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun skipJukebox(index: Int, offsetSeconds: Int): JukeboxStatus {
|
||||||
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun stopJukebox(): JukeboxStatus {
|
||||||
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun startJukebox(): JukeboxStatus {
|
||||||
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getJukeboxStatus(): JukeboxStatus {
|
||||||
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun setJukeboxGain(gain: Float): JukeboxStatus {
|
||||||
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getStarred(): SearchResult {
|
||||||
|
throw OfflineException("Starred not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getSongsByGenre(genre: String, count: Int, offset: Int): MusicDirectory {
|
||||||
|
throw OfflineException("Getting Songs By Genre not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getGenres(refresh: Boolean): List<Genre>? {
|
||||||
|
throw OfflineException("Getting Genres not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getUser(username: String): UserInfo {
|
||||||
|
throw OfflineException("Getting user info not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun createShare(
|
||||||
|
ids: List<String>,
|
||||||
|
description: String?,
|
||||||
|
expires: Long?
|
||||||
|
): List<Share> {
|
||||||
|
throw OfflineException("Creating shares not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getShares(refresh: Boolean): List<Share> {
|
||||||
|
throw OfflineException("Getting shares not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun deleteShare(id: String) {
|
||||||
|
throw OfflineException("Deleting shares not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun updateShare(id: String, description: String?, expires: Long?) {
|
||||||
|
throw OfflineException("Updating shares not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun star(id: String?, albumId: String?, artistId: String?) {
|
||||||
|
throw OfflineException("Star not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun unstar(id: String?, albumId: String?, artistId: String?) {
|
||||||
|
throw OfflineException("UnStar not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun getMusicFolders(refresh: Boolean): List<MusicFolder> {
|
||||||
|
throw OfflineException("Music folders not available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getAlbumList2(
|
||||||
|
type: String,
|
||||||
|
size: Int,
|
||||||
|
offset: Int,
|
||||||
|
musicFolderId: String?
|
||||||
|
): MusicDirectory {
|
||||||
|
throw OfflineException("getAlbumList2 isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getVideoUrl(id: String, useFlash: Boolean): String? {
|
||||||
|
throw OfflineException("getVideoUrl isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getChatMessages(since: Long?): List<ChatMessage?>? {
|
||||||
|
throw OfflineException("getChatMessages isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun addChatMessage(message: String) {
|
||||||
|
throw OfflineException("addChatMessage isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getBookmarks(): List<Bookmark?>? {
|
||||||
|
throw OfflineException("getBookmarks isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun deleteBookmark(id: String) {
|
||||||
|
throw OfflineException("deleteBookmark isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun createBookmark(id: String, position: Int) {
|
||||||
|
throw OfflineException("createBookmark isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getVideos(refresh: Boolean): MusicDirectory? {
|
||||||
|
throw OfflineException("getVideos isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getStarred2(): SearchResult {
|
||||||
|
throw OfflineException("getStarred2 isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ping() {
|
||||||
|
// Void
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLicenseValid(): Boolean = true
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getArtists(refresh: Boolean): Indexes {
|
||||||
|
throw OfflineException("getArtists isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
|
throw OfflineException("getArtist isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
|
throw OfflineException("getAlbum isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory? {
|
||||||
|
throw OfflineException("getPodcastEpisodes isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getDownloadInputStream(
|
||||||
|
song: MusicDirectory.Entry,
|
||||||
|
offset: Long,
|
||||||
|
maxBitrate: Int
|
||||||
|
): Pair<InputStream, Boolean> {
|
||||||
|
throw OfflineException("getDownloadInputStream isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun setRating(id: String, rating: Int) {
|
||||||
|
throw OfflineException("setRating isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(OfflineException::class)
|
||||||
|
override fun getPodcastsChannels(refresh: Boolean): List<PodcastsChannel> {
|
||||||
|
throw OfflineException("getPodcastsChannels isn't available in offline mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val COMPILE = Pattern.compile(" ")
|
||||||
|
private fun getName(file: File): String? {
|
||||||
|
var name = file.name
|
||||||
|
if (file.isDirectory) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
if (name.endsWith(".partial") || name.contains(".partial.") ||
|
||||||
|
name == Constants.ALBUM_ART_FILE
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
name = name.replace(".complete", "")
|
||||||
|
return FileUtil.getBaseName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("TooGenericExceptionCaught", "ComplexMethod", "LongMethod", "NestedBlockDepth")
|
||||||
|
private fun createEntry(file: File, name: String?): MusicDirectory.Entry {
|
||||||
|
val entry = MusicDirectory.Entry(file.path)
|
||||||
|
entry.isDirectory = file.isDirectory
|
||||||
|
entry.parent = file.parent
|
||||||
|
entry.size = file.length()
|
||||||
|
val root = FileUtil.getMusicDirectory().path
|
||||||
|
entry.path = file.path.replaceFirst(
|
||||||
|
String.format(Locale.ROOT, "^%s/", root).toRegex(), ""
|
||||||
|
)
|
||||||
|
entry.title = name
|
||||||
|
if (file.isFile) {
|
||||||
|
var artist: String? = null
|
||||||
|
var album: String? = null
|
||||||
|
var title: String? = null
|
||||||
|
var track: String? = null
|
||||||
|
var disc: String? = null
|
||||||
|
var year: String? = null
|
||||||
|
var genre: String? = null
|
||||||
|
var duration: String? = null
|
||||||
|
var hasVideo: String? = null
|
||||||
|
try {
|
||||||
|
val mmr = MediaMetadataRetriever()
|
||||||
|
mmr.setDataSource(file.path)
|
||||||
|
artist = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
|
||||||
|
album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)
|
||||||
|
title = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
||||||
|
track = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER)
|
||||||
|
disc = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER)
|
||||||
|
year = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR)
|
||||||
|
genre = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)
|
||||||
|
duration = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||||
|
hasVideo = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
|
||||||
|
mmr.release()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
entry.artist = artist ?: file.parentFile!!.parentFile!!.name
|
||||||
|
entry.album = album ?: file.parentFile!!.name
|
||||||
|
if (title != null) {
|
||||||
|
entry.title = title
|
||||||
|
}
|
||||||
|
entry.isVideo = hasVideo != null
|
||||||
|
Timber.i("Offline Stuff: %s", track)
|
||||||
|
if (track != null) {
|
||||||
|
var trackValue = 0
|
||||||
|
try {
|
||||||
|
val slashIndex = track.indexOf('/')
|
||||||
|
if (slashIndex > 0) {
|
||||||
|
track = track.substring(0, slashIndex)
|
||||||
|
}
|
||||||
|
trackValue = track.toInt()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Offline Stuff")
|
||||||
|
}
|
||||||
|
Timber.i("Offline Stuff: Setting Track: %d", trackValue)
|
||||||
|
entry.track = trackValue
|
||||||
|
}
|
||||||
|
if (disc != null) {
|
||||||
|
var discValue = 0
|
||||||
|
try {
|
||||||
|
val slashIndex = disc.indexOf('/')
|
||||||
|
if (slashIndex > 0) {
|
||||||
|
disc = disc.substring(0, slashIndex)
|
||||||
|
}
|
||||||
|
discValue = disc.toInt()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
entry.discNumber = discValue
|
||||||
|
}
|
||||||
|
if (year != null) {
|
||||||
|
var yearValue = 0
|
||||||
|
try {
|
||||||
|
yearValue = year.toInt()
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
entry.year = yearValue
|
||||||
|
}
|
||||||
|
if (genre != null) {
|
||||||
|
entry.genre = genre
|
||||||
|
}
|
||||||
|
if (duration != null) {
|
||||||
|
var durationValue: Long = 0
|
||||||
|
try {
|
||||||
|
durationValue = duration.toLong()
|
||||||
|
durationValue = TimeUnit.MILLISECONDS.toSeconds(durationValue)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
|
entry.setDuration(durationValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry.suffix = FileUtil.getExtension(file.name.replace(".complete", ""))
|
||||||
|
val albumArt = FileUtil.getAlbumArtFile(entry)
|
||||||
|
if (albumArt.exists()) {
|
||||||
|
entry.coverArt = albumArt.path
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NestedBlockDepth")
|
||||||
|
private fun recursiveAlbumSearch(
|
||||||
|
artistName: String,
|
||||||
|
file: File,
|
||||||
|
criteria: SearchCriteria,
|
||||||
|
albums: MutableList<MusicDirectory.Entry>,
|
||||||
|
songs: MutableList<MusicDirectory.Entry>
|
||||||
|
) {
|
||||||
|
var closeness: Int
|
||||||
|
for (albumFile in FileUtil.listMediaFiles(file)) {
|
||||||
|
if (albumFile.isDirectory) {
|
||||||
|
val albumName = getName(albumFile)
|
||||||
|
if (matchCriteria(criteria, albumName).also { closeness = it } > 0) {
|
||||||
|
val album = createEntry(albumFile, albumName)
|
||||||
|
album.artist = artistName
|
||||||
|
album.closeness = closeness
|
||||||
|
albums.add(album)
|
||||||
|
}
|
||||||
|
for (songFile in FileUtil.listMediaFiles(albumFile)) {
|
||||||
|
val songName = getName(songFile)
|
||||||
|
if (songFile.isDirectory) {
|
||||||
|
recursiveAlbumSearch(artistName, songFile, criteria, albums, songs)
|
||||||
|
} else if (matchCriteria(criteria, songName).also { closeness = it } > 0) {
|
||||||
|
val song = createEntry(albumFile, songName)
|
||||||
|
song.artist = artistName
|
||||||
|
song.album = albumName
|
||||||
|
song.closeness = closeness
|
||||||
|
songs.add(song)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val songName = getName(albumFile)
|
||||||
|
if (matchCriteria(criteria, songName).also { closeness = it } > 0) {
|
||||||
|
val song = createEntry(albumFile, songName)
|
||||||
|
song.artist = artistName
|
||||||
|
song.album = songName
|
||||||
|
song.closeness = closeness
|
||||||
|
songs.add(song)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchCriteria(criteria: SearchCriteria, name: String?): Int {
|
||||||
|
val query = criteria.query.lowercase(Locale.ROOT)
|
||||||
|
val queryParts = COMPILE.split(query)
|
||||||
|
val nameParts = COMPILE.split(
|
||||||
|
name!!.lowercase(Locale.ROOT)
|
||||||
|
)
|
||||||
|
var closeness = 0
|
||||||
|
for (queryPart in queryParts) {
|
||||||
|
for (namePart in nameParts) {
|
||||||
|
if (namePart == queryPart) {
|
||||||
|
closeness++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closeness
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listFilesRecursively(parent: File, children: MutableList<File>) {
|
||||||
|
for (file in FileUtil.listMediaFiles(parent)) {
|
||||||
|
if (file.isFile) {
|
||||||
|
children.add(file)
|
||||||
|
} else {
|
||||||
|
listFilesRecursively(file, children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,8 @@
|
||||||
/*
|
/*
|
||||||
This file is part of Subsonic.
|
* RestMusicService.kt
|
||||||
|
* Copyright (C) 2009-2021 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 2009 (C) Sindre Mehus
|
|
||||||
*/
|
*/
|
||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
@ -64,8 +52,8 @@ import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This Music Service implementation connects to a server using the Subsonic REST API
|
* This Music Service implementation connects to a server using the Subsonic REST API
|
||||||
* @author Sindre Mehus
|
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LargeClass")
|
||||||
open class RESTMusicService(
|
open class RESTMusicService(
|
||||||
private val subsonicAPIClient: SubsonicAPIClient,
|
private val subsonicAPIClient: SubsonicAPIClient,
|
||||||
private val fileStorage: PermanentFileStorage,
|
private val fileStorage: PermanentFileStorage,
|
||||||
|
@ -109,7 +97,7 @@ open class RESTMusicService(
|
||||||
override fun getIndexes(
|
override fun getIndexes(
|
||||||
musicFolderId: String?,
|
musicFolderId: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): Indexes? {
|
): Indexes {
|
||||||
val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "")
|
val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "")
|
||||||
|
|
||||||
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
|
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
|
||||||
|
@ -171,7 +159,7 @@ open class RESTMusicService(
|
||||||
id: String,
|
id: String,
|
||||||
name: String?,
|
name: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): MusicDirectory? {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = responseChecker.callWithResponseCheck { api ->
|
||||||
api.getMusicDirectory(id).execute()
|
api.getMusicDirectory(id).execute()
|
||||||
}
|
}
|
||||||
|
@ -268,7 +256,7 @@ open class RESTMusicService(
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getPlaylist(
|
override fun getPlaylist(
|
||||||
id: String,
|
id: String,
|
||||||
name: String?
|
name: String
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = responseChecker.callWithResponseCheck { api ->
|
||||||
api.getPlaylist(id).execute()
|
api.getPlaylist(id).execute()
|
||||||
|
@ -282,7 +270,7 @@ open class RESTMusicService(
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun savePlaylist(
|
private fun savePlaylist(
|
||||||
name: String?,
|
name: String,
|
||||||
playlist: MusicDirectory
|
playlist: MusicDirectory
|
||||||
) {
|
) {
|
||||||
val playlistFile = FileUtil.getPlaylistFile(
|
val playlistFile = FileUtil.getPlaylistFile(
|
||||||
|
@ -326,16 +314,14 @@ open class RESTMusicService(
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun createPlaylist(
|
override fun createPlaylist(
|
||||||
id: String?,
|
id: String,
|
||||||
name: String?,
|
name: String,
|
||||||
entries: List<MusicDirectory.Entry>
|
entries: List<MusicDirectory.Entry>
|
||||||
) {
|
) {
|
||||||
val pSongIds: MutableList<String> = ArrayList(entries.size)
|
val pSongIds: MutableList<String> = ArrayList(entries.size)
|
||||||
|
|
||||||
for ((id1) in entries) {
|
for ((id1) in entries) {
|
||||||
if (id1 != null) {
|
pSongIds.add(id1)
|
||||||
pSongIds.add(id1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
responseChecker.callWithResponseCheck { api ->
|
responseChecker.callWithResponseCheck { api ->
|
||||||
api.createPlaylist(id, name, pSongIds.toList()).execute()
|
api.createPlaylist(id, name, pSongIds.toList()).execute()
|
||||||
|
@ -400,8 +386,8 @@ open class RESTMusicService(
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getLyrics(
|
override fun getLyrics(
|
||||||
artist: String?,
|
artist: String,
|
||||||
title: String?
|
title: String
|
||||||
): Lyrics {
|
): Lyrics {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = responseChecker.callWithResponseCheck { api ->
|
||||||
api.getLyrics(artist, title).execute()
|
api.getLyrics(artist, title).execute()
|
||||||
|
@ -587,7 +573,7 @@ open class RESTMusicService(
|
||||||
): Pair<InputStream, Boolean> {
|
): Pair<InputStream, Boolean> {
|
||||||
val songOffset = if (offset < 0) 0 else offset
|
val songOffset = if (offset < 0) 0 else offset
|
||||||
|
|
||||||
val response = subsonicAPIClient.stream(song.id!!, maxBitrate, songOffset)
|
val response = subsonicAPIClient.stream(song.id, maxBitrate, songOffset)
|
||||||
checkStreamResponseError(response)
|
checkStreamResponseError(response)
|
||||||
|
|
||||||
if (response.stream == null) {
|
if (response.stream == null) {
|
||||||
|
@ -704,7 +690,7 @@ open class RESTMusicService(
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getGenres(
|
override fun getGenres(
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): List<Genre> {
|
): List<Genre>? {
|
||||||
val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() }
|
val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() }
|
||||||
|
|
||||||
return response.body()!!.genresList.toDomainEntityList()
|
return response.body()!!.genresList.toDomainEntityList()
|
||||||
|
@ -883,7 +869,6 @@ open class RESTMusicService(
|
||||||
companion object {
|
companion object {
|
||||||
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
|
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
|
||||||
private const val INDEXES_STORAGE_NAME = "indexes"
|
private const val INDEXES_STORAGE_NAME = "indexes"
|
||||||
private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"
|
|
||||||
private const val ARTISTS_STORAGE_NAME = "artists"
|
private const val ARTISTS_STORAGE_NAME = "artists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
@ -20,7 +19,6 @@ import org.moire.ultrasonic.util.Util
|
||||||
* Retrieves a list of songs and adds them to the now playing list
|
* Retrieves a list of songs and adds them to the now playing list
|
||||||
*/
|
*/
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
@KoinApiExtension
|
|
||||||
class DownloadHandler(
|
class DownloadHandler(
|
||||||
val mediaPlayerController: MediaPlayerController,
|
val mediaPlayerController: MediaPlayerController,
|
||||||
val networkAndStorageChecker: NetworkAndStorageChecker
|
val networkAndStorageChecker: NetworkAndStorageChecker
|
||||||
|
@ -226,7 +224,7 @@ class DownloadHandler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
root = musicService.getPlaylist(id, name)
|
root = musicService.getPlaylist(id, name!!)
|
||||||
}
|
}
|
||||||
getSongsRecursively(root, songs)
|
getSongsRecursively(root, songs)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package org.moire.ultrasonic.subsonic
|
package org.moire.ultrasonic.subsonic
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.koin.java.KoinJavaComponent.get
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
import org.moire.ultrasonic.featureflags.Feature
|
import org.moire.ultrasonic.featureflags.Feature
|
||||||
import org.moire.ultrasonic.featureflags.FeatureStorage
|
import org.moire.ultrasonic.featureflags.FeatureStorage
|
||||||
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
|
|
||||||
import org.moire.ultrasonic.util.ImageLoader
|
import org.moire.ultrasonic.util.ImageLoader
|
||||||
import org.moire.ultrasonic.util.LegacyImageLoader
|
import org.moire.ultrasonic.util.LegacyImageLoader
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
@ -12,7 +12,7 @@ import org.moire.ultrasonic.util.Util
|
||||||
/**
|
/**
|
||||||
* Handles the lifetime of the Image Loader
|
* Handles the lifetime of the Image Loader
|
||||||
*/
|
*/
|
||||||
class ImageLoaderProvider(val context: Context) {
|
class ImageLoaderProvider(val context: Context) : KoinComponent {
|
||||||
private var imageLoader: ImageLoader? = null
|
private var imageLoader: ImageLoader? = null
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
@ -33,12 +33,12 @@ class ImageLoaderProvider(val context: Context) {
|
||||||
context,
|
context,
|
||||||
Util.getImageLoaderConcurrency()
|
Util.getImageLoaderConcurrency()
|
||||||
)
|
)
|
||||||
val isNewImageLoaderEnabled = get(FeatureStorage::class.java)
|
val features: FeatureStorage = get()
|
||||||
.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER)
|
val isNewImageLoaderEnabled = features.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER)
|
||||||
imageLoader = if (isNewImageLoaderEnabled) {
|
imageLoader = if (isNewImageLoaderEnabled) {
|
||||||
SubsonicImageLoaderProxy(
|
SubsonicImageLoaderProxy(
|
||||||
legacyImageLoader,
|
legacyImageLoader,
|
||||||
get(SubsonicImageLoader::class.java)
|
get()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
legacyImageLoader
|
legacyImageLoader
|
||||||
|
|
|
@ -68,9 +68,11 @@ class ShareHandler(val context: Context) {
|
||||||
) {
|
) {
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
override fun doInBackground(): Share {
|
override fun doInBackground(): Share {
|
||||||
val ids: MutableList<String?> = ArrayList()
|
val ids: MutableList<String> = ArrayList()
|
||||||
if (shareDetails.Entries.isEmpty()) {
|
if (shareDetails.Entries.isEmpty()) {
|
||||||
ids.add(fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID))
|
fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID)?.let {
|
||||||
|
ids.add(it)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for ((id) in shareDetails.Entries) {
|
for ((id) in shareDetails.Entries) {
|
||||||
ids.add(id)
|
ids.add(id)
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* TimeLimitedCache.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import java.lang.ref.SoftReference
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class TimeLimitedCache<T>(expiresAfter: Long = 60L, timeUnit: TimeUnit = TimeUnit.MINUTES) {
|
||||||
|
private var value: SoftReference<T>? = null
|
||||||
|
private val expiresMillis: Long = TimeUnit.MILLISECONDS.convert(expiresAfter, timeUnit)
|
||||||
|
private var expires: Long = 0
|
||||||
|
|
||||||
|
fun get(): T? {
|
||||||
|
return if (System.currentTimeMillis() < expires) value!!.get() else null
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun set(value: T, ttl: Long = expiresMillis, timeUnit: TimeUnit = TimeUnit.MILLISECONDS) {
|
||||||
|
this.value = SoftReference(value)
|
||||||
|
expires = System.currentTimeMillis() + timeUnit.toMillis(ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
expires = 0L
|
||||||
|
value = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,8 +24,9 @@ import android.graphics.drawable.Drawable
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.Checkable
|
import android.widget.Checkable
|
||||||
import org.koin.java.KoinJavaComponent.get
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.core.component.get
|
||||||
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
@ -42,7 +43,7 @@ import timber.log.Timber
|
||||||
/**
|
/**
|
||||||
* Used to display songs and videos in a `ListView`.
|
* Used to display songs and videos in a `ListView`.
|
||||||
*/
|
*/
|
||||||
class SongView(context: Context) : UpdateView(context), Checkable {
|
class SongView(context: Context) : UpdateView(context), Checkable, KoinComponent {
|
||||||
|
|
||||||
var entry: MusicDirectory.Entry? = null
|
var entry: MusicDirectory.Entry? = null
|
||||||
private set
|
private set
|
||||||
|
@ -55,10 +56,9 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||||
private var downloadFile: DownloadFile? = null
|
private var downloadFile: DownloadFile? = null
|
||||||
private var playing = false
|
private var playing = false
|
||||||
private var viewHolder: SongViewHolder? = null
|
private var viewHolder: SongViewHolder? = null
|
||||||
|
private val features: FeatureStorage = get()
|
||||||
private val useFiveStarRating: Boolean =
|
private val useFiveStarRating: Boolean = features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
|
||||||
get(FeatureStorage::class.java).isFeatureEnabled(Feature.FIVE_STAR_RATING)
|
private val mediaPlayerController: MediaPlayerController by inject()
|
||||||
private val mediaPlayerControllerLazy = inject(MediaPlayerController::class.java)
|
|
||||||
|
|
||||||
fun setLayout(song: MusicDirectory.Entry) {
|
fun setLayout(song: MusicDirectory.Entry) {
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||||
updateBackground()
|
updateBackground()
|
||||||
|
|
||||||
entry = song
|
entry = song
|
||||||
downloadFile = mediaPlayerControllerLazy.value.getDownloadFileForSong(song)
|
downloadFile = mediaPlayerController.getDownloadFileForSong(song)
|
||||||
|
|
||||||
val artist = StringBuilder(60)
|
val artist = StringBuilder(60)
|
||||||
var bitRate: String? = null
|
var bitRate: String? = null
|
||||||
|
@ -223,7 +223,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||||
public override fun update() {
|
public override fun update() {
|
||||||
updateBackground()
|
updateBackground()
|
||||||
|
|
||||||
downloadFile = mediaPlayerControllerLazy.value.getDownloadFileForSong(entry)
|
downloadFile = mediaPlayerController.getDownloadFileForSong(entry)
|
||||||
|
|
||||||
updateDownloadStatus(downloadFile!!)
|
updateDownloadStatus(downloadFile!!)
|
||||||
|
|
||||||
|
@ -254,7 +254,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||||
if (rating > 4) starDrawable else starHollowDrawable
|
if (rating > 4) starDrawable else starHollowDrawable
|
||||||
)
|
)
|
||||||
|
|
||||||
val playing = mediaPlayerControllerLazy.value.currentPlaying === downloadFile
|
val playing = mediaPlayerController.currentPlaying === downloadFile
|
||||||
|
|
||||||
if (playing) {
|
if (playing) {
|
||||||
if (!this.playing) {
|
if (!this.playing) {
|
||||||
|
|
Loading…
Reference in New Issue