wip on autotagging
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1939,10 +1939,6 @@ export class ConfigPage extends LitElement {
|
||||
|
||||
// --- Search / Index section ---
|
||||
|
||||
private async pollIndexStatus(): Promise<void> {
|
||||
// Kept as no-op — status comes via events now.
|
||||
}
|
||||
|
||||
private renderSearchSection() {
|
||||
const s = this.indexStatus;
|
||||
|
||||
|
||||
@@ -492,6 +492,10 @@ export class CoverGrid
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Reference renderSplitGrid so the deferred split-grid
|
||||
// render path (and its track-event helpers) doesn't trip
|
||||
// noUnusedLocals. Never invoked at runtime.
|
||||
void this.renderSplitGrid;
|
||||
this.restoreSortPreferences();
|
||||
this.loadAlbums();
|
||||
|
||||
@@ -998,27 +1002,6 @@ export class CoverGrid
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise the dropdown to the current
|
||||
* selection: open the sole selected album's
|
||||
* dropdown, or close it when zero or many
|
||||
* albums are selected.
|
||||
*/
|
||||
private syncDropdownToSelection() {
|
||||
if (this.selectedAlbums.size === 1) {
|
||||
const [albumId] = this.selectedAlbums;
|
||||
const album = this.cachedFilteredAlbums.find(
|
||||
(a) => a.ID === albumId,
|
||||
);
|
||||
|
||||
if (album) {
|
||||
void this.openDropdown(album);
|
||||
}
|
||||
} else {
|
||||
this.closeDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Event delegation helpers
|
||||
* ==================================================================== */
|
||||
@@ -1923,7 +1906,9 @@ export class CoverGrid
|
||||
|
||||
/**
|
||||
* Dual virtualizer — dropdown sandwiched between
|
||||
* "before" and "after" grids.
|
||||
* "before" and "after" grids. Currently unreferenced
|
||||
* (the single-grid path is the active rendering mode);
|
||||
* kept here against the deferred split-grid layout.
|
||||
*/
|
||||
private renderSplitGrid() {
|
||||
const sm = this.scrollMgr;
|
||||
@@ -1978,6 +1963,7 @@ export class CoverGrid
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
/** Context menu + playlist submenu popups. */
|
||||
/** Trigger playlist submenu with resolved file paths. */
|
||||
private async handleShowPlaylistSubmenu() {
|
||||
|
||||
@@ -6,36 +6,20 @@ import {
|
||||
BrowseReleases,
|
||||
GetThumbnail,
|
||||
} from '@go/explore/Service';
|
||||
import type {
|
||||
MBReleaseGroup,
|
||||
MBRelease,
|
||||
MBTrack,
|
||||
} from '@go/explore/Service';
|
||||
import { GetAlbumTracks } from '@go/library/Library';
|
||||
import { library } from '@go/models';
|
||||
import type { explore } from '@go/models';
|
||||
type MBReleaseGroup = explore.MBReleaseGroup;
|
||||
type MBRelease = explore.MBRelease;
|
||||
type MBTrack = explore.MBTrack;
|
||||
import { exploreCache } from '../../store/explore-cache';
|
||||
import { exploreSettings } from '../../store/explore-settings';
|
||||
import { libraryStore } from '../../store/library-store';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '../library-status-indicator/library-status-indicator.js';
|
||||
|
||||
/* ── Constants ── */
|
||||
const CAA_GROUP_BASE = 'https://coverartarchive.org/release-group';
|
||||
|
||||
/* ── Utility functions (duplicated per Knowledge Pattern #9 — no cross-component imports) ── */
|
||||
|
||||
function CoverArtGroupURL(releaseGroupMBID: string): string {
|
||||
return `${CAA_GROUP_BASE}/${releaseGroupMBID}/front-250`;
|
||||
}
|
||||
|
||||
function nameToHue(name: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return Math.abs(hash) % 360;
|
||||
}
|
||||
|
||||
function extractYear(dateStr: string): string {
|
||||
if (!dateStr) return '';
|
||||
return dateStr.substring(0, 4);
|
||||
@@ -598,7 +582,9 @@ export class ExploreAlbumDetails extends LitElement {
|
||||
}
|
||||
|
||||
// Phase 1: hydrate tracklist from local library if available.
|
||||
const localRelease = await this.hydrateFromLibrary(mbid);
|
||||
// Awaited for the side effect of populating the local
|
||||
// tracklist; the return value isn't currently consumed.
|
||||
await this.hydrateFromLibrary(mbid);
|
||||
|
||||
// Library-only mode: local data is all we show.
|
||||
if (exploreSettings.libraryOnly) {
|
||||
@@ -899,7 +885,7 @@ export class ExploreAlbumDetails extends LitElement {
|
||||
});
|
||||
|
||||
const cluster: ReleaseCluster = {
|
||||
representative: sorted[0],
|
||||
representative: sorted[0]!,
|
||||
allReleases: sorted,
|
||||
fingerprint,
|
||||
score: 0, // filled in below
|
||||
|
||||
@@ -16,13 +16,12 @@ import {
|
||||
GetTrackThumbnails,
|
||||
ResolveReleaseGroupMBIDs,
|
||||
} from '@go/explore/Service';
|
||||
import type {
|
||||
MBArtist,
|
||||
MBReleaseGroup,
|
||||
LBTopRecording,
|
||||
LBTopReleaseGroup,
|
||||
LBSimilarArtist,
|
||||
} from '@go/explore/Service';
|
||||
import type { explore } from '@go/models';
|
||||
type MBArtist = explore.MBArtist;
|
||||
type MBReleaseGroup = explore.MBReleaseGroup;
|
||||
type LBTopRecording = explore.LBTopRecording;
|
||||
type LBTopReleaseGroup = explore.LBTopReleaseGroup;
|
||||
type LBSimilarArtist = explore.LBSimilarArtist;
|
||||
import { exploreCache } from '../../store/explore-cache';
|
||||
import { exploreSettings } from '../../store/explore-settings';
|
||||
import { libraryStore } from '../../store/library-store';
|
||||
@@ -31,7 +30,6 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '../library-status-indicator/library-status-indicator.js';
|
||||
|
||||
/* ── Constants ── */
|
||||
const CAA_GROUP_BASE = 'https://coverartarchive.org/release-group';
|
||||
|
||||
/** Desired section order for grouping release types. */
|
||||
const TYPE_ORDER = ['Albums', 'EP', 'Single', 'Other Albums'];
|
||||
@@ -55,14 +53,6 @@ const NON_STUDIO_SECONDARY_TYPES = new Set([
|
||||
|
||||
/* ── Utility functions (duplicated from explore-view per design decision) ── */
|
||||
|
||||
function CoverArtGroupURL(releaseGroupMBID: string): string {
|
||||
return `${CAA_GROUP_BASE}/${releaseGroupMBID}/front-250`;
|
||||
}
|
||||
|
||||
function CoverArtReleaseURL(releaseMBID: string): string {
|
||||
return `https://coverartarchive.org/release/${releaseMBID}/front-250`;
|
||||
}
|
||||
|
||||
function nameToHue(name: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
@@ -108,7 +98,6 @@ export class ExploreArtistDetails extends LitElement {
|
||||
@state() private loadingTopReleases = true;
|
||||
@state() private loadingReleases = true;
|
||||
@state() private errorArtist = '';
|
||||
@state() private errorTracks = '';
|
||||
@state() private errorReleases = '';
|
||||
@state() private similarArtists: LBSimilarArtist[] = [];
|
||||
@state() private loadingSimilar = true;
|
||||
@@ -1297,7 +1286,6 @@ export class ExploreArtistDetails extends LitElement {
|
||||
void this.batchResolveTrackThumbnails(tracks, mapping);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
this.errorTracks = msg;
|
||||
console.error(
|
||||
`[explore-artist] TopRecordingsForArtist error: ${msg}`,
|
||||
);
|
||||
@@ -1436,26 +1424,6 @@ export class ExploreArtistDetails extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a cover art thumbnail for a release group (or release)
|
||||
* via the backend proxy (local library → disk cache → CAA).
|
||||
* Updates thumbnailURLs reactively so the image pops in when ready.
|
||||
*/
|
||||
private resolveThumbnail(mbid: string, albumName: string, artistName: string) {
|
||||
if (!mbid || this.thumbnailURLs.has(mbid)) return;
|
||||
|
||||
// Mark as in-flight so we don't fire duplicate requests.
|
||||
this.thumbnailURLs.set(mbid, '');
|
||||
|
||||
GetThumbnail(mbid, albumName, artistName)
|
||||
.then((url) => {
|
||||
if (url) {
|
||||
this.thumbnailURLs = new Map(this.thumbnailURLs).set(mbid, url);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-resolve thumbnails in two phases:
|
||||
* 1. Batch call for cached/local art — instant.
|
||||
|
||||
@@ -2,13 +2,6 @@ import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, state, query as litQuery } from 'lit/decorators.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import { Search, GetThumbnail, GetThumbnails, GetArtistImageURL, GetPopularityBatch, RecordSearchClick } from '@go/explore/Service';
|
||||
import type { ThumbnailRequest } from '@go/explore/Service';
|
||||
import type {
|
||||
MBSearchResult,
|
||||
MBArtist,
|
||||
MBReleaseGroup,
|
||||
MBRecording,
|
||||
} from '@go/explore/Service';
|
||||
import { libraryStore } from '../../store/library-store';
|
||||
import { exploreCache } from '../../store/explore-cache';
|
||||
import { exploreSettings } from '../../store/explore-settings';
|
||||
@@ -16,6 +9,11 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '../library-status-indicator/library-status-indicator.js';
|
||||
import '../top-results-row/top-results-row.js';
|
||||
import type { explore } from '@go/models';
|
||||
type ThumbnailRequest = explore.ThumbnailRequest;
|
||||
type MBSearchResult = explore.MBSearchResult;
|
||||
type MBArtist = explore.MBArtist;
|
||||
type MBReleaseGroup = explore.MBReleaseGroup;
|
||||
type MBRecording = explore.MBRecording;
|
||||
|
||||
/* ── Constants ── */
|
||||
const DEBOUNCE_MS = 300;
|
||||
@@ -32,20 +30,20 @@ function editDistance(a: string, b: string): number {
|
||||
const matrix: number[][] = [];
|
||||
|
||||
for (let i = 0; i <= a.length; i++) matrix[i] = [i];
|
||||
for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
|
||||
for (let j = 0; j <= b.length; j++) matrix[0]![j] = j;
|
||||
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j] + 1,
|
||||
matrix[i][j - 1] + 1,
|
||||
matrix[i - 1][j - 1] + cost,
|
||||
matrix[i]![j] = Math.min(
|
||||
matrix[i - 1]![j]! + 1,
|
||||
matrix[i]![j - 1]! + 1,
|
||||
matrix[i - 1]![j - 1]! + cost,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[a.length][b.length];
|
||||
return matrix[a.length]![b.length]!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,16 +70,7 @@ function fuzzyMatch(query: string, name: string): boolean {
|
||||
);
|
||||
}
|
||||
const MAX_SECTION_RESULTS = 10;
|
||||
const CAA_GROUP_BASE = 'https://coverartarchive.org/release-group';
|
||||
|
||||
/**
|
||||
* Build a Cover Art Archive URL for a release-group front cover.
|
||||
* Mirrors the Wails CoverArtGroupURL binding but runs synchronously
|
||||
* on the frontend — avoids N async round-trips per render cycle.
|
||||
*/
|
||||
function CoverArtGroupURL(releaseGroupMBID: string): string {
|
||||
return `${CAA_GROUP_BASE}/${releaseGroupMBID}/front-250`;
|
||||
}
|
||||
|
||||
/** Hash a string to a hue value 0–360 for avatar coloring. */
|
||||
function nameToHue(name: string): number {
|
||||
@@ -894,9 +883,9 @@ export class ExploreView extends LitElement {
|
||||
|
||||
// Collect all non-empty MBIDs.
|
||||
const mbids: string[] = [];
|
||||
for (const a of result.artists) if (a.mbid) mbids.push(a.mbid);
|
||||
for (const rg of result.releaseGroups) if (rg.mbid) mbids.push(rg.mbid);
|
||||
for (const r of result.recordings) if (r.mbid) mbids.push(r.mbid);
|
||||
for (const a of result.artists ?? []) if (a.mbid) mbids.push(a.mbid);
|
||||
for (const rg of result.releaseGroups ?? []) if (rg.mbid) mbids.push(rg.mbid);
|
||||
for (const r of result.recordings ?? []) if (r.mbid) mbids.push(r.mbid);
|
||||
|
||||
if (mbids.length === 0) return;
|
||||
|
||||
@@ -927,16 +916,22 @@ export class ExploreView extends LitElement {
|
||||
return 0.35 * relevance + 0.50 * logPop + 0.15 * personal;
|
||||
};
|
||||
|
||||
// Re-sort each category.
|
||||
result.artists.sort((a, b) =>
|
||||
blendedScore(b.mbid, b.score) - blendedScore(a.mbid, a.score));
|
||||
result.releaseGroups.sort((a, b) =>
|
||||
blendedScore(b.mbid, b.score) - blendedScore(a.mbid, a.score));
|
||||
result.recordings.sort((a, b) =>
|
||||
blendedScore(b.mbid, b.score) - blendedScore(a.mbid, a.score));
|
||||
// Re-sort each category. Backend stamps a `score` field
|
||||
// onto entries before returning them, but the Wails-
|
||||
// generated MB types don't model it — cast through any to
|
||||
// read it on the way to the comparator.
|
||||
const cmp = (a: { mbid: string }, b: { mbid: string }): number =>
|
||||
blendedScore(b.mbid, (b as any).score ?? 0) - blendedScore(a.mbid, (a as any).score ?? 0);
|
||||
(result.artists ?? []).sort(cmp);
|
||||
(result.releaseGroups ?? []).sort(cmp);
|
||||
(result.recordings ?? []).sort(cmp);
|
||||
|
||||
// Trigger re-render.
|
||||
this.results = { ...result };
|
||||
// Trigger re-render. MBSearchResult is a Wails-generated
|
||||
// class with bound methods (convertValues), so request an
|
||||
// update directly rather than spreading the object — that
|
||||
// would drop the methods.
|
||||
this.results = result;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -967,8 +962,7 @@ export class ExploreView extends LitElement {
|
||||
|
||||
// Enrich artists: if MB result matches a library artist, add local images.
|
||||
if (result.artists) {
|
||||
for (let i = 0; i < result.artists.length; i++) {
|
||||
const a = result.artists[i];
|
||||
for (const a of result.artists) {
|
||||
const lib = (a.mbid && libArtistsByMBID.get(a.mbid)) ||
|
||||
libArtistsByName.get(a.name.toLowerCase());
|
||||
if (lib) {
|
||||
@@ -981,8 +975,7 @@ export class ExploreView extends LitElement {
|
||||
|
||||
// Enrich release groups: if MB result matches a library album, use local art.
|
||||
if (result.releaseGroups) {
|
||||
for (let i = 0; i < result.releaseGroups.length; i++) {
|
||||
const rg = result.releaseGroups[i];
|
||||
for (const rg of result.releaseGroups) {
|
||||
const lib = rg.mbid ? libAlbumsByMBID.get(rg.mbid) : undefined;
|
||||
if (lib) {
|
||||
(rg as any)._coverArt = lib.CoverArtMedium || lib.CoverArtSmall || '';
|
||||
@@ -1020,7 +1013,7 @@ export class ExploreView extends LitElement {
|
||||
if (prev.releaseGroups?.length) {
|
||||
const existing = new Set(
|
||||
(full.releaseGroups || []).map(
|
||||
(rg) => `${rg.title}|${rg.artistCredit}`.toLowerCase(),
|
||||
(rg: MBReleaseGroup) => `${rg.title}|${rg.artistCredit}`.toLowerCase(),
|
||||
),
|
||||
);
|
||||
for (const rg of prev.releaseGroups) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import type { library } from '@go/models';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
|
||||
@@ -48,9 +49,25 @@ export class LibraryFilter extends LitElement {
|
||||
}
|
||||
`];
|
||||
|
||||
private unsubscribeStore?: () => void;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.loadLibraries();
|
||||
|
||||
// The LibraryController already wires a reactive subscription to
|
||||
// libraryStore so the host re-renders on change, but it doesn't
|
||||
// refresh our cached `this.libraries` array. Subscribe directly
|
||||
// and re-fetch so LibraryAdded / Removed / Renamed events flow
|
||||
// into the dropdown without a restart.
|
||||
this.unsubscribeStore = libraryStore.subscribe(() => {
|
||||
this.loadLibraries();
|
||||
});
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.unsubscribeStore?.();
|
||||
}
|
||||
|
||||
private async loadLibraries() {
|
||||
|
||||
@@ -203,7 +203,7 @@ export class LibraryStatusIndicator extends LitElement {
|
||||
}
|
||||
|
||||
function capitalize(s: string): string {
|
||||
return s.length > 0 ? s[0].toUpperCase() + s.slice(1) : s;
|
||||
return s.length > 0 ? (s[0] ?? '').toUpperCase() + s.slice(1) : s;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { designTokens } from '../../styles/tokens.css';
|
||||
|
||||
import type { DragActiveDetail } from '@utils/drag-controller';
|
||||
|
||||
type View = 'home' | 'playlists' | 'artists' | 'genres' | 'albums' | 'tracks' | 'explore' | 'settings';
|
||||
type View = 'home' | 'playlists' | 'artists' | 'genres' | 'albums' | 'tracks' | 'explore' | 'autotag' | 'settings';
|
||||
|
||||
interface NavItem {
|
||||
id: View;
|
||||
@@ -149,6 +149,7 @@ export class AppSidebar extends LitElement {
|
||||
{ id: 'albums', label: 'Albums', icon: 'compact-disc' },
|
||||
{ id: 'tracks', label: 'Tracks', icon: 'music' },
|
||||
{ id: 'explore', label: 'Explore', icon: 'globe' },
|
||||
{ id: 'autotag', label: 'Autotag', icon: 'tag' },
|
||||
{ id: 'settings', label: 'Settings', icon: 'gear' },
|
||||
];
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export class TopResultsRow extends LitElement {
|
||||
// Per-card state: cover images.
|
||||
private images = new Map<string, string>();
|
||||
|
||||
static styles = [
|
||||
static override styles = [
|
||||
designTokens,
|
||||
css`
|
||||
:host {
|
||||
@@ -177,7 +177,7 @@ export class TopResultsRow extends LitElement {
|
||||
`,
|
||||
];
|
||||
|
||||
updated(changed: Map<string, unknown>) {
|
||||
override updated(changed: Map<string, unknown>) {
|
||||
if (changed.has('results')) {
|
||||
this.loadCardData();
|
||||
}
|
||||
@@ -227,7 +227,7 @@ export class TopResultsRow extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
override render() {
|
||||
if (!this.results?.length) return nothing;
|
||||
|
||||
return html`
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
CancelBatchWrite,
|
||||
} from '@go/tagwriter/TagWriter';
|
||||
import { GetTrackMBIDs } from '@go/library/Library';
|
||||
import type { TrackMBIDs } from '@go/library/Library';
|
||||
type TrackMBIDs = library.TrackMBIDs;
|
||||
import { ImageFilePicker, ReadFile } from '@go/frontendutil/FrontendUtil';
|
||||
import { libraryStore } from '../../store/library-store';
|
||||
import { EventsOn, EventsOff } from '@runtime/runtime';
|
||||
@@ -1646,7 +1646,9 @@ export class TrackDetails extends LitElement {
|
||||
// shows updated values and cover art. The store
|
||||
// invalidation is already in-flight from the event;
|
||||
// these calls await the pending fetch or start one.
|
||||
const [tracks, albums] = await Promise.all([
|
||||
// Awaited for the side effect of refreshing the
|
||||
// store; the album list is consumed elsewhere.
|
||||
const [tracks] = await Promise.all([
|
||||
libraryStore.getTracks(),
|
||||
libraryStore.getAlbums(),
|
||||
]);
|
||||
@@ -1776,8 +1778,9 @@ export class TrackDetails extends LitElement {
|
||||
this.errorMessage = '';
|
||||
this.cleanupPendingCoverArt();
|
||||
|
||||
// Refresh data from library store.
|
||||
const [tracks, albums] = await Promise.all([
|
||||
// Refresh data from library store; album list is
|
||||
// refreshed for side effects only.
|
||||
const [tracks] = await Promise.all([
|
||||
libraryStore.getTracks(),
|
||||
libraryStore.getAlbums(),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user