feat: a held album below the quality you settle for, or holding a video rip, stays wanted for a better copy, and the import replaces the old tracks with their likes and plays carried over (M502 #5524)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 5m20s
release / android (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 5m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 27s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m23s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 5m20s
release / android (push) Successful in 5m28s
release / Build signed APK (releases and dev) (push) Successful in 5m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 27s
release / Verify release artifacts (tag releases only) (push) Skipped
- acquisition_settings.upgrade_until (default High quality lossy, Lidarr's cutoff, note #5527; "" is off), an "Upgrade until" row under Searching. - The wanted worker judges held albums hourly: tier per format from size per second (the scanner records no bitrate), the album's worst. Below the cutoff it is wanted for quality; a video rip among its tracks always. - The schedule searches upgrades weekly, after open wants; a release no better than the copy held is refused ("not_better"). - The import matches each file to the track it would replace (recording, then disc/track) and replaces it when it is a rip or the file is a better tier: over the same path in one rename, or filed beside it and the old track folded in (library.ReplaceTrack: file first, then foldTrackInto). Nothing better: the release is set aside. - Collection › Upgrades (Lidarr's Cutoff Unmet); Suspect sources says when a clean copy was looked for instead of Lidarr's search. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ function setup(
|
||||
total: items.length,
|
||||
limit: 96,
|
||||
offset: 0,
|
||||
counts: { artists: items.length, wanted: 1, missing: 0, held: 16, following: 1, activity: 1, attention: 0, blocked: 0, unmatched: 0 },
|
||||
counts: { artists: items.length, wanted: 1, missing: 0, held: 16, upgrades: 0, following: 1, activity: 1, attention: 0, blocked: 0, unmatched: 0 },
|
||||
library
|
||||
};
|
||||
vi.mocked(createWantArtistsQuery).mockReturnValue(
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
// catalogue of artists first, then the sections that show state. Activity
|
||||
// carries a second, accented count when downloads are waiting on a person.
|
||||
// Unmatched, like Lidarr's Unmapped Files, holds the library albums no
|
||||
// MusicBrainz album matched (#5579).
|
||||
// MusicBrainz album matched (#5579). Upgrades, Lidarr's Cutoff Unmet,
|
||||
// holds the albums held but wanted for a better copy (#5524).
|
||||
|
||||
let { counts }: { counts?: WantCounts } = $props();
|
||||
|
||||
@@ -15,6 +16,7 @@
|
||||
{ href: '/collection', label: 'Catalogue', count: counts?.artists, exact: true, also: ['/collection/artist/'] },
|
||||
{ href: '/collection/wanted', label: 'Wanted', count: counts?.wanted },
|
||||
{ href: '/collection/missing', label: 'Missing files', count: counts?.missing },
|
||||
{ href: '/collection/upgrades', label: 'Upgrades', count: counts?.upgrades },
|
||||
{ href: '/collection/activity', label: 'Activity', count: counts?.activity, attention: counts?.attention },
|
||||
{ href: '/collection/following', label: 'Following', count: counts?.following },
|
||||
{ href: '/collection/blocked', label: 'Blocked', count: counts?.blocked },
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
// runs one now. Taking a held album off leaves its files and stops it
|
||||
// being fetched back if they go.
|
||||
//
|
||||
// Upgrades (#5524) are held albums wanted for a better copy: below the
|
||||
// quality set under Acquisition, or holding a video rip. They search and
|
||||
// download like an open want, and say what copy they have.
|
||||
//
|
||||
// The list reads like the library (#5572): artists A–Z, each with its
|
||||
// albums oldest first, and each album's tracks a click away. The filter
|
||||
// narrows it to an artist or album by name, on the server, so it finds
|
||||
@@ -166,6 +170,15 @@
|
||||
return `Imported ${n} ${relativeTime(g.imported_at)} from ${g.name}`;
|
||||
}
|
||||
|
||||
// A want the schedule still searches for: open, or held but wanted for a
|
||||
// better copy.
|
||||
const searchable = (w: Want) => !w.held_at || !!w.upgrade;
|
||||
|
||||
function upgradeLine(w: Want): string {
|
||||
if (w.upgrade === 'rip') return 'A track is a video rip; looking for a clean copy';
|
||||
return w.held_quality ? `You have ${w.held_quality}; looking for better` : 'Looking for a better copy';
|
||||
}
|
||||
|
||||
const attention = (w: Want) => w.grab?.live && (w.grab.import_state === 'review' || w.grab.import_state === 'failed');
|
||||
|
||||
function why(w: Want): string {
|
||||
@@ -231,11 +244,16 @@
|
||||
<p class="mt-3 text-text-primary">
|
||||
{#if artist}None of this artist's albums are in the catalogue.
|
||||
{:else if list === 'missing'}No missing albums are wanted back.
|
||||
{:else if list === 'upgrade'}No album is wanted for a better copy.
|
||||
{:else}Nothing is wanted.{/if}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-text-secondary">
|
||||
{#if list === 'missing'}
|
||||
An album whose files stay missing past the grace window on the Missing files page lands here.
|
||||
{:else if list === 'upgrade'}
|
||||
An album you have below the quality set under
|
||||
<a href="/admin/acquisition" class="text-accent-fg hover:underline">Acquisition</a>, or with a video
|
||||
rip among its tracks, lands here.
|
||||
{:else}
|
||||
A request you approve, a new release from a followed artist, or an album whose files went
|
||||
missing lands here.
|
||||
@@ -278,7 +296,10 @@
|
||||
<div class="text-xs text-text-muted" data-testid="want-why">
|
||||
{why(w)} · {w.held_at ? `held ${relativeTime(w.held_at)}` : `wanted ${relativeTime(w.wanted_at)}`}
|
||||
</div>
|
||||
{#if !w.held_at}
|
||||
{#if w.held_at && w.upgrade}
|
||||
<div class="truncate text-xs text-text-secondary" data-testid="want-upgrade">{upgradeLine(w)}</div>
|
||||
{/if}
|
||||
{#if searchable(w)}
|
||||
<div
|
||||
class="text-xs {attention(w) ? 'text-text-primary' : 'truncate text-text-secondary'}"
|
||||
data-testid="want-search"
|
||||
@@ -303,7 +324,7 @@
|
||||
<ListMusic size={12} strokeWidth={2} /> Tracks
|
||||
<ChevronDown size={12} strokeWidth={2} class={shown === w.id ? 'rotate-180' : ''} />
|
||||
</button>
|
||||
{#if !w.held_at}
|
||||
{#if searchable(w)}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onSearch(w)}
|
||||
@@ -339,7 +360,7 @@
|
||||
<WantTracks wantId={w.id} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if open === w.id && !w.held_at}
|
||||
{#if open === w.id && searchable(w)}
|
||||
<div class="sm:pl-[4.25rem]">
|
||||
<WantCandidates wantId={w.id} title={w.title} />
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,7 @@ function page(
|
||||
total: items.length,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
counts: { artists: 2, wanted: 2, missing: 1, held: 4, following: 1, activity: 1, attention: 0, blocked: 0, unmatched: 0 },
|
||||
counts: { artists: 2, wanted: 2, missing: 1, held: 4, upgrades: 0, following: 1, activity: 1, attention: 0, blocked: 0, unmatched: 0 },
|
||||
library
|
||||
};
|
||||
}
|
||||
@@ -314,4 +314,32 @@ describe('WantList', () => {
|
||||
expect(screen.getAllByTestId('want-row')).toHaveLength(2);
|
||||
expect(screen.queryByRole('link', { name: 'Blur' })).toBeNull();
|
||||
});
|
||||
|
||||
test('an upgrade is a held album searched for a better copy', async () => {
|
||||
setup(
|
||||
[
|
||||
want('u1', { held_at: ago(DAY), reason: 'library', upgrade: 'quality', held_quality: 'MP3, about 192 kbps' }),
|
||||
want('u2', { held_at: ago(DAY), reason: 'library', upgrade: 'rip', title: 'Humanz' })
|
||||
],
|
||||
'upgrade'
|
||||
);
|
||||
expect(createWantsQuery).toHaveBeenCalledWith('upgrade', '');
|
||||
const lines = screen.getAllByTestId('want-upgrade').map((e) => e.textContent ?? '');
|
||||
expect(lines[0]).toMatch(/you have mp3, about 192 kbps; looking for better/i);
|
||||
expect(lines[1]).toMatch(/video rip; looking for a clean copy/i);
|
||||
expect(screen.getAllByTestId('want-search')[0].textContent).toMatch(/not searched yet/i);
|
||||
await fireEvent.click(screen.getByRole('button', { name: /search for humanz now/i }));
|
||||
await waitFor(() => expect(searchWant).toHaveBeenCalledWith('u2'));
|
||||
});
|
||||
|
||||
test('a held album not wanted for a better copy has no search', () => {
|
||||
setup([want('h1', { held_at: ago(DAY), reason: 'library' })], 'held');
|
||||
expect(screen.queryByTestId('want-upgrade')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /search for/i })).toBeNull();
|
||||
});
|
||||
|
||||
test('with no upgrades it says what would land here', () => {
|
||||
setup([], 'upgrade');
|
||||
expect(screen.getByText(/no album is wanted for a better copy/i)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user