Merge remote-tracking branch 'base/develop' into check-server-features

This commit is contained in:
Maxence G 2021-05-29 15:28:25 +02:00
commit d8e7b991cd
No known key found for this signature in database
GPG Key ID: DC1FD9409E3FE284
51 changed files with 1541 additions and 1911 deletions

View File

@ -35,6 +35,13 @@ allprojects {
google()
maven { url 'https://jitpack.io' }
}
// Set Kotlin JVM target to the same for all subprojects
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
apply from: 'gradle_scripts/jacoco.gradle'

View File

@ -9,8 +9,22 @@ data class Artist(
var coverArt: String? = null,
var albumCount: Long? = null,
var closeness: Int = 0
) : Serializable, GenericEntry() {
) : Serializable, GenericEntry(), Comparable<Artist> {
companion object {
private const val serialVersionUID = -5790532593784846982L
}
override fun compareTo(other: Artist): Int {
when {
this.closeness == other.closeness -> {
return 0
}
this.closeness > other.closeness -> {
return -1
}
else -> {
return 1
}
}
}
}

View File

@ -36,7 +36,7 @@ class MusicDirectory {
}
data class Entry(
override var id: String? = null,
override var id: String,
var parent: String? = null,
var isDirectory: Boolean = false,
var title: String? = null,
@ -66,7 +66,7 @@ class MusicDirectory {
var bookmarkPosition: Int = 0,
var userRating: Int? = null,
var averageRating: Float? = null
) : Serializable, GenericEntry() {
) : Serializable, GenericEntry(), Comparable<Entry> {
fun setDuration(duration: Long) {
this.duration = duration.toInt()
}
@ -74,5 +74,19 @@ class MusicDirectory {
companion object {
private const val serialVersionUID = -3339106650010798108L
}
override fun compareTo(other: Entry): Int {
when {
this.closeness == other.closeness -> {
return 0
}
this.closeness > other.closeness -> {
return -1
}
else -> {
return 1
}
}
}
}
}

View File

@ -26,10 +26,10 @@ class AvatarRequestHandler(
?: throw IllegalArgumentException("Nullable username")
val response = apiClient.getAvatar(username)
if (response.hasError()) {
if (response.hasError() || response.stream == null) {
throw IOException("${response.apiError}")
} else {
return Result(Okio.source(response.stream), Picasso.LoadedFrom.NETWORK)
return Result(Okio.source(response.stream!!), Picasso.LoadedFrom.NETWORK)
}
}
}

View File

@ -24,10 +24,10 @@ class CoverArtRequestHandler(private val apiClient: SubsonicAPIClient) : Request
?: throw IllegalArgumentException("Nullable id")
val response = apiClient.getCoverArt(id)
if (response.hasError()) {
if (response.hasError() || response.stream == null) {
throw IOException("${response.apiError}")
} else {
return Result(Okio.source(response.stream), NETWORK)
return Result(Okio.source(response.stream!!), NETWORK)
}
}
}

View File

@ -39,7 +39,7 @@ class PasswordMD5Interceptor(private val password: String) : Interceptor {
val md5Digest = MessageDigest.getInstance("MD5")
return md5Digest.digest(
"$password$salt".toByteArray()
).toHexBytes().toLowerCase(Locale.getDefault())
).toHexBytes().lowercase(Locale.getDefault())
} catch (e: NoSuchAlgorithmException) {
throw IllegalStateException(e)
}

View File

@ -10,7 +10,7 @@ ext.versions = [
androidxcore : "1.5.0",
ktlint : "0.37.1",
ktlintGradle : "9.2.1",
detekt : "1.17.0",
detekt : "1.17.1",
jacoco : "0.8.7",
preferences : "1.1.1",
media : "1.3.1",
@ -20,16 +20,16 @@ ext.versions = [
androidSupportDesign : "1.3.0",
constraintLayout : "2.0.4",
multidex : "2.0.1",
room : "2.2.6",
kotlin : "1.4.32",
kotlinxCoroutines : "1.4.3-native-mt",
room : "2.3.0",
kotlin : "1.5.10",
kotlinxCoroutines : "1.5.0-native-mt",
viewModelKtx : "2.2.0",
retrofit : "2.6.4",
jackson : "2.9.5",
okhttp : "3.12.13",
twitterSerial : "0.1.6",
koin : "2.2.2",
koin : "3.0.2",
picasso : "2.71828",
sortListView : "1.0.1",

View File

@ -67,18 +67,10 @@
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
<ID>ReturnCount:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting</ID>
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
<ID>ReturnCount:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile()</ID>
<ID>ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action?</ID>
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap?</ID>
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { // Froyo or lower }</ID>
<ID>SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { }</ID>
<ID>SwallowedException:MediaPlayerService.kt$MediaPlayerService$catch (x: IndexOutOfBoundsException) { // Ignored }</ID>
<ID>SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() }</ID>
<ID>ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response&lt;out SubsonicResponse&gt;)</ID>
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
@ -89,7 +81,6 @@
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception</ID>
<ID>TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$x: IndexOutOfBoundsException</ID>
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
<ID>TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception</ID>
@ -98,7 +89,6 @@
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"</ID>
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
</CurrentIssues>

View File

@ -69,6 +69,8 @@ style:
ignorePropertyDeclaration: true
UnnecessaryAbstractClass:
active: false
ReturnCount:
max: 3
comments:
active: true

View File

@ -13,10 +13,6 @@ android {
targetSdkVersion versions.targetSdk
}
kotlinOptions {
jvmTarget = "1.8"
}
compileOptions {
// Sets Java compatibility to Java 8
sourceCompatibility JavaVersion.VERSION_1_8

View File

@ -56,7 +56,6 @@ android {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += "-Xopt-in=org.koin.core.component.KoinApiExtension"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -94,7 +93,6 @@ dependencies {
implementation other.kotlinStdlib
implementation other.kotlinxCoroutines
implementation other.koinAndroid
implementation other.koinViewModel
implementation other.okhttpLogging
implementation other.fastScroll
implementation other.sortListView

View File

@ -23,72 +23,6 @@
column="55"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" String lhs = lhsArtist.getName().toLowerCase();"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="97"
column="37"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" String rhs = rhsArtist.getName().toLowerCase();"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="98"
column="37"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" int index = lhs.indexOf(String.format(&quot;%s &quot;, 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(&quot;%s &quot;, article.toLowerCase()));"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="122"
column="54"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" String query = criteria.getQuery().toLowerCase();"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="466"
column="38"/>
</issue>
<issue
id="DefaultLocale"
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
errorLine1=" String[] nameParts = COMPILE.split(name.toLowerCase());"
errorLine2=" ~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="468"
column="43"/>
</issue>
<issue
id="InlinedApi"
message="Field requires API level 16 (current min is 14): `android.Manifest.permission#READ_EXTERNAL_STORAGE`"
@ -484,17 +418,6 @@
column="5"/>
</issue>
<issue
id="TrulyRandom"
message="Potentially insecure random numbers on Android 4.3 and older. Read https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html for more info."
errorLine1=" Random random = new java.security.SecureRandom();"
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java"
line="633"
column="37"/>
</issue>
<issue
id="AllowAllHostnameVerifier"
message="Using the `AllowAllHostnameVerifier` HostnameVerifier is unsafe because it always returns true, which could cause insecure network traffic due to trusting TLS/SSL server certificates for wrong hostnames"

View File

@ -162,7 +162,8 @@ public class PlayerFragment extends Fragment implements GestureDetector.OnGestur
setHasOptionsMenu(true);
useFiveStarRating = KoinJavaComponent.get(FeatureStorage.class).isFeatureEnabled(Feature.FIVE_STAR_RATING);
FeatureStorage features = KoinJavaComponent.get(FeatureStorage.class);
useFiveStarRating = features.isFeatureEnabled(Feature.FIVE_STAR_RATING);
swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100;
swipeVelocity = swipeDistance;

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ import kotlin.Lazy;
import static org.koin.java.KoinJavaComponent.inject;
/**
* Responsible for cleaning up files from the offline download cache on the filesystem
* Responsible for cleaning up files from the offline download cache on the filesystem.
*/
public class CacheCleaner
{

View File

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

View File

@ -28,7 +28,7 @@ import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.google.android.material.navigation.NavigationView
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
@ -126,7 +126,7 @@ class NavigationActivity : AppCompatActivity() {
navController.addOnDestinationChangedListener { _, destination, _ ->
val dest: String = try {
resources.getResourceName(destination.id)
} catch (e: Resources.NotFoundException) {
} catch (ignored: Resources.NotFoundException) {
destination.id.toString()
}
Timber.d("Navigated to $dest")

View File

@ -2,7 +2,7 @@ package org.moire.ultrasonic.di
import androidx.room.Room
import org.koin.android.ext.koin.androidContext
import org.koin.android.viewmodel.dsl.viewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.moire.ultrasonic.data.AppDatabase

View File

@ -13,8 +13,7 @@ internal val dateFormat: DateFormat by lazy {
SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
}
fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry().apply {
id = this@toDomainEntity.id
fun MusicDirectoryChild.toDomainEntity(): MusicDirectory.Entry = MusicDirectory.Entry(id).apply {
parent = this@toDomainEntity.parent
isDirectory = this@toDomainEntity.isDir
title = this@toDomainEntity.title

View File

@ -6,7 +6,6 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.Constants
@ -15,7 +14,6 @@ import org.moire.ultrasonic.util.Constants
* Displays a list of Albums from the media library
* TODO: Check refresh is working
*/
@KoinApiExtension
class AlbumListFragment : GenericListFragment<MusicDirectory.Entry, AlbumRowAdapter>() {
/**

View File

@ -5,14 +5,12 @@ import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Util
@KoinApiExtension
class AlbumListModel(application: Application) : GenericListModel(application) {
val albumList: MutableLiveData<List<MusicDirectory.Entry>> = MutableLiveData()

View File

@ -57,7 +57,7 @@ class AlbumRowAdapter(
imageLoader.loadImage(
holder.coverArt,
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
false, 0, false, true, R.drawable.unknown_album
)
}

View File

@ -3,7 +3,6 @@ package org.moire.ultrasonic.fragment
import android.os.Bundle
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.util.Constants
@ -11,7 +10,6 @@ import org.moire.ultrasonic.util.Constants
/**
* Displays the list of Artists from the media library
*/
@KoinApiExtension
class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
/**

View File

@ -23,14 +23,12 @@ import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.service.MusicService
/**
* Provides ViewModel which contains the list of available Artists
*/
@KoinApiExtension
class ArtistListModel(application: Application) : GenericListModel(application) {
private val artists: MutableLiveData<List<Artist>> = MutableLiveData()

View File

@ -62,7 +62,7 @@ class ArtistRowAdapter(
holder.coverArt.visibility = View.VISIBLE
imageLoader.loadImage(
holder.coverArt,
MusicDirectory.Entry().apply { coverArt = holder.coverArtId },
MusicDirectory.Entry("-1").apply { coverArt = holder.coverArtId },
false, 0, false, true, R.drawable.ic_contact_picture
)
} else {
@ -96,7 +96,7 @@ class ArtistRowAdapter(
}
private fun getSectionFromName(name: String): String {
var section = name.first().toUpperCase()
var section = name.first().uppercaseChar()
if (!section.isLetter()) section = '#'
return section.toString()
}

View File

@ -12,7 +12,7 @@ import androidx.navigation.fragment.findNavController
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputLayout
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient

View File

@ -13,8 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.core.component.KoinApiExtension
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
@ -31,7 +30,6 @@ import org.moire.ultrasonic.view.SelectMusicFolderView
* @param T: The type of data which will be used (must extend GenericEntry)
* @param TA: The Adapter to use (must extend GenericRowAdapter)
*/
@KoinApiExtension
abstract class GenericListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> : Fragment() {
internal val activeServerProvider: ActiveServerProvider by inject()
internal val serverSettingsModel: ServerSettingsModel by viewModel()

View File

@ -15,7 +15,6 @@ import java.net.UnknownHostException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinApiExtension
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider
@ -29,7 +28,6 @@ import org.moire.ultrasonic.util.Util
/**
* An abstract Model, which can be extended to retrieve a list of items from the API
*/
@KoinApiExtension
open class GenericListModel(application: Application) :
AndroidViewModel(application), KoinComponent {

View File

@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import org.koin.android.viewmodel.ext.android.viewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.fragment.EditServerFragment.Companion.EDIT_SERVER_INTENT_INDEX

View File

@ -28,13 +28,11 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import androidx.navigation.Navigation
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.security.SecureRandom
import java.util.Collections
import java.util.Random
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory
@ -61,7 +59,6 @@ import timber.log.Timber
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
* TODO: Refactor this fragment and model to extend the GenericListFragment
*/
@KoinApiExtension
class TrackCollectionFragment : Fragment() {
private var refreshAlbumListView: SwipeRefreshLayout? = null
@ -92,7 +89,7 @@ class TrackCollectionFragment : Fragment() {
private var cancellationToken: CancellationToken? = null
private val model: TrackCollectionModel by viewModels()
private val random: Random = SecureRandom()
private val random: Random = Random()
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
@ -258,7 +255,7 @@ class TrackCollectionFragment : Fragment() {
model.getMusicFolders(refresh)
if (playlistId != null) {
setTitle(playlistName)
setTitle(playlistName!!)
model.getPlaylist(playlistId, playlistName)
} else if (podcastChannelId != null) {
setTitle(getString(R.string.podcasts_label))
@ -282,12 +279,12 @@ class TrackCollectionFragment : Fragment() {
setTitle(name)
if (!isOffline() && Util.getShouldUseId3Tags()) {
if (isAlbum) {
model.getAlbum(refresh, id, name, parentId)
model.getAlbum(refresh, id!!, name, parentId)
} else {
model.getArtist(refresh, id, name)
model.getArtist(refresh, id!!, name)
}
} else {
model.getMusicDirectory(refresh, id, name, parentId)
model.getMusicDirectory(refresh, id!!, name, parentId)
}
}

View File

@ -13,7 +13,6 @@ import androidx.lifecycle.MutableLiveData
import java.util.LinkedList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicService
@ -24,7 +23,6 @@ import org.moire.ultrasonic.util.Util
* Model for retrieving different collections of tracks from the API
* TODO: Refactor this model to extend the GenericListModel
*/
@KoinApiExtension
class TrackCollectionModel(application: Application) : GenericListModel(application) {
private val allSongsId = "-1"
@ -43,7 +41,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
suspend fun getMusicDirectory(
refresh: Boolean,
id: String?,
id: String,
name: String?,
parentId: String?
) {
@ -53,7 +51,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
var root = MusicDirectory()
if (allSongsId == id) {
if (allSongsId == id && parentId != null) {
val musicDirectory = service.getMusicDirectory(
parentId, name, refresh
)
@ -73,12 +71,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
musicDirectory.findChild(allSongsId) == null &&
hasOnlyFolders(musicDirectory)
) {
val allSongs = MusicDirectory.Entry()
val allSongs = MusicDirectory.Entry(allSongsId)
allSongs.isDirectory = true
allSongs.artist = name
allSongs.parent = id
allSongs.id = allSongsId
allSongs.title = String.format(
context.resources.getString(R.string.select_album_all_songs), name
)
@ -122,7 +119,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
* TODO: This method should be moved to AlbumListModel,
* since it displays a list of albums by a specified artist.
*/
suspend fun getArtist(refresh: Boolean, id: String?, name: String?) {
suspend fun getArtist(refresh: Boolean, id: String, name: String?) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
@ -135,12 +132,11 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
musicDirectory.findChild(allSongsId) == null &&
hasOnlyFolders(musicDirectory)
) {
val allSongs = MusicDirectory.Entry()
val allSongs = MusicDirectory.Entry(allSongsId)
allSongs.isDirectory = true
allSongs.artist = name
allSongs.parent = id
allSongs.id = allSongsId
allSongs.title = String.format(
context.resources.getString(R.string.select_album_all_songs), name
)
@ -154,7 +150,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
}
suspend fun getAlbum(refresh: Boolean, id: String?, name: String?, parentId: String?) {
suspend fun getAlbum(refresh: Boolean, id: String, name: String?, parentId: String?) {
withContext(Dispatchers.IO) {
@ -162,7 +158,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
val musicDirectory: MusicDirectory
if (allSongsId == id) {
if (allSongsId == id && parentId != null) {
val root = MusicDirectory()
val songs: MutableCollection<MusicDirectory.Entry> = LinkedList()
@ -212,9 +208,9 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
val musicDirectory: MusicDirectory
if (Util.getShouldUseId3Tags()) {
musicDirectory = Util.getSongsFromSearchResult(service.starred2)
musicDirectory = Util.getSongsFromSearchResult(service.getStarred2())
} else {
musicDirectory = Util.getSongsFromSearchResult(service.starred)
musicDirectory = Util.getSongsFromSearchResult(service.getStarred())
}
currentDirectory.postValue(musicDirectory)
@ -241,7 +237,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
}
suspend fun getPlaylist(playlistId: String, playlistName: String?) {
suspend fun getPlaylist(playlistId: String, playlistName: String) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()

View File

@ -161,7 +161,7 @@ class FileLoggerTree : Timber.DebugTree() {
}
}
private fun getLogFileList(): Array<File> {
private fun getLogFileList(): Array<out File>? {
val directory = FileUtil.getUltrasonicDirectory()
return directory.listFiles { t -> t.name.matches(fileNameRegex) }
}

View File

@ -18,7 +18,8 @@ import timber.log.Timber
class AudioFocusHandler(private val context: Context) {
// TODO: This is a circular reference, try to remove it
// This should be doable by using the native MediaController framework
private val mediaPlayerControllerLazy = inject(MediaPlayerController::class.java)
private val mediaPlayerControllerLazy =
inject<MediaPlayerController>(MediaPlayerController::class.java)
private val audioManager by lazy {
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager

View File

@ -0,0 +1,470 @@
/*
* CachedMusicService.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.service
import android.graphics.Bitmap
import java.io.InputStream
import java.util.concurrent.TimeUnit
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.domain.Indexes
import org.moire.ultrasonic.domain.JukeboxStatus
import org.moire.ultrasonic.domain.Lyrics
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.domain.Playlist
import org.moire.ultrasonic.domain.PodcastsChannel
import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.domain.UserInfo
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.LRUCache
import org.moire.ultrasonic.util.TimeLimitedCache
import org.moire.ultrasonic.util.Util
@Suppress("TooManyFunctions")
class CachedMusicService(private val musicService: MusicService) : MusicService, KoinComponent {
private val activeServerProvider: ActiveServerProvider by inject()
private val cachedMusicDirectories: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
private val cachedArtist: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
private val cachedAlbum: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
private val cachedUserInfo: LRUCache<String?, TimeLimitedCache<UserInfo?>>
private val cachedLicenseValid = TimeLimitedCache<Boolean>(expiresAfter = 10, TimeUnit.MINUTES)
private val cachedIndexes = TimeLimitedCache<Indexes?>()
private val cachedArtists = TimeLimitedCache<Indexes?>()
private val cachedPlaylists = TimeLimitedCache<List<Playlist>?>()
private val cachedPodcastsChannels = TimeLimitedCache<List<PodcastsChannel>>()
private val cachedMusicFolders =
TimeLimitedCache<List<MusicFolder>?>(10, TimeUnit.HOURS)
private val cachedGenres = TimeLimitedCache<List<Genre>?>(10, TimeUnit.HOURS)
private var restUrl: String? = null
private var cachedMusicFolderId: String? = null
@Throws(Exception::class)
override fun ping() {
checkSettingsChanged()
musicService.ping()
}
@Throws(Exception::class)
override fun isLicenseValid(): Boolean {
checkSettingsChanged()
var isValid = cachedLicenseValid.get()
if (isValid == null) {
isValid = musicService.isLicenseValid()
cachedLicenseValid.set(isValid)
}
return isValid
}
@Throws(Exception::class)
override fun getMusicFolders(refresh: Boolean): List<MusicFolder> {
checkSettingsChanged()
if (refresh) {
cachedMusicFolders.clear()
}
val cache = cachedMusicFolders.get()
if (cache != null) return cache
val result = musicService.getMusicFolders(refresh)
cachedMusicFolders.set(result)
return result
}
@Throws(Exception::class)
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes {
checkSettingsChanged()
if (refresh) {
cachedIndexes.clear()
cachedMusicFolders.clear()
cachedMusicDirectories.clear()
}
var result = cachedIndexes.get()
if (result == null) {
result = musicService.getIndexes(musicFolderId, refresh)
cachedIndexes.set(result)
}
return result
}
@Throws(Exception::class)
override fun getArtists(refresh: Boolean): Indexes {
checkSettingsChanged()
if (refresh) {
cachedArtists.clear()
}
var result = cachedArtists.get()
if (result == null) {
result = musicService.getArtists(refresh)
cachedArtists.set(result)
}
return result
}
@Throws(Exception::class)
override fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory {
checkSettingsChanged()
var cache = if (refresh) null else cachedMusicDirectories[id]
var dir = cache?.get()
if (dir == null) {
dir = musicService.getMusicDirectory(id, name, refresh)
cache = TimeLimitedCache(
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
)
cache.set(dir)
cachedMusicDirectories.put(id, cache)
}
return dir
}
@Throws(Exception::class)
override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory {
checkSettingsChanged()
var cache = if (refresh) null else cachedArtist[id]
var dir = cache?.get()
if (dir == null) {
dir = musicService.getArtist(id, name, refresh)
cache = TimeLimitedCache(
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
)
cache.set(dir)
cachedArtist.put(id, cache)
}
return dir
}
@Throws(Exception::class)
override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory {
checkSettingsChanged()
var cache = if (refresh) null else cachedAlbum[id]
var dir = cache?.get()
if (dir == null) {
dir = musicService.getAlbum(id, name, refresh)
cache = TimeLimitedCache(
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
)
cache.set(dir)
cachedAlbum.put(id, cache)
}
return dir
}
@Throws(Exception::class)
override fun search(criteria: SearchCriteria): SearchResult? {
return musicService.search(criteria)
}
@Throws(Exception::class)
override fun getPlaylist(id: String, name: String): MusicDirectory {
return musicService.getPlaylist(id, name)
}
@Throws(Exception::class)
override fun getPodcastsChannels(refresh: Boolean): List<PodcastsChannel> {
checkSettingsChanged()
var result = if (refresh) null else cachedPodcastsChannels.get()
if (result == null) {
result = musicService.getPodcastsChannels(refresh)
cachedPodcastsChannels.set(result)
}
return result
}
@Throws(Exception::class)
override fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory? {
return musicService.getPodcastEpisodes(podcastChannelId)
}
@Throws(Exception::class)
override fun getPlaylists(refresh: Boolean): List<Playlist> {
checkSettingsChanged()
var result = if (refresh) null else cachedPlaylists.get()
if (result == null) {
result = musicService.getPlaylists(refresh)
cachedPlaylists.set(result)
}
return result
}
@Throws(Exception::class)
override fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>) {
cachedPlaylists.clear()
musicService.createPlaylist(id, name, entries)
}
@Throws(Exception::class)
override fun deletePlaylist(id: String) {
musicService.deletePlaylist(id)
}
@Throws(Exception::class)
override fun updatePlaylist(id: String, name: String?, comment: String?, pub: Boolean) {
musicService.updatePlaylist(id, name, comment, pub)
}
@Throws(Exception::class)
override fun getLyrics(artist: String, title: String): Lyrics? {
return musicService.getLyrics(artist, title)
}
@Throws(Exception::class)
override fun scrobble(id: String, submission: Boolean) {
musicService.scrobble(id, submission)
}
@Throws(Exception::class)
override fun getAlbumList(
type: String,
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
return musicService.getAlbumList(type, size, offset, musicFolderId)
}
@Throws(Exception::class)
override fun getAlbumList2(
type: String,
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
return musicService.getAlbumList2(type, size, offset, musicFolderId)
}
@Throws(Exception::class)
override fun getRandomSongs(size: Int): MusicDirectory {
return musicService.getRandomSongs(size)
}
@Throws(Exception::class)
override fun getStarred(): SearchResult = musicService.getStarred()
@Throws(Exception::class)
override fun getStarred2(): SearchResult = musicService.getStarred2()
@Throws(Exception::class)
override fun getCoverArt(
entry: MusicDirectory.Entry?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
return musicService.getCoverArt(entry, size, saveToFile, highQuality)
}
@Throws(Exception::class)
override fun getDownloadInputStream(
song: MusicDirectory.Entry,
offset: Long,
maxBitrate: Int
): Pair<InputStream, Boolean> {
return musicService.getDownloadInputStream(song, offset, maxBitrate)
}
@Throws(Exception::class)
override fun getVideoUrl(id: String, useFlash: Boolean): String? {
return musicService.getVideoUrl(id, useFlash)
}
@Throws(Exception::class)
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
return musicService.updateJukeboxPlaylist(ids)
}
@Throws(Exception::class)
override fun skipJukebox(index: Int, offsetSeconds: Int): JukeboxStatus {
return musicService.skipJukebox(index, offsetSeconds)
}
@Throws(Exception::class)
override fun stopJukebox(): JukeboxStatus {
return musicService.stopJukebox()
}
@Throws(Exception::class)
override fun startJukebox(): JukeboxStatus {
return musicService.startJukebox()
}
@Throws(Exception::class)
override fun getJukeboxStatus(): JukeboxStatus = musicService.getJukeboxStatus()
@Throws(Exception::class)
override fun setJukeboxGain(gain: Float): JukeboxStatus {
return musicService.setJukeboxGain(gain)
}
private fun checkSettingsChanged() {
val newUrl = activeServerProvider.getRestUrl(null)
val newFolderId = activeServerProvider.getActiveServer().musicFolderId
if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) {
cachedMusicFolders.clear()
cachedMusicDirectories.clear()
cachedLicenseValid.clear()
cachedIndexes.clear()
cachedPlaylists.clear()
cachedGenres.clear()
cachedAlbum.clear()
cachedArtist.clear()
cachedUserInfo.clear()
restUrl = newUrl
cachedMusicFolderId = newFolderId
}
}
@Throws(Exception::class)
override fun star(id: String?, albumId: String?, artistId: String?) {
musicService.star(id, albumId, artistId)
}
@Throws(Exception::class)
override fun unstar(id: String?, albumId: String?, artistId: String?) {
musicService.unstar(id, albumId, artistId)
}
@Throws(Exception::class)
override fun setRating(id: String, rating: Int) {
musicService.setRating(id, rating)
}
@Throws(Exception::class)
override fun getGenres(refresh: Boolean): List<Genre>? {
checkSettingsChanged()
if (refresh) {
cachedGenres.clear()
}
var result = cachedGenres.get()
if (result == null) {
result = musicService.getGenres(refresh)
cachedGenres.set(result)
}
val sorted = result?.toMutableList()
sorted?.sortWith { genre, genre2 ->
genre.name.compareTo(
genre2.name,
ignoreCase = true
)
}
return sorted
}
@Throws(Exception::class)
override fun getSongsByGenre(genre: String, count: Int, offset: Int): MusicDirectory {
return musicService.getSongsByGenre(genre, count, offset)
}
@Throws(Exception::class)
override fun getShares(refresh: Boolean): List<Share> {
return musicService.getShares(refresh)
}
@Throws(Exception::class)
override fun getChatMessages(since: Long?): List<ChatMessage?>? {
return musicService.getChatMessages(since)
}
@Throws(Exception::class)
override fun addChatMessage(message: String) {
musicService.addChatMessage(message)
}
@Throws(Exception::class)
override fun getBookmarks(): List<Bookmark?>? = musicService.getBookmarks()
@Throws(Exception::class)
override fun deleteBookmark(id: String) {
musicService.deleteBookmark(id)
}
@Throws(Exception::class)
override fun createBookmark(id: String, position: Int) {
musicService.createBookmark(id, position)
}
@Throws(Exception::class)
override fun getVideos(refresh: Boolean): MusicDirectory? {
checkSettingsChanged()
var cache =
if (refresh) null else cachedMusicDirectories[Constants.INTENT_EXTRA_NAME_VIDEOS]
var dir = cache?.get()
if (dir == null) {
dir = musicService.getVideos(refresh)
cache = TimeLimitedCache(
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
)
cache.set(dir)
cachedMusicDirectories.put(Constants.INTENT_EXTRA_NAME_VIDEOS, cache)
}
return dir
}
@Throws(Exception::class)
override fun getUser(username: String): UserInfo {
checkSettingsChanged()
var cache = cachedUserInfo[username]
var userInfo = cache?.get()
if (userInfo == null) {
userInfo = musicService.getUser(username)
cache = TimeLimitedCache(
Util.getDirectoryCacheTime().toLong(), TimeUnit.SECONDS
)
cache.set(userInfo)
cachedUserInfo.put(username, cache)
}
return userInfo
}
@Throws(Exception::class)
override fun createShare(
ids: List<String>,
description: String?,
expires: Long?
): List<Share> {
return musicService.createShare(ids, description, expires)
}
@Throws(Exception::class)
override fun deleteShare(id: String) {
musicService.deleteShare(id)
}
@Throws(Exception::class)
override fun updateShare(id: String, description: String?, expires: Long?) {
musicService.updateShare(id, description, expires)
}
@Throws(Exception::class)
override fun getAvatar(
username: String?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
return musicService.getAvatar(username, size, saveToFile, highQuality)
}
companion object {
private const val MUSIC_DIR_CACHE_SIZE = 100
}
init {
cachedMusicDirectories = LRUCache(MUSIC_DIR_CACHE_SIZE)
cachedArtist = LRUCache(MUSIC_DIR_CACHE_SIZE)
cachedAlbum = LRUCache(MUSIC_DIR_CACHE_SIZE)
cachedUserInfo = LRUCache(MUSIC_DIR_CACHE_SIZE)
}
}

View File

@ -19,8 +19,8 @@ import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.RandomAccessFile
import org.koin.core.component.KoinApiExtension
import org.koin.java.KoinJavaComponent.inject
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
@ -36,11 +36,10 @@ import timber.log.Timber
* @author Sindre Mehus
* @version $Id$
*/
@KoinApiExtension
class DownloadFile(
val song: MusicDirectory.Entry,
private val save: Boolean
) {
) : KoinComponent {
val partialFile: File
val completeFile: File
private val saveFile: File = FileUtil.getSongFile(song)
@ -59,7 +58,7 @@ class DownloadFile(
@Volatile
private var completeWhenDone = false
private val downloader = inject(Downloader::class.java)
private val downloader: Downloader by inject()
val progress: MutableLiveData<Int> = MutableLiveData(0)
@ -201,7 +200,6 @@ class DownloadFile(
return String.format("DownloadFile (%s)", song)
}
@KoinApiExtension
@Suppress("TooGenericExceptionCaught")
private inner class DownloadTask : CancellableTask() {
override fun execute() {
@ -310,7 +308,7 @@ class DownloadFile(
}
wifiLock?.release()
CacheCleaner().cleanSpace()
downloader.value.checkDownloads()
downloader.checkDownloads()
}
}

View File

@ -25,7 +25,6 @@ import java.net.URLEncoder
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.audiofx.VisualizerController
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
@ -40,7 +39,6 @@ import timber.log.Timber
/**
* Represents a Media Player which uses the mobile's resources for playback
*/
@KoinApiExtension
class LocalMediaPlayer(
private val audioFocusHandler: AudioFocusHandler,
private val context: Context
@ -106,7 +104,7 @@ class LocalMediaPlayer(
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mediaPlayer.audioSessionId)
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
context.sendBroadcast(i)
} catch (e: Throwable) {
} catch (ignored: Throwable) {
// Froyo or lower
}
mediaPlayerLooper = Looper.myLooper()
@ -466,7 +464,7 @@ class LocalMediaPlayer(
// the equalizer or visualizer with the player
try {
nextMediaPlayer!!.audioSessionId = mediaPlayer.audioSessionId
} catch (e: Throwable) {
} catch (ignored: Throwable) {
}
nextMediaPlayer!!.setDataSource(file.path)

View File

@ -7,9 +7,9 @@
package org.moire.ultrasonic.service
import android.content.Intent
import org.koin.core.component.KoinApiExtension
import org.koin.java.KoinJavaComponent.get
import org.koin.java.KoinJavaComponent.inject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.MusicDirectory
@ -30,7 +30,6 @@ import timber.log.Timber
* This class contains everything that is necessary for the Application UI
* to control the Media Player implementation.
*/
@KoinApiExtension
@Suppress("TooManyFunctions")
class MediaPlayerController(
private val downloadQueueSerializer: DownloadQueueSerializer,
@ -38,7 +37,7 @@ class MediaPlayerController(
private val downloader: Downloader,
private val shufflePlayBuffer: ShufflePlayBuffer,
private val localMediaPlayer: LocalMediaPlayer
) {
) : KoinComponent {
private var created = false
var suggestedPlaylistName: String? = null
@ -46,8 +45,8 @@ class MediaPlayerController(
var showVisualization = false
private var autoPlayStart = false
private val jukeboxMediaPlayer = inject(JukeboxMediaPlayer::class.java).value
private val activeServerProvider = inject(ActiveServerProvider::class.java).value
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
private val activeServerProvider: ActiveServerProvider by inject()
fun onCreate() {
if (created) return
@ -429,10 +428,7 @@ class MediaPlayerController(
get() {
try {
val username = activeServerProvider.getActiveServer().userName
val (_, _, _, _, _, _, _, _, _, _, _, _, jukeboxRole) = getMusicService().getUser(
username
)
return jukeboxRole
return getMusicService().getUser(username).jukeboxRole
} catch (e: Exception) {
Timber.w(e, "Error getting user information")
}
@ -465,7 +461,8 @@ class MediaPlayerController(
@Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
fun setSongRating(rating: Int) {
if (!get(FeatureStorage::class.java).isFeatureEnabled(Feature.FIVE_STAR_RATING)) return
val features: FeatureStorage = get()
if (!features.isFeatureEnabled(Feature.FIVE_STAR_RATING)) return
if (localMediaPlayer.currentPlaying == null) return
val song = localMediaPlayer.currentPlaying!!.song
song.userRating = rating

View File

@ -24,7 +24,6 @@ import android.view.KeyEvent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import org.koin.android.ext.android.inject
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.app.UApp
@ -49,7 +48,6 @@ import timber.log.Timber
* Android Foreground Service for playing music
* while the rest of the Ultrasonic App is in the background.
*/
@KoinApiExtension
@Suppress("LargeClass")
class MediaPlayerService : Service() {
private val binder: IBinder = SimpleServiceBinder(this)
@ -173,8 +171,7 @@ class MediaPlayerService : Service() {
fun setCurrentPlaying(currentPlayingIndex: Int) {
try {
localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex])
} catch (x: IndexOutOfBoundsException) {
// Ignored
} catch (ignored: IndexOutOfBoundsException) {
}
}

View File

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

View File

@ -18,7 +18,6 @@
*/
package org.moire.ultrasonic.service
import org.koin.core.component.KoinApiExtension
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.context.loadKoinModules
@ -30,7 +29,6 @@ import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE
import org.moire.ultrasonic.di.musicServiceModule
// TODO Refactor everywhere to use DI way to get MusicService, and then remove this class
@KoinApiExtension
object MusicServiceFactory : KoinComponent {
@JvmStatic
fun getMusicService(): MusicService {

View File

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

View File

@ -0,0 +1,696 @@
/*
* OfflineMusicService.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.service
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.File
import java.io.FileReader
import java.io.FileWriter
import java.io.InputStream
import java.io.Reader
import java.util.ArrayList
import java.util.HashSet
import java.util.LinkedList
import java.util.Locale
import java.util.Random
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.domain.Indexes
import org.moire.ultrasonic.domain.JukeboxStatus
import org.moire.ultrasonic.domain.Lyrics
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.domain.Playlist
import org.moire.ultrasonic.domain.PodcastsChannel
import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.domain.UserInfo
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util
import timber.log.Timber
// TODO: There are quite a number of deeply nested and complicated functions in this class..
// Simplify them :)
@Suppress("TooManyFunctions")
class OfflineMusicService : MusicService, KoinComponent {
private val activeServerProvider: ActiveServerProvider by inject()
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes {
val artists: MutableList<Artist> = ArrayList()
val root = FileUtil.getMusicDirectory()
for (file in FileUtil.listFiles(root)) {
if (file.isDirectory) {
val artist = Artist()
artist.id = file.path
artist.index = file.name.substring(0, 1)
artist.name = file.name
artists.add(artist)
}
}
val ignoredArticlesString = "The El La Los Las Le Les"
val ignoredArticles = COMPILE.split(ignoredArticlesString)
artists.sortWith { lhsArtist, rhsArtist ->
var lhs = lhsArtist.name!!.lowercase(Locale.ROOT)
var rhs = rhsArtist.name!!.lowercase(Locale.ROOT)
val lhs1 = lhs[0]
val rhs1 = rhs[0]
if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) {
return@sortWith 1
}
if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) {
return@sortWith -1
}
for (article in ignoredArticles) {
var index = lhs.indexOf(
String.format(Locale.ROOT, "%s ", article.lowercase(Locale.ROOT))
)
if (index == 0) {
lhs = lhs.substring(article.length + 1)
}
index = rhs.indexOf(
String.format(Locale.ROOT, "%s ", article.lowercase(Locale.ROOT))
)
if (index == 0) {
rhs = rhs.substring(article.length + 1)
}
}
lhs.compareTo(rhs)
}
return Indexes(0L, ignoredArticlesString, artists = artists)
}
override fun getMusicDirectory(
id: String,
name: String?,
refresh: Boolean
): MusicDirectory {
val dir = File(id)
val result = MusicDirectory()
result.name = dir.name
val seen: MutableCollection<String?> = HashSet()
for (file in FileUtil.listMediaFiles(dir)) {
val filename = getName(file)
if (filename != null && !seen.contains(filename)) {
seen.add(filename)
result.addChild(createEntry(file, filename))
}
}
return result
}
override fun getAvatar(
username: String?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
return try {
val bitmap = FileUtil.getAvatarBitmap(username, size, highQuality)
Util.scaleBitmap(bitmap, size)
} catch (ignored: Exception) {
null
}
}
override fun getCoverArt(
entry: MusicDirectory.Entry?,
size: Int,
saveToFile: Boolean,
highQuality: Boolean
): Bitmap? {
return try {
val bitmap = FileUtil.getAlbumArtBitmap(entry, size, highQuality)
Util.scaleBitmap(bitmap, size)
} catch (ignored: Exception) {
null
}
}
override fun search(criteria: SearchCriteria): SearchResult {
val artists: MutableList<Artist> = ArrayList()
val albums: MutableList<MusicDirectory.Entry> = ArrayList()
val songs: MutableList<MusicDirectory.Entry> = ArrayList()
val root = FileUtil.getMusicDirectory()
var closeness: Int
for (artistFile in FileUtil.listFiles(root)) {
val artistName = artistFile.name
if (artistFile.isDirectory) {
if (matchCriteria(criteria, artistName).also { closeness = it } > 0) {
val artist = Artist()
artist.id = artistFile.path
artist.index = artistFile.name.substring(0, 1)
artist.name = artistName
artist.closeness = closeness
artists.add(artist)
}
recursiveAlbumSearch(artistName, artistFile, criteria, albums, songs)
}
}
artists.sort()
albums.sort()
songs.sort()
return SearchResult(artists, albums, songs)
}
@Suppress("NestedBlockDepth", "TooGenericExceptionCaught")
override fun getPlaylists(refresh: Boolean): List<Playlist> {
val playlists: MutableList<Playlist> = ArrayList()
val root = FileUtil.getPlaylistDirectory()
var lastServer: String? = null
var removeServer = true
for (folder in FileUtil.listFiles(root)) {
if (folder.isDirectory) {
val server = folder.name
val fileList = FileUtil.listFiles(folder)
for (file in fileList) {
if (FileUtil.isPlaylistFile(file)) {
val id = file.name
val filename = server + ": " + FileUtil.getBaseName(id)
val playlist = Playlist(server, filename)
playlists.add(playlist)
}
}
if (server != lastServer && !fileList.isEmpty()) {
if (lastServer != null) {
removeServer = false
}
lastServer = server
}
} else {
// Delete legacy playlist files
try {
if (!folder.delete()) {
Timber.w("Failed to delete old playlist file: %s", folder.name)
}
} catch (e: Exception) {
Timber.w(e, "Failed to delete old playlist file: %s", folder.name)
}
}
}
if (removeServer) {
for (playlist in playlists) {
playlist.name = playlist.name.substring(playlist.id.length + 2)
}
}
return playlists
}
@Throws(Exception::class)
override fun getPlaylist(id: String, name: String): MusicDirectory {
var playlistName = name
var reader: Reader? = null
var buffer: BufferedReader? = null
return try {
val firstIndex = playlistName.indexOf(id)
if (firstIndex != -1) {
playlistName = playlistName.substring(id.length + 2)
}
val playlistFile = FileUtil.getPlaylistFile(id, playlistName)
reader = FileReader(playlistFile)
buffer = BufferedReader(reader)
val playlist = MusicDirectory()
var line = buffer.readLine()
if ("#EXTM3U" != line) return playlist
while (buffer.readLine().also { line = it } != null) {
val entryFile = File(line)
val entryName = getName(entryFile)
if (entryFile.exists() && entryName != null) {
playlist.addChild(createEntry(entryFile, entryName))
}
}
playlist
} finally {
Util.close(buffer)
Util.close(reader)
}
}
@Suppress("TooGenericExceptionCaught")
@Throws(Exception::class)
override fun createPlaylist(id: String, name: String, entries: List<MusicDirectory.Entry>) {
val playlistFile =
FileUtil.getPlaylistFile(activeServerProvider.getActiveServer().name, name)
val fw = FileWriter(playlistFile)
val bw = BufferedWriter(fw)
try {
fw.write("#EXTM3U\n")
for (e in entries) {
var filePath = FileUtil.getSongFile(e).absolutePath
if (!File(filePath).exists()) {
val ext = FileUtil.getExtension(filePath)
val base = FileUtil.getBaseName(filePath)
filePath = "$base.complete.$ext"
}
fw.write(
"""
$filePath
""".trimIndent()
)
}
} catch (ignored: Exception) {
Timber.w("Failed to save playlist: %s", name)
} finally {
bw.close()
fw.close()
}
}
override fun getRandomSongs(size: Int): MusicDirectory {
val root = FileUtil.getMusicDirectory()
val children: MutableList<File> = LinkedList()
listFilesRecursively(root, children)
val result = MusicDirectory()
if (children.isEmpty()) {
return result
}
val random = Random()
for (i in 0 until size) {
val file = children[random.nextInt(children.size)]
result.addChild(createEntry(file, getName(file)))
}
return result
}
@Throws(Exception::class)
override fun deletePlaylist(id: String) {
throw OfflineException("Playlists not available in offline mode")
}
@Throws(Exception::class)
override fun updatePlaylist(id: String, name: String?, comment: String?, pub: Boolean) {
throw OfflineException("Updating playlist not available in offline mode")
}
@Throws(Exception::class)
override fun getLyrics(artist: String, title: String): Lyrics? {
throw OfflineException("Lyrics not available in offline mode")
}
@Throws(Exception::class)
override fun scrobble(id: String, submission: Boolean) {
throw OfflineException("Scrobbling not available in offline mode")
}
@Throws(Exception::class)
override fun getAlbumList(
type: String,
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
throw OfflineException("Album lists not available in offline mode")
}
@Throws(Exception::class)
override fun updateJukeboxPlaylist(ids: List<String>?): JukeboxStatus {
throw OfflineException("Jukebox not available in offline mode")
}
@Throws(Exception::class)
override fun skipJukebox(index: Int, offsetSeconds: Int): JukeboxStatus {
throw OfflineException("Jukebox not available in offline mode")
}
@Throws(Exception::class)
override fun stopJukebox(): JukeboxStatus {
throw OfflineException("Jukebox not available in offline mode")
}
@Throws(Exception::class)
override fun startJukebox(): JukeboxStatus {
throw OfflineException("Jukebox not available in offline mode")
}
@Throws(Exception::class)
override fun getJukeboxStatus(): JukeboxStatus {
throw OfflineException("Jukebox not available in offline mode")
}
@Throws(Exception::class)
override fun setJukeboxGain(gain: Float): JukeboxStatus {
throw OfflineException("Jukebox not available in offline mode")
}
@Throws(Exception::class)
override fun getStarred(): SearchResult {
throw OfflineException("Starred not available in offline mode")
}
@Throws(Exception::class)
override fun getSongsByGenre(genre: String, count: Int, offset: Int): MusicDirectory {
throw OfflineException("Getting Songs By Genre not available in offline mode")
}
@Throws(Exception::class)
override fun getGenres(refresh: Boolean): List<Genre>? {
throw OfflineException("Getting Genres not available in offline mode")
}
@Throws(Exception::class)
override fun getUser(username: String): UserInfo {
throw OfflineException("Getting user info not available in offline mode")
}
@Throws(Exception::class)
override fun createShare(
ids: List<String>,
description: String?,
expires: Long?
): List<Share> {
throw OfflineException("Creating shares not available in offline mode")
}
@Throws(Exception::class)
override fun getShares(refresh: Boolean): List<Share> {
throw OfflineException("Getting shares not available in offline mode")
}
@Throws(Exception::class)
override fun deleteShare(id: String) {
throw OfflineException("Deleting shares not available in offline mode")
}
@Throws(Exception::class)
override fun updateShare(id: String, description: String?, expires: Long?) {
throw OfflineException("Updating shares not available in offline mode")
}
@Throws(Exception::class)
override fun star(id: String?, albumId: String?, artistId: String?) {
throw OfflineException("Star not available in offline mode")
}
@Throws(Exception::class)
override fun unstar(id: String?, albumId: String?, artistId: String?) {
throw OfflineException("UnStar not available in offline mode")
}
@Throws(Exception::class)
override fun getMusicFolders(refresh: Boolean): List<MusicFolder> {
throw OfflineException("Music folders not available in offline mode")
}
@Throws(OfflineException::class)
override fun getAlbumList2(
type: String,
size: Int,
offset: Int,
musicFolderId: String?
): MusicDirectory {
throw OfflineException("getAlbumList2 isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getVideoUrl(id: String, useFlash: Boolean): String? {
throw OfflineException("getVideoUrl isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getChatMessages(since: Long?): List<ChatMessage?>? {
throw OfflineException("getChatMessages isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun addChatMessage(message: String) {
throw OfflineException("addChatMessage isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getBookmarks(): List<Bookmark?>? {
throw OfflineException("getBookmarks isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun deleteBookmark(id: String) {
throw OfflineException("deleteBookmark isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun createBookmark(id: String, position: Int) {
throw OfflineException("createBookmark isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getVideos(refresh: Boolean): MusicDirectory? {
throw OfflineException("getVideos isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getStarred2(): SearchResult {
throw OfflineException("getStarred2 isn't available in offline mode")
}
override fun ping() {
// Void
}
override fun isLicenseValid(): Boolean = true
@Throws(OfflineException::class)
override fun getArtists(refresh: Boolean): Indexes {
throw OfflineException("getArtists isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory {
throw OfflineException("getArtist isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory {
throw OfflineException("getAlbum isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory? {
throw OfflineException("getPodcastEpisodes isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getDownloadInputStream(
song: MusicDirectory.Entry,
offset: Long,
maxBitrate: Int
): Pair<InputStream, Boolean> {
throw OfflineException("getDownloadInputStream isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun setRating(id: String, rating: Int) {
throw OfflineException("setRating isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getPodcastsChannels(refresh: Boolean): List<PodcastsChannel> {
throw OfflineException("getPodcastsChannels isn't available in offline mode")
}
companion object {
private val COMPILE = Pattern.compile(" ")
private fun getName(file: File): String? {
var name = file.name
if (file.isDirectory) {
return name
}
if (name.endsWith(".partial") || name.contains(".partial.") ||
name == Constants.ALBUM_ART_FILE
) {
return null
}
name = name.replace(".complete", "")
return FileUtil.getBaseName(name)
}
@Suppress("TooGenericExceptionCaught", "ComplexMethod", "LongMethod", "NestedBlockDepth")
private fun createEntry(file: File, name: String?): MusicDirectory.Entry {
val entry = MusicDirectory.Entry(file.path)
entry.isDirectory = file.isDirectory
entry.parent = file.parent
entry.size = file.length()
val root = FileUtil.getMusicDirectory().path
entry.path = file.path.replaceFirst(
String.format(Locale.ROOT, "^%s/", root).toRegex(), ""
)
entry.title = name
if (file.isFile) {
var artist: String? = null
var album: String? = null
var title: String? = null
var track: String? = null
var disc: String? = null
var year: String? = null
var genre: String? = null
var duration: String? = null
var hasVideo: String? = null
try {
val mmr = MediaMetadataRetriever()
mmr.setDataSource(file.path)
artist = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)
title = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
track = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER)
disc = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER)
year = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR)
genre = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)
duration = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
hasVideo = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
mmr.release()
} catch (ignored: Exception) {
}
entry.artist = artist ?: file.parentFile!!.parentFile!!.name
entry.album = album ?: file.parentFile!!.name
if (title != null) {
entry.title = title
}
entry.isVideo = hasVideo != null
Timber.i("Offline Stuff: %s", track)
if (track != null) {
var trackValue = 0
try {
val slashIndex = track.indexOf('/')
if (slashIndex > 0) {
track = track.substring(0, slashIndex)
}
trackValue = track.toInt()
} catch (ex: Exception) {
Timber.e(ex, "Offline Stuff")
}
Timber.i("Offline Stuff: Setting Track: %d", trackValue)
entry.track = trackValue
}
if (disc != null) {
var discValue = 0
try {
val slashIndex = disc.indexOf('/')
if (slashIndex > 0) {
disc = disc.substring(0, slashIndex)
}
discValue = disc.toInt()
} catch (ignored: Exception) {
}
entry.discNumber = discValue
}
if (year != null) {
var yearValue = 0
try {
yearValue = year.toInt()
} catch (ignored: Exception) {
}
entry.year = yearValue
}
if (genre != null) {
entry.genre = genre
}
if (duration != null) {
var durationValue: Long = 0
try {
durationValue = duration.toLong()
durationValue = TimeUnit.MILLISECONDS.toSeconds(durationValue)
} catch (ignored: Exception) {
}
entry.setDuration(durationValue)
}
}
entry.suffix = FileUtil.getExtension(file.name.replace(".complete", ""))
val albumArt = FileUtil.getAlbumArtFile(entry)
if (albumArt.exists()) {
entry.coverArt = albumArt.path
}
return entry
}
@Suppress("NestedBlockDepth")
private fun recursiveAlbumSearch(
artistName: String,
file: File,
criteria: SearchCriteria,
albums: MutableList<MusicDirectory.Entry>,
songs: MutableList<MusicDirectory.Entry>
) {
var closeness: Int
for (albumFile in FileUtil.listMediaFiles(file)) {
if (albumFile.isDirectory) {
val albumName = getName(albumFile)
if (matchCriteria(criteria, albumName).also { closeness = it } > 0) {
val album = createEntry(albumFile, albumName)
album.artist = artistName
album.closeness = closeness
albums.add(album)
}
for (songFile in FileUtil.listMediaFiles(albumFile)) {
val songName = getName(songFile)
if (songFile.isDirectory) {
recursiveAlbumSearch(artistName, songFile, criteria, albums, songs)
} else if (matchCriteria(criteria, songName).also { closeness = it } > 0) {
val song = createEntry(albumFile, songName)
song.artist = artistName
song.album = albumName
song.closeness = closeness
songs.add(song)
}
}
} else {
val songName = getName(albumFile)
if (matchCriteria(criteria, songName).also { closeness = it } > 0) {
val song = createEntry(albumFile, songName)
song.artist = artistName
song.album = songName
song.closeness = closeness
songs.add(song)
}
}
}
}
private fun matchCriteria(criteria: SearchCriteria, name: String?): Int {
val query = criteria.query.lowercase(Locale.ROOT)
val queryParts = COMPILE.split(query)
val nameParts = COMPILE.split(
name!!.lowercase(Locale.ROOT)
)
var closeness = 0
for (queryPart in queryParts) {
for (namePart in nameParts) {
if (namePart == queryPart) {
closeness++
}
}
}
return closeness
}
private fun listFilesRecursively(parent: File, children: MutableList<File>) {
for (file in FileUtil.listMediaFiles(parent)) {
if (file.isFile) {
children.add(file)
} else {
listFilesRecursively(file, children)
}
}
}
}
}

View File

@ -1,20 +1,8 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
* RestMusicService.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.service
@ -64,8 +52,8 @@ import timber.log.Timber
/**
* This Music Service implementation connects to a server using the Subsonic REST API
* @author Sindre Mehus
*/
@Suppress("LargeClass")
open class RESTMusicService(
private val subsonicAPIClient: SubsonicAPIClient,
private val fileStorage: PermanentFileStorage,
@ -109,7 +97,7 @@ open class RESTMusicService(
override fun getIndexes(
musicFolderId: String?,
refresh: Boolean
): Indexes? {
): Indexes {
val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "")
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
@ -171,7 +159,7 @@ open class RESTMusicService(
id: String,
name: String?,
refresh: Boolean
): MusicDirectory? {
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api ->
api.getMusicDirectory(id).execute()
}
@ -268,7 +256,7 @@ open class RESTMusicService(
@Throws(Exception::class)
override fun getPlaylist(
id: String,
name: String?
name: String
): MusicDirectory {
val response = responseChecker.callWithResponseCheck { api ->
api.getPlaylist(id).execute()
@ -282,7 +270,7 @@ open class RESTMusicService(
@Throws(IOException::class)
private fun savePlaylist(
name: String?,
name: String,
playlist: MusicDirectory
) {
val playlistFile = FileUtil.getPlaylistFile(
@ -326,16 +314,14 @@ open class RESTMusicService(
@Throws(Exception::class)
override fun createPlaylist(
id: String?,
name: String?,
id: String,
name: String,
entries: List<MusicDirectory.Entry>
) {
val pSongIds: MutableList<String> = ArrayList(entries.size)
for ((id1) in entries) {
if (id1 != null) {
pSongIds.add(id1)
}
pSongIds.add(id1)
}
responseChecker.callWithResponseCheck { api ->
api.createPlaylist(id, name, pSongIds.toList()).execute()
@ -400,8 +386,8 @@ open class RESTMusicService(
@Throws(Exception::class)
override fun getLyrics(
artist: String?,
title: String?
artist: String,
title: String
): Lyrics {
val response = responseChecker.callWithResponseCheck { api ->
api.getLyrics(artist, title).execute()
@ -587,7 +573,7 @@ open class RESTMusicService(
): Pair<InputStream, Boolean> {
val songOffset = if (offset < 0) 0 else offset
val response = subsonicAPIClient.stream(song.id!!, maxBitrate, songOffset)
val response = subsonicAPIClient.stream(song.id, maxBitrate, songOffset)
checkStreamResponseError(response)
if (response.stream == null) {
@ -704,7 +690,7 @@ open class RESTMusicService(
@Throws(Exception::class)
override fun getGenres(
refresh: Boolean
): List<Genre> {
): List<Genre>? {
val response = responseChecker.callWithResponseCheck { api -> api.getGenres().execute() }
return response.body()!!.genresList.toDomainEntityList()
@ -883,7 +869,6 @@ open class RESTMusicService(
companion object {
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
private const val INDEXES_STORAGE_NAME = "indexes"
private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder"
private const val ARTISTS_STORAGE_NAME = "artists"
}
}

View File

@ -5,7 +5,6 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import java.util.Collections
import java.util.LinkedList
import org.koin.core.component.KoinApiExtension
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory
@ -20,7 +19,6 @@ import org.moire.ultrasonic.util.Util
* Retrieves a list of songs and adds them to the now playing list
*/
@Suppress("LongParameterList")
@KoinApiExtension
class DownloadHandler(
val mediaPlayerController: MediaPlayerController,
val networkAndStorageChecker: NetworkAndStorageChecker
@ -226,7 +224,7 @@ class DownloadHandler(
}
}
} else {
root = musicService.getPlaylist(id, name)
root = musicService.getPlaylist(id, name!!)
}
getSongsRecursively(root, songs)
}

View File

@ -1,10 +1,10 @@
package org.moire.ultrasonic.subsonic
import android.content.Context
import org.koin.java.KoinJavaComponent.get
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
import org.moire.ultrasonic.subsonic.loader.image.SubsonicImageLoader
import org.moire.ultrasonic.util.ImageLoader
import org.moire.ultrasonic.util.LegacyImageLoader
import org.moire.ultrasonic.util.Util
@ -12,7 +12,7 @@ import org.moire.ultrasonic.util.Util
/**
* Handles the lifetime of the Image Loader
*/
class ImageLoaderProvider(val context: Context) {
class ImageLoaderProvider(val context: Context) : KoinComponent {
private var imageLoader: ImageLoader? = null
@Synchronized
@ -33,12 +33,12 @@ class ImageLoaderProvider(val context: Context) {
context,
Util.getImageLoaderConcurrency()
)
val isNewImageLoaderEnabled = get(FeatureStorage::class.java)
.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER)
val features: FeatureStorage = get()
val isNewImageLoaderEnabled = features.isFeatureEnabled(Feature.NEW_IMAGE_DOWNLOADER)
imageLoader = if (isNewImageLoaderEnabled) {
SubsonicImageLoaderProxy(
legacyImageLoader,
get(SubsonicImageLoader::class.java)
get()
)
} else {
legacyImageLoader

View File

@ -68,9 +68,11 @@ class ShareHandler(val context: Context) {
) {
@Throws(Throwable::class)
override fun doInBackground(): Share {
val ids: MutableList<String?> = ArrayList()
val ids: MutableList<String> = ArrayList()
if (shareDetails.Entries.isEmpty()) {
ids.add(fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID))
fragment.arguments?.getString(Constants.INTENT_EXTRA_NAME_ID)?.let {
ids.add(it)
}
} else {
for ((id) in shareDetails.Entries) {
ids.add(id)

View File

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

View File

@ -24,8 +24,9 @@ import android.graphics.drawable.Drawable
import android.text.TextUtils
import android.view.LayoutInflater
import android.widget.Checkable
import org.koin.java.KoinJavaComponent.get
import org.koin.java.KoinJavaComponent.inject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory
@ -42,7 +43,7 @@ import timber.log.Timber
/**
* Used to display songs and videos in a `ListView`.
*/
class SongView(context: Context) : UpdateView(context), Checkable {
class SongView(context: Context) : UpdateView(context), Checkable, KoinComponent {
var entry: MusicDirectory.Entry? = null
private set
@ -55,10 +56,9 @@ class SongView(context: Context) : UpdateView(context), Checkable {
private var downloadFile: DownloadFile? = null
private var playing = false
private var viewHolder: SongViewHolder? = null
private val useFiveStarRating: Boolean =
get(FeatureStorage::class.java).isFeatureEnabled(Feature.FIVE_STAR_RATING)
private val mediaPlayerControllerLazy = inject(MediaPlayerController::class.java)
private val features: FeatureStorage = get()
private val useFiveStarRating: Boolean = features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
private val mediaPlayerController: MediaPlayerController by inject()
fun setLayout(song: MusicDirectory.Entry) {
@ -96,7 +96,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
updateBackground()
entry = song
downloadFile = mediaPlayerControllerLazy.value.getDownloadFileForSong(song)
downloadFile = mediaPlayerController.getDownloadFileForSong(song)
val artist = StringBuilder(60)
var bitRate: String? = null
@ -223,7 +223,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
public override fun update() {
updateBackground()
downloadFile = mediaPlayerControllerLazy.value.getDownloadFileForSong(entry)
downloadFile = mediaPlayerController.getDownloadFileForSong(entry)
updateDownloadStatus(downloadFile!!)
@ -254,7 +254,7 @@ class SongView(context: Context) : UpdateView(context), Checkable {
if (rating > 4) starDrawable else starHollowDrawable
)
val playing = mediaPlayerControllerLazy.value.currentPlaying === downloadFile
val playing = mediaPlayerController.currentPlaying === downloadFile
if (playing) {
if (!this.playing) {