feat: a wanted list — follow artists on release-type profiles; requests and re-acquisition become wants (M502 #5520)
release / govulncheck (push) Successful in 15s
release / web (push) Failing after 1m21s
release / go (push) Successful in 1m35s
release / integration (push) Failing after 4m57s
release / android (push) Successful in 5m11s
release / Build signed APK (releases and dev) (push) Successful in 5m28s
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 1m21s
release / go (push) Successful in 1m35s
release / integration (push) Failing after 4m57s
release / android (push) Successful in 5m11s
release / Build signed APK (releases and dev) (push) Successful in 5m28s
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
Minstrel keeps its own list of what it is to get, keyed by MusicBrainz release group. A want comes from an approved request, a new release by a followed artist, or an album whose files went missing, and is held once the library has a full edition of it. - Migration 0081: release_type_profiles (Standard seeded: Album, EP, Single, studio only), follows, follow_seen_groups and wants. lidarr_requests becomes requests with plain *_mbid columns; approved album and track requests move onto the wanted list, approved artist requests become follows, and the library's album artists start out followed. missing_reacquisitions and the retry settings go. - internal/wanted: follows are checked against MusicBrainz discographies daily. The first check sets a baseline, and only releases after it are wanted unless the follow asked for what already exists. Profiles filter by primary and secondary type, so live recordings stay out unless a profile ticks Live. - internal/requests (was lidarrrequests): approving puts the request on the wanted list, and an artist request can be approved on a chosen profile. A request completes when its want is held. - The re-acquisition sweeper wants a missing album back instead of asking Lidarr; searching for it is the search schedule's (#5521). - Web: an admin Wanted page (Wanted, Missing files, Held, Following), a follow control on artist pages, a release-type profiles card on Integrations, "Approve on a profile" on Requests, and want state on Missing files. The artist detail API returns the artist's MBID. - Android: the request body sends artist_mbid, album_mbid and track_mbid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,10 +9,11 @@
|
||||
import { errMessage } from '#lib/api/errors.js';
|
||||
import { pushToast } from '#lib/stores/toast.svelte.js';
|
||||
|
||||
// Policy for turning a missing file back into a Lidarr request
|
||||
// (milestone #290). Lives on the missing-files page rather than under
|
||||
// Integrations because this is where the operator meets the problem it
|
||||
// solves; Lidarr is the mechanism, not the subject.
|
||||
// Policy for putting an album whose files went missing back on the wanted
|
||||
// list (milestone #290, M502 #5520). Lives on the missing-files page rather
|
||||
// than under Integrations because this is where the operator meets the
|
||||
// problem it solves. Searching for a want, and retrying, is the search
|
||||
// schedule's job, so there is no retry policy here.
|
||||
|
||||
let saved = $state<ReacquisitionSettings | null>(null);
|
||||
let form = $state<ReacquisitionSettings | null>(null);
|
||||
@@ -31,11 +32,7 @@
|
||||
return {
|
||||
enabled: s.enabled,
|
||||
grace_hours: s.grace_hours,
|
||||
backoff_base_hours: s.backoff_base_hours,
|
||||
backoff_max_hours: s.backoff_max_hours,
|
||||
max_attempts: s.max_attempts,
|
||||
max_per_pass: s.max_per_pass,
|
||||
auto_approve: s.auto_approve
|
||||
max_per_pass: s.max_per_pass
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,28 +67,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Plain-language echo of what the numbers add up to. The individual fields
|
||||
// are meaningless on their own — "6 hours" says nothing until you know it
|
||||
// doubles — so the card states the resulting schedule.
|
||||
const schedule = $derived.by(() => {
|
||||
if (!form) return '';
|
||||
const steps: string[] = [];
|
||||
let hours = form.backoff_base_hours;
|
||||
for (let i = 0; i < form.max_attempts; i++) {
|
||||
steps.push(`${hours}h`);
|
||||
hours = Math.min(hours * 2, form.backoff_max_hours);
|
||||
}
|
||||
return steps.join(' → ');
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="space-y-4 rounded-xl border border-border bg-surface p-5">
|
||||
<div>
|
||||
<h3 class="font-display text-lg font-medium text-text-primary">Automatic re-acquisition</h3>
|
||||
<p class="mt-1 text-sm text-text-secondary">
|
||||
When a file has been missing for a while, Minstrel can ask Lidarr for the album
|
||||
again by itself. Requests are made per album, not per track — a whole folder
|
||||
going missing is one request, not forty.
|
||||
When a file has been missing for a while, Minstrel can put its album back on the
|
||||
<a href="/admin/wanted?tab=missing" class="text-accent-fg hover:underline">wanted list</a>
|
||||
by itself. It's wanted per album, not per track: a whole folder going missing is one
|
||||
want, not forty.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +91,7 @@
|
||||
<label class="flex items-start gap-3">
|
||||
<input type="checkbox" bind:checked={form.enabled} class="mt-1" />
|
||||
<span>
|
||||
<span class="text-sm text-text-primary">Ask Lidarr for missing albums automatically</span>
|
||||
<span class="text-sm text-text-primary">Want missing albums back automatically</span>
|
||||
<span class="block text-xs text-text-secondary">
|
||||
Off leaves missing files listed here and does nothing about them.
|
||||
</span>
|
||||
@@ -115,11 +100,11 @@
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<label class="block">
|
||||
<span class="text-sm text-text-primary">Wait before the first attempt</span>
|
||||
<span class="text-sm text-text-primary">Wait before wanting it back</span>
|
||||
<span class="block text-xs text-text-secondary">
|
||||
Hours a file must be missing before anything is requested. A drive that
|
||||
Hours a file must be missing before its album is wanted. A drive that
|
||||
didn't mount, or a container that started before its media, resolves
|
||||
itself well inside a day — this is what stops those becoming requests.
|
||||
itself well inside a day; this is what stops those becoming wants.
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
@@ -132,53 +117,10 @@
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm text-text-primary">Attempts before giving up</span>
|
||||
<span class="block text-xs text-text-secondary">
|
||||
After this many tries the album is left alone. It's picked up again if the
|
||||
file comes back and goes missing later.
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
bind:value={form.max_attempts}
|
||||
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-text-primary focus-visible:outline-solid focus-visible:outline-2
|
||||
focus-visible:outline-accent"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm text-text-primary">First retry gap (hours)</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="168"
|
||||
bind:value={form.backoff_base_hours}
|
||||
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-text-primary focus-visible:outline-solid focus-visible:outline-2
|
||||
focus-visible:outline-accent"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm text-text-primary">Longest gap (hours)</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="720"
|
||||
bind:value={form.backoff_max_hours}
|
||||
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-text-primary focus-visible:outline-solid focus-visible:outline-2
|
||||
focus-visible:outline-accent"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm text-text-primary">Albums per sweep</span>
|
||||
<span class="block text-xs text-text-secondary">
|
||||
Ceiling on requests made each hour, so a large loss trickles.
|
||||
Ceiling on albums wanted each hour, so a large loss trickles.
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
@@ -192,33 +134,17 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="rounded-md bg-surface-hover px-3 py-2 text-xs text-text-secondary">
|
||||
Retry schedule: <span class="font-mono text-text-primary">{schedule}</span> after the
|
||||
first attempt.
|
||||
</p>
|
||||
|
||||
<label class="flex items-start gap-3">
|
||||
<input type="checkbox" bind:checked={form.auto_approve} class="mt-1" />
|
||||
<span>
|
||||
<span class="text-sm text-text-primary">Send requests to Lidarr without approval</span>
|
||||
<span class="block text-xs text-text-secondary">
|
||||
Off means requests appear in the Requests queue for you to approve, and
|
||||
nothing is downloaded until you do.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{#if saved && saved.unnameable_albums > 0}
|
||||
<!-- Stated rather than left to be inferred: without this, an operator
|
||||
watching those rows never get a request would reasonably conclude
|
||||
watching those rows never get wanted would reasonably conclude
|
||||
the feature is broken. -->
|
||||
<p class="flex items-start gap-2 rounded-md bg-surface-hover px-3 py-2 text-xs text-text-secondary">
|
||||
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
|
||||
<span>
|
||||
{saved.unnameable_albums}
|
||||
{saved.unnameable_albums === 1 ? 'album has' : 'albums have'} missing files but no
|
||||
MusicBrainz ID, so {saved.unnameable_albums === 1 ? 'it' : 'they'} can't be requested —
|
||||
Lidarr has no way to identify the release. A re-scan after tagging fixes it.
|
||||
MusicBrainz ID, so {saved.unnameable_albums === 1 ? 'it' : 'they'} can't be wanted:
|
||||
there's no way to identify the release. A re-scan after tagging fixes it.
|
||||
</span>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user