release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m17s
release / go (push) Successful in 1m34s
release / integration (push) Failing after 5m34s
release / android (push) Successful in 5m36s
release / Build signed APK (releases and dev) (push) Successful in 5m44s
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
Like Lidarr's Unmapped Files. A local band's album, which MusicBrainz will never have, is left off: never matched again and not counted. A re-tagged album is matched again on the next listing. The Collection's verdict now says when library albums need matching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
3.9 KiB
Svelte
107 lines
3.9 KiB
Svelte
<script lang="ts">
|
|
import { Search } from 'lucide-svelte';
|
|
import { createCatalogueSearchQuery } from '#lib/api/catalogue.js';
|
|
import type { CatalogueSearchResult, UnmatchedAlbum } from '#lib/api/types.js';
|
|
|
|
// Naming the MusicBrainz album an unmatched album is (#5579): a search of
|
|
// MusicBrainz's albums, started from the album's own title and artist, or
|
|
// a link pasted from musicbrainz.org. MusicBrainz takes a request a second,
|
|
// so the search runs when it's asked for, not on each keystroke.
|
|
|
|
let {
|
|
album,
|
|
busy = false,
|
|
onMatch
|
|
}: { album: UnmatchedAlbum; busy?: boolean; onMatch: (ref: string) => void } = $props();
|
|
|
|
// svelte-ignore state_referenced_locally
|
|
let text = $state(`${album.title} ${album.artist}`.trim());
|
|
// svelte-ignore state_referenced_locally
|
|
let asked = $state(`${album.title} ${album.artist}`.trim());
|
|
let link = $state('');
|
|
|
|
const searchStore = $derived(createCatalogueSearchQuery(asked, 'album'));
|
|
const search = $derived($searchStore);
|
|
const results = $derived((search.data ?? []) as CatalogueSearchResult[]);
|
|
</script>
|
|
|
|
<div class="mt-3 space-y-3 rounded-md border border-border bg-bg p-3" data-testid="unmatched-match">
|
|
<form
|
|
class="flex gap-2"
|
|
onsubmit={(e) => {
|
|
e.preventDefault();
|
|
asked = text.trim();
|
|
}}
|
|
>
|
|
<input
|
|
type="search"
|
|
bind:value={text}
|
|
aria-label="Search MusicBrainz albums"
|
|
class="min-w-0 flex-1 rounded-md border border-border bg-surface px-2.5 py-1.5 text-sm text-text-primary placeholder:text-text-muted"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
class="inline-flex shrink-0 items-center gap-1 rounded-md border border-border px-2.5 py-1 text-xs text-text-secondary hover:bg-surface-hover hover:text-text-primary"
|
|
>
|
|
<Search size={14} strokeWidth={1.75} aria-hidden="true" /> Search
|
|
</button>
|
|
</form>
|
|
|
|
{#if search.isPending && asked}
|
|
<p class="text-xs text-text-secondary">Asking MusicBrainz…</p>
|
|
{:else if search.isError}
|
|
<p class="text-xs text-error-fg">Couldn't reach MusicBrainz. A pasted link works the same.</p>
|
|
{:else if asked && results.length === 0}
|
|
<p class="text-xs text-text-secondary">MusicBrainz has no album like that.</p>
|
|
{:else if results.length > 0}
|
|
<ul class="divide-y divide-border">
|
|
{#each results as r (r.mbid)}
|
|
<li class="flex items-center gap-3 py-2" data-testid="unmatched-result">
|
|
<div class="min-w-0 flex-1">
|
|
<div class="truncate text-sm text-text-primary">{r.name}</div>
|
|
<div class="truncate text-xs text-text-secondary">{r.secondary_text}</div>
|
|
</div>
|
|
<a
|
|
href="https://musicbrainz.org/release-group/{r.mbid}"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
class="shrink-0 text-xs text-text-secondary hover:text-text-primary hover:underline">MusicBrainz</a
|
|
>
|
|
<button
|
|
type="button"
|
|
disabled={busy}
|
|
onclick={() => onMatch(r.mbid)}
|
|
aria-label="Match to {r.name}"
|
|
class="shrink-0 rounded-md border border-border px-2.5 py-1 text-xs text-text-primary hover:bg-surface-hover disabled:opacity-50"
|
|
>
|
|
This one
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
|
|
<form
|
|
class="flex gap-2 border-t border-border pt-3"
|
|
onsubmit={(e) => {
|
|
e.preventDefault();
|
|
if (link.trim()) onMatch(link.trim());
|
|
}}
|
|
>
|
|
<input
|
|
type="text"
|
|
bind:value={link}
|
|
placeholder="Or paste a MusicBrainz album link"
|
|
aria-label="MusicBrainz album link"
|
|
class="min-w-0 flex-1 rounded-md border border-border bg-surface px-2.5 py-1.5 text-sm text-text-primary placeholder:text-text-muted"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={busy || !link.trim()}
|
|
class="shrink-0 rounded-md border border-border px-2.5 py-1 text-xs text-text-primary hover:bg-surface-hover disabled:opacity-50"
|
|
>
|
|
Match
|
|
</button>
|
|
</form>
|
|
</div>
|