diff --git a/build.gradle b/build.gradle index 597d384a..d6d7c22f 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt index 466d4832..b7112e2f 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt @@ -9,8 +9,22 @@ data class Artist( var coverArt: String? = null, var albumCount: Long? = null, var closeness: Int = 0 -) : Serializable, GenericEntry() { +) : Serializable, GenericEntry(), Comparable { 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 + } + } + } } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index 7523dd12..cdd035a5 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -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 { 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 + } + } + } } } diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt index 9ea19237..ab8ac70e 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt +++ b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/AvatarRequestHandler.kt @@ -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) } } } diff --git a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt index bc1f197a..7e242479 100644 --- a/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt +++ b/core/subsonic-api-image-loader/src/main/kotlin/org/moire/ultrasonic/subsonic/loader/image/CoverArtRequestHandler.kt @@ -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) } } } diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/PasswordMD5Interceptor.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/PasswordMD5Interceptor.kt index 75d6a48e..8a1c8388 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/PasswordMD5Interceptor.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/interceptors/PasswordMD5Interceptor.kt @@ -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) } diff --git a/dependencies.gradle b/dependencies.gradle index 9d5e4339..aa0f257b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -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", diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 6423c4f4..e1cb4ed7 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -67,18 +67,10 @@ NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() 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 ) NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() - ReturnCount:ActiveServerProvider.kt$ActiveServerProvider$ fun getActiveServer(): ServerSetting ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String - ReturnCount:FileLoggerTree.kt$FileLoggerTree$ private fun getNextLogFile() - ReturnCount:MediaPlayerService.kt$MediaPlayerService$private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? - ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getAvatar( username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? ReturnCount:RESTMusicService.kt$RESTMusicService$@Throws(Exception::class) override fun getCoverArt( entry: MusicDirectory.Entry?, size: Int, saveToFile: Boolean, highQuality: Boolean ): Bitmap? ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean - ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onOptionsItemSelected(item: MenuItem): Boolean - SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { // Froyo or lower } - SwallowedException:LocalMediaPlayer.kt$LocalMediaPlayer$catch (e: Throwable) { } - SwallowedException:MediaPlayerService.kt$MediaPlayerService$catch (x: IndexOutOfBoundsException) { // Ignored } SwallowedException:NavigationActivity.kt$NavigationActivity$catch (e: Resources.NotFoundException) { destination.id.toString() } ThrowsCount:ApiCallResponseChecker.kt$ApiCallResponseChecker.Companion$@Throws(SubsonicRESTException::class, IOException::class) fun checkResponseSuccessful(response: Response<out SubsonicResponse>) TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception @@ -89,7 +81,6 @@ TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$e: Exception - TooGenericExceptionCaught:MediaPlayerService.kt$MediaPlayerService$x: IndexOutOfBoundsException TooGenericExceptionCaught:SongView.kt$SongView$e: Exception TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable TooGenericExceptionCaught:VideoPlayer.kt$VideoPlayer$e: Exception @@ -98,7 +89,6 @@ TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment - UnusedPrivateMember:RESTMusicService.kt$RESTMusicService.Companion$private const val INDEXES_FOLDER_STORAGE_NAME = "indexes_folder" UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle diff --git a/detekt-config.yml b/detekt-config.yml index 9301b0b7..d095b0b5 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -69,6 +69,8 @@ style: ignorePropertyDeclaration: true UnnecessaryAbstractClass: active: false + ReturnCount: + max: 3 comments: active: true diff --git a/gradle_scripts/android-module-bootstrap.gradle b/gradle_scripts/android-module-bootstrap.gradle index f9fa091a..847827e7 100644 --- a/gradle_scripts/android-module-bootstrap.gradle +++ b/gradle_scripts/android-module-bootstrap.gradle @@ -13,10 +13,6 @@ android { targetSdkVersion versions.targetSdk } - kotlinOptions { - jvmTarget = "1.8" - } - compileOptions { // Sets Java compatibility to Java 8 sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index f8530d95..f9426351 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -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 diff --git a/ultrasonic/lint-baseline.xml b/ultrasonic/lint-baseline.xml index c34a884d..21a3f032 100644 --- a/ultrasonic/lint-baseline.xml +++ b/ultrasonic/lint-baseline.xml @@ -23,72 +23,6 @@ column="55"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - . - - 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 = inject(ActiveServerProvider.class); - - private static final int MUSIC_DIR_CACHE_SIZE = 100; - - private final MusicService musicService; - private final LRUCache> cachedMusicDirectories; - private final LRUCache> cachedArtist; - private final LRUCache> cachedAlbum; - private final LRUCache> cachedUserInfo; - private final TimeLimitedCache cachedLicenseValid = new TimeLimitedCache<>(120, TimeUnit.SECONDS); - private final TimeLimitedCache cachedIndexes = new TimeLimitedCache<>(60 * 60, TimeUnit.SECONDS); - private final TimeLimitedCache cachedArtists = new TimeLimitedCache<>(60 * 60, TimeUnit.SECONDS); - private final TimeLimitedCache> cachedPlaylists = new TimeLimitedCache<>(3600, TimeUnit.SECONDS); - private final TimeLimitedCache> cachedPodcastsChannels = new TimeLimitedCache<>(3600, TimeUnit.SECONDS); - private final TimeLimitedCache> cachedMusicFolders = new TimeLimitedCache<>(10 * 3600, TimeUnit.SECONDS); - private final TimeLimitedCache> 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 getMusicFolders(boolean refresh) throws Exception - { - checkSettingsChanged(); - if (refresh) - { - cachedMusicFolders.clear(); - } - List 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 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 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 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 getPodcastsChannels(boolean refresh) throws Exception { - checkSettingsChanged(); - List 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 getPlaylists(boolean refresh) throws Exception - { - checkSettingsChanged(); - List 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 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 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 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 getGenres(boolean refresh) throws Exception - { - checkSettingsChanged(); - if (refresh) - { - cachedGenres.clear(); - } - List result = cachedGenres.get(); - - if (result == null) - { - result = musicService.getGenres(refresh); - cachedGenres.set(result); - } - - Collections.sort(result, new Comparator() - { - @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 getShares(boolean refresh) throws Exception - { - return musicService.getShares(refresh); - } - - @Override - public List getChatMessages(Long since) throws Exception - { - return musicService.getChatMessages(since); - } - - @Override - public void addChatMessage(String message) throws Exception - { - musicService.addChatMessage(message); - } - - @Override - public List 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 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 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 createShare(List 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); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.java deleted file mode 100644 index 09aee4a5..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.java +++ /dev/null @@ -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 . - - 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 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 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 getPodcastsChannels(boolean refresh) throws Exception; - - List getPlaylists(boolean refresh) throws Exception; - - void createPlaylist(String id, String name, List 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 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 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 getShares(boolean refresh) throws Exception; - - List getChatMessages(Long since) throws Exception; - - void addChatMessage(String message) throws Exception; - - List 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 createShare(List 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; -} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineException.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineException.java deleted file mode 100644 index 845ba693..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineException.java +++ /dev/null @@ -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 . - - 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); - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java deleted file mode 100644 index 6bff4eb4..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.java +++ /dev/null @@ -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 . - - 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 = inject(ActiveServerProvider.class); - - @Override - public Indexes getIndexes(String musicFolderId, boolean refresh) - { - List 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 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 artists = new ArrayList<>(); - List albums = new ArrayList<>(); - List 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 albums, List 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 getPlaylists(boolean refresh) - { - List 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 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 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 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 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 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 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 createShare(List ids, String description, Long expires) throws Exception - { - throw new OfflineException("Creating shares not available in offline mode"); - } - - @Override - public List 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 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 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 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 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 getPodcastsChannels(boolean refresh) { - Timber.w("OfflineMusicService.getPodcastsChannels was called but it isn't available"); - return null; - } -} diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java index a63c8875..c09d6edc 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java @@ -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 { diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/TimeLimitedCache.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/TimeLimitedCache.java deleted file mode 100644 index 1a39a440..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/TimeLimitedCache.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - 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 -{ - - private SoftReference 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(value); - expires = System.currentTimeMillis() + timeUnit.toMillis(ttl); - } - - public void clear() - { - expires = 0L; - value = null; - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index f81c99b8..126454b5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -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") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt index 31d63ffd..1b430ae1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverter.kt index 72c6e00c..adff7ee5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIMusicDirectoryConverter.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index f5b4b4e4..75b93057 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -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() { /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt index 7c927044..5a6e1ba8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListModel.kt @@ -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> = MutableLiveData() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt index 2a5f1c46..e3974614 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumRowAdapter.kt @@ -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 ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index ddda850b..6b48979c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -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() { /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt index 2611e3d2..d58e97ca 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt @@ -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> = MutableLiveData() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt index dfd86ef9..2e312225 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt @@ -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() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index b2f6bf4a..d8776c14 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt index 723193ad..2862eaa8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListFragment.kt @@ -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> : Fragment() { internal val activeServerProvider: ActiveServerProvider by inject() internal val serverSettingsModel: ServerSettingsModel by viewModel() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt index 810e90f5..bf377eed 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt @@ -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 { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index a7093210..bde3efd5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index b5ecc21f..2d544ef1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -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) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt index f0d5e4e2..c6ca882a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionModel.kt @@ -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 = 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() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/log/FileLoggerTree.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/log/FileLoggerTree.kt index c6b701e9..28be7d2a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/log/FileLoggerTree.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/log/FileLoggerTree.kt @@ -161,7 +161,7 @@ class FileLoggerTree : Timber.DebugTree() { } } - private fun getLogFileList(): Array { + private fun getLogFileList(): Array? { val directory = FileUtil.getUltrasonicDirectory() return directory.listFiles { t -> t.name.matches(fileNameRegex) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt index dbd107b8..0d3e7791 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/AudioFocusHandler.kt @@ -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::class.java) private val audioManager by lazy { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt new file mode 100644 index 00000000..0f280258 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -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> + private val cachedArtist: LRUCache> + private val cachedAlbum: LRUCache> + private val cachedUserInfo: LRUCache> + private val cachedLicenseValid = TimeLimitedCache(expiresAfter = 10, TimeUnit.MINUTES) + private val cachedIndexes = TimeLimitedCache() + private val cachedArtists = TimeLimitedCache() + private val cachedPlaylists = TimeLimitedCache?>() + private val cachedPodcastsChannels = TimeLimitedCache>() + private val cachedMusicFolders = + TimeLimitedCache?>(10, TimeUnit.HOURS) + private val cachedGenres = TimeLimitedCache?>(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 { + 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 { + 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 { + 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) { + 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 { + 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?): 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? { + 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 { + return musicService.getShares(refresh) + } + + @Throws(Exception::class) + override fun getChatMessages(since: Long?): List? { + return musicService.getChatMessages(since) + } + + @Throws(Exception::class) + override fun addChatMessage(message: String) { + musicService.addChatMessage(message) + } + + @Throws(Exception::class) + override fun getBookmarks(): List? = 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, + description: String?, + expires: Long? + ): List { + 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) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index 6c75e62d..fd98da9b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -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 = 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() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index 26b96e36..eae455c1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -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) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 1706cf7e..e8f3c4f0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt index de935eef..fdc3b4cb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt @@ -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) { } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt new file mode 100644 index 00000000..f4f88592 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -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? + + @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 + + @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 + + @Throws(Exception::class) + fun getPlaylists(refresh: Boolean): List + + @Throws(Exception::class) + fun createPlaylist(id: String, name: String, entries: List) + + @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 + + // 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?): 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 + + @Throws(Exception::class) + fun getChatMessages(since: Long?): List? + + @Throws(Exception::class) + fun addChatMessage(message: String) + + @Throws(Exception::class) + fun getBookmarks(): List? + + @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, description: String?, expires: Long?): List + + @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? +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt index 0f3f33df..02ddb4a5 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt @@ -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 { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineException.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineException.kt new file mode 100644 index 00000000..5301a8d4 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineException.kt @@ -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 + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt new file mode 100644 index 00000000..f1fc7f43 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -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 = 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 = 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 = ArrayList() + val albums: MutableList = ArrayList() + val songs: MutableList = 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 { + val playlists: MutableList = 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) { + 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 = 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?): 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? { + 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, + description: String?, + expires: Long? + ): List { + throw OfflineException("Creating shares not available in offline mode") + } + + @Throws(Exception::class) + override fun getShares(refresh: Boolean): List { + 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 { + 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? { + 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? { + 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 { + 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 { + 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, + songs: MutableList + ) { + 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) { + for (file in FileUtil.listMediaFiles(parent)) { + if (file.isFile) { + children.add(file) + } else { + listFilesRecursively(file, children) + } + } + } + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 61898ceb..fd9f2de6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -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 . - - 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 ) { val pSongIds: MutableList = 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 { 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 { + ): List? { 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" } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt index 487e552a..b30490ac 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/DownloadHandler.kt @@ -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) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt index 549b9e8e..3ee6bf53 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt index e52e98d9..4ae89551 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt @@ -68,9 +68,11 @@ class ShareHandler(val context: Context) { ) { @Throws(Throwable::class) override fun doInBackground(): Share { - val ids: MutableList = ArrayList() + val ids: MutableList = 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) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeLimitedCache.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeLimitedCache.kt new file mode 100644 index 00000000..09afdb41 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/TimeLimitedCache.kt @@ -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(expiresAfter: Long = 60L, timeUnit: TimeUnit = TimeUnit.MINUTES) { + private var value: SoftReference? = 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 + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt index 165f4ab1..3a4638c7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/SongView.kt @@ -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) {