mirror of
https://github.com/markuryy/shark.git
synced 2025-12-13 12:01:01 +00:00
feat(services): add LRCLIB service, scan utility, and context menus
This commit is contained in:
137
src/lib/library/lyricScanner.ts
Normal file
137
src/lib/library/lyricScanner.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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
|
||||
*/
|
||||
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);
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(results.length, results.length, `Found ${results.length} tracks without lyrics`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user