Cleanup unused functions from RESTMusicService,

put the caching functionality into the ImageLoader
This commit is contained in:
tzugen 2021-06-08 17:12:55 +02:00
parent 3c554caf2e
commit be356d9c0a
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
8 changed files with 95 additions and 210 deletions

View File

@ -2,7 +2,6 @@ package org.moire.ultrasonic.util;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.StatFs; import android.os.StatFs;
import timber.log.Timber;
import org.moire.ultrasonic.data.ActiveServerProvider; import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Playlist; import org.moire.ultrasonic.domain.Playlist;
@ -19,6 +18,7 @@ import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import kotlin.Lazy; import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject; import static org.koin.java.KoinJavaComponent.inject;
@ -88,6 +88,7 @@ public class CacheCleaner
// No songs left in the folder // No songs left in the folder
if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath())) if (children.length == 1 && children[0].getPath().equals(FileUtil.getAlbumArtFile(dir).getPath()))
{ {
// Delete Artwork files
Util.delete(FileUtil.getAlbumArtFile(dir)); Util.delete(FileUtil.getAlbumArtFile(dir));
children = dir.listFiles(); children = dir.listFiles();
} }

View File

@ -137,9 +137,19 @@ public class FileUtil
public static String getAlbumArtKey(MusicDirectory.Entry entry, boolean large) public static String getAlbumArtKey(MusicDirectory.Entry entry, boolean large)
{ {
File albumDir = getAlbumDirectory(entry); 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; return null;
} }
@ -149,6 +159,7 @@ public class FileUtil
} }
public static File getAvatarFile(String username) public static File getAvatarFile(String username)
{ {
File albumArtDir = getAlbumArtDirectory(); File albumArtDir = getAlbumArtDirectory();
@ -159,7 +170,7 @@ public class FileUtil
} }
String md5Hex = Util.md5Hex(username); 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) public static File getAlbumArtFile(File albumDir)
{ {
File albumArtDir = getAlbumArtDirectory(); File albumArtDir = getAlbumArtDirectory();
String key = getAlbumArtKey(albumDir, true);
if (albumArtDir == null || albumDir == null) if (key == null || albumArtDir == null)
{ {
return null; return null;
} }
String md5Hex = Util.md5Hex(albumDir.getPath()); return new File(albumArtDir, key);
return new File(albumArtDir, String.format("%s.jpeg", md5Hex));
} }
/** /**
* Get the album art file for a given cache 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 * @return File object. Not guaranteed that it exists
*/ */
public static File getAlbumArtFile(String cacheKey) public static File getAlbumArtFile(String cacheKey)

View File

@ -1,23 +1,30 @@
package org.moire.ultrasonic.imageloader package org.moire.ultrasonic.imageloader
import android.content.Context import android.content.Context
import android.text.TextUtils
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator import com.squareup.picasso.RequestCreator
import java.io.File 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.BuildConfig
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.util.FileUtil 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. * Our new image loader which uses Picasso as a backend.
*/ */
class ImageLoader( class ImageLoader(
context: Context, context: Context,
apiClient: SubsonicAPIClient, private val apiClient: SubsonicAPIClient,
private val config: ImageLoaderConfig 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 { private fun resolveSize(requested: Int, large: Boolean): Int {
if (requested <= 0) { if (requested <= 0) {
return if (large) config.largeSize else config.defaultSize return if (large) config.largeSize else config.defaultSize

View File

@ -6,7 +6,6 @@
*/ */
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.graphics.Bitmap
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -255,15 +254,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
@Throws(Exception::class) @Throws(Exception::class)
override fun getStarred2(): SearchResult = musicService.getStarred2() 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) @Throws(Exception::class)
override fun getDownloadInputStream( override fun getDownloadInputStream(
song: MusicDirectory.Entry, song: MusicDirectory.Entry,
@ -446,15 +436,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
musicService.updateShare(id, description, expires) 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 { companion object {
private const val MUSIC_DIR_CACHE_SIZE = 100 private const val MUSIC_DIR_CACHE_SIZE = 100
} }

View File

@ -60,6 +60,7 @@ class DownloadFile(
private var completeWhenDone = false private var completeWhenDone = false
private val downloader: Downloader by inject() private val downloader: Downloader by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
val progress: MutableLiveData<Int> = MutableLiveData(0) val progress: MutableLiveData<Int> = MutableLiveData(0)
@ -276,7 +277,7 @@ class DownloadFile(
if (isCancelled) { if (isCancelled) {
throw Exception(String.format("Download of '%s' was cancelled", song)) throw Exception(String.format("Download of '%s' was cancelled", song))
} }
downloadAndSaveCoverArt(musicService) downloadAndSaveCoverArt()
} }
if (isPlaying) { if (isPlaying) {
@ -330,12 +331,11 @@ class DownloadFile(
return String.format("DownloadTask (%s)", song) return String.format("DownloadTask (%s)", song)
} }
private fun downloadAndSaveCoverArt(musicService: MusicService) { private fun downloadAndSaveCoverArt() {
try { try {
if (!TextUtils.isEmpty(song.coverArt)) { if (!TextUtils.isEmpty(song.coverArt)) {
// Download the largest size that we can display in the UI // Download the largest size that we can display in the UI
val size = ImageLoaderProvider.config.largeSize imageLoaderProvider.getImageLoader().cacheCoverArt(song)
musicService.getCoverArt(song, size = size, saveToFile = true)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to get cover art.") Timber.e(e, "Failed to get cover art.")

View File

@ -6,7 +6,6 @@
*/ */
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.graphics.Bitmap
import java.io.InputStream import java.io.InputStream
import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.ChatMessage
@ -111,20 +110,6 @@ interface MusicService {
@Throws(Exception::class) @Throws(Exception::class)
fun getStarred2(): SearchResult 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 * Return response [InputStream] and a [Boolean] that indicates if this response is
* partial. * partial.

View File

@ -6,7 +6,6 @@
*/ */
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import java.io.BufferedReader import java.io.BufferedReader
import java.io.BufferedWriter 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.SearchResult
import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.domain.UserInfo import org.moire.ultrasonic.domain.UserInfo
import org.moire.ultrasonic.imageloader.BitmapUtils
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -119,32 +117,6 @@ class OfflineMusicService : MusicService, KoinComponent {
return result 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 { override fun search(criteria: SearchCriteria): SearchResult {
val artists: MutableList<Artist> = ArrayList() val artists: MutableList<Artist> = ArrayList()
val albums: MutableList<MusicDirectory.Entry> = ArrayList() val albums: MutableList<MusicDirectory.Entry> = ArrayList()

View File

@ -6,15 +6,11 @@
*/ */
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import android.graphics.Bitmap
import android.text.TextUtils
import java.io.BufferedWriter import java.io.BufferedWriter
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.FileWriter import java.io.FileWriter
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException 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.cache.serializers.getMusicFolderListSerializer
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline 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.Bookmark
import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre 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.toDomainEntity
import org.moire.ultrasonic.domain.toDomainEntityList import org.moire.ultrasonic.domain.toDomainEntityList
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
import org.moire.ultrasonic.imageloader.BitmapUtils
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
@ -489,80 +483,6 @@ open class RESTMusicService(
return response.body()!!.starred2.toDomainEntity() 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) @Throws(Exception::class)
override fun getDownloadInputStream( override fun getDownloadInputStream(
song: MusicDirectory.Entry, 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 { companion object {
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder" private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
private const val INDEXES_STORAGE_NAME = "indexes" private const val INDEXES_STORAGE_NAME = "indexes"
private const val ARTISTS_STORAGE_NAME = "artists" 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
)
}
}
}
} }
} }