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.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();
}

View File

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

View File

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

View File

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

View File

@ -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<Int> = 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.")

View File

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

View File

@ -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<Artist> = ArrayList()
val albums: MutableList<MusicDirectory.Entry> = ArrayList()

View File

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