feat: the library's albums go on the wanted list as held, once; after that the list is the truth (M502 #5567)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m24s
release / go (push) Successful in 1m44s
release / integration (push) Successful in 5m19s
release / android (push) Successful in 5m31s
release / Build signed APK (releases and dev) (push) Successful in 5m35s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped

A one-time job in the wanted worker lists every album in the library as a
held want, one per release group, with a record that it happened
(acquisition_settings.library_listed_at). Albums whose tags name only a
release are named on MusicBrainz first, 120 a pass; what nothing names is
counted and shown on the Held tab.

Re-acquisition now wants back only albums on the list. Taking a held album
off the list leaves its files, and it is never fetched back; albums put in
the library folder by hand afterwards are not added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 16:13:46 -04:00
co-authored by Claude Opus 5.5
parent bc198a9389
commit 2c677b3184
19 changed files with 965 additions and 172 deletions
+71 -38
View File
@@ -1,61 +1,94 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import { createRawSnippet } from 'svelte';
import { mockInfiniteQuery } from '../../test-utils/query';
import { pageUrlModule } from '#test-utils/mocks/appState.js';
import type { WantCounts, WantsPage } from '#lib/api/types.js';
import { afterEach, describe, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/svelte";
import { createRawSnippet } from "svelte";
import { mockInfiniteQuery } from "../../test-utils/query";
import { pageUrlModule } from "#test-utils/mocks/appState.js";
import type { WantCounts, WantsPage } from "#lib/api/types.js";
const state = vi.hoisted(() => ({ pageUrl: new URL('http://localhost/wanted') }));
vi.mock('$app/state', () => pageUrlModule(state));
vi.mock('#lib/api/wanted.js', () => ({ createWantsQuery: vi.fn() }));
const state = vi.hoisted(() => ({
pageUrl: new URL("http://localhost/wanted"),
}));
vi.mock("$app/state", () => pageUrlModule(state));
vi.mock("#lib/api/wanted.js", () => ({ createWantsQuery: vi.fn() }));
import WantedLayout from './+layout.svelte';
import { createWantsQuery } from '#lib/api/wanted.js';
import WantedLayout from "./+layout.svelte";
import { createWantsQuery } from "#lib/api/wanted.js";
const counts: WantCounts = { wanted: 5, missing: 1, held: 9, following: 2, activity: 3, attention: 0, blocked: 4 };
const children = createRawSnippet(() => ({ render: () => `<p data-testid="tab-body">body</p>` }));
const counts: WantCounts = {
wanted: 5,
missing: 1,
held: 9,
following: 2,
activity: 3,
attention: 0,
blocked: 4,
};
const children = createRawSnippet(() => ({
render: () => `<p data-testid="tab-body">body</p>`,
}));
afterEach(() => vi.clearAllMocks());
function setup(c: WantCounts = counts, path = '/wanted') {
function setup(c: WantCounts = counts, path = "/wanted") {
state.pageUrl = new URL(`http://localhost${path}`);
const page: WantsPage = { items: [], total: 0, limit: 50, offset: 0, counts: c };
const page: WantsPage = {
items: [],
total: 0,
limit: 50,
offset: 0,
counts: c,
library: { listed_at: null, unlisted: 0 },
};
vi.mocked(createWantsQuery).mockReturnValue(
mockInfiniteQuery({ pages: [page] }) as unknown as ReturnType<typeof createWantsQuery>
mockInfiniteQuery({ pages: [page] }) as unknown as ReturnType<
typeof createWantsQuery
>,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return render(WantedLayout, { children } as any);
}
describe('/wanted layout', () => {
test('tabs are links carrying their counts, the current one marked', () => {
setup(counts, '/wanted/activity');
const nav = screen.getByRole('navigation', { name: /wanted sections/i });
const links = Array.from(nav.querySelectorAll('a')).map((a) => [a.getAttribute('href'), a.textContent?.replace(/\s+/g, ' ').trim()]);
expect(links).toEqual([
['/wanted', 'Wanted 5'],
['/wanted/activity', 'Activity 3'],
['/wanted/missing', 'Missing files 1'],
['/wanted/held', 'Held 9'],
['/wanted/following', 'Following 2'],
['/wanted/blocked', 'Blocked 4']
describe("/wanted layout", () => {
test("tabs are links carrying their counts, the current one marked", () => {
setup(counts, "/wanted/activity");
const nav = screen.getByRole("navigation", { name: /wanted sections/i });
const links = Array.from(nav.querySelectorAll("a")).map((a) => [
a.getAttribute("href"),
a.textContent?.replace(/\s+/g, " ").trim(),
]);
expect(screen.getByRole('link', { name: /activity/i })).toHaveAttribute('aria-current', 'page');
expect(screen.getByRole('link', { name: /^wanted/i })).not.toHaveAttribute('aria-current');
expect(screen.getByTestId('tab-body')).toBeTruthy();
expect(links).toEqual([
["/wanted", "Wanted 5"],
["/wanted/activity", "Activity 3"],
["/wanted/missing", "Missing files 1"],
["/wanted/held", "Held 9"],
["/wanted/following", "Following 2"],
["/wanted/blocked", "Blocked 4"],
]);
expect(screen.getByRole("link", { name: /activity/i })).toHaveAttribute(
"aria-current",
"page",
);
expect(screen.getByRole("link", { name: /^wanted/i })).not.toHaveAttribute(
"aria-current",
);
expect(screen.getByTestId("tab-body")).toBeTruthy();
});
test('when nothing needs a person the verdict says so', () => {
test("when nothing needs a person the verdict says so", () => {
setup();
expect(screen.getByTestId('wanted-verdict').textContent).toMatch(/5 albums wanted, 3 downloading\. Nothing needs you\./);
expect(screen.queryByTestId('tab-attention')).toBeNull();
expect(screen.getByTestId("wanted-verdict").textContent).toMatch(
/5 albums wanted, 3 downloading\. Nothing needs you\./,
);
expect(screen.queryByTestId("tab-attention")).toBeNull();
});
test('downloads waiting on a person lead the page and mark Activity', () => {
test("downloads waiting on a person lead the page and mark Activity", () => {
setup({ ...counts, attention: 2 });
const verdict = screen.getByTestId('wanted-verdict');
const verdict = screen.getByTestId("wanted-verdict");
expect(verdict.textContent).toMatch(/2 downloads need you/);
expect(screen.getByRole('link', { name: /see activity/i }).getAttribute('href')).toBe('/wanted/activity');
expect(screen.getByTestId('tab-attention').textContent).toBe('2');
expect(
screen.getByRole("link", { name: /see activity/i }).getAttribute("href"),
).toBe("/wanted/activity");
expect(screen.getByTestId("tab-attention").textContent).toBe("2");
});
});