diff --git a/web/src/lib/components/AcoustIDCard.svelte b/web/src/lib/components/AcoustIDCard.svelte index 3227fa5d..2871725e 100644 --- a/web/src/lib/components/AcoustIDCard.svelte +++ b/web/src/lib/components/AcoustIDCard.svelte @@ -10,6 +10,8 @@ } from '#lib/api/admin.js'; import { errMessage } from '#lib/api/errors.js'; import { pushToast } from '#lib/stores/toast.svelte.js'; + import SettingsGroup from '#lib/components/settings/SettingsGroup.svelte'; + import SettingRow from '#lib/components/settings/SettingRow.svelte'; // AcoustID recording-id lookup (M401): fills the MusicBrainz ids that // untagged tracks lack, so the similarity arms can see them. Off until the @@ -122,194 +124,93 @@ } } - const rowClass = 'flex items-center justify-between gap-4 py-2'; - const inputClass = - 'rounded-md border border-border bg-background px-3 py-1.5 text-sm text-text-primary ' + - 'placeholder:text-text-muted focus:outline-hidden focus:ring-2 focus:ring-accent'; + const button = + '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'; + const field = 'rounded-md border border-border bg-background px-3 py-1.5 text-sm'; -
-
-
-

AcoustID

-

- Finds MusicBrainz ids for untagged tracks, so Songs-like and radio can place them. -

-
- {#if overview} - {#if overview.ready && !overview.status.problem} - - - {overview.status.running ? 'Looking up' : 'On'} - - {:else} - - - {#if overview.ready}Needs attention{:else if !overview.settings.api_key_set}Needs a key{:else}Off{/if} - - {/if} - {/if} -
- + {#if loadError} -

+

Couldn't load AcoustID settings.

{:else if overview === null} -

Loading AcoustID settings…

+

Loading AcoustID settings…

{:else} + + {#if overview.ready && !overview.status.problem} + {overview.status.running ? 'Looking up' : 'On'} + {:else} + + {#if overview.ready}Needs attention{:else if !overview.settings.api_key_set}Needs a key{:else}Off{/if} + + {/if} + {#if overview.status.problem} -

- {overview.status.problem} -

+

{overview.status.problem}

{/if} - -
- - -
- -
- - {#if overview.settings.api_key_set} - - {/if} -
-
- - -
- - {#if !scoreOk} -

- Minimum score must be from 0.5 to 1. -

- {/if} - -
- + {/if} + + + + +
+ {#if !scoreOk} +

Minimum score must be from 0.5 to 1.

+ {/if} +
{#if overview.coverage.total > 0} -
-

Recording ids

-
-
-
From tags
-
{overview.coverage.from_tag.toLocaleString()}
-
-
-
Looked up
-
{overview.coverage.from_acoustid.toLocaleString()}
-
-
-
None
-
{withoutId.toLocaleString()}
-
-
-
Waiting to be looked up
-
{overview.coverage.pending.toLocaleString()}
-
-
-
- No match -
-
{overview.coverage.no_match.toLocaleString()}
-
-
-
- Ambiguous -
-
{overview.coverage.ambiguous.toLocaleString()}
-
-
-
- Failed -
-
{overview.coverage.failed.toLocaleString()}
-
-
-
+ {@const c = overview.coverage} +
+ + -
+
- {#if overview.coverage.ambiguous + overview.coverage.failed > 0} -
+ {#if c.ambiguous + c.failed > 0} +
- Tracks the lookup couldn't settle ({( - overview.coverage.ambiguous + overview.coverage.failed - ).toLocaleString()}) + Tracks the lookup couldn't settle ({(c.ambiguous + c.failed).toLocaleString()}) {#if unsettled === null}

Loading tracks…

@@ -319,12 +220,10 @@
  • {t.title} - {t.artist} · {t.album} + {t.artist} · {t.album} - {t.state === 'ambiguous' ? `${t.candidates} recordings` : 'failed'} + {t.state === 'ambiguous' ? `${t.candidates} recordings` : "couldn't fingerprint"}
  • {/each} @@ -334,4 +233,4 @@ {/if} {/if} {/if} -
    + diff --git a/web/src/lib/components/AcoustIDCard.test.ts b/web/src/lib/components/AcoustIDCard.test.ts index ae08ab8c..b4938eac 100644 --- a/web/src/lib/components/AcoustIDCard.test.ts +++ b/web/src/lib/components/AcoustIDCard.test.ts @@ -43,7 +43,7 @@ afterEach(() => vi.clearAllMocks()); async function renderCard(o: AcoustIDOverview = base) { vi.mocked(getAcoustID).mockResolvedValue(o); render(AcoustIDCard); - await screen.findByRole("checkbox", { name: /look up untagged tracks/i }); + await screen.findByRole("switch", { name: /look up untagged tracks/i }); } const saveButton = () => screen.getByRole("button", { name: /^save$/i }); @@ -52,22 +52,10 @@ const lookUpNow = () => screen.getByRole("button", { name: /look up now/i }); describe("AcoustIDCard", () => { test("shows where the library’s recording ids came from", async () => { await renderCard(); - const cov = screen.getByTestId("acoustid-coverage"); + const cov = screen.getByTestId("acoustid-coverage").textContent?.replace(/\s+/g, " "); // 1000 - 700 - 150 have no id. - for (const [label, n] of [ - ["From tags", "700"], - ["Looked up", "150"], - ["None", "150"], - ["Waiting to be looked up", "55"], - ["No match", "80"], - ["Ambiguous", "12"], - ["Failed", "3"], - ]) { - const row = [...cov.querySelectorAll("dt")].find( - (dt) => dt.textContent?.trim() === label, - ); - expect(row?.nextElementSibling?.textContent?.trim(), label).toBe(n); - } + expect(cov).toMatch(/Recording ids 700 from tags · 150 looked up · 150 none/); + expect(cov).toMatch(/Without one 55 waiting · 80 no match · 12 ambiguous · 3 failed/); }); test("never shows the stored key, and sends one only when typed", async () => { diff --git a/web/src/lib/components/AcquisitionCard.svelte b/web/src/lib/components/AcquisitionCard.svelte index 5e711b97..3484b060 100644 --- a/web/src/lib/components/AcquisitionCard.svelte +++ b/web/src/lib/components/AcquisitionCard.svelte @@ -9,6 +9,7 @@ import { errMessage } from '#lib/api/errors.js'; import { pushToast } from '#lib/stores/toast.svelte.js'; import AcquisitionServiceRow from './AcquisitionServiceRow.svelte'; + import SettingsGroup from '#lib/components/settings/SettingsGroup.svelte'; // Where Minstrel looks for music and what downloads it (M502 #5518), // Fabled Librarian's model: two lists of connections the operator adds and @@ -88,109 +89,78 @@ } const addButtonClass = - 'rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover ' + - 'focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent'; + 'rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary'; {#snippet addMenu(kind: 'search' | 'download', label: string)} - {#if adding === kind} -
    - {#each choices[kind] as choice (choice.protocol)} - + {/each} + - {choice.name} - {choice.note} - - {/each} - -
    - {:else} - - {/if} + + {:else} + + {/if} + {/snippet} -
    -
    -

    - Indexers and download clients -

    -

    - Two jobs, kept apart: something to search with, and something to download with. Minstrel - speaks Prowlarr, Torznab and the qBittorrent WebUI, so a stock setup is a complete one. -

    -
    - +
    {#if loadError} -

    +

    Couldn't load indexers and download clients.

    {:else if services === null}

    Loading indexers and download clients…

    {:else} +

    {#if !canFetch} - - No download client is switched on, so nothing can be downloaded. - + No download client is switched on, so nothing can be downloaded. {:else if !canSearch} - - No indexer is switched on, so nothing new can be found. - + No indexer is switched on, so nothing new can be found. {:else} Music can be searched for and downloaded. {/if}

    -
    -
    -

    Indexers: where music is looked for

    -

    - Every indexer switched on here is searched, and the results are merged. If one is - unreachable the search still runs, narrower. -

    -
    - {#each indexers as service (service.code)} - - {:else} -

    No indexer yet. Prowlarr is the usual choice.

    - {/each} - {#if searchingExchanges.length > 0} -

    - Also searched: {searchingExchanges.map((s) => s.label).join(', ')}, set up under download - clients below. -

    - {/if} - {@render addMenu('search', 'Add indexer')} +
    + + {#each indexers as service (service.code)} + + {:else} +

    No indexer yet. Prowlarr is the usual choice.

    + {/each} + {#if searchingExchanges.length > 0} +

    + Also searched: {searchingExchanges.map((s) => s.label).join(', ')}, under download clients. +

    + {/if} + {@render addMenu('search', 'Add indexer')} +
    -
    -
    -

    - Download clients: what actually downloads -

    -

    - What a grab is handed to. A FabledExchange here can do more than download, and its entry - says which of its jobs are switched on. -

    -
    - {#each clients as service (service.code)} - - {:else} -

    No download client yet.

    - {/each} - {@render addMenu('download', 'Add download client')} +
    + + {#each clients as service (service.code)} + + {:else} +

    No download client yet.

    + {/each} + {@render addMenu('download', 'Add download client')} +
    {/if} -
    + diff --git a/web/src/lib/components/AcquisitionServiceRow.svelte b/web/src/lib/components/AcquisitionServiceRow.svelte index dfe2ad99..4c765368 100644 --- a/web/src/lib/components/AcquisitionServiceRow.svelte +++ b/web/src/lib/components/AcquisitionServiceRow.svelte @@ -159,7 +159,7 @@ 'focus-visible:outline-accent disabled:cursor-not-allowed disabled:opacity-50'; -
    +