music (web): order album tracks by track number, not readdir order
meta.json is written in ffprobe/readdir order (arbitrary), and the web player rendered/queued it as-is — so albums like Andrew Bird's "The Mysterious Production of Eggs" showed scrambled (11, 7, 5, 14, 1, …). The web Track type didn't even carry the `track` tag. Add `track` to the Track type + a shared sortTracks(): by track NUMBER (parsed from the "n/total" tag), falling back to tag title only for tracks without a number. Applied to the rendered tracklist (setAlbum) and both play queues. Verified it reorders that album to 1→14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
TYPE_ORDER,
|
TYPE_ORDER,
|
||||||
coverUrl,
|
coverUrl,
|
||||||
isAudio,
|
isAudio,
|
||||||
|
sortTracks,
|
||||||
toRel,
|
toRel,
|
||||||
type AlbumMeta,
|
type AlbumMeta,
|
||||||
type Discography,
|
type Discography,
|
||||||
@@ -66,7 +67,7 @@ export const MusicDetail = () => {
|
|||||||
if (audio.length) {
|
if (audio.length) {
|
||||||
try {
|
try {
|
||||||
const meta = await get<AlbumMeta>(`/music/meta?path=${encodeURIComponent(rel)}`);
|
const meta = await get<AlbumMeta>(`/music/meta?path=${encodeURIComponent(rel)}`);
|
||||||
if (!cancelled) setAlbum(meta.tracks);
|
if (!cancelled) setAlbum(sortTracks(meta.tracks));
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setAlbum(audio.sort().map((f) => ({ file: f })));
|
if (!cancelled) setAlbum(audio.sort().map((f) => ({ file: f })));
|
||||||
}
|
}
|
||||||
@@ -99,7 +100,7 @@ export const MusicDetail = () => {
|
|||||||
const playAlbum = async (albumRel: string, startIndex = 0) => {
|
const playAlbum = async (albumRel: string, startIndex = 0) => {
|
||||||
try {
|
try {
|
||||||
const meta = await get<AlbumMeta>(`/music/meta?path=${encodeURIComponent(albumRel)}`);
|
const meta = await get<AlbumMeta>(`/music/meta?path=${encodeURIComponent(albumRel)}`);
|
||||||
const queue: PlayerTrack[] = meta.tracks.map((t) => ({ albumRel, file: t.file, title: t.title, artist: t.artist }));
|
const queue: PlayerTrack[] = sortTracks(meta.tracks).map((t) => ({ albumRel, file: t.file, title: t.title, artist: t.artist }));
|
||||||
player.playQueue(queue, startIndex);
|
player.playQueue(queue, startIndex);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
|
|||||||
@@ -8,8 +8,33 @@ export type DirEntry = { name: string; type: 'directory' | 'file'; size: number;
|
|||||||
export type LsResult = { entries: DirEntry[] };
|
export type LsResult = { entries: DirEntry[] };
|
||||||
export type ManifestAlbum = { v: string; cover: boolean; tracks: number; disco?: boolean };
|
export type ManifestAlbum = { v: string; cover: boolean; tracks: number; disco?: boolean };
|
||||||
export type Manifest = { albums: Record<string, ManifestAlbum> };
|
export type Manifest = { albums: Record<string, ManifestAlbum> };
|
||||||
export type Track = { file: string; title?: string; artist?: string };
|
export type Track = { file: string; title?: string; artist?: string; track?: string };
|
||||||
export type AlbumMeta = { path: string; cover?: string; tracks: Track[] };
|
export type AlbumMeta = { path: string; cover?: string; tracks: Track[] };
|
||||||
|
|
||||||
|
/** Parse a track-number tag ("7", "07", "7/14") to a number, or null when absent/unparseable. */
|
||||||
|
const trackNo = (t: Track): number | null => {
|
||||||
|
const raw = t.track?.split('/')[0]?.trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
const n = parseInt(raw, 10);
|
||||||
|
return Number.isFinite(n) ? n : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical album track order: by the `track` NUMBER, falling back to the tag title only for tracks
|
||||||
|
* that have no number (numbered tracks always precede unnumbered ones; filename breaks a final tie).
|
||||||
|
* meta.json is in ffprobe/readdir order (arbitrary), so every consumer must sort with this.
|
||||||
|
*/
|
||||||
|
export const sortTracks = <T extends Track>(tracks: T[]): T[] => {
|
||||||
|
const key = (t: Track) => (t.title || t.file).toLowerCase();
|
||||||
|
return [...tracks].sort((a, b) => {
|
||||||
|
const na = trackNo(a);
|
||||||
|
const nb = trackNo(b);
|
||||||
|
if (na !== null && nb !== null) return na - nb || key(a).localeCompare(key(b));
|
||||||
|
if (na !== null) return -1;
|
||||||
|
if (nb !== null) return 1;
|
||||||
|
return key(a).localeCompare(key(b));
|
||||||
|
});
|
||||||
|
};
|
||||||
export type Discography = { artist: string; albums: Record<string, string> };
|
export type Discography = { artist: string; albums: Record<string, string> };
|
||||||
|
|
||||||
const AUDIO_EXT = new Set(['mp3', 'flac', 'm4a', 'aac', 'ogg', 'opus', 'wav', 'wma', 'mp4']);
|
const AUDIO_EXT = new Set(['mp3', 'flac', 'm4a', 'aac', 'ogg', 'opus', 'wav', 'wma', 'mp4']);
|
||||||
|
|||||||
Reference in New Issue
Block a user