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:
@@ -0,0 +1,155 @@
|
||||
<script lang="ts">
|
||||
import { BellPlus, BellRing } from 'lucide-svelte';
|
||||
import { useQueryClient } from '@tanstack/svelte-query';
|
||||
import {
|
||||
createFollowQuery,
|
||||
createReleaseProfilesQuery,
|
||||
putFollow,
|
||||
deleteFollow
|
||||
} from '#lib/api/wanted.js';
|
||||
import { qk } from '#lib/api/queries.js';
|
||||
import { errMessage } from '#lib/api/errors.js';
|
||||
import { pushToast } from '#lib/stores/toast.svelte.js';
|
||||
import Modal from './Modal.svelte';
|
||||
|
||||
// Follow an artist for the wanted list (M502 #5520): their new releases are
|
||||
// wanted, filtered by a release-type profile. Admin-only; the artist page
|
||||
// shows it when the artist carries a MusicBrainz id.
|
||||
|
||||
let { artistMbid, artistName }: { artistMbid: string; artistName: string } = $props();
|
||||
|
||||
const client = useQueryClient();
|
||||
const followStore = $derived(createFollowQuery(artistMbid));
|
||||
const follow = $derived($followStore);
|
||||
const profilesStore = createReleaseProfilesQuery();
|
||||
const profiles = $derived($profilesStore.data?.profiles ?? []);
|
||||
|
||||
const following = $derived(follow.data?.following === true);
|
||||
|
||||
let open = $state(false);
|
||||
let profileChoice = $state('');
|
||||
let wantExisting = $state(false);
|
||||
let saving = $state(false);
|
||||
|
||||
function openModal() {
|
||||
profileChoice = follow.data?.profile_id ?? profiles.find((p) => p.is_default)?.id ?? '';
|
||||
wantExisting = false;
|
||||
open = true;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await Promise.all([
|
||||
client.invalidateQueries({ queryKey: qk.adminFollow(artistMbid) }),
|
||||
client.invalidateQueries({ queryKey: ['adminFollows'] }),
|
||||
client.invalidateQueries({ queryKey: ['adminWants'] })
|
||||
]);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving = true;
|
||||
try {
|
||||
await putFollow(artistMbid, {
|
||||
artist_name: artistName,
|
||||
profile_id: profileChoice || undefined,
|
||||
want_existing: wantExisting
|
||||
});
|
||||
open = false;
|
||||
pushToast(
|
||||
wantExisting
|
||||
? `Following ${artistName}. What they've released that isn't here is wanted on the next check.`
|
||||
: `Following ${artistName}. New releases will be wanted.`
|
||||
);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
pushToast(errMessage(e), 'error');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function unfollow() {
|
||||
saving = true;
|
||||
try {
|
||||
await deleteFollow(artistMbid);
|
||||
open = false;
|
||||
pushToast(`No longer following ${artistName}. What was already wanted stays wanted.`);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
pushToast(errMessage(e), 'error');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={openModal}
|
||||
disabled={follow.isPending}
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary disabled:opacity-50"
|
||||
>
|
||||
{#if following}
|
||||
<BellRing size={14} strokeWidth={1.5} />
|
||||
Following · {follow.data?.profile_name}
|
||||
{:else}
|
||||
<BellPlus size={14} strokeWidth={1.5} />
|
||||
Follow
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<Modal title={following ? `Following ${artistName}` : `Follow ${artistName}`} {open} onClose={() => (open = false)}>
|
||||
<p class="-mt-2 text-sm text-text-secondary">
|
||||
New releases from {artistName} go on the wanted list. The profile says which kinds of
|
||||
release are wanted.
|
||||
</p>
|
||||
|
||||
<label class="mt-4 block">
|
||||
<span class="block text-sm text-text-secondary">Release-type profile</span>
|
||||
<select
|
||||
bind:value={profileChoice}
|
||||
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary focus:outline-hidden focus:ring-2 focus:ring-accent"
|
||||
>
|
||||
{#each profiles as p (p.id)}
|
||||
<option value={p.id}>{p.name}{p.is_default ? ' (default)' : ''}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="mt-3 flex items-start gap-3">
|
||||
<input type="checkbox" bind:checked={wantExisting} class="mt-1" />
|
||||
<span>
|
||||
<span class="text-sm text-text-primary">Also want what they've released that isn't here</span>
|
||||
<span class="block text-xs text-text-secondary">
|
||||
Off wants only releases from now on.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-5 flex justify-end gap-2">
|
||||
{#if following}
|
||||
<button
|
||||
type="button"
|
||||
onclick={unfollow}
|
||||
disabled={saving}
|
||||
class="mr-auto rounded-md px-3 py-1.5 text-sm text-text-muted hover:bg-surface-hover hover:text-text-primary disabled:opacity-50"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (open = false)}
|
||||
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={save}
|
||||
disabled={saving || !profileChoice}
|
||||
class="rounded-md bg-action-primary px-3 py-1.5 text-sm text-action-fg disabled:opacity-50"
|
||||
>
|
||||
{following ? 'Save' : 'Follow'}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
Reference in New Issue
Block a user