From 6fc34363c8a1cf570590c038bb4b018837a84048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 27 Jul 2026 11:50:34 +0000 Subject: [PATCH] music (web): order album tracks by track number, not readdir order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../officerdev/src/apps/Music/MusicDetail.tsx | 5 ++-- .../officerdev/src/apps/Music/shared.ts | 27 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx b/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx index a5db4b76..49e01828 100644 --- a/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx +++ b/src/workspaces/officerdev/src/apps/Music/MusicDetail.tsx @@ -10,6 +10,7 @@ import { TYPE_ORDER, coverUrl, isAudio, + sortTracks, toRel, type AlbumMeta, type Discography, @@ -66,7 +67,7 @@ export const MusicDetail = () => { if (audio.length) { try { const meta = await get(`/music/meta?path=${encodeURIComponent(rel)}`); - if (!cancelled) setAlbum(meta.tracks); + if (!cancelled) setAlbum(sortTracks(meta.tracks)); } catch { if (!cancelled) setAlbum(audio.sort().map((f) => ({ file: f }))); } @@ -99,7 +100,7 @@ export const MusicDetail = () => { const playAlbum = async (albumRel: string, startIndex = 0) => { try { const meta = await get(`/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); } catch { /* ignore */ diff --git a/src/workspaces/officerdev/src/apps/Music/shared.ts b/src/workspaces/officerdev/src/apps/Music/shared.ts index 191f93a9..fd062ac9 100644 --- a/src/workspaces/officerdev/src/apps/Music/shared.ts +++ b/src/workspaces/officerdev/src/apps/Music/shared.ts @@ -8,8 +8,33 @@ export type DirEntry = { name: string; type: 'directory' | 'file'; size: number; export type LsResult = { entries: DirEntry[] }; export type ManifestAlbum = { v: string; cover: boolean; tracks: number; disco?: boolean }; export type Manifest = { albums: Record }; -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[] }; + +/** 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 = (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 }; const AUDIO_EXT = new Set(['mp3', 'flac', 'm4a', 'aac', 'ogg', 'opus', 'wav', 'wma', 'mp4']);