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 c09d6edc..0b80bae3 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/CacheCleaner.java @@ -2,7 +2,6 @@ package org.moire.ultrasonic.util; import android.os.AsyncTask; import android.os.StatFs; -import timber.log.Timber; import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.domain.Playlist; @@ -19,6 +18,7 @@ import java.util.Set; import java.util.SortedSet; import kotlin.Lazy; +import timber.log.Timber; import static org.koin.java.KoinJavaComponent.inject; @@ -88,6 +88,7 @@ public class CacheCleaner // No songs left in the folder if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) { + // Delete Artwork files Util.delete(FileUtil.getAlbumArtFile(dir)); children = dir.listFiles(); } diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java index 6bf557db..3d3f8b27 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/FileUtil.java @@ -137,9 +137,19 @@ public class FileUtil public static String getAlbumArtKey(MusicDirectory.Entry entry, boolean large) { File albumDir = getAlbumDirectory(entry); - File albumArtDir = getAlbumArtDirectory(); - if (albumArtDir == null || albumDir == null) { + return getAlbumArtKey(albumDir, large); + } + + /** + * Get the cache key for a given album entry + * @param albumDir The album directory + * @param large Whether to get the key for the large or the default image + * @return String The hash key + */ + public static String getAlbumArtKey(File albumDir, boolean large) + { + if (albumDir == null) { return null; } @@ -149,6 +159,7 @@ public class FileUtil } + public static File getAvatarFile(String username) { File albumArtDir = getAlbumArtDirectory(); @@ -159,7 +170,7 @@ public class FileUtil } String md5Hex = Util.md5Hex(username); - return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); + return new File(albumArtDir, String.format("%s%s", md5Hex, SUFFIX_LARGE)); } /** @@ -170,20 +181,20 @@ public class FileUtil public static File getAlbumArtFile(File albumDir) { File albumArtDir = getAlbumArtDirectory(); + String key = getAlbumArtKey(albumDir, true); - if (albumArtDir == null || albumDir == null) + if (key == null || albumArtDir == null) { return null; } - String md5Hex = Util.md5Hex(albumDir.getPath()); - return new File(albumArtDir, String.format("%s.jpeg", md5Hex)); + return new File(albumArtDir, key); } /** * Get the album art file for a given cache key - * @param cacheKey + * @param cacheKey The key (== the filename) * @return File object. Not guaranteed that it exists */ public static File getAlbumArtFile(String cacheKey) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index f0a71f33..52f7e5ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -1,23 +1,30 @@ package org.moire.ultrasonic.imageloader import android.content.Context +import android.text.TextUtils import android.view.View import android.widget.ImageView import com.squareup.picasso.Picasso import com.squareup.picasso.RequestCreator import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.domain.MusicDirectory +import org.moire.ultrasonic.service.RESTMusicService import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.Util +import timber.log.Timber /** * Our new image loader which uses Picasso as a backend. */ class ImageLoader( context: Context, - apiClient: SubsonicAPIClient, + private val apiClient: SubsonicAPIClient, private val config: ImageLoaderConfig ) { @@ -111,6 +118,56 @@ class ImageLoader( } } + /** + * Download a cover art file and cache it on disk + */ + fun cacheCoverArt( + entry: MusicDirectory.Entry + ) { + + // Synchronize on the entry so that we don't download concurrently for + // the same song. + synchronized(entry) { + // Always download the large size.. + val size = config.largeSize + + // Check cache to avoid downloading existing files + val file = FileUtil.getAlbumArtFile(entry) + + // Return if have a cache hit + if (file.exists()) return + + // Can't load empty string ids + val id = entry.coverArt + if (TextUtils.isEmpty(id)) return + + // Query the API + Timber.d("Loading cover art for: %s", entry) + val response = apiClient.getCoverArt(id!!, size.toLong()) + RESTMusicService.checkStreamResponseError(response) + + // Check for failure + if (response.stream == null) return + + // Write Response stream to file + var inputStream: InputStream? = null + try { + inputStream = response.stream + val bytes = Util.toByteArray(inputStream) + + var outputStream: OutputStream? = null + try { + outputStream = FileOutputStream(file) + outputStream.write(bytes) + } finally { + Util.close(outputStream) + } + } finally { + Util.close(inputStream) + } + } + } + private fun resolveSize(requested: Int, large: Boolean): Int { if (requested <= 0) { return if (large) config.largeSize else config.defaultSize diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index 020d95d0..30e5ff10 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -6,7 +6,6 @@ */ package org.moire.ultrasonic.service -import android.graphics.Bitmap import java.io.InputStream import java.util.concurrent.TimeUnit import org.koin.core.component.KoinComponent @@ -255,15 +254,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, @Throws(Exception::class) override fun getStarred2(): SearchResult = musicService.getStarred2() - @Throws(Exception::class) - override fun getCoverArt( - entry: MusicDirectory.Entry, - size: Int, - saveToFile: Boolean - ): Bitmap? { - return musicService.getCoverArt(entry, size, saveToFile) - } - @Throws(Exception::class) override fun getDownloadInputStream( song: MusicDirectory.Entry, @@ -446,15 +436,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, musicService.updateShare(id, description, expires) } - @Throws(Exception::class) - override fun getAvatar( - username: String?, - size: Int, - saveToFile: Boolean - ): Bitmap? { - return musicService.getAvatar(username, size, saveToFile) - } - companion object { private const val MUSIC_DIR_CACHE_SIZE = 100 } 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 5babce2b..f8e88257 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -60,6 +60,7 @@ class DownloadFile( private var completeWhenDone = false private val downloader: Downloader by inject() + private val imageLoaderProvider: ImageLoaderProvider by inject() val progress: MutableLiveData = MutableLiveData(0) @@ -276,7 +277,7 @@ class DownloadFile( if (isCancelled) { throw Exception(String.format("Download of '%s' was cancelled", song)) } - downloadAndSaveCoverArt(musicService) + downloadAndSaveCoverArt() } if (isPlaying) { @@ -330,12 +331,11 @@ class DownloadFile( return String.format("DownloadTask (%s)", song) } - private fun downloadAndSaveCoverArt(musicService: MusicService) { + private fun downloadAndSaveCoverArt() { try { if (!TextUtils.isEmpty(song.coverArt)) { // Download the largest size that we can display in the UI - val size = ImageLoaderProvider.config.largeSize - musicService.getCoverArt(song, size = size, saveToFile = true) + imageLoaderProvider.getImageLoader().cacheCoverArt(song) } } catch (e: Exception) { Timber.e(e, "Failed to get cover art.") diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index dc3b8cee..b9e5f5f3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -6,7 +6,6 @@ */ 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 @@ -111,20 +110,6 @@ interface MusicService { @Throws(Exception::class) fun getStarred2(): SearchResult - @Throws(Exception::class) - fun getCoverArt( - entry: MusicDirectory.Entry, - size: Int, - saveToFile: Boolean - ): Bitmap? - - @Throws(Exception::class) - fun getAvatar( - username: String?, - size: Int, - saveToFile: Boolean - ): Bitmap? - /** * Return response [InputStream] and a [Boolean] that indicates if this response is * partial. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index e6e0f425..a4ad2ca9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -6,7 +6,6 @@ */ package org.moire.ultrasonic.service -import android.graphics.Bitmap import android.media.MediaMetadataRetriever import java.io.BufferedReader import java.io.BufferedWriter @@ -40,7 +39,6 @@ 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.imageloader.BitmapUtils import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util @@ -119,32 +117,6 @@ class OfflineMusicService : MusicService, KoinComponent { return result } - override fun getAvatar( - username: String?, - size: Int, - saveToFile: Boolean - ): Bitmap? { - return try { - val bitmap = BitmapUtils.getAvatarBitmapFromDisk(username, size) - Util.scaleBitmap(bitmap, size) - } catch (ignored: Exception) { - null - } - } - - override fun getCoverArt( - entry: MusicDirectory.Entry, - size: Int, - saveToFile: Boolean - ): Bitmap? { - return try { - val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(entry, size) - Util.scaleBitmap(bitmap, size) - } catch (ignored: Exception) { - null - } - } - override fun search(criteria: SearchCriteria): SearchResult { val artists: MutableList = ArrayList() val albums: MutableList = ArrayList() 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 013fb7e5..625a8af0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -6,15 +6,11 @@ */ package org.moire.ultrasonic.service -import android.graphics.Bitmap -import android.text.TextUtils import java.io.BufferedWriter import java.io.File -import java.io.FileOutputStream import java.io.FileWriter import java.io.IOException import java.io.InputStream -import java.io.OutputStream import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException @@ -27,7 +23,6 @@ import org.moire.ultrasonic.cache.serializers.getIndexesSerializer import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline -import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isServerScalingEnabled import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre @@ -46,7 +41,6 @@ import org.moire.ultrasonic.domain.toDomainEntitiesList import org.moire.ultrasonic.domain.toDomainEntity import org.moire.ultrasonic.domain.toDomainEntityList import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity -import org.moire.ultrasonic.imageloader.BitmapUtils import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -489,80 +483,6 @@ open class RESTMusicService( return response.body()!!.starred2.toDomainEntity() } - // This is only called by DownloadFile to cache the cover art for offline use - @Throws(Exception::class) - override fun getCoverArt( - entry: MusicDirectory.Entry, - size: Int, - saveToFile: Boolean - ): Bitmap? { - // Synchronize on the entry so that we don't download concurrently for - // the same song. - synchronized(entry) { - // Use cached file, if existing. - var bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(entry, size) - val serverScaling = isServerScalingEnabled() - - if (bitmap == null) { - Timber.d("Loading cover art for: %s", entry) - - val id = entry.coverArt - - // Can't load empty string ids - if (TextUtils.isEmpty(id)) { - return null - } - - val response = subsonicAPIClient.getCoverArt(id!!, size.toLong()) - checkStreamResponseError(response) - - if (response.stream == null) { - return null // Failed to load - } - - var inputStream: InputStream? = null - try { - inputStream = response.stream - val bytes = Util.toByteArray(inputStream) - - // If we aren't allowing server-side scaling, always save the file to disk - // because it will be unmodified - if (!serverScaling || saveToFile) { - var outputStream: OutputStream? = null - try { - outputStream = FileOutputStream( - FileUtil.getAlbumArtFile(entry) - ) - outputStream.write(bytes) - } finally { - Util.close(outputStream) - } - } - - bitmap = BitmapUtils.getSampledBitmap(bytes, size) - } finally { - Util.close(inputStream) - } - } - - // Return scaled bitmap - return Util.scaleBitmap(bitmap, size) - } - } - - @Throws(SubsonicRESTException::class, IOException::class) - private fun checkStreamResponseError(response: StreamResponse) { - if (response.hasError() || response.stream == null) { - if (response.apiError != null) { - throw SubsonicRESTException(response.apiError!!) - } else { - throw IOException( - "Failed to make endpoint request, code: " + response.responseHttpCode - ) - } - } - } - @Throws(Exception::class) override fun getDownloadInputStream( song: MusicDirectory.Entry, @@ -811,64 +731,22 @@ open class RESTMusicService( } } - // TODO: Implement file caching in Picasso AvatarRequestHandler, - // and then use Picasso to handle this cache - // This method is called from nowhere (all avatars are loaded directly using Picasso) - @Throws(Exception::class) - override fun getAvatar( - username: String?, - size: Int, - saveToFile: Boolean - ): Bitmap? { - // Synchronize on the username so that we don't download concurrently for - // the same user. - if (username == null) { - return null - } - - synchronized(username) { - // Use cached file, if existing. - var bitmap = BitmapUtils.getAvatarBitmapFromDisk(username, size) - - if (bitmap == null) { - var inputStream: InputStream? = null - try { - val response = subsonicAPIClient.getAvatar(username) - - if (response.hasError()) return null - - inputStream = response.stream - val bytes = Util.toByteArray(inputStream) - - // If we aren't allowing server-side scaling, always save the file to disk - // because it will be unmodified - if (saveToFile) { - var outputStream: OutputStream? = null - - try { - outputStream = FileOutputStream( - FileUtil.getAvatarFile(username) - ) - outputStream.write(bytes) - } finally { - Util.close(outputStream) - } - } - - bitmap = BitmapUtils.getSampledBitmap(bytes, size) - } finally { - Util.close(inputStream) - } - } - - // Return scaled bitmap - return Util.scaleBitmap(bitmap, size) - } - } - companion object { private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder" private const val INDEXES_STORAGE_NAME = "indexes" private const val ARTISTS_STORAGE_NAME = "artists" + + @Throws(SubsonicRESTException::class, IOException::class) + fun checkStreamResponseError(response: StreamResponse) { + if (response.hasError() || response.stream == null) { + if (response.apiError != null) { + throw SubsonicRESTException(response.apiError!!) + } else { + throw IOException( + "Failed to make endpoint request, code: " + response.responseHttpCode + ) + } + } + } } }