fix(library): open track details by path, not from the whole library
Track details from the queue, a playlist or a smart playlist looked the track up in libraryStore's whole-library array, and returned silently when it had not loaded, so the menu item did nothing. That dependency is also why every track was fetched eagerly at startup (20.5 MB at 26k tracks). GetTracksByPaths answers for the paths in hand, and trackCache holds the answers: bounded, coalesced into one call per task, and forgetting what the retag, removal and scan events name. Every opener, and track-details' own refresh after a save, go through it. A source sweep pins the whole-library array to the store and the Tracks view. Closes #279
This commit is contained in:
1 parent
3cddf70c4d
commit
4a9fe1d207
14 files changed
+747
-270
No files matched your search
@@ -56,12 +56,9 @@ import {
|
||||
createTrackCardDragImage,
|
||||
removeDragImage,
|
||||
} from '@utils/drag-image';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import '@components/playlist-picker/playlist-picker.js';
|
||||
import { loadTrackDetails } from '@utils/lazy-track-details.js';
|
||||
import { tracksByFilePath, tracksForPaths } from '@utils/track-index.js';
|
||||
import { showTrackDetailsForPath, showBatchTrackDetailsForPaths } from '@utils/track-details-opener.js';
|
||||
import type { TrackDetails } from '@components/track-details/track-details.js';
|
||||
import type { CoverArtUrls } from '@components/track-details/track-details.js';
|
||||
import '@components/phantom-resolver/phantom-resolver.js';
|
||||
import type { PhantomResolver } from '@components/phantom-resolver/phantom-resolver.js';
|
||||
import '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js';
|
||||
@@ -758,76 +755,21 @@ export class PlaylistDetails
|
||||
}
|
||||
|
||||
private async openTrackDetails(filePath: string) {
|
||||
const tracks = libraryStore.getCachedTracks();
|
||||
const track = tracks
|
||||
? tracksByFilePath(tracks).get(filePath)
|
||||
: undefined;
|
||||
|
||||
if (!track) return;
|
||||
|
||||
const ready = await loadTrackDetails(
|
||||
await showTrackDetailsForPath(
|
||||
() => this.trackDetailsDialog,
|
||||
filePath,
|
||||
() => void this.openTrackDetails(filePath),
|
||||
);
|
||||
|
||||
if (!ready) return;
|
||||
|
||||
const coverArt = track.CoverArtPath
|
||||
? {
|
||||
coverArtPath: track.CoverArtPath,
|
||||
coverArtSmall: track.CoverArtSmall,
|
||||
coverArtMedium: track.CoverArtMedium,
|
||||
coverArtLarge: track.CoverArtLarge,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
this.trackDetailsDialog?.show(
|
||||
track,
|
||||
coverArt,
|
||||
);
|
||||
}
|
||||
|
||||
private async openBatchTrackDetails(
|
||||
filePaths: string[],
|
||||
) {
|
||||
const cachedTracks =
|
||||
libraryStore.getCachedTracks();
|
||||
|
||||
if (!cachedTracks) return;
|
||||
|
||||
const tracks = tracksForPaths(
|
||||
cachedTracks,
|
||||
await showBatchTrackDetailsForPaths(
|
||||
() => this.trackDetailsDialog,
|
||||
filePaths,
|
||||
);
|
||||
|
||||
if (tracks.length === 0) return;
|
||||
|
||||
const ready = await loadTrackDetails(
|
||||
() => void this.openBatchTrackDetails(filePaths),
|
||||
);
|
||||
|
||||
if (!ready) return;
|
||||
|
||||
const first = tracks[0]!;
|
||||
const albumNames = new Set(tracks.map((t) => t.Album));
|
||||
let coverArt: CoverArtUrls | null = null;
|
||||
let coverArtMixed = false;
|
||||
|
||||
if (albumNames.size === 1 && first.CoverArtPath) {
|
||||
coverArt = {
|
||||
coverArtPath: first.CoverArtPath,
|
||||
coverArtSmall: first.CoverArtSmall,
|
||||
coverArtMedium: first.CoverArtMedium,
|
||||
coverArtLarge: first.CoverArtLarge,
|
||||
};
|
||||
} else if (albumNames.size > 1) {
|
||||
coverArtMixed = true;
|
||||
}
|
||||
|
||||
this.trackDetailsDialog?.showBatch(
|
||||
tracks,
|
||||
coverArt,
|
||||
coverArtMixed,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,12 +51,8 @@ import {
|
||||
createTrackCardDragImage,
|
||||
removeDragImage,
|
||||
} from '@utils/drag-image';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { loadTrackDetails } from '@utils/lazy-track-details.js';
|
||||
import { tracksByFilePath } from '@utils/track-index.js';
|
||||
import { showTrackDetailsForPath, showBatchTrackDetailsForPaths } from '@utils/track-details-opener.js';
|
||||
import type { TrackDetails } from '@components/track-details/track-details.js';
|
||||
import type { CoverArtUrls } from '@components/track-details/track-details.js';
|
||||
import {
|
||||
creditLink,
|
||||
trackLink,
|
||||
@@ -1542,90 +1538,30 @@ export class QueuePanel
|
||||
}
|
||||
|
||||
private async openTrackDetails(index: number) {
|
||||
const queueTrack =
|
||||
this.queue.tracks[index];
|
||||
const queueTrack = this.queue.tracks[index];
|
||||
|
||||
if (!queueTrack) return;
|
||||
|
||||
const tracks =
|
||||
libraryStore.getCachedTracks();
|
||||
const track = tracks
|
||||
? tracksByFilePath(tracks).get(
|
||||
queueTrack.filePath,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!track) return;
|
||||
|
||||
const ready = await loadTrackDetails(
|
||||
await showTrackDetailsForPath(
|
||||
() => this.trackDetailsDialog,
|
||||
queueTrack.filePath,
|
||||
() => void this.openTrackDetails(index),
|
||||
);
|
||||
|
||||
if (!ready) return;
|
||||
|
||||
const coverArt = track.CoverArtPath
|
||||
? {
|
||||
coverArtPath: track.CoverArtPath,
|
||||
coverArtSmall: track.CoverArtSmall,
|
||||
coverArtMedium: track.CoverArtMedium,
|
||||
coverArtLarge: track.CoverArtLarge,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
this.trackDetailsDialog?.show(
|
||||
track,
|
||||
coverArt,
|
||||
);
|
||||
}
|
||||
|
||||
private async openBatchTrackDetails(
|
||||
indices: number[],
|
||||
) {
|
||||
const queueTracks = this.queue.tracks;
|
||||
const cachedTracks =
|
||||
libraryStore.getCachedTracks();
|
||||
const filePaths = indices
|
||||
.map((i) => queueTracks[i]?.filePath)
|
||||
.filter((fp): fp is string => fp != null);
|
||||
|
||||
if (!cachedTracks) return;
|
||||
|
||||
const byPath = tracksByFilePath(cachedTracks);
|
||||
const tracks = indices
|
||||
.map((i) => queueTracks[i])
|
||||
.filter((qt) => qt != null)
|
||||
.map((qt) => byPath.get(qt.filePath))
|
||||
.filter(
|
||||
(t): t is library.Track =>
|
||||
t != null,
|
||||
);
|
||||
|
||||
if (tracks.length === 0) return;
|
||||
|
||||
const ready = await loadTrackDetails(
|
||||
await showBatchTrackDetailsForPaths(
|
||||
() => this.trackDetailsDialog,
|
||||
filePaths,
|
||||
() => void this.openBatchTrackDetails(indices),
|
||||
);
|
||||
|
||||
if (!ready) return;
|
||||
|
||||
const first = tracks[0]!;
|
||||
const albumNames = new Set(tracks.map((t) => t.Album));
|
||||
let coverArt: CoverArtUrls | null = null;
|
||||
let coverArtMixed = false;
|
||||
|
||||
if (albumNames.size === 1 && first.CoverArtPath) {
|
||||
coverArt = {
|
||||
coverArtPath: first.CoverArtPath,
|
||||
coverArtSmall: first.CoverArtSmall,
|
||||
coverArtMedium: first.CoverArtMedium,
|
||||
coverArtLarge: first.CoverArtLarge,
|
||||
};
|
||||
} else if (albumNames.size > 1) {
|
||||
coverArtMixed = true;
|
||||
}
|
||||
|
||||
this.trackDetailsDialog?.showBatch(
|
||||
tracks,
|
||||
coverArt,
|
||||
coverArtMixed,
|
||||
);
|
||||
}
|
||||
|
||||
private onContextPlaylistActionComplete = () => {
|
||||
|
||||
@@ -52,11 +52,8 @@ import '@lit-labs/virtualizer';
|
||||
import type { LitVirtualizer } from '@lit-labs/virtualizer';
|
||||
import { flow } from '@lit-labs/virtualizer/layouts/flow.js';
|
||||
import '@components/playlist-picker/playlist-picker.js';
|
||||
import { loadTrackDetails } from '@utils/lazy-track-details.js';
|
||||
import { tracksByFilePath, tracksForPaths } from '@utils/track-index.js';
|
||||
import { showTrackDetailsForPath, showBatchTrackDetailsForPaths } from '@utils/track-details-opener.js';
|
||||
import type { TrackDetails } from '@components/track-details/track-details.js';
|
||||
import type { CoverArtUrls } from '@components/track-details/track-details.js';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import { formatMilliseconds } from '@utils/time';
|
||||
import {
|
||||
creditLink,
|
||||
@@ -1145,76 +1142,21 @@ export class SmartPlaylistDetails
|
||||
}
|
||||
|
||||
private async openTrackDetails(filePath: string) {
|
||||
const tracks = libraryStore.getCachedTracks();
|
||||
const track = tracks
|
||||
? tracksByFilePath(tracks).get(filePath)
|
||||
: undefined;
|
||||
|
||||
if (!track) return;
|
||||
|
||||
const ready = await loadTrackDetails(
|
||||
await showTrackDetailsForPath(
|
||||
() => this.trackDetailsDialog,
|
||||
filePath,
|
||||
() => void this.openTrackDetails(filePath),
|
||||
);
|
||||
|
||||
if (!ready) return;
|
||||
|
||||
const coverArt = track.CoverArtPath
|
||||
? {
|
||||
coverArtPath: track.CoverArtPath,
|
||||
coverArtSmall: track.CoverArtSmall,
|
||||
coverArtMedium: track.CoverArtMedium,
|
||||
coverArtLarge: track.CoverArtLarge,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
this.trackDetailsDialog?.show(
|
||||
track,
|
||||
coverArt,
|
||||
);
|
||||
}
|
||||
|
||||
private async openBatchTrackDetails(
|
||||
filePaths: string[],
|
||||
) {
|
||||
const cachedTracks =
|
||||
libraryStore.getCachedTracks();
|
||||
|
||||
if (!cachedTracks) return;
|
||||
|
||||
const tracks = tracksForPaths(
|
||||
cachedTracks,
|
||||
await showBatchTrackDetailsForPaths(
|
||||
() => this.trackDetailsDialog,
|
||||
filePaths,
|
||||
);
|
||||
|
||||
if (tracks.length === 0) return;
|
||||
|
||||
const ready = await loadTrackDetails(
|
||||
() => void this.openBatchTrackDetails(filePaths),
|
||||
);
|
||||
|
||||
if (!ready) return;
|
||||
|
||||
const first = tracks[0]!;
|
||||
const albumNames = new Set(tracks.map((t) => t.Album));
|
||||
let coverArt: CoverArtUrls | null = null;
|
||||
let coverArtMixed = false;
|
||||
|
||||
if (albumNames.size === 1 && first.CoverArtPath) {
|
||||
coverArt = {
|
||||
coverArtPath: first.CoverArtPath,
|
||||
coverArtSmall: first.CoverArtSmall,
|
||||
coverArtMedium: first.CoverArtMedium,
|
||||
coverArtLarge: first.CoverArtLarge,
|
||||
};
|
||||
} else if (albumNames.size > 1) {
|
||||
coverArtMixed = true;
|
||||
}
|
||||
|
||||
this.trackDetailsDialog?.showBatch(
|
||||
tracks,
|
||||
coverArt,
|
||||
coverArtMixed,
|
||||
);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
import { GetTrackMBIDs } from '@go/library/library.js';
|
||||
type TrackMBIDs = library.TrackMBIDs;
|
||||
import { ImageFilePicker, ReadFile } from '@go/frontendutil/frontendutil.js';
|
||||
import { libraryStore } from '../../store/library-store';
|
||||
import { trackCache } from '../../store/track-cache';
|
||||
import { batchCoverArt } from '@utils/track-details-opener.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
|
||||
@@ -1317,10 +1318,13 @@ export class TrackDetails extends LitElement {
|
||||
const container = img.parentElement;
|
||||
|
||||
if (container) {
|
||||
container.innerHTML =
|
||||
'<div class="cover-placeholder">' +
|
||||
'<wa-icon name="music"></wa-icon>' +
|
||||
'</div>';
|
||||
const placeholder = document.createElement('div');
|
||||
const icon = document.createElement('wa-icon');
|
||||
|
||||
placeholder.className = 'cover-placeholder';
|
||||
icon.setAttribute('name', 'music');
|
||||
placeholder.append(icon);
|
||||
container.replaceChildren(placeholder);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1690,20 +1694,11 @@ export class TrackDetails extends LitElement {
|
||||
// invalidation, which refreshes all other views.
|
||||
this.exitEditMode();
|
||||
|
||||
// Re-fetch track and album data so the dialog
|
||||
// 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.
|
||||
// Awaited for the side effect of refreshing the
|
||||
// store; the album list is consumed elsewhere.
|
||||
const [tracks] = await Promise.all([
|
||||
libraryStore.getTracks(),
|
||||
libraryStore.getAlbums(),
|
||||
]);
|
||||
|
||||
const updated = tracks.find(
|
||||
(t) => t.FilePath === filePath,
|
||||
);
|
||||
// Re-read this one track so the dialog shows the
|
||||
// values and cover art just written. Not the whole
|
||||
// library (#279): the views that hold it refetch on
|
||||
// the event, and only if something is showing them.
|
||||
const [updated] = await trackCache.refresh([filePath]);
|
||||
|
||||
if (updated) {
|
||||
this.track = updated;
|
||||
@@ -1828,39 +1823,18 @@ export class TrackDetails extends LitElement {
|
||||
this.errorMessage = '';
|
||||
this.cleanupPendingCoverArt();
|
||||
|
||||
// Refresh data from library store; album list is
|
||||
// refreshed for side effects only.
|
||||
const [tracks] = await Promise.all([
|
||||
libraryStore.getTracks(),
|
||||
libraryStore.getAlbums(),
|
||||
]);
|
||||
// Re-read the tracks just written, in the order the batch
|
||||
// holds them (#279) — not the whole library to filter.
|
||||
// `refresh`, because the write's event may not have reached
|
||||
// the cache before its own result did.
|
||||
const refreshed = await trackCache.refresh(this.batchFilePaths);
|
||||
|
||||
// Re-resolve batch tracks.
|
||||
const pathSet = new Set(this.batchFilePaths);
|
||||
const refreshed = tracks.filter((t) =>
|
||||
pathSet.has(t.FilePath),
|
||||
);
|
||||
this.batchTracks = refreshed;
|
||||
|
||||
// Re-resolve cover art state.
|
||||
const first = refreshed[0];
|
||||
const albumNames = new Set(refreshed.map((t) => t.Album));
|
||||
const { coverArt, mixed } = batchCoverArt(refreshed);
|
||||
|
||||
if (albumNames.size === 1 && first?.CoverArtPath) {
|
||||
this.coverArt = {
|
||||
coverArtPath: first.CoverArtPath,
|
||||
coverArtSmall: first.CoverArtSmall,
|
||||
coverArtMedium: first.CoverArtMedium,
|
||||
coverArtLarge: first.CoverArtLarge,
|
||||
};
|
||||
this.batchCoverArtMixed = false;
|
||||
} else if (albumNames.size > 1) {
|
||||
this.coverArt = null;
|
||||
this.batchCoverArtMixed = true;
|
||||
} else {
|
||||
this.coverArt = null;
|
||||
this.batchCoverArtMixed = false;
|
||||
}
|
||||
this.coverArt = coverArt;
|
||||
this.batchCoverArtMixed = mixed;
|
||||
};
|
||||
|
||||
// -- Shared edit logic --
|
||||
@@ -2103,6 +2077,8 @@ export class TrackDetails extends LitElement {
|
||||
// as a base64-encoded string (standard encoding/json
|
||||
// behaviour for []byte).
|
||||
const result = await ReadFile(filePath);
|
||||
// SAFETY: the binding is typed as the Go []byte, but the
|
||||
// wire value is the base64 string encoding/json made of it.
|
||||
const b64 = result as unknown as string;
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
|
||||
Reference in new issue
Block a user