mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 11:41:02 +00:00
174 lines
4.8 KiB
TypeScript
174 lines
4.8 KiB
TypeScript
/**
|
|
* 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<boolean> {
|
|
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<void> {
|
|
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<TrackWithoutLyrics[]> {
|
|
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<TrackWithoutLyrics[]> {
|
|
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
|
|
}));
|
|
}
|