commit
bf013c02d6
|
@ -9,8 +9,22 @@ data class Artist(
|
||||||
var coverArt: String? = null,
|
var coverArt: String? = null,
|
||||||
var albumCount: Long? = null,
|
var albumCount: Long? = null,
|
||||||
var closeness: Int = 0
|
var closeness: Int = 0
|
||||||
) : Serializable, GenericEntry() {
|
) : Serializable, GenericEntry(), Comparable<Artist> {
|
||||||
companion object {
|
companion object {
|
||||||
private const val serialVersionUID = -5790532593784846982L
|
private const val serialVersionUID = -5790532593784846982L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: Artist): Int {
|
||||||
|
when {
|
||||||
|
this.closeness == other.closeness -> {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
this.closeness > other.closeness -> {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ class MusicDirectory {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Entry(
|
data class Entry(
|
||||||
override var id: String? = null,
|
override var id: String,
|
||||||
var parent: String? = null,
|
var parent: String? = null,
|
||||||
var isDirectory: Boolean = false,
|
var isDirectory: Boolean = false,
|
||||||
var title: String? = null,
|
var title: String? = null,
|
||||||
|
@ -66,7 +66,7 @@ class MusicDirectory {
|
||||||
var bookmarkPosition: Int = 0,
|
var bookmarkPosition: Int = 0,
|
||||||
var userRating: Int? = null,
|
var userRating: Int? = null,
|
||||||
var averageRating: Float? = null
|
var averageRating: Float? = null
|
||||||
) : Serializable, GenericEntry() {
|
) : Serializable, GenericEntry(), Comparable<Entry> {
|
||||||
fun setDuration(duration: Long) {
|
fun setDuration(duration: Long) {
|
||||||
this.duration = duration.toInt()
|
this.duration = duration.toInt()
|
||||||
}
|
}
|
||||||
|
@ -74,5 +74,19 @@ class MusicDirectory {
|
||||||
companion object {
|
companion object {
|
||||||
private const val serialVersionUID = -3339106650010798108L
|
private const val serialVersionUID = -3339106650010798108L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: Entry): Int {
|
||||||
|
when {
|
||||||
|
this.closeness == other.closeness -> {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
this.closeness > other.closeness -> {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,10 @@ class AvatarRequestHandler(
|
||||||
?: throw IllegalArgumentException("Nullable username")
|
?: throw IllegalArgumentException("Nullable username")
|
||||||
|
|
||||||
val response = apiClient.getAvatar(username)
|
val response = apiClient.getAvatar(username)
|
||||||
if (response.hasError()) {
|
if (response.hasError() || response.stream == null) {
|
||||||
throw IOException("${response.apiError}")
|
throw IOException("${response.apiError}")
|
||||||
} else {
|
} else {
|
||||||
return Result(Okio.source(response.stream), Picasso.LoadedFrom.NETWORK)
|
return Result(Okio.source(response.stream!!), Picasso.LoadedFrom.NETWORK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,10 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request
|
||||||
?: throw IllegalArgumentException("Nullable id")
|
?: throw IllegalArgumentException("Nullable id")
|
||||||
|
|
||||||
val response = apiClient.getCoverArt(id)
|
val response = apiClient.getCoverArt(id)
|
||||||
if (response.hasError()) {
|
if (response.hasError() || response.stream == null) {
|
||||||
throw IOException("${response.apiError}")
|
throw IOException("${response.apiError}")
|
||||||
} else {
|
} else {
|
||||||
return Result(Okio.source(response.stream), NETWORK)
|
return Result(Okio.source(response.stream!!), NETWORK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,18 +67,10 @@
|
||||||
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
|
||||||
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
|
||||||
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
|
||||||
<ID>ReturnCount:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</ID>
|
|
||||||
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
|
||||||
<ID>ReturnCount:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
|
|
||||||
<ID>ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
|
|
||||||
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
|
||||||
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
|
||||||
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
|
||||||
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
|
||||||
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
|
|
||||||
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { // Froyo or lower }</ID>
|
|
||||||
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { }</ID>
|
|
||||||
<ID>SwallowedException:MediaPlayerService.kt$MediaPlayerService$catch (x: IndexOutOfBoundsException) { // Ignored }</ID>
|
|
||||||
<ID>SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() }</ID>
|
<ID>SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() }</ID>
|
||||||
<ID>ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>)</ID>
|
<ID>ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>)</ID>
|
||||||
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
|
||||||
|
@ -89,7 +81,6 @@
|
||||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
|
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$x: IndexOutOfBoundsException</ID>
|
|
||||||
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
|
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
|
||||||
<ID>TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception</ID>
|
||||||
|
@ -98,7 +89,6 @@
|
||||||
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
||||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||||
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
||||||
<ID>UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"</ID>
|
|
||||||
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
|
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
|
||||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||||
</CurrentIssues>
|
</CurrentIssues>
|
||||||
|
|
|
@ -69,6 +69,8 @@ style:
|
||||||
ignorePropertyDeclaration: true
|
ignorePropertyDeclaration: true
|
||||||
UnnecessaryAbstractClass:
|
UnnecessaryAbstractClass:
|
||||||
active: false
|
active: false
|
||||||
|
ReturnCount:
|
||||||
|
max: 3
|
||||||
|
|
||||||
comments:
|
comments:
|
||||||
active: true
|
active: true
|
||||||
|
|
|
@ -23,72 +23,6 @@
|
||||||
column="55"/>
|
column="55"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" String lhs = lhsArtist.getName().toLowerCase();"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="97"
|
|
||||||
column="37"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" String rhs = rhsArtist.getName().toLowerCase();"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="98"
|
|
||||||
column="37"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" int index = lhs.indexOf(String.format("%s ", article.toLowerCase()));"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="115"
|
|
||||||
column="58"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" index = rhs.indexOf(String.format("%s ", article.toLowerCase()));"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="122"
|
|
||||||
column="54"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" String query = criteria.getQuery().toLowerCase();"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="466"
|
|
||||||
column="38"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
|
||||||
id="DefaultLocale"
|
|
||||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
|
||||||
errorLine1=" String[] nameParts = COMPILE.split(name.toLowerCase());"
|
|
||||||
errorLine2=" ~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="468"
|
|
||||||
column="43"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="InlinedApi"
|
id="InlinedApi"
|
||||||
message="Field requires API level 16 (current min is 14): `android.Manifest.permission#READ_EXTERNAL_STORAGE`"
|
message="Field requires API level 16 (current min is 14): `android.Manifest.permission#READ_EXTERNAL_STORAGE`"
|
||||||
|
@ -484,17 +418,6 @@
|
||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="TrulyRandom"
|
|
||||||
message="Potentially insecure random numbers on Android 4.3 and older. Read https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html for more info."
|
|
||||||
errorLine1=" Random random = new java.security.SecureRandom();"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
|
|
||||||
line="633"
|
|
||||||
column="37"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="AllowAllHostnameVerifier"
|
id="AllowAllHostnameVerifier"
|
||||||
message="Using the `AllowAllHostnameVerifier` HostnameVerifier is unsafe because it always returns true, which could cause insecure network traffic due to trusting TLS/SSL server certificates for wrong hostnames"
|
message="Using the `AllowAllHostnameVerifier` HostnameVerifier is unsafe because it always returns true, which could cause insecure network traffic due to trusting TLS/SSL server certificates for wrong hostnames"
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -124,7 +124,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||||
val dest: String = try {
|
val dest: String = try {
|
||||||
resources.getResourceName(destination.id)
|
resources.getResourceName(destination.id)
|
||||||
} catch (e: Resources.NotFoundException) {
|
} catch (ignored: Resources.NotFoundException) {
|
||||||
destination.id.toString()
|
destination.id.toString()
|
||||||
}
|
}
|
||||||
Timber.d("Navigated to $dest")
|
Timber.d("Navigated to $dest")
|
||||||
|
|
|
@ -13,8 +13,7 @@ internal val dateFormat: DateFormat by lazy {
|
||||||
SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry().apply {
|
fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry(id).apply {
|
||||||
id = this@toDomainEntity.id
|
|
||||||
parent = this@toDomainEntity.parent
|
parent = this@toDomainEntity.parent
|
||||||
isDirectory = this@toDomainEntity.isDir
|
isDirectory = this@toDomainEntity.isDir
|
||||||
title = this@toDomainEntity.title
|
title = this@toDomainEntity.title
|
||||||
|
|
|
@ -57,7 +57,7 @@ class AlbumRowAdapter(
|
||||||
|
|
||||||
imageLoader.loadImage(
|
imageLoader.loadImage(
|
||||||
holder.coverArt,
|
holder.coverArt,
|
||||||
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
|
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
||||||
false, 0, false, true, R.drawable.unknown_album
|
false, 0, false, true, R.drawable.unknown_album
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ class ArtistRowAdapter(
|
||||||
holder.coverArt.visibility = View.VISIBLE
|
holder.coverArt.visibility = View.VISIBLE
|
||||||
imageLoader.loadImage(
|
imageLoader.loadImage(
|
||||||
holder.coverArt,
|
holder.coverArt,
|
||||||
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
|
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
|
||||||
false, 0, false, true, R.drawable.ic_contact_picture
|
false, 0, false, true, R.drawable.ic_contact_picture
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -28,7 +28,6 @@ import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Random
|
import java.util.Random
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
@ -92,7 +91,7 @@ class TrackCollectionFragment : Fragment() {
|
||||||
private var cancellationToken: CancellationToken? = null
|
private var cancellationToken: CancellationToken? = null
|
||||||
|
|
||||||
private val model: TrackCollectionModel by viewModels()
|
private val model: TrackCollectionModel by viewModels()
|
||||||
private val random: Random = SecureRandom()
|
private val random: Random = Random()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Util.applyTheme(this.context)
|
Util.applyTheme(this.context)
|
||||||
|
@ -258,7 +257,7 @@ class TrackCollectionFragment : Fragment() {
|
||||||
model.getMusicFolders(refresh)
|
model.getMusicFolders(refresh)
|
||||||
|
|
||||||
if (playlistId != null) {
|
if (playlistId != null) {
|
||||||
setTitle(playlistName)
|
setTitle(playlistName!!)
|
||||||
model.getPlaylist(playlistId, playlistName)
|
model.getPlaylist(playlistId, playlistName)
|
||||||
} else if (podcastChannelId != null) {
|
} else if (podcastChannelId != null) {
|
||||||
setTitle(getString(R.string.podcasts_label))
|
setTitle(getString(R.string.podcasts_label))
|
||||||
|
@ -282,12 +281,12 @@ class TrackCollectionFragment : Fragment() {
|
||||||
setTitle(name)
|
setTitle(name)
|
||||||
if (!isOffline() && Util.getShouldUseId3Tags()) {
|
if (!isOffline() && Util.getShouldUseId3Tags()) {
|
||||||
if (isAlbum) {
|
if (isAlbum) {
|
||||||
model.getAlbum(refresh, id, name, parentId)
|
model.getAlbum(refresh, id!!, name, parentId)
|
||||||
} else {
|
} else {
|
||||||
model.getArtist(refresh, id, name)
|
model.getArtist(refresh, id!!, name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
model.getMusicDirectory(refresh, id, name, parentId)
|
model.getMusicDirectory(refresh, id!!, name, parentId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
|
|
||||||
suspend fun getMusicDirectory(
|
suspend fun getMusicDirectory(
|
||||||
refresh: Boolean,
|
refresh: Boolean,
|
||||||
id: String?,
|
id: String,
|
||||||
name: String?,
|
name: String?,
|
||||||
parentId: String?
|
parentId: String?
|
||||||
) {
|
) {
|
||||||
|
@ -53,7 +53,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
|
|
||||||
var root = MusicDirectory()
|
var root = MusicDirectory()
|
||||||
|
|
||||||
if (allSongsId == id) {
|
if (allSongsId == id && parentId != null) {
|
||||||
val musicDirectory = service.getMusicDirectory(
|
val musicDirectory = service.getMusicDirectory(
|
||||||
parentId, name, refresh
|
parentId, name, refresh
|
||||||
)
|
)
|
||||||
|
@ -73,12 +73,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
musicDirectory.findChild(allSongsId) == null &&
|
musicDirectory.findChild(allSongsId) == null &&
|
||||||
hasOnlyFolders(musicDirectory)
|
hasOnlyFolders(musicDirectory)
|
||||||
) {
|
) {
|
||||||
val allSongs = MusicDirectory.Entry()
|
val allSongs = MusicDirectory.Entry(allSongsId)
|
||||||
|
|
||||||
allSongs.isDirectory = true
|
allSongs.isDirectory = true
|
||||||
allSongs.artist = name
|
allSongs.artist = name
|
||||||
allSongs.parent = id
|
allSongs.parent = id
|
||||||
allSongs.id = allSongsId
|
|
||||||
allSongs.title = String.format(
|
allSongs.title = String.format(
|
||||||
context.resources.getString(R.string.select_album_all_songs), name
|
context.resources.getString(R.string.select_album_all_songs), name
|
||||||
)
|
)
|
||||||
|
@ -122,7 +121,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
* TODO: This method should be moved to AlbumListModel,
|
* TODO: This method should be moved to AlbumListModel,
|
||||||
* since it displays a list of albums by a specified artist.
|
* since it displays a list of albums by a specified artist.
|
||||||
*/
|
*/
|
||||||
suspend fun getArtist(refresh: Boolean, id: String?, name: String?) {
|
suspend fun getArtist(refresh: Boolean, id: String, name: String?) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
|
@ -135,12 +134,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
musicDirectory.findChild(allSongsId) == null &&
|
musicDirectory.findChild(allSongsId) == null &&
|
||||||
hasOnlyFolders(musicDirectory)
|
hasOnlyFolders(musicDirectory)
|
||||||
) {
|
) {
|
||||||
val allSongs = MusicDirectory.Entry()
|
val allSongs = MusicDirectory.Entry(allSongsId)
|
||||||
|
|
||||||
allSongs.isDirectory = true
|
allSongs.isDirectory = true
|
||||||
allSongs.artist = name
|
allSongs.artist = name
|
||||||
allSongs.parent = id
|
allSongs.parent = id
|
||||||
allSongs.id = allSongsId
|
|
||||||
allSongs.title = String.format(
|
allSongs.title = String.format(
|
||||||
context.resources.getString(R.string.select_album_all_songs), name
|
context.resources.getString(R.string.select_album_all_songs), name
|
||||||
)
|
)
|
||||||
|
@ -154,7 +152,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?) {
|
suspend fun getAlbum(refresh: Boolean, id: String, name: String?, parentId: String?) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
|
@ -162,7 +160,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
|
|
||||||
val musicDirectory: MusicDirectory
|
val musicDirectory: MusicDirectory
|
||||||
|
|
||||||
if (allSongsId == id) {
|
if (allSongsId == id && parentId != null) {
|
||||||
val root = MusicDirectory()
|
val root = MusicDirectory()
|
||||||
|
|
||||||
val songs: MutableCollection<MusicDirectory.Entry> = LinkedList()
|
val songs: MutableCollection<MusicDirectory.Entry> = LinkedList()
|
||||||
|
@ -212,9 +210,9 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
val musicDirectory: MusicDirectory
|
val musicDirectory: MusicDirectory
|
||||||
|
|
||||||
if (Util.getShouldUseId3Tags()) {
|
if (Util.getShouldUseId3Tags()) {
|
||||||
musicDirectory = Util.getSongsFromSearchResult(service.starred2)
|
musicDirectory = Util.getSongsFromSearchResult(service.getStarred2())
|
||||||
} else {
|
} else {
|
||||||
musicDirectory = Util.getSongsFromSearchResult(service.starred)
|
musicDirectory = Util.getSongsFromSearchResult(service.getStarred())
|
||||||
}
|
}
|
||||||
|
|
||||||
currentDirectory.postValue(musicDirectory)
|
currentDirectory.postValue(musicDirectory)
|
||||||
|
@ -241,7 +239,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPlaylist(playlistId: String, playlistName: String?) {
|
suspend fun getPlaylist(playlistId: String, playlistName: String) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
|
|
|
@ -161,7 +161,7 @@ class FileLoggerTree : Timber.DebugTree() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLogFileList(): Array<File> {
|
private fun getLogFileList(): Array<out File>? {
|
||||||
val directory = FileUtil.getUltrasonicDirectory()
|
val directory = FileUtil.getUltrasonicDirectory()
|
||||||
return directory.listFiles { t -> t.name.matches(fileNameRegex) }
|
return directory.listFiles { t -> t.name.matches(fileNameRegex) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.java.KoinJavaComponent.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 {
|
||||||
|
private val activeServerProvider = inject(
|
||||||
|
ActiveServerProvider::class.java
|
||||||
|
)
|
||||||
|
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.value.getRestUrl(null)
|
||||||
|
val newFolderId = activeServerProvider.value.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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -106,7 +106,7 @@ class LocalMediaPlayer(
|
||||||
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId)
|
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId)
|
||||||
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
||||||
context.sendBroadcast(i)
|
context.sendBroadcast(i)
|
||||||
} catch (e: Throwable) {
|
} catch (ignored: Throwable) {
|
||||||
// Froyo or lower
|
// Froyo or lower
|
||||||
}
|
}
|
||||||
mediaPlayerLooper = Looper.myLooper()
|
mediaPlayerLooper = Looper.myLooper()
|
||||||
|
@ -466,7 +466,7 @@ class LocalMediaPlayer(
|
||||||
// the equalizer or visualizer with the player
|
// the equalizer or visualizer with the player
|
||||||
try {
|
try {
|
||||||
nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId
|
nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId
|
||||||
} catch (e: Throwable) {
|
} catch (ignored: Throwable) {
|
||||||
}
|
}
|
||||||
|
|
||||||
nextMediaPlayer!!.setDataSource(file.path)
|
nextMediaPlayer!!.setDataSource(file.path)
|
||||||
|
|
|
@ -429,10 +429,7 @@ class MediaPlayerController(
|
||||||
get() {
|
get() {
|
||||||
try {
|
try {
|
||||||
val username = activeServerProvider.getActiveServer().userName
|
val username = activeServerProvider.getActiveServer().userName
|
||||||
val (_, _, _, _, _, _, _, _, _, _, _, _, jukeboxRole) = getMusicService().getUser(
|
return getMusicService().getUser(username).jukeboxRole
|
||||||
username
|
|
||||||
)
|
|
||||||
return jukeboxRole
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.w(e, "Error getting user information")
|
Timber.w(e, "Error getting user information")
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import android.view.KeyEvent
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.core.component.KoinApiExtension
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.activity.NavigationActivity
|
import org.moire.ultrasonic.activity.NavigationActivity
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
|
@ -49,7 +48,6 @@ import timber.log.Timber
|
||||||
* Android Foreground Service for playing music
|
* Android Foreground Service for playing music
|
||||||
* while the rest of the Ultrasonic App is in the background.
|
* while the rest of the Ultrasonic App is in the background.
|
||||||
*/
|
*/
|
||||||
@KoinApiExtension
|
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
class MediaPlayerService : Service() {
|
class MediaPlayerService : Service() {
|
||||||
private val binder: IBinder = SimpleServiceBinder(this)
|
private val binder: IBinder = SimpleServiceBinder(this)
|
||||||
|
@ -173,8 +171,7 @@ class MediaPlayerService : Service() {
|
||||||
fun setCurrentPlaying(currentPlayingIndex: Int) {
|
fun setCurrentPlaying(currentPlayingIndex: Int) {
|
||||||
try {
|
try {
|
||||||
localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex])
|
localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex])
|
||||||
} catch (x: IndexOutOfBoundsException) {
|
} catch (ignored: IndexOutOfBoundsException) {
|
||||||
// Ignored
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
/*
|
||||||
|
* MusicService.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import java.io.InputStream
|
||||||
|
import org.moire.ultrasonic.domain.Bookmark
|
||||||
|
import org.moire.ultrasonic.domain.ChatMessage
|
||||||
|
import org.moire.ultrasonic.domain.Genre
|
||||||
|
import org.moire.ultrasonic.domain.Indexes
|
||||||
|
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||||
|
import org.moire.ultrasonic.domain.Lyrics
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
|
import org.moire.ultrasonic.domain.MusicFolder
|
||||||
|
import org.moire.ultrasonic.domain.Playlist
|
||||||
|
import org.moire.ultrasonic.domain.PodcastsChannel
|
||||||
|
import org.moire.ultrasonic.domain.SearchCriteria
|
||||||
|
import org.moire.ultrasonic.domain.SearchResult
|
||||||
|
import org.moire.ultrasonic.domain.Share
|
||||||
|
import org.moire.ultrasonic.domain.UserInfo
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
|
interface MusicService {
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun ping()
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun isLicenseValid(): Boolean
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getGenres(refresh: Boolean): List<Genre>?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun star(id: String?, albumId: String?, artistId: String?)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun unstar(id: String?, albumId: String?, artistId: String?)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun setRating(id: String, rating: Int)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getMusicFolders(refresh: Boolean): List<MusicFolder>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getArtists(refresh: Boolean): Indexes
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun search(criteria: SearchCriteria): SearchResult?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getPlaylist(id: String, name: String): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getPodcastsChannels(refresh: Boolean): List<PodcastsChannel>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getPlaylists(refresh: Boolean): List<Playlist>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun deletePlaylist(id: String)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun updatePlaylist(id: String, name: String?, comment: String?, pub: Boolean)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getLyrics(artist: String, title: String): Lyrics?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun scrobble(id: String, submission: Boolean)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getAlbumList(type: String, size: Int, offset: Int, musicFolderId: String?): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getAlbumList2(
|
||||||
|
type: String,
|
||||||
|
size: Int,
|
||||||
|
offset: Int,
|
||||||
|
musicFolderId: String?
|
||||||
|
): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getRandomSongs(size: Int): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getSongsByGenre(genre: String, count: Int, offset: Int): MusicDirectory
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getStarred(): SearchResult
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getStarred2(): SearchResult
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getCoverArt(
|
||||||
|
entry: MusicDirectory.Entry?,
|
||||||
|
size: Int,
|
||||||
|
saveToFile: Boolean,
|
||||||
|
highQuality: Boolean
|
||||||
|
): Bitmap?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getAvatar(username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean): Bitmap?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return response [InputStream] and a [Boolean] that indicates if this response is
|
||||||
|
* partial.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getDownloadInputStream(
|
||||||
|
song: MusicDirectory.Entry,
|
||||||
|
offset: Long,
|
||||||
|
maxBitrate: Int
|
||||||
|
): Pair<InputStream, Boolean>
|
||||||
|
|
||||||
|
// TODO: Refactor and remove this call (see RestMusicService implementation)
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getVideoUrl(id: String, useFlash: Boolean): String?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun skipJukebox(index: Int, offsetSeconds: Int): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun stopJukebox(): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun startJukebox(): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getJukeboxStatus(): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun setJukeboxGain(gain: Float): JukeboxStatus
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getShares(refresh: Boolean): List<Share>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getChatMessages(since: Long?): List<ChatMessage?>?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun addChatMessage(message: String)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getBookmarks(): List<Bookmark?>?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun deleteBookmark(id: String)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun createBookmark(id: String, position: Int)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getVideos(refresh: Boolean): MusicDirectory?
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getUser(username: String): UserInfo
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun createShare(ids: List<String>, description: String?, expires: Long?): List<Share>
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun deleteShare(id: String)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun updateShare(id: String, description: String?, expires: Long?)
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory?
|
||||||
|
}
|
|
@ -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,697 @@
|
||||||
|
/*
|
||||||
|
* 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.java.KoinJavaComponent.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 {
|
||||||
|
private val activeServerProvider = inject(
|
||||||
|
ActiveServerProvider::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
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!!.toLowerCase(Locale.ROOT)
|
||||||
|
var rhs = rhsArtist.name!!.toLowerCase(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.toLowerCase(Locale.ROOT))
|
||||||
|
)
|
||||||
|
if (index == 0) {
|
||||||
|
lhs = lhs.substring(article.length + 1)
|
||||||
|
}
|
||||||
|
index = rhs.indexOf(
|
||||||
|
String.format(Locale.ROOT, "%s ", article.toLowerCase(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.value.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.toLowerCase(Locale.ROOT)
|
||||||
|
val queryParts = COMPILE.split(query)
|
||||||
|
val nameParts = COMPILE.split(
|
||||||
|
name!!.toLowerCase(Locale.ROOT)
|
||||||
|
)
|
||||||
|
var closeness = 0
|
||||||
|
for (queryPart in queryParts) {
|
||||||
|
for (namePart in nameParts) {
|
||||||
|
if (namePart == queryPart) {
|
||||||
|
closeness++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closeness
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listFilesRecursively(parent: File, children: MutableList<File>) {
|
||||||
|
for (file in FileUtil.listMediaFiles(parent)) {
|
||||||
|
if (file.isFile) {
|
||||||
|
children.add(file)
|
||||||
|
} else {
|
||||||
|
listFilesRecursively(file, children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,8 @@
|
||||||
/*
|
/*
|
||||||
This file is part of Subsonic.
|
* RestMusicService.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
Subsonic is free software: you can redistribute it and/or modify
|
*
|
||||||
it under the terms of the GNU General Public License as published by
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Subsonic is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Copyright 2009 (C) Sindre Mehus
|
|
||||||
*/
|
*/
|
||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
|
@ -64,8 +52,8 @@ import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This Music Service implementation connects to a server using the Subsonic REST API
|
* This Music Service implementation connects to a server using the Subsonic REST API
|
||||||
* @author Sindre Mehus
|
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LargeClass")
|
||||||
open class RESTMusicService(
|
open class RESTMusicService(
|
||||||
private val subsonicAPIClient: SubsonicAPIClient,
|
private val subsonicAPIClient: SubsonicAPIClient,
|
||||||
private val fileStorage: PermanentFileStorage,
|
private val fileStorage: PermanentFileStorage,
|
||||||
|
@ -109,7 +97,7 @@ open class RESTMusicService(
|
||||||
override fun getIndexes(
|
override fun getIndexes(
|
||||||
musicFolderId: String?,
|
musicFolderId: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): Indexes? {
|
): Indexes {
|
||||||
val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "")
|
val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "")
|
||||||
|
|
||||||
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
|
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
|
||||||
|
@ -171,7 +159,7 @@ open class RESTMusicService(
|
||||||
id: String,
|
id: String,
|
||||||
name: String?,
|
name: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): MusicDirectory? {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = responseChecker.callWithResponseCheck { api ->
|
||||||
api.getMusicDirectory(id).execute()
|
api.getMusicDirectory(id).execute()
|
||||||
}
|
}
|
||||||
|
@ -268,7 +256,7 @@ open class RESTMusicService(
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getPlaylist(
|
override fun getPlaylist(
|
||||||
id: String,
|
id: String,
|
||||||
name: String?
|
name: String
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = responseChecker.callWithResponseCheck { api ->
|
||||||
api.getPlaylist(id).execute()
|
api.getPlaylist(id).execute()
|
||||||
|
@ -282,7 +270,7 @@ open class RESTMusicService(
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun savePlaylist(
|
private fun savePlaylist(
|
||||||
name: String?,
|
name: String,
|
||||||
playlist: MusicDirectory
|
playlist: MusicDirectory
|
||||||
) {
|
) {
|
||||||
val playlistFile = FileUtil.getPlaylistFile(
|
val playlistFile = FileUtil.getPlaylistFile(
|
||||||
|
@ -326,16 +314,14 @@ open class RESTMusicService(
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun createPlaylist(
|
override fun createPlaylist(
|
||||||
id: String?,
|
id: String,
|
||||||
name: String?,
|
name: String,
|
||||||
entries: List<MusicDirectory.Entry>
|
entries: List<MusicDirectory.Entry>
|
||||||
) {
|
) {
|
||||||
val pSongIds: MutableList<String> = ArrayList(entries.size)
|
val pSongIds: MutableList<String> = ArrayList(entries.size)
|
||||||
|
|
||||||
for ((id1) in entries) {
|
for ((id1) in entries) {
|
||||||
if (id1 != null) {
|
pSongIds.add(id1)
|
||||||
pSongIds.add(id1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
responseChecker.callWithResponseCheck { api ->
|
responseChecker.callWithResponseCheck { api ->
|
||||||
api.createPlaylist(id, name, pSongIds.toList()).execute()
|
api.createPlaylist(id, name, pSongIds.toList()).execute()
|
||||||
|
@ -400,8 +386,8 @@ open class RESTMusicService(
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getLyrics(
|
override fun getLyrics(
|
||||||
artist: String?,
|
artist: String,
|
||||||
title: String?
|
title: String
|
||||||
): Lyrics {
|
): Lyrics {
|
||||||
val response = responseChecker.callWithResponseCheck { api ->
|
val response = responseChecker.callWithResponseCheck { api ->
|
||||||
api.getLyrics(artist, title).execute()
|
api.getLyrics(artist, title).execute()
|
||||||
|
@ -587,7 +573,7 @@ open class RESTMusicService(
|
||||||
): Pair<InputStream, Boolean> {
|
): Pair<InputStream, Boolean> {
|
||||||
val songOffset = if (offset < 0) 0 else offset
|
val songOffset = if (offset < 0) 0 else offset
|
||||||
|
|
||||||
val response = subsonicAPIClient.stream(song.id!!, maxBitrate, songOffset)
|
val response = subsonicAPIClient.stream(song.id, maxBitrate, songOffset)
|
||||||
checkStreamResponseError(response)
|
checkStreamResponseError(response)
|
||||||
|
|
||||||
if (response.stream == null) {
|
if (response.stream == null) {
|
||||||
|
@ -704,7 +690,7 @@ open class RESTMusicService(
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getGenres(
|
override fun getGenres(
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): List<Genre> {
|
): List<Genre>? {
|
||||||
val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() }
|
val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() }
|
||||||
|
|
||||||
return response.body()!!.genresList.toDomainEntityList()
|
return response.body()!!.genresList.toDomainEntityList()
|
||||||
|
@ -883,7 +869,6 @@ open class RESTMusicService(
|
||||||
companion object {
|
companion object {
|
||||||
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
|
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
|
||||||
private const val INDEXES_STORAGE_NAME = "indexes"
|
private const val INDEXES_STORAGE_NAME = "indexes"
|
||||||
private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"
|
|
||||||
private const val ARTISTS_STORAGE_NAME = "artists"
|
private const val ARTISTS_STORAGE_NAME = "artists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,7 +226,7 @@ class DownloadHandler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
root = musicService.getPlaylist(id, name)
|
root = musicService.getPlaylist(id, name!!)
|
||||||
}
|
}
|
||||||
getSongsRecursively(root, songs)
|
getSongsRecursively(root, songs)
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,9 +68,11 @@ class ShareHandler(val context: Context) {
|
||||||
) {
|
) {
|
||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
override fun doInBackground(): Share {
|
override fun doInBackground(): Share {
|
||||||
val ids: MutableList<String?> = ArrayList()
|
val ids: MutableList<String> = ArrayList()
|
||||||
if (shareDetails.Entries.isEmpty()) {
|
if (shareDetails.Entries.isEmpty()) {
|
||||||
ids.add(fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID))
|
fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID)?.let {
|
||||||
|
ids.add(it)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for ((id) in shareDetails.Entries) {
|
for ((id) in shareDetails.Entries) {
|
||||||
ids.add(id)
|
ids.add(id)
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* TimeLimitedCache.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import java.lang.ref.SoftReference
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class TimeLimitedCache<T>(expiresAfter: Long = 60L, timeUnit: TimeUnit = TimeUnit.MINUTES) {
|
||||||
|
private var value: SoftReference<T>? = null
|
||||||
|
private val expiresMillis: Long = TimeUnit.MILLISECONDS.convert(expiresAfter, timeUnit)
|
||||||
|
private var expires: Long = 0
|
||||||
|
|
||||||
|
fun get(): T? {
|
||||||
|
return if (System.currentTimeMillis() < expires) value!!.get() else null
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun set(value: T, ttl: Long = expiresMillis, timeUnit: TimeUnit = TimeUnit.MILLISECONDS) {
|
||||||
|
this.value = SoftReference(value)
|
||||||
|
expires = System.currentTimeMillis() + timeUnit.toMillis(ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
expires = 0L
|
||||||
|
value = null
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue