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:
@@ -9,7 +9,7 @@ import { homedir } from 'node:os';
|
|||||||
// (relative to the Music root):
|
// (relative to the Music root):
|
||||||
//
|
//
|
||||||
// DATA_PATH/music/cache/<rel>/meta.json { path, cover?, tracks: [{ file, title, artist, albumArtist,
|
// 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? }],
|
// videos?: [{ file, title, durationSec, width, height, poster? }],
|
||||||
// images?: [{ file }] } (phone IndexMeta schema)
|
// images?: [{ file }] } (phone IndexMeta schema)
|
||||||
// DATA_PATH/music/cache/<rel>/cover.jpg compressed (<=600px, jpeg q5)
|
// DATA_PATH/music/cache/<rel>/cover.jpg compressed (<=600px, jpeg q5)
|
||||||
@@ -89,6 +89,7 @@ export type IndexTrack = {
|
|||||||
albumArtist?: string;
|
albumArtist?: string;
|
||||||
album?: string;
|
album?: string;
|
||||||
track?: string;
|
track?: string;
|
||||||
|
disc?: number; // disc / part-of-set number, from ID3 TPOS (the `disc` tag "n" or "n/total")
|
||||||
year?: string;
|
year?: string;
|
||||||
durationSec?: number;
|
durationSec?: number;
|
||||||
lyrics?: 'lrc' | 'txt'; // available lyrics format ('lrc' = synced/timestamped); text via /lyrics. Absent = none.
|
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;
|
for (const [k, val] of Object.entries(fmt.tags ?? {})) tags[k.toLowerCase()] = val;
|
||||||
const durNum = fmt.duration ? Math.round(parseFloat(fmt.duration)) : undefined;
|
const durNum = fmt.duration ? Math.round(parseFloat(fmt.duration)) : undefined;
|
||||||
const yearMatch = tags.date?.match(/\d{4}/);
|
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`.
|
// Embedded lyrics: USLT/foobar keys — `lyrics`, `lyrics-<lang>`, `unsyncedlyrics`.
|
||||||
let lyricsText: string | undefined;
|
let lyricsText: string | undefined;
|
||||||
for (const [k, val] of Object.entries(tags)) {
|
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,
|
albumArtist: tags.album_artist || tags.albumartist || undefined,
|
||||||
album: tags.album || undefined,
|
album: tags.album || undefined,
|
||||||
track: tags.track || undefined,
|
track: tags.track || undefined,
|
||||||
|
disc: Number.isFinite(discNum) ? discNum : undefined,
|
||||||
year: yearMatch ? yearMatch[0] : undefined,
|
year: yearMatch ? yearMatch[0] : undefined,
|
||||||
durationSec: Number.isFinite(durNum) ? durNum : undefined,
|
durationSec: Number.isFinite(durNum) ? durNum : undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user