feat(dl): add metadata, lyrics, and cover art tagging

Introduce metadata handling for online downloads:
- Embed cover art and lyrics (synced/unsynced) into MP3 files
- Save cover art to album folders and .lrc lyric files as sidecars
- Fetch and parse album/track metadata and lyrics from Deezer API
- Add user settings for artwork and lyrics embedding, LRC export, and cover quality
- Refactor queue manager to run continuously in background
This commit is contained in:
2025-10-02 10:57:27 -04:00
parent d1edc8b7f7
commit 36c0bc7dc7
11 changed files with 568 additions and 15 deletions

View File

@@ -6,6 +6,7 @@
import { settings, loadSettings } from '$lib/stores/settings';
import { scanPlaylists, type Playlist } from '$lib/library/scanner';
import { downloadQueue } from '$lib/stores/downloadQueue';
import { deezerQueueManager } from '$lib/services/deezer/queueManager';
let { children } = $props();
@@ -22,6 +23,9 @@
onMount(async () => {
await loadSettings();
await loadPlaylists();
// Start background queue processor
deezerQueueManager.start();
});
async function loadPlaylists() {

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { downloadQueue, clearCompleted, removeFromQueue, type QueueItem } from '$lib/stores/downloadQueue';
import { deezerQueueManager } from '$lib/services/deezer/queueManager';
let queueItems = $state<QueueItem[]>([]);
@@ -15,9 +14,6 @@
.map(id => state.queue[id])
.filter(item => item !== undefined);
});
// Start queue processor
deezerQueueManager.start();
});
onDestroy(() => {

View File

@@ -7,6 +7,11 @@
setDeezerConcurrency,
setDeezerFormat,
setDeezerOverwrite,
setEmbedCoverArt,
setSaveCoverToFolder,
setEmbedLyrics,
setSaveLrcFile,
setCoverImageQuality,
loadSettings
} from '$lib/stores/settings';
import { clearLibrary as clearLibraryDb } from '$lib/library/database';
@@ -17,6 +22,11 @@
let currentDeezerConcurrency = $state<number>(1);
let currentDeezerFormat = $state<'FLAC' | 'MP3_320' | 'MP3_128'>('FLAC');
let currentDeezerOverwrite = $state<boolean>(false);
let currentEmbedCoverArt = $state<boolean>(true);
let currentSaveCoverToFolder = $state<boolean>(true);
let currentEmbedLyrics = $state<boolean>(true);
let currentSaveLrcFile = $state<boolean>(true);
let currentCoverImageQuality = $state<number>(90);
let activeTab = $state<'library' | 'deezer' | 'advanced'>('library');
onMount(async () => {
@@ -26,6 +36,11 @@
currentDeezerConcurrency = $settings.deezerConcurrency;
currentDeezerFormat = $settings.deezerFormat;
currentDeezerOverwrite = $settings.deezerOverwrite;
currentEmbedCoverArt = $settings.embedCoverArt;
currentSaveCoverToFolder = $settings.saveCoverToFolder;
currentEmbedLyrics = $settings.embedLyrics;
currentSaveLrcFile = $settings.saveLrcFile;
currentCoverImageQuality = $settings.coverImageQuality;
});
$effect(() => {
@@ -34,6 +49,11 @@
currentDeezerConcurrency = $settings.deezerConcurrency;
currentDeezerFormat = $settings.deezerFormat;
currentDeezerOverwrite = $settings.deezerOverwrite;
currentEmbedCoverArt = $settings.embedCoverArt;
currentSaveCoverToFolder = $settings.saveCoverToFolder;
currentEmbedLyrics = $settings.embedLyrics;
currentSaveLrcFile = $settings.saveLrcFile;
currentCoverImageQuality = $settings.coverImageQuality;
});
async function selectMusicFolder() {
@@ -196,6 +216,66 @@
<label for="deezer-overwrite">Overwrite existing files</label>
</div>
</div>
<fieldset>
<legend>Metadata & Artwork</legend>
<div class="field-row">
<input
id="embed-cover"
type="checkbox"
bind:checked={currentEmbedCoverArt}
onchange={() => setEmbedCoverArt(currentEmbedCoverArt)}
/>
<label for="embed-cover">Embed cover art in files</label>
</div>
<div class="field-row">
<input
id="save-cover"
type="checkbox"
bind:checked={currentSaveCoverToFolder}
onchange={() => setSaveCoverToFolder(currentSaveCoverToFolder)}
/>
<label for="save-cover">Save cover art to album folder</label>
</div>
<div class="field-row">
<input
id="embed-lyrics"
type="checkbox"
bind:checked={currentEmbedLyrics}
onchange={() => setEmbedLyrics(currentEmbedLyrics)}
/>
<label for="embed-lyrics">Embed lyrics in files</label>
</div>
<div class="field-row">
<input
id="save-lrc"
type="checkbox"
bind:checked={currentSaveLrcFile}
onchange={() => setSaveLrcFile(currentSaveLrcFile)}
/>
<label for="save-lrc">Save .lrc lyric files (for Rockbox/FLAC)</label>
</div>
<div class="field-row-stacked">
<label for="cover-quality">Cover Image Quality</label>
<div class="slider-container">
<input
id="cover-quality"
type="range"
min="60"
max="100"
bind:value={currentCoverImageQuality}
onchange={() => setCoverImageQuality(currentCoverImageQuality)}
/>
<span class="slider-value">{currentCoverImageQuality}%</span>
</div>
<small class="help-text">JPEG quality for cover images (default: 90%)</small>
</div>
</fieldset>
</section>
{:else if activeTab === 'advanced'}
<section class="tab-content">