music indexer: add disc number to each track (from ID3 TPOS)

ffprobe surfaces ID3 TPOS as the `disc` tag ("n" or "n/total"); parse the leading
number → IndexTrack.disc?: number in meta.json (undefined when absent/unparseable),
alongside `track`. Verified on Pink Floyd - The Wall (Disc 1 → 1, Disc 2 → 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 21:49:30 +00:00
co-authored by Claude Opus 4.8
parent 0e711d281c
commit 40080f7f1e
+5 -1
View File
@@ -9,7 +9,7 @@ import { homedir } from 'node:os';
// (relative to the Music root):
//
// DATA_PATH/music/cache/<rel>/meta.json { path, cover?, tracks: [{ file, title, artist, albumArtist,
// album, track, year, durationSec, lyrics?:'lrc'|'txt' }],
// album, track, disc?, year, durationSec, lyrics?:'lrc'|'txt' }],
// videos?: [{ file, title, durationSec, width, height, poster? }],
// images?: [{ file }] } (phone IndexMeta schema)
// DATA_PATH/music/cache/<rel>/cover.jpg compressed (<=600px, jpeg q5)
@@ -89,6 +89,7 @@ export type IndexTrack = {
albumArtist?: string;
album?: string;
track?: string;
disc?: number; // disc / part-of-set number, from ID3 TPOS (the `disc` tag "n" or "n/total")
year?: string;
durationSec?: number;
lyrics?: 'lrc' | 'txt'; // available lyrics format ('lrc' = synced/timestamped); text via /lyrics. Absent = none.
@@ -344,6 +345,8 @@ async function ffprobeTrack(absPath: string, file: string): Promise<{ track: Ind
for (const [k, val] of Object.entries(fmt.tags ?? {})) tags[k.toLowerCase()] = val;
const durNum = fmt.duration ? Math.round(parseFloat(fmt.duration)) : undefined;
const yearMatch = tags.date?.match(/\d{4}/);
// Disc / part-of-set from ID3 TPOS (ffprobe surfaces it as `disc`, "n" or "n/total").
const discNum = tags.disc ? parseInt(tags.disc.split('/')[0]!.trim(), 10) : NaN;
// Embedded lyrics: USLT/foobar keys — `lyrics`, `lyrics-<lang>`, `unsyncedlyrics`.
let lyricsText: string | undefined;
for (const [k, val] of Object.entries(tags)) {
@@ -360,6 +363,7 @@ async function ffprobeTrack(absPath: string, file: string): Promise<{ track: Ind
albumArtist: tags.album_artist || tags.albumartist || undefined,
album: tags.album || undefined,
track: tags.track || undefined,
disc: Number.isFinite(discNum) ? discNum : undefined,
year: yearMatch ? yearMatch[0] : undefined,
durationSec: Number.isFinite(durNum) ? durNum : undefined,
},