feat: M3 weighted shuffle v1 — real /api/radio with scoring #21

Merged
bvandeusen merged 262 commits from dev into main 2026-04-27 11:00:29 -04:00
Showing only changes of commit 6aec03fc02 - Show all commits
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.player
import android.content.ComponentName
import android.content.Context
import android.os.Bundle
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
@@ -90,7 +91,12 @@ class PlayerController @Inject constructor(
val controller =
try {
connectController()
} catch (e: Exception) {
} catch (
// Service-connection failure is a "show error, move on" boundary —
// any throwable from the Media3 IPC handshake gets surfaced via
// playbackError. Specific-exception handling buys nothing here.
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
uiStateInternal.value =
uiStateInternal.value.copy(playbackError = e.message ?: "Player unavailable")
return
@@ -132,7 +138,13 @@ class PlayerController @Inject constructor(
{
try {
cont.resume(future.get())
} catch (e: Exception) {
} catch (
// ListenableFuture.get() throws ExecutionException +
// InterruptedException + CancellationException — handle
// them uniformly by forwarding to the continuation;
// the caller catches at the outer boundary.
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
cont.resumeWithException(e)
}
},
@@ -141,19 +153,24 @@ class PlayerController @Inject constructor(
cont.invokeOnCancellation { future.cancel(/* mayInterruptIfRunning = */ false) }
}
private fun TrackRef.toMediaItem(source: String?): MediaItem =
MediaItem.Builder()
private fun TrackRef.toMediaItem(source: String?): MediaItem {
val metadata = MediaMetadata.Builder()
.setTitle(title)
.setArtist(artistName)
.setAlbumTitle(albumTitle)
.apply {
if (source != null) setExtras(sourceExtras(source))
}
.build()
return MediaItem.Builder()
.setMediaId(id)
.setUri(streamUrl)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(title)
.setArtist(artistName)
.setAlbumTitle(albumTitle)
.apply { source?.let { setExtras(android.os.Bundle().apply { putString(MINSTREL_SOURCE_KEY, it) }) } }
.build(),
)
.setMediaMetadata(metadata)
.build()
}
private fun sourceExtras(source: String): Bundle =
Bundle().apply { putString(MINSTREL_SOURCE_KEY, source) }
companion object {
const val MINSTREL_SOURCE_KEY: String = "minstrel_source"