/** * Library scanner for tracks without lyrics files */ import { readDir, exists, readFile } from '@tauri-apps/plugin-fs'; import { parseBuffer } from 'music-metadata'; import type { AudioFormat } from '$lib/types/track'; import { upsertTrack, getTracksWithoutLyrics, type DbTrack } from '$lib/library/database'; export interface TrackWithoutLyrics { path: string; filename: string; title: string; artist: string; album: string; duration: number; // in seconds format: AudioFormat; } /** * Check if a track has an accompanying .lrc file */ async function hasLyricsFile(audioFilePath: string): Promise { const lrcPath = audioFilePath.replace(/\.[^.]+$/, '.lrc'); return await exists(lrcPath); } /** * Get audio format from file extension */ function getAudioFormat(filename: string): AudioFormat { const ext = filename.toLowerCase().split('.').pop(); switch (ext) { case 'flac': return 'flac'; case 'mp3': return 'mp3'; case 'opus': return 'opus'; case 'ogg': return 'ogg'; case 'm4a': return 'm4a'; case 'wav': return 'wav'; default: return 'unknown'; } } /** * Scan a single directory for audio files without lyrics */ async function scanDirectoryForMissingLyrics( dirPath: string, results: TrackWithoutLyrics[] ): Promise { const audioExtensions = ['.flac', '.mp3', '.opus', '.ogg', '.m4a', '.wav']; try { const entries = await readDir(dirPath); for (const entry of entries) { const fullPath = `${dirPath}/${entry.name}`; if (entry.isDirectory) { // Recursively scan subdirectories await scanDirectoryForMissingLyrics(fullPath, results); } else { // Check if it's an audio file const hasAudioExt = audioExtensions.some(ext => entry.name.toLowerCase().endsWith(ext) ); if (hasAudioExt) { // Check if it has a .lrc file const hasLyrics = await hasLyricsFile(fullPath); if (!hasLyrics) { // Read metadata try { const fileData = await readFile(fullPath); const metadata = await parseBuffer( fileData, { mimeType: `audio/${getAudioFormat(entry.name)}` }, { duration: true, skipCovers: true } ); const title = metadata.common.title || entry.name.replace(/\.[^.]+$/, ''); const artist = metadata.common.artist || metadata.common.albumartist || 'Unknown Artist'; const album = metadata.common.album || 'Unknown Album'; const duration = metadata.format.duration || 0; // Only add if we have minimum required metadata if (title && artist && album && duration > 0) { results.push({ path: fullPath, filename: entry.name, title, artist, album, duration, format: getAudioFormat(entry.name) }); } } catch (error) { console.warn(`[LyricScanner] Could not read metadata for ${fullPath}:`, error); } } } } } } catch (error) { console.error(`[LyricScanner] Error scanning directory ${dirPath}:`, error); } } /** * Scan the music library for tracks without .lrc files * Results are cached in the database */ export async function scanForTracksWithoutLyrics( musicFolderPath: string, onProgress?: (current: number, total: number, message: string) => void ): Promise { const results: TrackWithoutLyrics[] = []; if (onProgress) { onProgress(0, 0, 'Scanning for tracks without lyrics...'); } await scanDirectoryForMissingLyrics(musicFolderPath, results); // Save results to database if (onProgress) { onProgress(results.length, results.length, 'Caching results...'); } for (const track of results) { await upsertTrack({ path: track.path, title: track.title, artist: track.artist, album: track.album, duration: Math.round(track.duration), format: track.format, has_lyrics: false }); } if (onProgress) { onProgress(results.length, results.length, `Found ${results.length} tracks without lyrics`); } return results; } /** * Load cached tracks without lyrics from database */ export async function loadCachedTracksWithoutLyrics(): Promise { const dbTracks = await getTracksWithoutLyrics(); return dbTracks.map((track: DbTrack) => ({ path: track.path, filename: track.path.split('/').pop() || track.path, title: track.title, artist: track.artist, album: track.album, duration: track.duration, format: track.format as AudioFormat })); }