From 5ac36b749bd8ab8c263a9c7f1bac97e7fb75f38e Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 26 May 2021 23:17:52 +0200 Subject: [PATCH] Convert all Services to Kotlin --- .../ultrasonic/service/CachedMusicService.kt | 978 +++++----- .../moire/ultrasonic/service/MusicService.kt | 237 ++- .../ultrasonic/service/OfflineException.kt | 39 +- .../ultrasonic/service/OfflineMusicService.kt | 1592 ++++++++--------- 4 files changed, 1328 insertions(+), 1518 deletions(-) diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/CachedMusicService.kt index 5d81f7e0..417802ca 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/CachedMusicService.kt @@ -16,524 +16,470 @@ Copyright 2009 (C) Sindre Mehus */ -package org.moire.ultrasonic.service; +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; +import android.graphics.Bitmap +import org.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.domain.Bookmark +import org.moire.ultrasonic.domain.ChatMessage +import org.moire.ultrasonic.domain.Genre +import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.JukeboxStatus +import org.moire.ultrasonic.domain.Lyrics +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.MusicFolder +import org.moire.ultrasonic.domain.Playlist +import org.moire.ultrasonic.domain.PodcastsChannel +import org.moire.ultrasonic.domain.SearchCriteria +import org.moire.ultrasonic.domain.SearchResult +import org.moire.ultrasonic.domain.Share +import org.moire.ultrasonic.domain.UserInfo +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.LRUCache +import org.moire.ultrasonic.util.TimeLimitedCache +import org.moire.ultrasonic.util.Util +import java.io.InputStream +import java.util.concurrent.TimeUnit /** * @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); - } -} +class CachedMusicService(private val musicService: MusicService) : MusicService { + private val activeServerProvider = inject( + ActiveServerProvider::class.java + ) + private val cachedMusicDirectories: LRUCache> + private val cachedArtist: LRUCache> + private val cachedAlbum: LRUCache> + private val cachedUserInfo: LRUCache> + private val cachedLicenseValid = TimeLimitedCache(120, TimeUnit.SECONDS) + private val cachedIndexes = TimeLimitedCache(60 * 60, TimeUnit.SECONDS) + private val cachedArtists = TimeLimitedCache(60 * 60, TimeUnit.SECONDS) + private val cachedPlaylists = TimeLimitedCache?>(3600, TimeUnit.SECONDS) + private val cachedPodcastsChannels = + TimeLimitedCache>(3600, TimeUnit.SECONDS) + private val cachedMusicFolders = + TimeLimitedCache?>(10 * 3600, TimeUnit.SECONDS) + private val cachedGenres = TimeLimitedCache?>(10 * 3600, TimeUnit.SECONDS) + 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 result = cachedLicenseValid.get() + if (result == null) { + result = musicService.isLicenseValid() + cachedLicenseValid[result, if (result) 30L * 60L else 2L * 60L] = TimeUnit.SECONDS + } + return result + } + + @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.value.getRestUrl(null) + val newFolderId = activeServerProvider.value.getActiveServer().musicFolderId + if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) { + cachedMusicFolders.clear() + cachedMusicDirectories.clear() + cachedLicenseValid.clear() + cachedIndexes.clear() + cachedPlaylists.clear() + cachedGenres.clear() + cachedAlbum.clear() + cachedArtist.clear() + cachedUserInfo.clear() + restUrl = newUrl + cachedMusicFolderId = newFolderId + } + } + + @Throws(Exception::class) + override fun star(id: String?, albumId: String?, artistId: String?) { + musicService.star(id, albumId, artistId) + } + + @Throws(Exception::class) + override fun unstar(id: String?, albumId: String?, artistId: String?) { + musicService.unstar(id, albumId, artistId) + } + + @Throws(Exception::class) + override fun setRating(id: String, rating: Int) { + musicService.setRating(id, rating) + } + + @Throws(Exception::class) + override fun getGenres(refresh: Boolean): List? { + 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) + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.kt index 09aee4a5..4705d78a 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/MusicService.kt @@ -1,151 +1,192 @@ /* - 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 + * MusicService.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. */ -package org.moire.ultrasonic.service; +package org.moire.ultrasonic.service -import android.graphics.Bitmap; +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 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; +interface MusicService { + @Throws(Exception::class) + fun ping() -import java.io.InputStream; -import java.util.List; + @Throws(Exception::class) + fun isLicenseValid(): Boolean -import kotlin.Pair; + @Throws(Exception::class) + fun getGenres(refresh: Boolean): List? -/** - * @author Sindre Mehus - */ -public interface MusicService -{ + @Throws(Exception::class) + fun star(id: String?, albumId: String?, artistId: String?) - void ping() throws Exception; + @Throws(Exception::class) + fun unstar(id: String?, albumId: String?, artistId: String?) - boolean isLicenseValid() throws Exception; + @Throws(Exception::class) + fun setRating(id: String, rating: Int) - List getGenres(boolean refresh) throws Exception; + @Throws(Exception::class) + fun getMusicFolders(refresh: Boolean): List - void star(String id, String albumId, String artistId) throws Exception; + @Throws(Exception::class) + fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes - void unstar(String id, String albumId, String artistId) throws Exception; + @Throws(Exception::class) + fun getArtists(refresh: Boolean): Indexes - void setRating(String id, int rating) throws Exception; + @Throws(Exception::class) + fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory - List getMusicFolders(boolean refresh) throws Exception; + @Throws(Exception::class) + fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory - Indexes getIndexes(String musicFolderId, boolean refresh) throws Exception; + @Throws(Exception::class) + fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory - Indexes getArtists(boolean refresh) throws Exception; + @Throws(Exception::class) + fun search(criteria: SearchCriteria): SearchResult? - MusicDirectory getMusicDirectory(String id, String name, boolean refresh) throws Exception; + @Throws(Exception::class) + fun getPlaylist(id: String, name: String): MusicDirectory - MusicDirectory getArtist(String id, String name, boolean refresh) throws Exception; + @Throws(Exception::class) + fun getPodcastsChannels(refresh: Boolean): List - MusicDirectory getAlbum(String id, String name, boolean refresh) throws Exception; + @Throws(Exception::class) + fun getPlaylists(refresh: Boolean): List - SearchResult search(SearchCriteria criteria) throws Exception; + @Throws(Exception::class) + fun createPlaylist(id: String, name: String, entries: List) - MusicDirectory getPlaylist(String id, String name) throws Exception; + @Throws(Exception::class) + fun deletePlaylist(id: String) - List getPodcastsChannels(boolean refresh) throws Exception; + @Throws(Exception::class) + fun updatePlaylist(id: String, name: String?, comment: String?, pub: Boolean) - List getPlaylists(boolean refresh) throws Exception; + @Throws(Exception::class) + fun getLyrics(artist: String, title: String): Lyrics? - void createPlaylist(String id, String name, List entries) throws Exception; + @Throws(Exception::class) + fun scrobble(id: String, submission: Boolean) - void deletePlaylist(String id) throws Exception; + @Throws(Exception::class) + fun getAlbumList(type: String, size: Int, offset: Int, musicFolderId: String?): MusicDirectory - void updatePlaylist(String id, String name, String comment, boolean pub) throws Exception; + @Throws(Exception::class) + fun getAlbumList2( + type: String, + size: Int, + offset: Int, + musicFolderId: String? + ): MusicDirectory - Lyrics getLyrics(String artist, String title) throws Exception; + @Throws(Exception::class) + fun getRandomSongs(size: Int): MusicDirectory - void scrobble(String id, boolean submission) throws Exception; + @Throws(Exception::class) + fun getSongsByGenre(genre: String, count: Int, offset: Int): MusicDirectory - MusicDirectory getAlbumList(String type, int size, int offset, String musicFolderId) throws Exception; + @Throws(Exception::class) + fun getStarred(): SearchResult - MusicDirectory getAlbumList2(String type, int size, int offset, String musicFolderId) throws Exception; + @Throws(Exception::class) + fun getStarred2(): SearchResult - MusicDirectory getRandomSongs(int size) throws Exception; + @Throws(Exception::class) + fun getCoverArt( + entry: MusicDirectory.Entry?, + size: Int, + saveToFile: Boolean, + highQuality: Boolean + ): Bitmap? - MusicDirectory getSongsByGenre(String genre, int count, int offset) throws Exception; + @Throws(Exception::class) + fun getAvatar(username: String?, size: Int, saveToFile: Boolean, highQuality: Boolean): Bitmap? - SearchResult getStarred() throws Exception; + /** + * 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 - SearchResult getStarred2() throws Exception; + // TODO: Refactor and remove this call (see RestMusicService implementation) + @Throws(Exception::class) + fun getVideoUrl(id: String, useFlash: Boolean): String? - Bitmap getCoverArt(MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality) throws Exception; + @Throws(Exception::class) + fun updateJukeboxPlaylist(ids: List?): JukeboxStatus - Bitmap getAvatar(String username, int size, boolean saveToFile, boolean highQuality) throws Exception; + @Throws(Exception::class) + fun skipJukebox(index: Int, offsetSeconds: Int): JukeboxStatus - /** - * 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; + @Throws(Exception::class) + fun stopJukebox(): JukeboxStatus - // TODO: Refactor and remove this call (see RestMusicService implementation) - String getVideoUrl(String id, boolean useFlash) throws Exception; + @Throws(Exception::class) + fun startJukebox(): JukeboxStatus - JukeboxStatus updateJukeboxPlaylist(List ids) throws Exception; + @Throws(Exception::class) + fun getJukeboxStatus(): JukeboxStatus - JukeboxStatus skipJukebox(int index, int offsetSeconds) throws Exception; + @Throws(Exception::class) + fun setJukeboxGain(gain: Float): JukeboxStatus - JukeboxStatus stopJukebox() throws Exception; + @Throws(Exception::class) + fun getShares(refresh: Boolean): List - JukeboxStatus startJukebox() throws Exception; + @Throws(Exception::class) + fun getChatMessages(since: Long?): List? - JukeboxStatus getJukeboxStatus() throws Exception; + @Throws(Exception::class) + fun addChatMessage(message: String) - JukeboxStatus setJukeboxGain(float gain) throws Exception; + @Throws(Exception::class) + fun getBookmarks(): List? - List getShares(boolean refresh) throws Exception; + @Throws(Exception::class) + fun deleteBookmark(id: String) - List getChatMessages(Long since) throws Exception; + @Throws(Exception::class) + fun createBookmark(id: String, position: Int) - void addChatMessage(String message) throws Exception; + @Throws(Exception::class) + fun getVideos(refresh: Boolean): MusicDirectory? - List getBookmarks() throws Exception; + @Throws(Exception::class) + fun getUser(username: String): UserInfo - void deleteBookmark(String id) throws Exception; + @Throws(Exception::class) + fun createShare(ids: List, description: String?, expires: Long?): List - void createBookmark(String id, int position) throws Exception; + @Throws(Exception::class) + fun deleteShare(id: String) - MusicDirectory getVideos(boolean refresh) throws Exception; + @Throws(Exception::class) + fun updateShare(id: String, description: String?, expires: Long?) - 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; + @Throws(Exception::class) + fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory? } \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineException.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineException.kt index 845ba693..5833e133 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineException.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineException.kt @@ -1,35 +1,16 @@ /* - 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 + * OfflineException.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. */ -package org.moire.ultrasonic.service; +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); - } -} +class OfflineException(message: String?) : Exception(message) { + companion object { + private const val serialVersionUID = -4479642294747429444L + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.kt index d5c1fac1..6b696964 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -1,877 +1,719 @@ /* - 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 + * 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 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) throws OfflineException { - throw new OfflineException("OfflineMusicService.getAlbumList2 was called but it isn't available"); - } - - @Override - public String getVideoUrl(String id, boolean useFlash) throws OfflineException { - throw new OfflineException("OfflineMusicService.getVideoUrl was called but it isn't available"); - } - - @Override - public List getChatMessages(Long since) throws OfflineException { - throw new OfflineException("OfflineMusicService.getChatMessages was called but it isn't available"); - } - - @Override - public void addChatMessage(String message) throws OfflineException { - throw new OfflineException("OfflineMusicService.addChatMessage was called but it isn't available"); - } - - @Override - public List getBookmarks() throws OfflineException { - throw new OfflineException("OfflineMusicService.getBookmarks was called but it isn't available"); - } - - @Override - public void deleteBookmark(String id) throws OfflineException { - throw new OfflineException("OfflineMusicService.deleteBookmark was called but it isn't available"); - } - - @Override - public void createBookmark(String id, int position) throws OfflineException { - throw new OfflineException("OfflineMusicService.createBookmark was called but it isn't available"); - } - - @Override - public MusicDirectory getVideos(boolean refresh) throws OfflineException { - throw new OfflineException("OfflineMusicService.getVideos was called but it isn't available"); - } - - @Override - public SearchResult getStarred2() throws OfflineException { - throw new OfflineException("OfflineMusicService.getStarred2 was called but it isn't available"); - } - - @Override - public void ping() { - } - - @Override - public boolean isLicenseValid() { - return true; - } - - @Override - public Indexes getArtists(boolean refresh) throws OfflineException { - throw new OfflineException("OfflineMusicService.getArtists was called but it isn't available"); - } - - @Override - public MusicDirectory getArtist(String id, String name, boolean refresh) throws OfflineException { - throw new OfflineException("OfflineMusicService.getArtist was called but it isn't available"); - } - - @Override - public MusicDirectory getAlbum(String id, String name, boolean refresh) throws OfflineException { - throw new OfflineException("OfflineMusicService.getAlbum was called but it isn't available"); - } - - @Override - public MusicDirectory getPodcastEpisodes(String podcastChannelId) throws OfflineException { - throw new OfflineException("OfflineMusicService.getPodcastEpisodes was called but it isn't available"); - } - - @Override - public Pair getDownloadInputStream(MusicDirectory.Entry song, long offset, int maxBitrate) throws OfflineException { - throw new OfflineException("OfflineMusicService.getDownloadInputStream was called but it isn't available"); - } - - @Override - public void setRating(String id, int rating) throws OfflineException { - throw new OfflineException("OfflineMusicService.setRating was called but it isn't available"); - } - - @Override - public List getPodcastsChannels(boolean refresh) throws OfflineException { - throw new OfflineException("OfflineMusicService.getPodcastsChannels was called but it isn't available"); - } -} +package org.moire.ultrasonic.service + +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import org.koin.java.KoinJavaComponent.inject +import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.Bookmark +import org.moire.ultrasonic.domain.ChatMessage +import org.moire.ultrasonic.domain.Genre +import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.JukeboxStatus +import org.moire.ultrasonic.domain.Lyrics +import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.domain.MusicFolder +import org.moire.ultrasonic.domain.Playlist +import org.moire.ultrasonic.domain.PodcastsChannel +import org.moire.ultrasonic.domain.SearchCriteria +import org.moire.ultrasonic.domain.SearchResult +import org.moire.ultrasonic.domain.Share +import org.moire.ultrasonic.domain.UserInfo +import org.moire.ultrasonic.util.Constants +import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.Util +import timber.log.Timber +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.security.SecureRandom +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 + +class OfflineMusicService : MusicService { + private val activeServerProvider = inject( + ActiveServerProvider::class.java + ) + + 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!!.toLowerCase(Locale.ROOT) + var rhs = rhsArtist.name!!.toLowerCase(Locale.ROOT) + val lhs1 = lhs[0] + val rhs1 = rhs[0] + if (Character.isDigit(lhs1) && !Character.isDigit(rhs1)) { + return@sortWith 1 + } + if (Character.isDigit(rhs1) && !Character.isDigit(lhs1)) { + return@sortWith -1 + } + for (article in ignoredArticles) { + var index = lhs.indexOf(String.format("%s ", article.toLowerCase(Locale.ROOT))) + if (index == 0) { + lhs = lhs.substring(article.length + 1) + } + index = rhs.indexOf(String.format("%s ", article.toLowerCase(Locale.ROOT))) + if (index == 0) { + rhs = rhs.substring(article.length + 1) + } + } + lhs.compareTo(rhs) + } + + return Indexes(0L, ignoredArticlesString, artists = artists) + } + + override fun getMusicDirectory( + id: String, + name: String?, + refresh: Boolean + ): MusicDirectory { + val dir = File(id) + val result = MusicDirectory() + result.name = dir.name + + val seen: MutableCollection = 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 (e: 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 (e: 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.sortWith { lhs, rhs -> + when { + lhs.closeness == rhs.closeness -> { + return@sortWith 0 + } + lhs.closeness > rhs.closeness -> { + return@sortWith -1 + } + else -> { + return@sortWith 1 + } + } + } + + albums.sortWith { lhs, rhs -> + when { + lhs.closeness == rhs.closeness -> { + return@sortWith 0 + } + lhs.closeness > rhs.closeness -> { + return@sortWith -1 + } + else -> { + return@sortWith 1 + } + } + } + + songs.sortWith { lhs, rhs -> + when { + lhs.closeness == rhs.closeness -> { + return@sortWith 0 + } + lhs.closeness > rhs.closeness -> { + return@sortWith -1 + } + else -> { + return@sortWith 1 + } + } + } + + return SearchResult(artists, albums, songs) + } + + 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) + } + } + + @Throws(Exception::class) + override fun createPlaylist(id: String, name: String, entries: List) { + val playlistFile = + FileUtil.getPlaylistFile(activeServerProvider.value.getActiveServer().name, name) + val fw = FileWriter(playlistFile) + val bw = BufferedWriter(fw) + try { + fw.write("#EXTM3U\n") + for (e in entries) { + var filePath = FileUtil.getSongFile(e).absolutePath + if (!File(filePath).exists()) { + val ext = FileUtil.getExtension(filePath) + val base = FileUtil.getBaseName(filePath) + filePath = "$base.complete.$ext" + } + fw.write( + """ + $filePath + + """.trimIndent() + ) + } + } catch (e: 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 = SecureRandom() + 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("OfflineMusicService.getAlbumList2 was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getVideoUrl(id: String, useFlash: Boolean): String? { + throw OfflineException("OfflineMusicService.getVideoUrl was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getChatMessages(since: Long?): List? { + throw OfflineException("OfflineMusicService.getChatMessages was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun addChatMessage(message: String) { + throw OfflineException("OfflineMusicService.addChatMessage was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getBookmarks(): List? { + throw OfflineException("OfflineMusicService.getBookmarks was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun deleteBookmark(id: String) { + throw OfflineException("OfflineMusicService.deleteBookmark was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun createBookmark(id: String, position: Int) { + throw OfflineException("OfflineMusicService.createBookmark was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getVideos(refresh: Boolean): MusicDirectory? { + throw OfflineException("OfflineMusicService.getVideos was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getStarred2(): SearchResult { + throw OfflineException("OfflineMusicService.getStarred2 was called but it isn't available") + } + + override fun ping() {} + + override fun isLicenseValid(): Boolean = true + + @Throws(OfflineException::class) + override fun getArtists(refresh: Boolean): Indexes { + throw OfflineException("OfflineMusicService.getArtists was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getArtist(id: String, name: String?, refresh: Boolean): MusicDirectory { + throw OfflineException("OfflineMusicService.getArtist was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory { + throw OfflineException("OfflineMusicService.getAlbum was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory? { + throw OfflineException("OfflineMusicService.getPodcastEpisodes was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getDownloadInputStream( + song: MusicDirectory.Entry, + offset: Long, + maxBitrate: Int + ): Pair { + throw OfflineException("OfflineMusicService.getDownloadInputStream was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun setRating(id: String, rating: Int) { + throw OfflineException("OfflineMusicService.setRating was called but it isn't available") + } + + @Throws(OfflineException::class) + override fun getPodcastsChannels(refresh: Boolean): List { + throw OfflineException("OfflineMusicService.getPodcastsChannels was called but it isn't available") + } + + 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) + } + + 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("^%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 + } + + 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.toLowerCase(Locale.ROOT) + val queryParts = COMPILE.split(query) + val nameParts = COMPILE.split( + name!!.toLowerCase(Locale.ROOT) + ) + var closeness = 0 + for (queryPart in queryParts) { + for (namePart in nameParts) { + if (namePart == queryPart) { + closeness++ + } + } + } + return closeness + } + + private fun listFilesRecursively(parent: File, children: MutableList) { + for (file in FileUtil.listMediaFiles(parent)) { + if (file.isFile) { + children.add(file) + } else { + listFilesRecursively(file, children) + } + } + } + } +} \ No newline at end of file