feat: Minstrel no longer talks to Lidarr: its connection, Delete via Lidarr and the unmonitor options are gone, and the track reports are plain quarantine (M502 #5525)
release / govulncheck (push) Successful in 15s
release / web (push) Failing after 17s
release / go (push) Successful in 1m13s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m9s
release / Build signed APK (releases and dev) (push) Successful in 5m19s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 15s
release / web (push) Failing after 17s
release / go (push) Successful in 1m13s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m9s
release / Build signed APK (releases and dev) (push) Successful in 5m19s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
Migration 0091 drops lidarr_config and renames the quarantine tables, types, indexes and the history column off the Lidarr name. The Integrations Lidarr card, the Reports "Delete via Lidarr" action, the merge and remove-track unmonitor options, the Lidarr audit actions and error codes go, on the server, web and Android. The README describes acquisition as it is now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
-3
@@ -27,8 +27,6 @@ class AdminQuarantineRepository @Inject constructor(
|
||||
suspend fun resolve(trackId: String) = api.resolve(trackId)
|
||||
|
||||
suspend fun deleteFile(trackId: String) = api.deleteFile(trackId)
|
||||
|
||||
suspend fun deleteViaLidarr(trackId: String) = api.deleteViaLidarr(trackId)
|
||||
}
|
||||
|
||||
// ── Mappers ──
|
||||
@@ -49,7 +47,6 @@ private fun AdminQuarantineItemWire.toDomain(): AdminQuarantineItemRef =
|
||||
artistName = artistName,
|
||||
albumTitle = albumTitle,
|
||||
albumId = albumId,
|
||||
lidarrAlbumMbid = lidarrAlbumMbid,
|
||||
reportCount = reportCount,
|
||||
latestAt = latestAt,
|
||||
reasonCounts = reasonCounts,
|
||||
|
||||
@@ -154,7 +154,7 @@ private fun SectionList(counts: AdminCounts, navController: NavHostController) {
|
||||
SectionCard(
|
||||
icon = Lucide.Inbox,
|
||||
title = "Requests",
|
||||
subtitle = "Approve or reject Lidarr requests",
|
||||
subtitle = "Approve or reject requests for new music",
|
||||
count = counts.requests,
|
||||
onClick = { navController.navigate(AdminRequests) },
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -85,7 +84,6 @@ fun AdminQuarantineScreen(
|
||||
rows = s.rows,
|
||||
onResolve = viewModel::resolve,
|
||||
onDeleteFile = viewModel::deleteFile,
|
||||
onDeleteViaLidarr = viewModel::deleteViaLidarr,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -97,7 +95,6 @@ private fun QueueList(
|
||||
rows: List<AdminQuarantineItemRef>,
|
||||
onResolve: (String) -> Unit,
|
||||
onDeleteFile: (String) -> Unit,
|
||||
onDeleteViaLidarr: (String) -> Unit,
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(items = rows, key = { it.trackId }) { row ->
|
||||
@@ -105,7 +102,6 @@ private fun QueueList(
|
||||
row = row,
|
||||
onResolve = { onResolve(row.trackId) },
|
||||
onDeleteFile = { onDeleteFile(row.trackId) },
|
||||
onDeleteViaLidarr = { onDeleteViaLidarr(row.trackId) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
@@ -117,7 +113,6 @@ private fun QuarantineRow(
|
||||
row: AdminQuarantineItemRef,
|
||||
onResolve: () -> Unit,
|
||||
onDeleteFile: () -> Unit,
|
||||
onDeleteViaLidarr: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -166,7 +161,6 @@ private fun QuarantineRow(
|
||||
ActionButtons(
|
||||
onResolve = onResolve,
|
||||
onDeleteFile = onDeleteFile,
|
||||
onDeleteViaLidarr = onDeleteViaLidarr,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -175,7 +169,6 @@ private fun QuarantineRow(
|
||||
private fun ActionButtons(
|
||||
onResolve: () -> Unit,
|
||||
onDeleteFile: () -> Unit,
|
||||
onDeleteViaLidarr: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -183,6 +176,5 @@ private fun ActionButtons(
|
||||
) {
|
||||
TextButton(onClick = onResolve) { Text("Resolve") }
|
||||
OutlinedButton(onClick = onDeleteFile) { Text("Delete file") }
|
||||
Button(onClick = onDeleteViaLidarr) { Text("Delete + Lidarr") }
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -82,7 +82,6 @@ class AdminQuarantineViewModel @Inject constructor(
|
||||
|
||||
fun resolve(trackId: String) = act(trackId) { repository.resolve(it) }
|
||||
fun deleteFile(trackId: String) = act(trackId) { repository.deleteFile(it) }
|
||||
fun deleteViaLidarr(trackId: String) = act(trackId) { repository.deleteViaLidarr(it) }
|
||||
|
||||
private fun act(trackId: String, action: suspend (String) -> Unit) {
|
||||
val before = internal.value
|
||||
|
||||
@@ -61,7 +61,7 @@ fun AdminRequestsScreen(
|
||||
AdminRequestsUiState.Loading -> LoadingCentered()
|
||||
AdminRequestsUiState.Empty -> EmptyState(
|
||||
title = "No requests waiting",
|
||||
body = "When users ask Lidarr for new music, their pending " +
|
||||
body = "When users ask for new music, their pending " +
|
||||
"requests show up here for approval.",
|
||||
)
|
||||
is AdminRequestsUiState.Error -> ErrorRetry(
|
||||
|
||||
@@ -19,7 +19,7 @@ import javax.inject.Singleton
|
||||
* leaks here: (1) the session cookie was being attached to every
|
||||
* external request the shared OkHttpClient services — including
|
||||
* Coil image fetches to artwork.musicbrainz.org / coverartarchive.org
|
||||
* / Lidarr's /MediaCover endpoints — exposing the session
|
||||
* — exposing the session
|
||||
* identifier to third-party logging; (2) a 401 from any of those
|
||||
* external hosts silently wiped the user's Minstrel session.
|
||||
* Restricting both attach + clear to placeholder-host requests
|
||||
|
||||
@@ -21,7 +21,7 @@ import javax.inject.Singleton
|
||||
* server URL in Settings without an app relaunch (Phase 11 wiring).
|
||||
*
|
||||
* Only requests whose host is the [PLACEHOLDER_HOST] sentinel are
|
||||
* rewritten. Absolute external URLs — Lidarr-surfaced artwork from
|
||||
* rewritten. Absolute external URLs — artwork from
|
||||
* artwork.musicbrainz.org / coverartarchive.org, for example — must
|
||||
* reach their authored host unchanged; rewriting them to the
|
||||
* Minstrel host produced 404s the user saw as missing Discover
|
||||
|
||||
@@ -105,19 +105,6 @@ object ErrorCopy {
|
||||
"cleartext_public" to
|
||||
"This server is on the internet, so its URL must start with https://. " +
|
||||
"Plain http:// only works on your home network.",
|
||||
"lidarr_unreachable" to
|
||||
"Lidarr is unreachable right now. Try again, or check Admin › Acquisition.",
|
||||
"lidarr_disabled" to "Lidarr integration is not enabled.",
|
||||
"lidarr_auth_failed" to "Lidarr authentication failed.",
|
||||
"lidarr_defaults_incomplete" to
|
||||
"Lidarr is missing a default quality profile or root folder. " +
|
||||
"Set them in Admin › Acquisition.",
|
||||
"lidarr_server_error" to "Lidarr returned an error. Check Lidarr's logs for the cause.",
|
||||
"lidarr_rejected" to
|
||||
"Lidarr rejected the request. Check the server logs for the field-level reason.",
|
||||
"lidarr_album_lookup_failed" to
|
||||
"Lidarr doesn't recognize this album. Try Resolve or Delete file instead.",
|
||||
"album_mbid_missing" to "This track has no Lidarr album to remove.",
|
||||
"request_not_pending" to "This request is no longer pending.",
|
||||
"request_not_found" to "That request no longer exists.",
|
||||
"not_followable" to "Various Artists and MusicBrainz placeholders can't be followed.",
|
||||
|
||||
+3
-7
@@ -8,10 +8,9 @@ import retrofit2.http.Path
|
||||
/**
|
||||
* Retrofit interface for `/api/admin/quarantine`.
|
||||
*
|
||||
* Three resolution endpoints:
|
||||
* - `resolve` → admin reviewed, no action taken (clears flags).
|
||||
* - `delete-file` → drop the local file; Lidarr keeps the track.
|
||||
* - `delete-via-lidarr` → ask Lidarr to remove the track upstream too.
|
||||
* Two resolution endpoints:
|
||||
* - `resolve` → admin reviewed, no action taken (clears flags).
|
||||
* - `delete-file` → delete the track's file and row.
|
||||
*/
|
||||
interface AdminQuarantineApi {
|
||||
@GET("api/admin/quarantine")
|
||||
@@ -22,7 +21,4 @@ interface AdminQuarantineApi {
|
||||
|
||||
@POST("api/admin/quarantine/{trackId}/delete-file")
|
||||
suspend fun deleteFile(@Path("trackId") trackId: String)
|
||||
|
||||
@POST("api/admin/quarantine/{trackId}/delete-via-lidarr")
|
||||
suspend fun deleteViaLidarr(@Path("trackId") trackId: String)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.fabledsword.minstrel.api.endpoints
|
||||
|
||||
import com.fabledsword.minstrel.models.wire.ArtistSuggestionWire
|
||||
import com.fabledsword.minstrel.models.wire.CreateRequestBody
|
||||
import com.fabledsword.minstrel.models.wire.LidarrSearchResultWire
|
||||
import com.fabledsword.minstrel.models.wire.CatalogueSearchResultWire
|
||||
import com.fabledsword.minstrel.models.wire.SnoozeSuggestionBody
|
||||
import com.fabledsword.minstrel.models.wire.SuggestionSnoozeWire
|
||||
import retrofit2.http.Body
|
||||
@@ -30,7 +30,7 @@ interface DiscoverApi {
|
||||
suspend fun search(
|
||||
@Query("q") query: String,
|
||||
@Query("kind") kind: String,
|
||||
): List<LidarrSearchResultWire>
|
||||
): List<CatalogueSearchResultWire>
|
||||
|
||||
@POST("api/requests")
|
||||
suspend fun createRequest(@Body body: CreateRequestBody)
|
||||
|
||||
+2
-2
@@ -75,8 +75,8 @@ data class LikeTogglePayload(
|
||||
)
|
||||
|
||||
/**
|
||||
* Persisted payload for `MutationKind.REQUEST_CREATE` (Discover →
|
||||
* Lidarr request that landed during a connectivity hiccup). Optional
|
||||
* Persisted payload for `MutationKind.REQUEST_CREATE` (a Discover
|
||||
* request that landed during a connectivity hiccup). Optional
|
||||
* fields stay null when not applicable; the replayer rebuilds the
|
||||
* `CreateRequestBody` from these.
|
||||
*/
|
||||
|
||||
+7
-7
@@ -4,13 +4,13 @@ import com.fabledsword.minstrel.api.endpoints.DiscoverApi
|
||||
import com.fabledsword.minstrel.cache.mutations.MutationQueue
|
||||
import com.fabledsword.minstrel.cache.mutations.RequestCreatePayload
|
||||
import com.fabledsword.minstrel.models.ArtistSuggestionRef
|
||||
import com.fabledsword.minstrel.models.LidarrRequestKind
|
||||
import com.fabledsword.minstrel.models.LidarrSearchResultRef
|
||||
import com.fabledsword.minstrel.models.RequestKind
|
||||
import com.fabledsword.minstrel.models.CatalogueResultRef
|
||||
import com.fabledsword.minstrel.models.SeedContributionRef
|
||||
import com.fabledsword.minstrel.models.SuggestionSnoozeRef
|
||||
import com.fabledsword.minstrel.models.wire.ArtistSuggestionWire
|
||||
import com.fabledsword.minstrel.models.wire.CreateRequestBody
|
||||
import com.fabledsword.minstrel.models.wire.LidarrSearchResultWire
|
||||
import com.fabledsword.minstrel.models.wire.CatalogueSearchResultWire
|
||||
import com.fabledsword.minstrel.models.wire.SeedContributionWire
|
||||
import com.fabledsword.minstrel.models.wire.SnoozeSuggestionBody
|
||||
import com.fabledsword.minstrel.models.wire.SuggestionSnoozeWire
|
||||
@@ -28,7 +28,7 @@ import javax.inject.Singleton
|
||||
enum class RequestOutcome { ACCEPTED, QUEUED }
|
||||
|
||||
/**
|
||||
* Read-through Discover surface: Lidarr search, suggestion feed, and
|
||||
* Read-through Discover surface: catalogue search, suggestion feed, and
|
||||
* the request-creation write path. Mirrors the Flutter
|
||||
* `DiscoverApi` + `mutationQueueProvider` integration —
|
||||
* `feedback_offline_first_for_server_writes` says writes never go
|
||||
@@ -113,7 +113,7 @@ class DiscoverRepository @Inject constructor(
|
||||
code in HTTP_CLIENT_ERR_MIN..HTTP_CLIENT_ERR_MAX &&
|
||||
code != HTTP_TIMEOUT && code != HTTP_TOO_MANY
|
||||
|
||||
suspend fun search(query: String, kind: LidarrRequestKind): List<LidarrSearchResultRef> =
|
||||
suspend fun search(query: String, kind: RequestKind): List<CatalogueResultRef> =
|
||||
api.search(query = query, kind = kind.wire).map { it.toDomain() }
|
||||
|
||||
/**
|
||||
@@ -123,7 +123,7 @@ class DiscoverRepository @Inject constructor(
|
||||
* 2xx, [RequestOutcome.QUEUED] when the call was buffered.
|
||||
*/
|
||||
suspend fun createRequest(
|
||||
kind: LidarrRequestKind,
|
||||
kind: RequestKind,
|
||||
artistMbid: String,
|
||||
artistName: String,
|
||||
albumMbid: String? = null,
|
||||
@@ -163,7 +163,7 @@ class DiscoverRepository @Inject constructor(
|
||||
|
||||
// ── Mappers (internal — wire types stay out of UI) ──
|
||||
|
||||
private fun LidarrSearchResultWire.toDomain(): LidarrSearchResultRef = LidarrSearchResultRef(
|
||||
private fun CatalogueSearchResultWire.toDomain(): CatalogueResultRef = CatalogueResultRef(
|
||||
mbid = mbid,
|
||||
name = name,
|
||||
secondaryText = secondaryText,
|
||||
|
||||
@@ -38,8 +38,8 @@ import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.Search as LucideSearch
|
||||
import com.fabledsword.minstrel.discover.data.RequestOutcome
|
||||
import com.fabledsword.minstrel.models.ArtistSuggestionRef
|
||||
import com.fabledsword.minstrel.models.LidarrRequestKind
|
||||
import com.fabledsword.minstrel.models.LidarrSearchResultRef
|
||||
import com.fabledsword.minstrel.models.RequestKind
|
||||
import com.fabledsword.minstrel.models.CatalogueResultRef
|
||||
import com.fabledsword.minstrel.models.SuggestionSnoozeRef
|
||||
import com.fabledsword.minstrel.nav.Discover
|
||||
import com.fabledsword.minstrel.shared.widgets.ShellContentWindowInsets
|
||||
@@ -161,7 +161,7 @@ private fun SearchBar(
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
singleLine = true,
|
||||
placeholder = { Text("Search Lidarr for new music") },
|
||||
placeholder = { Text("Search MusicBrainz for new music") },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { onSubmit() }),
|
||||
trailingIcon = {
|
||||
@@ -173,7 +173,7 @@ private fun SearchBar(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KindChips(kind: LidarrRequestKind, onChange: (LidarrRequestKind) -> Unit) {
|
||||
private fun KindChips(kind: RequestKind, onChange: (RequestKind) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -181,13 +181,13 @@ private fun KindChips(kind: LidarrRequestKind, onChange: (LidarrRequestKind) ->
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
selected = kind == LidarrRequestKind.ARTIST,
|
||||
onClick = { onChange(LidarrRequestKind.ARTIST) },
|
||||
selected = kind == RequestKind.ARTIST,
|
||||
onClick = { onChange(RequestKind.ARTIST) },
|
||||
label = { Text("Artists") },
|
||||
)
|
||||
FilterChip(
|
||||
selected = kind == LidarrRequestKind.ALBUM,
|
||||
onClick = { onChange(LidarrRequestKind.ALBUM) },
|
||||
selected = kind == RequestKind.ALBUM,
|
||||
onClick = { onChange(RequestKind.ALBUM) },
|
||||
label = { Text("Albums") },
|
||||
)
|
||||
}
|
||||
@@ -316,9 +316,9 @@ private fun SuggestionsHeader() {
|
||||
|
||||
@Composable
|
||||
private fun ResultsList(
|
||||
rows: List<LidarrSearchResultRef>,
|
||||
rows: List<CatalogueResultRef>,
|
||||
locallyRequestedMbids: Set<String>,
|
||||
onRequest: (LidarrSearchResultRef) -> Unit,
|
||||
onRequest: (CatalogueResultRef) -> Unit,
|
||||
) {
|
||||
if (rows.isEmpty()) {
|
||||
CenteredMessage("Nothing to add for that search yet.")
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.composables.icons.lucide.Disc3
|
||||
import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.User
|
||||
import com.fabledsword.minstrel.models.ArtistSuggestionRef
|
||||
import com.fabledsword.minstrel.models.LidarrSearchResultRef
|
||||
import com.fabledsword.minstrel.models.CatalogueResultRef
|
||||
import com.fabledsword.minstrel.models.SuggestionSnoozeRef
|
||||
import com.fabledsword.minstrel.shared.widgets.ServerImage
|
||||
|
||||
@@ -123,7 +123,7 @@ internal fun SnoozedTile(row: SuggestionSnoozeRef, onUnsnooze: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
internal fun ResultTile(
|
||||
row: LidarrSearchResultRef,
|
||||
row: CatalogueResultRef,
|
||||
locallyRequested: Boolean,
|
||||
onRequest: () -> Unit,
|
||||
) {
|
||||
|
||||
+11
-11
@@ -8,8 +8,8 @@ import com.fabledsword.minstrel.connectivity.recoveries
|
||||
import com.fabledsword.minstrel.discover.data.DiscoverRepository
|
||||
import com.fabledsword.minstrel.discover.data.RequestOutcome
|
||||
import com.fabledsword.minstrel.models.ArtistSuggestionRef
|
||||
import com.fabledsword.minstrel.models.LidarrRequestKind
|
||||
import com.fabledsword.minstrel.models.LidarrSearchResultRef
|
||||
import com.fabledsword.minstrel.models.RequestKind
|
||||
import com.fabledsword.minstrel.models.CatalogueResultRef
|
||||
import com.fabledsword.minstrel.models.SuggestionSnoozeRef
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -24,7 +24,7 @@ import javax.inject.Inject
|
||||
|
||||
data class DiscoverState(
|
||||
val query: String = "",
|
||||
val kind: LidarrRequestKind = LidarrRequestKind.ARTIST,
|
||||
val kind: RequestKind = RequestKind.ARTIST,
|
||||
val suggestions: SuggestionState = SuggestionState.Loading,
|
||||
val results: ResultsState = ResultsState.Idle,
|
||||
val locallyRequestedMbids: Set<String> = emptySet(),
|
||||
@@ -51,7 +51,7 @@ sealed interface ResultsState {
|
||||
/** No active search — the screen shows the suggestion feed. */
|
||||
data object Idle : ResultsState
|
||||
data object Loading : ResultsState
|
||||
data class Loaded(val items: List<LidarrSearchResultRef>) : ResultsState
|
||||
data class Loaded(val items: List<CatalogueResultRef>) : ResultsState
|
||||
data class Error(val message: String) : ResultsState
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class DiscoverViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setKind(kind: LidarrRequestKind) {
|
||||
fun setKind(kind: RequestKind) {
|
||||
internal.update { it.copy(kind = kind) }
|
||||
if (internal.value.query.isNotBlank()) runSearch()
|
||||
}
|
||||
@@ -177,14 +177,14 @@ class DiscoverViewModel @Inject constructor(
|
||||
* QUEUED) so the screen can pick the snackbar wording and update
|
||||
* the local "requested" overlay.
|
||||
*/
|
||||
suspend fun requestRow(row: LidarrSearchResultRef): RequestOutcome {
|
||||
suspend fun requestRow(row: CatalogueResultRef): RequestOutcome {
|
||||
val kind = internal.value.kind
|
||||
val outcome = repository.createRequest(
|
||||
kind = kind,
|
||||
artistMbid = if (kind == LidarrRequestKind.ARTIST) row.mbid else row.artistMbid,
|
||||
artistName = if (kind == LidarrRequestKind.ARTIST) row.name else row.secondaryText,
|
||||
albumMbid = if (kind == LidarrRequestKind.ALBUM) row.mbid else null,
|
||||
albumTitle = if (kind == LidarrRequestKind.ALBUM) row.name else null,
|
||||
artistMbid = if (kind == RequestKind.ARTIST) row.mbid else row.artistMbid,
|
||||
artistName = if (kind == RequestKind.ARTIST) row.name else row.secondaryText,
|
||||
albumMbid = if (kind == RequestKind.ALBUM) row.mbid else null,
|
||||
albumTitle = if (kind == RequestKind.ALBUM) row.name else null,
|
||||
)
|
||||
markRequested(row.mbid)
|
||||
return outcome
|
||||
@@ -192,7 +192,7 @@ class DiscoverViewModel @Inject constructor(
|
||||
|
||||
suspend fun requestSuggestion(s: ArtistSuggestionRef): RequestOutcome {
|
||||
val outcome = repository.createRequest(
|
||||
kind = LidarrRequestKind.ARTIST,
|
||||
kind = RequestKind.ARTIST,
|
||||
artistMbid = s.mbid,
|
||||
artistName = s.name,
|
||||
)
|
||||
|
||||
@@ -25,7 +25,6 @@ data class AdminQuarantineItemRef(
|
||||
val artistName: String,
|
||||
val albumTitle: String,
|
||||
val albumId: String,
|
||||
val lidarrAlbumMbid: String? = null,
|
||||
val reportCount: Int = 0,
|
||||
val latestAt: String = "",
|
||||
val reasonCounts: Map<String, Int> = emptyMap(),
|
||||
|
||||
@@ -4,25 +4,24 @@ import kotlinx.datetime.Instant
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Kind of Lidarr request being created. Wire form is the lowercase
|
||||
* Kind of request being created. Wire form is the lowercase
|
||||
* enum name; the helper [wire] keeps that mapping in one place.
|
||||
*/
|
||||
enum class LidarrRequestKind {
|
||||
enum class RequestKind {
|
||||
ARTIST, ALBUM, TRACK;
|
||||
|
||||
val wire: String get() = name.lowercase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lidarr search hit. Mirrors the Flutter client's
|
||||
* `LidarrSearchResult` — `mbid` is the result's own MBID; `artistMbid`
|
||||
* Catalogue search hit — `mbid` is the result's own MBID; `artistMbid`
|
||||
* and `albumMbid` are filled when the row is an album/track and the
|
||||
* UI needs the parent IDs to build the request.
|
||||
*
|
||||
* `inLibrary` and `requested` together cover the "already-handled"
|
||||
* cases — UI greys the row + swaps the Request button for a pill.
|
||||
*/
|
||||
data class LidarrSearchResultRef(
|
||||
data class CatalogueResultRef(
|
||||
val mbid: String,
|
||||
val name: String,
|
||||
val secondaryText: String = "",
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.fabledsword.minstrel.models
|
||||
|
||||
/**
|
||||
* One quarantined track owned by the caller. Mirrors the Flutter
|
||||
* `QuarantineMineRow` (web `LidarrQuarantineMineRow`).
|
||||
* `QuarantineMineRow` (web `QuarantineMineRow`).
|
||||
*
|
||||
* `reasonLabel` provides the display string mapped from the raw
|
||||
* server-side reason key — UI renders it as a pill on the row.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.fabledsword.minstrel.models
|
||||
|
||||
/**
|
||||
* Status of a Lidarr request. Stored on the wire as a plain string;
|
||||
* Status of a request. Stored on the wire as a plain string;
|
||||
* the typed enum keeps switch / when blocks exhaustive and lets the
|
||||
* UI map directly to a status pill color without raw-string matches.
|
||||
*/
|
||||
@@ -23,7 +23,7 @@ enum class RequestStatus {
|
||||
}
|
||||
|
||||
/**
|
||||
* One Lidarr request the user has submitted. Mirrors
|
||||
* One request the user has submitted. Mirrors
|
||||
* the Flutter client's `AdminRequest` —
|
||||
* shared between the user-side `/api/requests` view and the admin
|
||||
* cross-user view since the wire shape is identical.
|
||||
|
||||
@@ -29,7 +29,6 @@ data class AdminQuarantineItemWire(
|
||||
@SerialName("artist_name") val artistName: String = "",
|
||||
@SerialName("album_title") val albumTitle: String = "",
|
||||
@SerialName("album_id") val albumId: String = "",
|
||||
@SerialName("lidarr_album_mbid") val lidarrAlbumMbid: String? = null,
|
||||
@SerialName("report_count") val reportCount: Int = 0,
|
||||
@SerialName("latest_at") val latestAt: String = "",
|
||||
@SerialName("reason_counts") val reasonCounts: Map<String, Int> = emptyMap(),
|
||||
|
||||
@@ -4,8 +4,8 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* One row of `GET /api/catalogue/search` (the MusicBrainz search that replaced
|
||||
* Lidarr's, M502 #5519). Mirrors `web/src/lib/api/types.ts
|
||||
* One row of `GET /api/catalogue/search` (the MusicBrainz search, M502
|
||||
* #5519). Mirrors `web/src/lib/api/types.ts
|
||||
* CatalogueSearchResult`. `imageUrl` is the server's own art endpoint,
|
||||
* relative, so it goes through ServerImage.
|
||||
*
|
||||
@@ -14,7 +14,7 @@ import kotlinx.serialization.Serializable
|
||||
* empty so missing keys don't throw on decode.
|
||||
*/
|
||||
@Serializable
|
||||
data class LidarrSearchResultWire(
|
||||
data class CatalogueSearchResultWire(
|
||||
val mbid: String = "",
|
||||
val name: String = "",
|
||||
@SerialName("secondary_text") val secondaryText: String = "",
|
||||
@@ -27,8 +27,7 @@ data class LidarrSearchResultWire(
|
||||
|
||||
/**
|
||||
* `GET /api/discover/suggestions` row — an out-of-library artist
|
||||
* surfaced from ListenBrainz-derived seeds. `imageUrl` is resolved
|
||||
* on-demand from Lidarr server-side and may be empty.
|
||||
* surfaced from ListenBrainz-derived seeds. `imageUrl` may be empty.
|
||||
*/
|
||||
@Serializable
|
||||
data class ArtistSuggestionWire(
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
||||
/**
|
||||
* One row of `GET /api/quarantine/mine`. Mirrors
|
||||
* the Flutter client's `QuarantineMineRow`
|
||||
* (web `LidarrQuarantineMineRow`).
|
||||
* (web `QuarantineMineRow`).
|
||||
*
|
||||
* Reason values: `bad_rip` / `wrong_file` / `wrong_tags` / `duplicate`
|
||||
* / `other`. Stored as a plain string on the wire; the UI does the
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import javax.inject.Singleton
|
||||
enum class CancelOutcome { Synced, Queued }
|
||||
|
||||
/**
|
||||
* Read-through accessor for the caller's own Lidarr requests +
|
||||
* Read-through accessor for the caller's own requests +
|
||||
* cancel-while-pending write. No Room caching for v1 — same rationale
|
||||
* as HistoryRepository; the offline-snapshot mirror lands with a
|
||||
* later slice. Cancel is offline-first: direct REST on the happy
|
||||
|
||||
@@ -78,7 +78,7 @@ fun RequestsScreen(
|
||||
UiState.Loading -> LoadingCentered()
|
||||
UiState.Empty -> EmptyState(
|
||||
title = "Nothing requested yet",
|
||||
body = "Use Discover to ask Lidarr for new music; your " +
|
||||
body = "Use Discover to ask for new music; your " +
|
||||
"requests show up here.",
|
||||
)
|
||||
is UiState.Error -> ErrorRetry(
|
||||
@@ -218,7 +218,7 @@ private fun CancelConfirmDialog(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Cancel request?") },
|
||||
text = { Text("Cancel “$displayName”? Lidarr will stop searching for it.") },
|
||||
text = { Text("Cancel “$displayName”? Your request is withdrawn.") },
|
||||
confirmButton = {
|
||||
Button(onClick = onConfirm) { Text("Cancel request") }
|
||||
},
|
||||
|
||||
@@ -135,9 +135,8 @@ class AuthCookieInterceptorTest {
|
||||
}
|
||||
|
||||
// Drift #569 regression guard: a 401 from an external host must
|
||||
// NOT wipe the user's Minstrel session. A misbehaving CDN or a
|
||||
// Lidarr that's 401-ing on /MediaCover URLs used to silently sign
|
||||
// the user out.
|
||||
// NOT wipe the user's Minstrel session. A misbehaving CDN that's
|
||||
// 401-ing on artwork URLs used to silently sign the user out.
|
||||
@Test
|
||||
fun `does NOT clear cookie on 401 from non-placeholder host`() {
|
||||
authStore.setSessionCookie("session=keep")
|
||||
|
||||
@@ -67,7 +67,7 @@ class BaseUrlInterceptorTest {
|
||||
|
||||
@Test
|
||||
fun `passes external absolute URL through unchanged`() {
|
||||
// Lidarr-surfaced artwork hosts (artwork.musicbrainz.org,
|
||||
// External artwork hosts (artwork.musicbrainz.org,
|
||||
// coverartarchive.org) are absolute external URLs. The
|
||||
// interceptor must NOT rewrite them to the Minstrel base —
|
||||
// doing so produces 404s the user saw as missing Discover
|
||||
|
||||
Reference in New Issue
Block a user