import { MUSIC_CARD_EMPTY_ARTIST, MUSIC_CARD_EMPTY_TITLE } from './MusicCardConstants' const DEFAULT_TIME_TEXT = '00:00' const LYRIC_TIMESTAMP_PATTERN = /\[(\d{2}):(\d{2})(?:\.(\d{2,3}))?\]/g const isNonEmpty = (value?: string): boolean => { return !!value && value.trim() !== '' } export interface MusicCardSnapshot { title: string artist: string coverPath: string hasCoverImage: boolean hasSong: boolean isPlaying: boolean currentPositionMs: number durationMs: number currentTimeText: string durationTimeText: string lyricLine1: string lyricLine2: string hasLyric: boolean filePath: string updatedAtMs: number lyricText: string } export interface MusicCardLyricLines { line1: string line2: string hasLyric: boolean } export class MusicCardBindingData { title: string = MUSIC_CARD_EMPTY_TITLE artist: string = MUSIC_CARD_EMPTY_ARTIST coverPath: string = '' hasCoverImage: boolean = false hasSong: boolean = false isPlaying: boolean = false currentPositionMs: number = 0 durationMs: number = 0 currentTimeText: string = DEFAULT_TIME_TEXT durationTimeText: string = DEFAULT_TIME_TEXT lyricLine1: string = MUSIC_CARD_EMPTY_TITLE lyricLine2: string = MUSIC_CARD_EMPTY_ARTIST hasLyric: boolean = false filePath: string = '' updatedAtMs: number = 0 } export function createEmptyMusicCardSnapshot(): MusicCardSnapshot { return { title: '', artist: '', coverPath: '', hasCoverImage: false, hasSong: false, isPlaying: false, currentPositionMs: 0, durationMs: 0, currentTimeText: DEFAULT_TIME_TEXT, durationTimeText: DEFAULT_TIME_TEXT, lyricLine1: MUSIC_CARD_EMPTY_TITLE, lyricLine2: MUSIC_CARD_EMPTY_ARTIST, hasLyric: false, filePath: '', updatedAtMs: 0, lyricText: '' } } export function resolveMusicCardLyricLines( lyricText: string, positionMs: number ): MusicCardLyricLines { const trimmedText = lyricText ? lyricText.trim() : '' if (trimmedText === '') { return { line1: '', line2: '', hasLyric: false } } const entries: Array<{ timeMs: number; text: string }> = [] const rawLines = trimmedText.split(/\r?\n/) for (let i = 0; i < rawLines.length; i++) { const rawLine = rawLines[i].trim() if (rawLine === '') { continue } const timestamps: Array<{ timeMs: number }> = [] let lastMatchEnd = 0 LYRIC_TIMESTAMP_PATTERN.lastIndex = 0 let match = LYRIC_TIMESTAMP_PATTERN.exec(rawLine) while (match) { const minutes = parseInt(match[1], 10) const seconds = parseInt(match[2], 10) const msPart = match[3] let msValue = 0 if (msPart) { msValue = msPart.length === 2 ? parseInt(msPart, 10) * 10 : parseInt(msPart, 10) } const timeMs = minutes * 60 * 1000 + seconds * 1000 + msValue timestamps.push({ timeMs }) lastMatchEnd = LYRIC_TIMESTAMP_PATTERN.lastIndex match = LYRIC_TIMESTAMP_PATTERN.exec(rawLine) } if (timestamps.length === 0) { continue } const text = rawLine.slice(lastMatchEnd).trim() if (text === '') { continue } for (let j = 0; j < timestamps.length; j++) { entries.push({ timeMs: timestamps[j].timeMs, text }) } } if (entries.length === 0) { return { line1: '', line2: '', hasLyric: false } } entries.sort((lhs, rhs) => lhs.timeMs - rhs.timeMs) let currentIndex = -1 for (let i = 0; i < entries.length; i++) { if (entries[i].timeMs <= positionMs) { currentIndex = i } else { break } } if (currentIndex === -1) { currentIndex = 0 } const currentLine = entries[currentIndex] const nextLine = entries[currentIndex + 1] return { line1: currentLine.text, line2: nextLine ? nextLine.text : '', hasLyric: true } } export function buildMusicCardBindingData(snapshot: MusicCardSnapshot): MusicCardBindingData { const source = snapshot || createEmptyMusicCardSnapshot() const lyricLines = resolveMusicCardLyricLines(source.lyricText, source.currentPositionMs) const data = new MusicCardBindingData() data.hasSong = Boolean(source.hasSong) data.title = data.hasSong && isNonEmpty(source.title) ? source.title : MUSIC_CARD_EMPTY_TITLE data.artist = data.hasSong && isNonEmpty(source.artist) ? source.artist : MUSIC_CARD_EMPTY_ARTIST data.coverPath = source.coverPath ?? '' data.hasCoverImage = Boolean(source.hasCoverImage) data.isPlaying = Boolean(source.isPlaying) data.currentPositionMs = source.currentPositionMs ?? 0 data.durationMs = source.durationMs ?? 0 data.currentTimeText = isNonEmpty(source.currentTimeText) ? source.currentTimeText : DEFAULT_TIME_TEXT data.durationTimeText = isNonEmpty(source.durationTimeText) ? source.durationTimeText : DEFAULT_TIME_TEXT data.filePath = source.filePath ?? '' data.updatedAtMs = source.updatedAtMs ?? 0 let lyricLine1 = data.title let lyricLine2 = data.artist let hasLyric = false if (lyricLines.hasLyric) { lyricLine1 = lyricLines.line1 lyricLine2 = lyricLines.line2 hasLyric = true } else if ( source.hasLyric && (isNonEmpty(source.lyricLine1) || isNonEmpty(source.lyricLine2)) ) { lyricLine1 = isNonEmpty(source.lyricLine1) ? source.lyricLine1 : data.title lyricLine2 = isNonEmpty(source.lyricLine2) ? source.lyricLine2 : data.artist hasLyric = true } data.lyricLine1 = lyricLine1 data.lyricLine2 = lyricLine2 data.hasLyric = hasLyric return data }