album art on lock scree and test fix
This commit is contained in:
parent
179c51a6fe
commit
9594e963bc
@ -14,8 +14,8 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
test:
|
||||||
name: Build APK and Run Tests
|
name: Run Tests
|
||||||
runs-on: linux/amd64
|
runs-on: linux/amd64
|
||||||
container:
|
container:
|
||||||
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||||||
@ -60,6 +60,42 @@ jobs:
|
|||||||
cargo test
|
cargo test
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build Android APK
|
||||||
|
runs-on: linux/amd64
|
||||||
|
needs: test
|
||||||
|
container:
|
||||||
|
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
src-tauri/target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Cache Node dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-bun-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
bun install
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: bun run build
|
run: bun run build
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,142 @@
|
|||||||
|
package com.dtourolle.jellytau.player
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.LruCache
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory cache for album artwork bitmaps with LRU eviction.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - LruCache for efficient memory usage (1/8 of heap, typically 12-16MB)
|
||||||
|
* - Automatic bitmap scaling to 512x512 max (lock screen optimal size)
|
||||||
|
* - Async HTTP downloads using Dispatchers.IO (non-blocking)
|
||||||
|
* - Graceful error handling for network failures and corrupted images
|
||||||
|
* - Singleton pattern for app-wide access
|
||||||
|
*
|
||||||
|
* Cache lifecycle: In-memory only, cleared on app termination.
|
||||||
|
*/
|
||||||
|
class AlbumArtCache(context: Context) {
|
||||||
|
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
|
||||||
|
private val cacheSize = maxMemory / 8 // Use 1/8 of available heap
|
||||||
|
|
||||||
|
private val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
|
||||||
|
override fun sizeOf(key: String, bitmap: Bitmap): Int {
|
||||||
|
return bitmap.byteCount / 1024 // Size in KB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get artwork bitmap for the given URL.
|
||||||
|
*
|
||||||
|
* Checks memory cache first, then downloads from network if not cached.
|
||||||
|
* Scaling and error handling are done transparently.
|
||||||
|
*
|
||||||
|
* @param url The Jellyfin server artwork URL
|
||||||
|
* @return The bitmap, or null if download failed or URL is invalid
|
||||||
|
*/
|
||||||
|
suspend fun getArtwork(url: String): Bitmap? {
|
||||||
|
// Check memory cache first
|
||||||
|
memoryCache.get(url)?.let { return it }
|
||||||
|
|
||||||
|
// Download from network if not cached
|
||||||
|
return downloadAndCache(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download artwork from network and add to cache.
|
||||||
|
*
|
||||||
|
* Runs on IO dispatcher to avoid blocking the main thread.
|
||||||
|
* Automatically scales large images to 512x512 max.
|
||||||
|
* On failure, logs error and returns null.
|
||||||
|
*
|
||||||
|
* @param url The Jellyfin server artwork URL
|
||||||
|
* @return The cached bitmap, or null if download/decode failed
|
||||||
|
*/
|
||||||
|
private suspend fun downloadAndCache(url: String): Bitmap? = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val connection = URL(url).openConnection() as HttpURLConnection
|
||||||
|
connection.doInput = true
|
||||||
|
connection.connectTimeout = 5000
|
||||||
|
connection.readTimeout = 5000
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
|
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||||
|
android.util.Log.w("AlbumArtCache", "Failed to download artwork: HTTP ${connection.responseCode}")
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
|
val input = connection.inputStream
|
||||||
|
val bitmap = BitmapFactory.decodeStream(input)
|
||||||
|
input.close()
|
||||||
|
connection.disconnect()
|
||||||
|
|
||||||
|
bitmap?.let {
|
||||||
|
// Scale down if too large (lock screen doesn't need full resolution)
|
||||||
|
val scaled = scaleDownIfNeeded(it, MAX_ARTWORK_SIZE)
|
||||||
|
memoryCache.put(url, scaled)
|
||||||
|
android.util.Log.d("AlbumArtCache", "Cached artwork: ${scaled.width}x${scaled.height}")
|
||||||
|
scaled
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("AlbumArtCache", "Failed to download artwork: ${e.message}", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale down bitmap if it exceeds max size while maintaining aspect ratio.
|
||||||
|
*
|
||||||
|
* @param bitmap The original bitmap
|
||||||
|
* @param maxSize Maximum width or height (e.g., 512)
|
||||||
|
* @return Original bitmap if smaller than maxSize, else scaled version
|
||||||
|
*/
|
||||||
|
private fun scaleDownIfNeeded(bitmap: Bitmap, maxSize: Int): Bitmap {
|
||||||
|
if (bitmap.width <= maxSize && bitmap.height <= maxSize) return bitmap
|
||||||
|
|
||||||
|
val ratio = minOf(
|
||||||
|
maxSize.toFloat() / bitmap.width,
|
||||||
|
maxSize.toFloat() / bitmap.height
|
||||||
|
)
|
||||||
|
|
||||||
|
val newWidth = (bitmap.width * ratio).toInt()
|
||||||
|
val newHeight = (bitmap.height * ratio).toInt()
|
||||||
|
|
||||||
|
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cached bitmaps from memory.
|
||||||
|
*
|
||||||
|
* Useful for memory-constrained situations or settings reset.
|
||||||
|
*/
|
||||||
|
fun clear() {
|
||||||
|
memoryCache.evictAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_ARTWORK_SIZE = 512 // 512x512 max for lock screen
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var instance: AlbumArtCache? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create singleton instance.
|
||||||
|
*
|
||||||
|
* Thread-safe with double-checked locking pattern.
|
||||||
|
*
|
||||||
|
* @param context Android context for initialization
|
||||||
|
* @return The singleton AlbumArtCache instance
|
||||||
|
*/
|
||||||
|
fun getInstance(context: Context): AlbumArtCache {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
instance ?: AlbumArtCache(context).also { instance = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -143,6 +143,8 @@ class JellyTauPlayer(private val appContext: Context) {
|
|||||||
private var currentArtist: String = ""
|
private var currentArtist: String = ""
|
||||||
private var currentAlbum: String? = null
|
private var currentAlbum: String? = null
|
||||||
private var currentDurationMs: Long = 0
|
private var currentDurationMs: Long = 0
|
||||||
|
private var currentArtworkUrl: String? = null
|
||||||
|
private var currentArtworkBitmap: android.graphics.Bitmap? = null
|
||||||
|
|
||||||
/** Media type enum */
|
/** Media type enum */
|
||||||
enum class MediaType { AUDIO, VIDEO }
|
enum class MediaType { AUDIO, VIDEO }
|
||||||
@ -550,6 +552,8 @@ class JellyTauPlayer(private val appContext: Context) {
|
|||||||
currentArtist = artist ?: ""
|
currentArtist = artist ?: ""
|
||||||
currentAlbum = album
|
currentAlbum = album
|
||||||
currentDurationMs = durationMs
|
currentDurationMs = durationMs
|
||||||
|
currentArtworkUrl = artworkUrl
|
||||||
|
currentArtworkBitmap = null // Reset on new track
|
||||||
|
|
||||||
// Detect media type
|
// Detect media type
|
||||||
currentMediaType = if (mediaType.equals("video", ignoreCase = true)) {
|
currentMediaType = if (mediaType.equals("video", ignoreCase = true)) {
|
||||||
@ -666,6 +670,20 @@ class JellyTauPlayer(private val appContext: Context) {
|
|||||||
|
|
||||||
// Start the foreground service for lockscreen controls
|
// Start the foreground service for lockscreen controls
|
||||||
startPlaybackService()
|
startPlaybackService()
|
||||||
|
|
||||||
|
// Download album art asynchronously (non-blocking)
|
||||||
|
currentArtworkUrl?.let { url ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
try {
|
||||||
|
val bitmap = AlbumArtCache.getInstance(appContext).getArtwork(url)
|
||||||
|
currentArtworkBitmap = bitmap
|
||||||
|
updatePlaybackServiceNotification(exoPlayer.isPlaying)
|
||||||
|
android.util.Log.d("JellyTauPlayer", "Album art loaded: ${bitmap?.width}x${bitmap?.height}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("JellyTauPlayer", "Failed to load album art", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -990,7 +1008,8 @@ class JellyTauPlayer(private val appContext: Context) {
|
|||||||
album = currentAlbum,
|
album = currentAlbum,
|
||||||
duration = currentDurationMs,
|
duration = currentDurationMs,
|
||||||
position = (exoPlayer.currentPosition).coerceAtLeast(0),
|
position = (exoPlayer.currentPosition).coerceAtLeast(0),
|
||||||
isPlaying = isPlaying
|
isPlaying = isPlaying,
|
||||||
|
artworkBitmap = currentArtworkBitmap
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.w("JellyTauPlayer", "Playback service not available for notification update")
|
android.util.Log.w("JellyTauPlayer", "Playback service not available for notification update")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user