feat: autotag mixed-bag splitting, search relevance fixes, and multi-library download imports
Build & publish Arch package / arch-package (push) Successful in 2m2s
Search index maintenance / maintain-index (push) Successful in 7s

Autotag: detect "junk drawer" folders with no artist/album consensus
and split them into synthetic per-cluster groups instead of forcing
one match on an unrelated pile of tracks; repair tagging_items rows
left behind by a prior scan orphan-cleanup gap.

Explore: fix an exact artist-name search being drowned out by its own
catalog entries in intent-prior scoring, and prune stale in_library
bookkeeping left behind when a referenced library row is deleted.

Download: fix a multi-library regression where every import failed
with "no library root configured" — the importer resolved the
library root from a legacy single-library config field that nothing
populates in the current multi-library model. It now resolves the
destination library per-request from the request's own library_id.
Also widen the Soulseek search window (12s -> 20s), measured against
real request history to be missing available peers on live queries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Agd9af5hE7qzti2ackiS
This commit is contained in:
2026-08-10 11:52:26 -04:00
co-authored by Claude Sonnet 5
parent e190fd75b9
commit cbd82a5a74
70 changed files with 3617 additions and 129 deletions
@@ -11,8 +11,10 @@ import { designTokens } from '../../styles/tokens.css';
import type {
DownloadDescriptor,
DownloadProvider,
ProviderField,
} from '@store/download-store';
import { downloadStore } from '@store/download-store';
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
import './config-section';
/**
@@ -162,6 +164,20 @@ export class DownloadClients extends LitElement {
.add-row {
margin-top: 0.8em;
}
.field-row {
display: flex;
gap: 0.5em;
align-items: flex-end;
}
.field-row wa-input {
flex: 1;
}
.field-row .browse-button {
flex-shrink: 0;
}
`,
];
@@ -257,7 +273,7 @@ export class DownloadClients extends LitElement {
<wa-select
label="Client type"
.value=${this.newKind}
@wa-change=${this.onKindChange}
@change=${this.onKindChange}
>
${this.descriptors.map(
(d) => html`<wa-option value=${d.kind}>${d.name}</wa-option>`,
@@ -277,7 +293,7 @@ export class DownloadClients extends LitElement {
<wa-input
label="Name"
.value=${this.draftName}
@wa-input=${(e: Event) => {
@input=${(e: Event) => {
this.draftName = (e.target as HTMLInputElement).value;
}}
></wa-input>
@@ -311,25 +327,25 @@ export class DownloadClients extends LitElement {
<wa-input
label="Name"
.value=${this.draftName}
@wa-input=${(e: Event) => {
@input=${(e: Event) => {
this.draftName = (e.target as HTMLInputElement).value;
}}
></wa-input>
${descriptor ? this.renderFields(descriptor) : nothing}
${descriptor ? this.renderFields(descriptor, provider) : nothing}
<wa-input
label="Priority"
type="number"
.value=${String(provider.priority)}
@wa-input=${(e: Event) => {
@input=${(e: Event) => {
this.draft['__priority'] = (e.target as HTMLInputElement).value;
}}
></wa-input>
<wa-switch
?checked=${provider.enabled}
@wa-change=${(e: Event) => {
@change=${(e: Event) => {
this.draft['__enabled'] = (e.target as HTMLInputElement)
.checked
? '1'
@@ -355,28 +371,61 @@ export class DownloadClients extends LitElement {
`;
}
/** Renders one input per descriptor field. */
private renderFields(descriptor: DownloadDescriptor) {
return (descriptor.fields ?? []).map(
(field) => html`
<wa-input
label=${field.label}
placeholder=${field.placeholder ?? ''}
type=${field.secret ? 'password' : 'text'}
.value=${this.draft[field.key] ?? ''}
@wa-input=${(e: Event) => {
this.draft = {
...this.draft,
[field.key]: (e.target as HTMLInputElement).value,
};
}}
>
${field.help ? html`<span slot="hint">${field.help}</span>` : nothing}
</wa-input>
`,
);
/** Renders one input per descriptor field, plus a folder browse
* button for path fields and an "already set" placeholder for
* secrets the provider already has a stored value for. */
private renderFields(descriptor: DownloadDescriptor, provider?: DownloadProvider) {
return (descriptor.fields ?? []).map((field) => {
const isSet = field.secret && provider?.setSecrets?.[field.key];
const placeholder = isSet
? '•••••••• (unchanged — enter a new value to replace it)'
: (field.placeholder ?? '');
return html`
<div class="field-row">
<wa-input
label=${field.label}
placeholder=${placeholder}
type=${field.secret ? 'password' : 'text'}
.value=${this.draft[field.key] ?? ''}
@input=${(e: Event) => {
this.draft = {
...this.draft,
[field.key]: (e.target as HTMLInputElement).value,
};
}}
>
${field.help ? html`<span slot="hint">${field.help}</span>` : nothing}
</wa-input>
${field.path
? html`
<wa-button
size="small"
appearance="outlined"
class="browse-button"
@click=${() => this.browseForFolder(field)}
>
Browse
</wa-button>
`
: nothing}
</div>
`;
});
}
private browseForFolder = async (field: ProviderField) => {
try {
const dir = await DirectoryPicker();
if (dir) {
this.draft = { ...this.draft, [field.key]: dir };
}
} catch (err) {
console.error('Failed to open directory picker:', err);
}
};
private descriptorFor(kind: string): DownloadDescriptor | undefined {
return this.descriptors.find((d) => d.kind === kind);
}
@@ -315,7 +315,7 @@ export class DuplicateTracksDialog extends LitElement {
<wa-switch
size="small"
?checked=${this.applyToAll}
@wa-change=${(e: Event) => {
@change=${(e: Event) => {
this.applyToAll = (
e.target as HTMLInputElement
).checked;
@@ -123,6 +123,16 @@ export class ExploreAlbumDetails extends LitElement {
/** True when this album is already on the wanted list. */
@state() private isWanted = false;
/**
* Library to attach downloads/wants to. The library-filter UI that
* would normally set libraryStore's selection isn't mounted anywhere
* currently, so that selection is always null here — falling back to
* `?? 0` would send a library id that doesn't exist and fail the
* download_requests/download_wants foreign key. Resolved to the
* selected library, or the first one if none is selected.
*/
@state() private targetLibraryId: number | null = null;
/** Unsubscribe handle for the download store. */
private downloadUnsub: (() => void) | null = null;
@@ -494,6 +504,8 @@ export class ExploreAlbumDetails extends LitElement {
this.syncWanted();
});
void this.resolveTargetLibraryId();
// A background BrowseReleases fetch (cold album, versions +
// tracklist not cached yet) finished — re-fetch the versions once
// per release group so they fill in without the initial request
@@ -1511,7 +1523,7 @@ export class ExploreAlbumDetails extends LitElement {
size="small"
appearance="outlined"
@click=${() => {
this.pickerOpen = true;
void this.openPicker();
}}
>
<wa-icon slot="start" name="download"></wa-icon>
@@ -1553,6 +1565,11 @@ export class ExploreAlbumDetails extends LitElement {
`;
}
/** Resolves the library to attach downloads/wants to. */
private async resolveTargetLibraryId(): Promise<void> {
this.targetLibraryId = await libraryStore.getDefaultLibraryId();
}
/** Reflects the store's view of whether this album is wanted. */
private syncWanted(): void {
this.isWanted = this.releaseGroupMBID
@@ -1567,10 +1584,18 @@ export class ExploreAlbumDetails extends LitElement {
if (wantId) {
await downloadStore.removeWant(wantId);
} else {
if (!this.targetLibraryId) {
await this.resolveTargetLibraryId();
}
if (!this.targetLibraryId) {
console.error('Could not update the wanted list: no library available');
return;
}
await downloadStore.addWant({
mbid: this.releaseGroupMBID,
entity: 'release-group',
libraryId: libraryStore.getSelectedLibraryId() ?? 0,
libraryId: this.targetLibraryId,
artist: this.releaseGroup?.artistCredit ?? '',
title: this.albumName,
scope: 'future',
@@ -1584,15 +1609,28 @@ export class ExploreAlbumDetails extends LitElement {
this.syncWanted();
}
private async openPicker(): Promise<void> {
if (!this.targetLibraryId) {
await this.resolveTargetLibraryId();
}
if (!this.targetLibraryId) {
console.error('Cannot search for downloads: no library available');
return;
}
this.pickerOpen = true;
}
private renderPicker() {
if (!this.pickerOpen) return nothing;
if (!this.pickerOpen || !this.targetLibraryId) return nothing;
const tracks = this.currentTracks();
return html`
<download-picker
?open=${this.pickerOpen}
library-id=${libraryStore.getSelectedLibraryId() ?? 0}
library-id=${this.targetLibraryId}
artist=${this.releaseGroup?.artistCredit ?? ''}
album=${this.albumName}
release-group-mbid=${this.releaseGroupMBID ?? ''}
@@ -1933,10 +1933,16 @@ export class ExploreArtistDetails extends LitElement {
if (wantId) {
await downloadStore.removeWant(wantId);
} else {
const libraryId = await libraryStore.getDefaultLibraryId();
if (!libraryId) {
console.error('Could not update the wanted list: no library available');
return;
}
await downloadStore.addWant({
mbid: this.artistMBID,
entity: 'artist',
libraryId: libraryStore.getSelectedLibraryId() ?? 0,
libraryId,
artist: this.displayName,
title: this.displayName,
scope: 'future',
@@ -325,10 +325,19 @@ export class WantedView extends LitElement {
/** Widens or narrows what an artist subscription covers. */
private async toggleScope(want: Want): Promise<void> {
try {
const libraryId =
want.libraryId || (await libraryStore.getDefaultLibraryId());
if (!libraryId) {
console.error(
'Could not change what this subscription covers: no library available',
);
return;
}
await downloadStore.addWant({
mbid: want.mbid,
entity: 'artist',
libraryId: want.libraryId || (libraryStore.getSelectedLibraryId() ?? 0),
libraryId,
artist: want.artist,
title: want.title,
scope: want.scope === 'all' ? 'future' : 'all',