release v2026.05.11.2: signing key + library infinite scroll #40
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package com.fabledsword.minstrel.cache.audio_cache
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defaults for the 2-bucket audio cache. Matches the Flutter client.
|
||||||
|
*
|
||||||
|
* - `likedCapBytes`: cap for the protected bucket — cached files for
|
||||||
|
* tracks the user has liked. Evicted only after the rolling bucket
|
||||||
|
* is fully drained (almost never under normal use).
|
||||||
|
* - `rollingCapBytes`: cap for the unprotected bucket — incidental /
|
||||||
|
* auto-prefetch downloads. LRU within this bucket.
|
||||||
|
*
|
||||||
|
* Phase 11 (Settings) will allow user overrides; the values are kept
|
||||||
|
* generous so first-time installs work well without configuration.
|
||||||
|
*/
|
||||||
|
data class CacheConfig(
|
||||||
|
val likedCapBytes: Long = DEFAULT_LIKED_CAP_BYTES,
|
||||||
|
val rollingCapBytes: Long = DEFAULT_ROLLING_CAP_BYTES,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_LIKED_CAP_BYTES: Long = 200L * 1024 * 1024 // 200 MiB
|
||||||
|
const val DEFAULT_ROLLING_CAP_BYTES: Long = 150L * 1024 * 1024 // 150 MiB
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.fabledsword.minstrel.player
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.media3.common.AudioAttributes
|
||||||
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.database.StandaloneDatabaseProvider
|
||||||
|
import androidx.media3.datasource.cache.CacheDataSink
|
||||||
|
import androidx.media3.datasource.cache.CacheDataSource
|
||||||
|
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||||
|
import androidx.media3.datasource.cache.SimpleCache
|
||||||
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
|
import com.fabledsword.minstrel.cache.audio_cache.CacheConfig
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the process-singleton ExoPlayer with our shared OkHttp +
|
||||||
|
* SimpleCache chain. The MinstrelPlayerService (Phase 6.2) calls
|
||||||
|
* `build()` once during onCreate.
|
||||||
|
*
|
||||||
|
* `simpleCache` is exposed as a val so the AudioCacheEvictionWorker
|
||||||
|
* (Phase 12.3) can call `cache.removeSpan(...)` when evicting tracks
|
||||||
|
* to disk-clear their cached spans.
|
||||||
|
*
|
||||||
|
* The default `LeastRecentlyUsedCacheEvictor` provides the floor LRU
|
||||||
|
* (sizeBytes cap = rollingCap); our policy layer in the worker layers
|
||||||
|
* the 2-bucket protection on top by feeding `removeSpan` only for
|
||||||
|
* unprotected tracks.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class PlayerFactory @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val okHttpClient: OkHttpClient,
|
||||||
|
private val cacheConfig: CacheConfig,
|
||||||
|
) {
|
||||||
|
private val cacheDir: File = File(context.cacheDir, "audio_cache").apply { mkdirs() }
|
||||||
|
|
||||||
|
val simpleCache: SimpleCache = SimpleCache(
|
||||||
|
cacheDir,
|
||||||
|
LeastRecentlyUsedCacheEvictor(cacheConfig.rollingCapBytes),
|
||||||
|
StandaloneDatabaseProvider(context),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun build(): ExoPlayer {
|
||||||
|
val httpDataSource = OkHttpDataSource.Factory(okHttpClient)
|
||||||
|
val cacheDataSource = CacheDataSource.Factory()
|
||||||
|
.setCache(simpleCache)
|
||||||
|
.setUpstreamDataSourceFactory(httpDataSource)
|
||||||
|
.setCacheWriteDataSinkFactory(
|
||||||
|
CacheDataSink.Factory()
|
||||||
|
.setCache(simpleCache)
|
||||||
|
.setFragmentSize(C.LENGTH_UNSET.toLong()),
|
||||||
|
)
|
||||||
|
val mediaSourceFactory = DefaultMediaSourceFactory(context)
|
||||||
|
.setDataSourceFactory(cacheDataSource)
|
||||||
|
|
||||||
|
return ExoPlayer.Builder(context)
|
||||||
|
.setMediaSourceFactory(mediaSourceFactory)
|
||||||
|
.setAudioAttributes(
|
||||||
|
AudioAttributes.Builder()
|
||||||
|
.setUsage(C.USAGE_MEDIA)
|
||||||
|
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||||
|
.build(),
|
||||||
|
/* handleAudioFocus = */ true,
|
||||||
|
)
|
||||||
|
.setHandleAudioBecomingNoisy(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.fabledsword.minstrel.player
|
||||||
|
|
||||||
|
import com.fabledsword.minstrel.cache.audio_cache.CacheConfig
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player + audio-cache wiring. PlayerFactory has @Inject so it doesn't
|
||||||
|
* need a @Provides here; this module exists to provide the
|
||||||
|
* configuration values (CacheConfig) it depends on, and any future
|
||||||
|
* player-related singletons that aren't @Inject-constructible.
|
||||||
|
*
|
||||||
|
* MinstrelPlayerService (Phase 6.2) injects PlayerFactory and calls
|
||||||
|
* `build()` from `onCreate`. The eviction worker (Phase 12.3) injects
|
||||||
|
* PlayerFactory too and reads `simpleCache` from it.
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object PlayerModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideCacheConfig(): CacheConfig = CacheConfig()
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ kotlinx-serialization = "1.7.3"
|
|||||||
kotlinx-coroutines = "1.9.0"
|
kotlinx-coroutines = "1.9.0"
|
||||||
kotlinx-datetime = "0.6.1"
|
kotlinx-datetime = "0.6.1"
|
||||||
kotlinx-serialization-converter = "1.0.0"
|
kotlinx-serialization-converter = "1.0.0"
|
||||||
media3 = "1.4.1"
|
media3 = "1.10.1"
|
||||||
coil = "3.0.0-rc02"
|
coil = "3.0.0-rc02"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
work = "2.10.0"
|
work = "2.10.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user