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

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:
2026-10-11 13:48:20 -04:00
co-authored by Claude Opus 5.5
parent c54792c51e
commit 3a16732883
170 changed files with 994 additions and 6242 deletions
@@ -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") }
}
}
@@ -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.",
@@ -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)
@@ -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.
*/
@@ -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,
) {
@@ -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(
@@ -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
@@ -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