mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-11 09:10:57 +01:00
Merge remote-tracking branch 'base/develop' into check-server-features
This commit is contained in:
commit
d8e7b991cd
@ -35,6 +35,13 @@ allprojects {
|
||||
google()
|
||||
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'
|
||||
|
@ -9,8 +9,22 @@ data class Artist(
|
||||
var coverArt: String? = null,
|
||||
var albumCount: Long? = null,
|
||||
var closeness: Int = 0
|
||||
) : Serializable, GenericEntry() {
|
||||
) : Serializable, GenericEntry(), Comparable<Artist> {
|
||||
companion object {
|
||||
private const val serialVersionUID = -5790532593784846982L
|
||||
}
|
||||
|
||||
override fun compareTo(other: Artist): Int {
|
||||
when {
|
||||
this.closeness == other.closeness -> {
|
||||
return 0
|
||||
}
|
||||
this.closeness > other.closeness -> {
|
||||
return -1
|
||||
}
|
||||
else -> {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class MusicDirectory {
|
||||
}
|
||||
|
||||
data class Entry(
|
||||
override var id: String? = null,
|
||||
override var id: String,
|
||||
var parent: String? = null,
|
||||
var isDirectory: Boolean = false,
|
||||
var title: String? = null,
|
||||
@ -66,7 +66,7 @@ class MusicDirectory {
|
||||
var bookmarkPosition: Int = 0,
|
||||
var userRating: Int? = null,
|
||||
var averageRating: Float? = null
|
||||
) : Serializable, GenericEntry() {
|
||||
) : Serializable, GenericEntry(), Comparable<Entry> {
|
||||
fun setDuration(duration: Long) {
|
||||
this.duration = duration.toInt()
|
||||
}
|
||||
@ -74,5 +74,19 @@ class MusicDirectory {
|
||||
companion object {
|
||||
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")
|
||||
|
||||
val response = apiClient.getAvatar(username)
|
||||
if (response.hasError()) {
|
||||
if (response.hasError() || response.stream == null) {
|
||||
throw IOException("${response.apiError}")
|
||||
} 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")
|
||||
|
||||
val response = apiClient.getCoverArt(id)
|
||||
if (response.hasError()) {
|
||||
if (response.hasError() || response.stream == null) {
|
||||
throw IOException("${response.apiError}")
|
||||
} 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")
|
||||
return md5Digest.digest(
|
||||
"$password$salt".toByteArray()
|
||||
).toHexBytes().toLowerCase(Locale.getDefault())
|
||||
).toHexBytes().lowercase(Locale.getDefault())
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
throw IllegalStateException(e)
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ ext.versions = [
|
||||
androidxcore : "1.5.0",
|
||||
ktlint : "0.37.1",
|
||||
ktlintGradle : "9.2.1",
|
||||
detekt : "1.17.0",
|
||||
detekt : "1.17.1",
|
||||
jacoco : "0.8.7",
|
||||
preferences : "1.1.1",
|
||||
media : "1.3.1",
|
||||
@ -20,16 +20,16 @@ ext.versions = [
|
||||
androidSupportDesign : "1.3.0",
|
||||
constraintLayout : "2.0.4",
|
||||
multidex : "2.0.1",
|
||||
room : "2.2.6",
|
||||
kotlin : "1.4.32",
|
||||
kotlinxCoroutines : "1.4.3-native-mt",
|
||||
room : "2.3.0",
|
||||
kotlin : "1.5.10",
|
||||
kotlinxCoroutines : "1.5.0-native-mt",
|
||||
viewModelKtx : "2.2.0",
|
||||
|
||||
retrofit : "2.6.4",
|
||||
jackson : "2.9.5",
|
||||
okhttp : "3.12.13",
|
||||
twitterSerial : "0.1.6",
|
||||
koin : "2.2.2",
|
||||
koin : "3.0.2",
|
||||
picasso : "2.71828",
|
||||
sortListView : "1.0.1",
|
||||
|
||||
|
@ -67,18 +67,10 @@
|
||||
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
||||
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||
<ID>ReturnCount:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</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: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 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>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>
|
||||
@ -89,7 +81,6 @@
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$x: IndexOutOfBoundsException</ID>
|
||||
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception</ID>
|
||||
@ -98,7 +89,6 @@
|
||||
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</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:FragmentTitle.kt$FragmentTitle</ID>
|
||||
</CurrentIssues>
|
||||
|
@ -69,6 +69,8 @@ style:
|
||||
ignorePropertyDeclaration: true
|
||||
UnnecessaryAbstractClass:
|
||||
active: false
|
||||
ReturnCount:
|
||||
max: 3
|
||||
|
||||
comments:
|
||||
active: true
|
||||
|
@ -13,10 +13,6 @@ android {
|
||||
targetSdkVersion versions.targetSdk
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// Sets Java compatibility to Java 8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
|
@ -56,7 +56,6 @@ android {
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs += "-Xopt-in=org.koin.core.component.KoinApiExtension"
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@ -94,7 +93,6 @@ dependencies {
|
||||
implementation other.kotlinStdlib
|
||||
implementation other.kotlinxCoroutines
|
||||
implementation other.koinAndroid
|
||||
implementation other.koinViewModel
|
||||
implementation other.okhttpLogging
|
||||
implementation other.fastScroll
|
||||
implementation other.sortListView
|
||||
|
@ -23,72 +23,6 @@
|
||||
column="55"/>
|
||||
</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
|
||||
id="InlinedApi"
|
||||
message="Field requires API level 16 (current min is 14): `android.Manifest.permission#READ_EXTERNAL_STORAGE`"
|
||||
@ -484,17 +418,6 @@
|
||||
column="5"/>
|
||||
</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
|
||||
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"
|
||||
|
@ -162,7 +162,8 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
|
@ -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 com.google.android.material.navigation.NavigationView
|
||||
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.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
@ -126,7 +126,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
val dest: String = try {
|
||||
resources.getResourceName(destination.id)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
} catch (ignored: Resources.NotFoundException) {
|
||||
destination.id.toString()
|
||||
}
|
||||
Timber.d("Navigated to $dest")
|
||||
|
@ -2,7 +2,7 @@ package org.moire.ultrasonic.di
|
||||
|
||||
import androidx.room.Room
|
||||
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.dsl.module
|
||||
import org.moire.ultrasonic.data.AppDatabase
|
||||
|
@ -13,8 +13,7 @@ internal val dateFormat: DateFormat by lazy {
|
||||
SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
}
|
||||
|
||||
fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry().apply {
|
||||
id = this@toDomainEntity.id
|
||||
fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry(id).apply {
|
||||
parent = this@toDomainEntity.parent
|
||||
isDirectory = this@toDomainEntity.isDir
|
||||
title = this@toDomainEntity.title
|
||||
|
@ -6,7 +6,6 @@ import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
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
|
||||
* TODO: Check refresh is working
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class AlbumListFragment : GenericListFragment<MusicDirectory.Entry, AlbumRowAdapter>() {
|
||||
|
||||
/**
|
||||
|
@ -5,14 +5,12 @@ import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Util
|
||||
|
||||
@KoinApiExtension
|
||||
class AlbumListModel(application: Application) : GenericListModel(application) {
|
||||
|
||||
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()
|
||||
|
@ -57,7 +57,7 @@ class AlbumRowAdapter(
|
||||
|
||||
imageLoader.loadImage(
|
||||
holder.coverArt,
|
||||
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
|
||||
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
||||
false, 0, false, true, R.drawable.unknown_album
|
||||
)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package org.moire.ultrasonic.fragment
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
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
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
|
||||
|
||||
/**
|
||||
|
@ -23,14 +23,12 @@ import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
|
||||
/**
|
||||
* Provides ViewModel which contains the list of available Artists
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class ArtistListModel(application: Application) : GenericListModel(application) {
|
||||
private val artists: MutableLiveData<List<Artist>> = MutableLiveData()
|
||||
|
||||
|
@ -62,7 +62,7 @@ class ArtistRowAdapter(
|
||||
holder.coverArt.visibility = View.VISIBLE
|
||||
imageLoader.loadImage(
|
||||
holder.coverArt,
|
||||
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
|
||||
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
||||
false, 0, false, true, R.drawable.ic_contact_picture
|
||||
)
|
||||
} else {
|
||||
@ -96,7 +96,7 @@ class ArtistRowAdapter(
|
||||
}
|
||||
|
||||
private fun getSectionFromName(name: String): String {
|
||||
var section = name.first().toUpperCase()
|
||||
var section = name.first().uppercaseChar()
|
||||
if (!section.isLetter()) section = '#'
|
||||
return section.toString()
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
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.R
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||
|
@ -13,8 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.viewmodel.ext.android.viewModel
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
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 TA: The Adapter to use (must extend GenericRowAdapter)
|
||||
*/
|
||||
@KoinApiExtension
|
||||
abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> : Fragment() {
|
||||
internal val activeServerProvider: ActiveServerProvider by inject()
|
||||
internal val serverSettingsModel: ServerSettingsModel by viewModel()
|
||||
|
@ -15,7 +15,6 @@ import java.net.UnknownHostException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
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
|
||||
*/
|
||||
@KoinApiExtension
|
||||
open class GenericListModel(application: Application) :
|
||||
AndroidViewModel(application), KoinComponent {
|
||||
|
||||
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
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.data.ActiveServerProvider
|
||||
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.navigation.Navigation
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import java.security.SecureRandom
|
||||
import java.util.Collections
|
||||
import java.util.Random
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
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.
|
||||
* TODO: Refactor this fragment and model to extend the GenericListFragment
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class TrackCollectionFragment : Fragment() {
|
||||
|
||||
private var refreshAlbumListView: SwipeRefreshLayout? = null
|
||||
@ -92,7 +89,7 @@ class TrackCollectionFragment : Fragment() {
|
||||
private var cancellationToken: CancellationToken? = null
|
||||
|
||||
private val model: TrackCollectionModel by viewModels()
|
||||
private val random: Random = SecureRandom()
|
||||
private val random: Random = Random()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Util.applyTheme(this.context)
|
||||
@ -258,7 +255,7 @@ class TrackCollectionFragment : Fragment() {
|
||||
model.getMusicFolders(refresh)
|
||||
|
||||
if (playlistId != null) {
|
||||
setTitle(playlistName)
|
||||
setTitle(playlistName!!)
|
||||
model.getPlaylist(playlistId, playlistName)
|
||||
} else if (podcastChannelId != null) {
|
||||
setTitle(getString(R.string.podcasts_label))
|
||||
@ -282,12 +279,12 @@ class TrackCollectionFragment : Fragment() {
|
||||
setTitle(name)
|
||||
if (!isOffline() && Util.getShouldUseId3Tags()) {
|
||||
if (isAlbum) {
|
||||
model.getAlbum(refresh, id, name, parentId)
|
||||
model.getAlbum(refresh, id!!, name, parentId)
|
||||
} else {
|
||||
model.getArtist(refresh, id, name)
|
||||
model.getArtist(refresh, id!!, name)
|
||||
}
|
||||
} 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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
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
|
||||
* TODO: Refactor this model to extend the GenericListModel
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class TrackCollectionModel(application: Application) : GenericListModel(application) {
|
||||
|
||||
private val allSongsId = "-1"
|
||||
@ -43,7 +41,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||
|
||||
suspend fun getMusicDirectory(
|
||||
refresh: Boolean,
|
||||
id: String?,
|
||||
id: String,
|
||||
name: String?,
|
||||
parentId: String?
|
||||
) {
|
||||
@ -53,7 +51,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||
|
||||
var root = MusicDirectory()
|
||||
|
||||
if (allSongsId == id) {
|
||||
if (allSongsId == id && parentId != null) {
|
||||
val musicDirectory = service.getMusicDirectory(
|
||||
parentId, name, refresh
|
||||
)
|
||||
@ -73,12 +71,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||
musicDirectory.findChild(allSongsId) == null &&
|
||||
hasOnlyFolders(musicDirectory)
|
||||
) {
|
||||
val allSongs = MusicDirectory.Entry()
|
||||
val allSongs = MusicDirectory.Entry(allSongsId)
|
||||
|
||||
allSongs.isDirectory = true
|
||||
allSongs.artist = name
|
||||
allSongs.parent = id
|
||||
allSongs.id = allSongsId
|
||||
allSongs.title = String.format(
|
||||
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,
|
||||
* 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) {
|
||||
val service = MusicServiceFactory.getMusicService()
|
||||
@ -135,12 +132,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||
musicDirectory.findChild(allSongsId) == null &&
|
||||
hasOnlyFolders(musicDirectory)
|
||||
) {
|
||||
val allSongs = MusicDirectory.Entry()
|
||||
val allSongs = MusicDirectory.Entry(allSongsId)
|
||||
|
||||
allSongs.isDirectory = true
|
||||
allSongs.artist = name
|
||||
allSongs.parent = id
|
||||
allSongs.id = allSongsId
|
||||
allSongs.title = String.format(
|
||||
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) {
|
||||
|
||||
@ -162,7 +158,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||
|
||||
val musicDirectory: MusicDirectory
|
||||
|
||||
if (allSongsId == id) {
|
||||
if (allSongsId == id && parentId != null) {
|
||||
val root = MusicDirectory()
|
||||
|
||||
val songs: MutableCollection<MusicDirectory.Entry> = LinkedList()
|
||||
@ -212,9 +208,9 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||
val musicDirectory: MusicDirectory
|
||||
|
||||
if (Util.getShouldUseId3Tags()) {
|
||||
musicDirectory = Util.getSongsFromSearchResult(service.starred2)
|
||||
musicDirectory = Util.getSongsFromSearchResult(service.getStarred2())
|
||||
} else {
|
||||
musicDirectory = Util.getSongsFromSearchResult(service.starred)
|
||||
musicDirectory = Util.getSongsFromSearchResult(service.getStarred())
|
||||
}
|
||||
|
||||
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) {
|
||||
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()
|
||||
return directory.listFiles { t -> t.name.matches(fileNameRegex) }
|
||||
}
|
||||
|
@ -18,7 +18,8 @@ import timber.log.Timber
|
||||
class AudioFocusHandler(private val context: Context) {
|
||||
// TODO: This is a circular reference, try to remove it
|
||||
// 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 {
|
||||
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.OutputStream
|
||||
import java.io.RandomAccessFile
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
@ -36,11 +36,10 @@ import timber.log.Timber
|
||||
* @author Sindre Mehus
|
||||
* @version $Id$
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class DownloadFile(
|
||||
val song: MusicDirectory.Entry,
|
||||
private val save: Boolean
|
||||
) {
|
||||
) : KoinComponent {
|
||||
val partialFile: File
|
||||
val completeFile: File
|
||||
private val saveFile: File = FileUtil.getSongFile(song)
|
||||
@ -59,7 +58,7 @@ class DownloadFile(
|
||||
@Volatile
|
||||
private var completeWhenDone = false
|
||||
|
||||
private val downloader = inject(Downloader::class.java)
|
||||
private val downloader: Downloader by inject()
|
||||
|
||||
val progress: MutableLiveData<Int> = MutableLiveData(0)
|
||||
|
||||
@ -201,7 +200,6 @@ class DownloadFile(
|
||||
return String.format("DownloadFile (%s)", song)
|
||||
}
|
||||
|
||||
@KoinApiExtension
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private inner class DownloadTask : CancellableTask() {
|
||||
override fun execute() {
|
||||
@ -310,7 +308,7 @@ class DownloadFile(
|
||||
}
|
||||
wifiLock?.release()
|
||||
CacheCleaner().cleanSpace()
|
||||
downloader.value.checkDownloads()
|
||||
downloader.checkDownloads()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,6 @@ import java.net.URLEncoder
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||
import org.moire.ultrasonic.audiofx.VisualizerController
|
||||
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
|
||||
*/
|
||||
@KoinApiExtension
|
||||
class LocalMediaPlayer(
|
||||
private val audioFocusHandler: AudioFocusHandler,
|
||||
private val context: Context
|
||||
@ -106,7 +104,7 @@ class LocalMediaPlayer(
|
||||
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId)
|
||||
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
||||
context.sendBroadcast(i)
|
||||
} catch (e: Throwable) {
|
||||
} catch (ignored: Throwable) {
|
||||
// Froyo or lower
|
||||
}
|
||||
mediaPlayerLooper = Looper.myLooper()
|
||||
@ -466,7 +464,7 @@ class LocalMediaPlayer(
|
||||
// the equalizer or visualizer with the player
|
||||
try {
|
||||
nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId
|
||||
} catch (e: Throwable) {
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
|
||||
nextMediaPlayer!!.setDataSource(file.path)
|
||||
|
@ -7,9 +7,9 @@
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import android.content.Intent
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.koin.java.KoinJavaComponent.get
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
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
|
||||
* to control the Media Player implementation.
|
||||
*/
|
||||
@KoinApiExtension
|
||||
@Suppress("TooManyFunctions")
|
||||
class MediaPlayerController(
|
||||
private val downloadQueueSerializer: DownloadQueueSerializer,
|
||||
@ -38,7 +37,7 @@ class MediaPlayerController(
|
||||
private val downloader: Downloader,
|
||||
private val shufflePlayBuffer: ShufflePlayBuffer,
|
||||
private val localMediaPlayer: LocalMediaPlayer
|
||||
) {
|
||||
) : KoinComponent {
|
||||
|
||||
private var created = false
|
||||
var suggestedPlaylistName: String? = null
|
||||
@ -46,8 +45,8 @@ class MediaPlayerController(
|
||||
var showVisualization = false
|
||||
private var autoPlayStart = false
|
||||
|
||||
private val jukeboxMediaPlayer = inject(JukeboxMediaPlayer::class.java).value
|
||||
private val activeServerProvider = inject(ActiveServerProvider::class.java).value
|
||||
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
fun onCreate() {
|
||||
if (created) return
|
||||
@ -429,10 +428,7 @@ class MediaPlayerController(
|
||||
get() {
|
||||
try {
|
||||
val username = activeServerProvider.getActiveServer().userName
|
||||
val (_, _, _, _, _, _, _, _, _, _, _, _, jukeboxRole) = getMusicService().getUser(
|
||||
username
|
||||
)
|
||||
return jukeboxRole
|
||||
return getMusicService().getUser(username).jukeboxRole
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Error getting user information")
|
||||
}
|
||||
@ -465,7 +461,8 @@ class MediaPlayerController(
|
||||
|
||||
@Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
|
||||
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
|
||||
val song = localMediaPlayer.currentPlaying!!.song
|
||||
song.userRating = rating
|
||||
|
@ -24,7 +24,6 @@ import android.view.KeyEvent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.activity.NavigationActivity
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
@ -49,7 +48,6 @@ import timber.log.Timber
|
||||
* Android Foreground Service for playing music
|
||||
* while the rest of the Ultrasonic App is in the background.
|
||||
*/
|
||||
@KoinApiExtension
|
||||
@Suppress("LargeClass")
|
||||
class MediaPlayerService : Service() {
|
||||
private val binder: IBinder = SimpleServiceBinder(this)
|
||||
@ -173,8 +171,7 @@ class MediaPlayerService : Service() {
|
||||
fun setCurrentPlaying(currentPlayingIndex: Int) {
|
||||
try {
|
||||
localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex])
|
||||
} catch (x: IndexOutOfBoundsException) {
|
||||
// Ignored
|
||||
} catch (ignored: IndexOutOfBoundsException) {
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.context.loadKoinModules
|
||||
@ -30,7 +29,6 @@ import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE
|
||||
import org.moire.ultrasonic.di.musicServiceModule
|
||||
|
||||
// TODO Refactor everywhere to use DI way to get MusicService, and then remove this class
|
||||
@KoinApiExtension
|
||||
object MusicServiceFactory : KoinComponent {
|
||||
@JvmStatic
|
||||
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.
|
||||
|
||||
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
|
||||
* RestMusicService.kt
|
||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
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
|
||||
* @author Sindre Mehus
|
||||
*/
|
||||
@Suppress("LargeClass")
|
||||
open class RESTMusicService(
|
||||
private val subsonicAPIClient: SubsonicAPIClient,
|
||||
private val fileStorage: PermanentFileStorage,
|
||||
@ -109,7 +97,7 @@ open class RESTMusicService(
|
||||
override fun getIndexes(
|
||||
musicFolderId: String?,
|
||||
refresh: Boolean
|
||||
): Indexes? {
|
||||
): Indexes {
|
||||
val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "")
|
||||
|
||||
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
|
||||
@ -171,7 +159,7 @@ open class RESTMusicService(
|
||||
id: String,
|
||||
name: String?,
|
||||
refresh: Boolean
|
||||
): MusicDirectory? {
|
||||
): MusicDirectory {
|
||||
val response = responseChecker.callWithResponseCheck { api ->
|
||||
api.getMusicDirectory(id).execute()
|
||||
}
|
||||
@ -268,7 +256,7 @@ open class RESTMusicService(
|
||||
@Throws(Exception::class)
|
||||
override fun getPlaylist(
|
||||
id: String,
|
||||
name: String?
|
||||
name: String
|
||||
): MusicDirectory {
|
||||
val response = responseChecker.callWithResponseCheck { api ->
|
||||
api.getPlaylist(id).execute()
|
||||
@ -282,7 +270,7 @@ open class RESTMusicService(
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun savePlaylist(
|
||||
name: String?,
|
||||
name: String,
|
||||
playlist: MusicDirectory
|
||||
) {
|
||||
val playlistFile = FileUtil.getPlaylistFile(
|
||||
@ -326,16 +314,14 @@ open class RESTMusicService(
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun createPlaylist(
|
||||
id: String?,
|
||||
name: String?,
|
||||
id: String,
|
||||
name: String,
|
||||
entries: List<MusicDirectory.Entry>
|
||||
) {
|
||||
val pSongIds: MutableList<String> = ArrayList(entries.size)
|
||||
|
||||
for ((id1) in entries) {
|
||||
if (id1 != null) {
|
||||
pSongIds.add(id1)
|
||||
}
|
||||
pSongIds.add(id1)
|
||||
}
|
||||
responseChecker.callWithResponseCheck { api ->
|
||||
api.createPlaylist(id, name, pSongIds.toList()).execute()
|
||||
@ -400,8 +386,8 @@ open class RESTMusicService(
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun getLyrics(
|
||||
artist: String?,
|
||||
title: String?
|
||||
artist: String,
|
||||
title: String
|
||||
): Lyrics {
|
||||
val response = responseChecker.callWithResponseCheck { api ->
|
||||
api.getLyrics(artist, title).execute()
|
||||
@ -587,7 +573,7 @@ open class RESTMusicService(
|
||||
): Pair<InputStream, Boolean> {
|
||||
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)
|
||||
|
||||
if (response.stream == null) {
|
||||
@ -704,7 +690,7 @@ open class RESTMusicService(
|
||||
@Throws(Exception::class)
|
||||
override fun getGenres(
|
||||
refresh: Boolean
|
||||
): List<Genre> {
|
||||
): List<Genre>? {
|
||||
val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() }
|
||||
|
||||
return response.body()!!.genresList.toDomainEntityList()
|
||||
@ -883,7 +869,6 @@ open class RESTMusicService(
|
||||
companion object {
|
||||
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
|
||||
private const val INDEXES_STORAGE_NAME = "indexes"
|
||||
private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"
|
||||
private const val ARTISTS_STORAGE_NAME = "artists"
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import java.util.Collections
|
||||
import java.util.LinkedList
|
||||
import org.koin.core.component.KoinApiExtension
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
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
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@KoinApiExtension
|
||||
class DownloadHandler(
|
||||
val mediaPlayerController: MediaPlayerController,
|
||||
val networkAndStorageChecker: NetworkAndStorageChecker
|
||||
@ -226,7 +224,7 @@ class DownloadHandler(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
root = musicService.getPlaylist(id, name)
|
||||
root = musicService.getPlaylist(id, name!!)
|
||||
}
|
||||
getSongsRecursively(root, songs)
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.moire.ultrasonic.subsonic
|
||||
|
||||
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.FeatureStorage
|
||||
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
|
||||
import org.moire.ultrasonic.util.ImageLoader
|
||||
import org.moire.ultrasonic.util.LegacyImageLoader
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -12,7 +12,7 @@ import org.moire.ultrasonic.util.Util
|
||||
/**
|
||||
* Handles the lifetime of the Image Loader
|
||||
*/
|
||||
class ImageLoaderProvider(val context: Context) {
|
||||
class ImageLoaderProvider(val context: Context) : KoinComponent {
|
||||
private var imageLoader: ImageLoader? = null
|
||||
|
||||
@Synchronized
|
||||
@ -33,12 +33,12 @@ class ImageLoaderProvider(val context: Context) {
|
||||
context,
|
||||
Util.getImageLoaderConcurrency()
|
||||
)
|
||||
val isNewImageLoaderEnabled = get(FeatureStorage::class.java)
|
||||
.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER)
|
||||
val features: FeatureStorage = get()
|
||||
val isNewImageLoaderEnabled = features.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER)
|
||||
imageLoader = if (isNewImageLoaderEnabled) {
|
||||
SubsonicImageLoaderProxy(
|
||||
legacyImageLoader,
|
||||
get(SubsonicImageLoader::class.java)
|
||||
get()
|
||||
)
|
||||
} else {
|
||||
legacyImageLoader
|
||||
|
@ -68,9 +68,11 @@ class ShareHandler(val context: Context) {
|
||||
) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Share {
|
||||
val ids: MutableList<String?> = ArrayList()
|
||||
val ids: MutableList<String> = ArrayList()
|
||||
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 {
|
||||
for ((id) in shareDetails.Entries) {
|
||||
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.view.LayoutInflater
|
||||
import android.widget.Checkable
|
||||
import org.koin.java.KoinJavaComponent.get
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
@ -42,7 +43,7 @@ import timber.log.Timber
|
||||
/**
|
||||
* 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
|
||||
private set
|
||||
@ -55,10 +56,9 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||
private var downloadFile: DownloadFile? = null
|
||||
private var playing = false
|
||||
private var viewHolder: SongViewHolder? = null
|
||||
|
||||
private val useFiveStarRating: Boolean =
|
||||
get(FeatureStorage::class.java).isFeatureEnabled(Feature.FIVE_STAR_RATING)
|
||||
private val mediaPlayerControllerLazy = inject(MediaPlayerController::class.java)
|
||||
private val features: FeatureStorage = get()
|
||||
private val useFiveStarRating: Boolean = features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
|
||||
private val mediaPlayerController: MediaPlayerController by inject()
|
||||
|
||||
fun setLayout(song: MusicDirectory.Entry) {
|
||||
|
||||
@ -96,7 +96,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||
updateBackground()
|
||||
|
||||
entry = song
|
||||
downloadFile = mediaPlayerControllerLazy.value.getDownloadFileForSong(song)
|
||||
downloadFile = mediaPlayerController.getDownloadFileForSong(song)
|
||||
|
||||
val artist = StringBuilder(60)
|
||||
var bitRate: String? = null
|
||||
@ -223,7 +223,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||
public override fun update() {
|
||||
updateBackground()
|
||||
|
||||
downloadFile = mediaPlayerControllerLazy.value.getDownloadFileForSong(entry)
|
||||
downloadFile = mediaPlayerController.getDownloadFileForSong(entry)
|
||||
|
||||
updateDownloadStatus(downloadFile!!)
|
||||
|
||||
@ -254,7 +254,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
|
||||
if (rating > 4) starDrawable else starHollowDrawable
|
||||
)
|
||||
|
||||
val playing = mediaPlayerControllerLazy.value.currentPlaying === downloadFile
|
||||
val playing = mediaPlayerController.currentPlaying === downloadFile
|
||||
|
||||
if (playing) {
|
||||
if (!this.playing) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user