From 0d7361db4bebf3e1dcdef620436958cad31f0d67 Mon Sep 17 00:00:00 2001 From: Markury Date: Thu, 2 Oct 2025 11:39:56 -0400 Subject: [PATCH] refactor: migrate audio metadata tagging from ts to rust - Add id3 and metaflac crates for native audio tagging - Create tagger.rs with separate tag_mp3() and tag_flac() functions - Implement tag_audio_file Tauri command for unified tagging interface - Support full metadata: title, artist, album, track#, ISRC, BPM, lyrics, cover art - Create TypeScript wrapper (tagger.ts) for calling Rust backend - Update downloader.ts to use Rust tagging for both MP3 and FLAC - Remove browser-id3-writer dependency (no browser FLAC support) - Inline lyrics parsing in addToQueue.ts (no longer needed in tagger) --- bun.lock | 3 - package.json | 1 - src-tauri/Cargo.lock | 23 ++ src-tauri/Cargo.toml | 2 + src-tauri/src/lib.rs | 20 +- src-tauri/src/tagger.rs | 328 ++++++++++++++++++++++++++ src/lib/services/deezer/addToQueue.ts | 17 +- src/lib/services/deezer/downloader.ts | 36 +-- src/lib/services/deezer/tagger.ts | 277 +++++++--------------- 9 files changed, 492 insertions(+), 215 deletions(-) create mode 100644 src-tauri/src/tagger.rs diff --git a/bun.lock b/bun.lock index 0951ea6..f9d0857 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,6 @@ "@tauri-apps/plugin-sql": "^2.3.0", "@tauri-apps/plugin-store": "~2", "blowfish-node": "^1.1.4", - "browser-id3-writer": "^6.3.1", "music-metadata": "^11.9.0", "uuid": "^13.0.0", }, @@ -208,8 +207,6 @@ "blowfish-node": ["blowfish-node@1.1.4", "", {}, "sha512-Iahpxc/cutT0M0tgwV5goklB+EzDuiYLgwJg050AmUG2jSIOpViWMLdnRgBxzZuNfswAgHSUiIdvmNdgL2v6DA=="], - "browser-id3-writer": ["browser-id3-writer@6.3.1", "", {}, "sha512-sRA4Uq9Q3NsmXiVpLvIDxzomtgCdbw6SY85A6fw7dUQGRVoOBg1/buFv6spPhYiSo6FlVtN5OJQTvvhbmfx9rQ=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], diff --git a/package.json b/package.json index d8cc4d6..2941ebe 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@tauri-apps/plugin-sql": "^2.3.0", "@tauri-apps/plugin-store": "~2", "blowfish-node": "^1.1.4", - "browser-id3-writer": "^6.3.1", "music-metadata": "^11.9.0", "uuid": "^13.0.0" }, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9d36c3e..35cd80f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6,6 +6,8 @@ version = 4 name = "Shark" version = "0.1.0" dependencies = [ + "id3", + "metaflac", "serde", "serde_json", "tauri", @@ -1964,6 +1966,17 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id3" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aadb14a5ba1a0d58ecd4a29bfc9b8f1d119eee24aa01a62c1ec93eb9630a1d86" +dependencies = [ + "bitflags 2.9.4", + "byteorder", + "flate2", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2354,6 +2367,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metaflac" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf25a3451319c52a4a56d956475fbbb763bfb8420e2187d802485cb0fd8d965" +dependencies = [ + "byteorder", + "hex", +] + [[package]] name = "mime" version = "0.3.17" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 951edcc..be1a07b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,4 +27,6 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-http = { version = "2", features = ["unsafe-headers"] } tauri-plugin-sql = { version = "2", features = ["sqlite"] } +id3 = "1.16.3" +metaflac = "0.2.8" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c6eabc8..de76242 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,11 +1,29 @@ use tauri_plugin_sql::{Migration, MigrationKind}; +mod tagger; + // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } +/// Tag an audio file with metadata, cover art, and lyrics +#[tauri::command] +fn tag_audio_file( + path: String, + metadata: tagger::TaggingMetadata, + cover_data: Option>, + embed_lyrics: bool, +) -> Result<(), String> { + tagger::tag_audio_file( + &path, + &metadata, + cover_data.as_deref(), + embed_lyrics, + ) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let migrations = vec![ @@ -58,7 +76,7 @@ pub fn run() { .plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) - .invoke_handler(tauri::generate_handler![greet]) + .invoke_handler(tauri::generate_handler![greet, tag_audio_file]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/tagger.rs b/src-tauri/src/tagger.rs new file mode 100644 index 0000000..9912d8d --- /dev/null +++ b/src-tauri/src/tagger.rs @@ -0,0 +1,328 @@ +use id3::{Tag as ID3Tag, TagLike, Version, frame::{Picture, PictureType}}; +use metaflac::Tag as FlacTag; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +/// Metadata structure for audio file tagging +/// Matches DeezerTrack interface from TypeScript +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaggingMetadata { + pub title: Option, + pub artist: Option, + pub artists: Option>, + pub album: Option, + pub album_artist: Option, + pub track_number: Option, + pub disc_number: Option, + pub year: Option, + pub genre: Option>, + pub duration: Option, // in seconds + pub bpm: Option, + pub isrc: Option, + pub label: Option, + pub barcode: Option, + pub copyright: Option, + pub explicit: Option, + pub replay_gain: Option, + pub source_id: Option, + pub lyrics_unsync: Option, +} + +/// Tag an audio file (MP3 or FLAC) with metadata +pub fn tag_audio_file( + path: &str, + metadata: &TaggingMetadata, + cover_data: Option<&[u8]>, + embed_lyrics: bool, +) -> Result<(), String> { + let path_obj = Path::new(path); + + // Determine file type by extension + let extension = path_obj + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.to_lowercase()) + .ok_or_else(|| "File has no extension".to_string())?; + + match extension.as_str() { + "mp3" => tag_mp3(path, metadata, cover_data, embed_lyrics), + "flac" => tag_flac(path, metadata, cover_data, embed_lyrics), + _ => Err(format!("Unsupported file format: {}", extension)), + } +} + +/// Tag MP3 file using id3 crate +fn tag_mp3( + path: &str, + metadata: &TaggingMetadata, + cover_data: Option<&[u8]>, + embed_lyrics: bool, +) -> Result<(), String> { + // Read or create tag + let mut tag = ID3Tag::read_from_path(path) + .unwrap_or_else(|_| ID3Tag::new()); + + // Basic metadata + if let Some(ref title) = metadata.title { + tag.set_title(title); + } + + if let Some(ref artists) = metadata.artists { + if !artists.is_empty() { + tag.set_artist(artists.join(", ")); + } + } else if let Some(ref artist) = metadata.artist { + tag.set_artist(artist); + } + + if let Some(ref album) = metadata.album { + tag.set_album(album); + } + + if let Some(ref album_artist) = metadata.album_artist { + tag.set_album_artist(album_artist); + } + + // Track and disc numbers + if let Some(track_number) = metadata.track_number { + tag.set_track(track_number); + } + + if let Some(disc_number) = metadata.disc_number { + tag.set_disc(disc_number); + } + + // Year + if let Some(year) = metadata.year { + tag.set_year(year); + } + + // Genre + if let Some(ref genres) = metadata.genre { + if let Some(first_genre) = genres.first() { + tag.set_genre(first_genre); + } + } + + // Duration (milliseconds) + if let Some(duration) = metadata.duration { + tag.set_duration(duration * 1000); + } + + // Additional metadata as text frames + if let Some(ref isrc) = metadata.isrc { + tag.add_frame(id3::frame::Frame::text("TSRC", isrc.clone())); + } + + if let Some(ref label) = metadata.label { + tag.add_frame(id3::frame::Frame::text("TPUB", label.clone())); + } + + if let Some(bpm) = metadata.bpm { + tag.add_frame(id3::frame::Frame::text("TBPM", bpm.to_string())); + } + + if let Some(ref copyright) = metadata.copyright { + tag.add_frame(id3::frame::Frame::text("TCOP", copyright.clone())); + } + + // Custom text frames (TXXX) + if let Some(ref barcode) = metadata.barcode { + use id3::frame::{ExtendedText, Content}; + let ext_text = ExtendedText { + description: "BARCODE".to_string(), + value: barcode.clone(), + }; + tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(ext_text))); + } + + if let Some(explicit) = metadata.explicit { + use id3::frame::{ExtendedText, Content}; + let ext_text = ExtendedText { + description: "ITUNESADVISORY".to_string(), + value: if explicit { "1" } else { "0" }.to_string(), + }; + tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(ext_text))); + } + + if let Some(ref replay_gain) = metadata.replay_gain { + use id3::frame::{ExtendedText, Content}; + let ext_text = ExtendedText { + description: "REPLAYGAIN_TRACK_GAIN".to_string(), + value: replay_gain.clone(), + }; + tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(ext_text))); + } + + if let Some(ref source_id) = metadata.source_id { + use id3::frame::{ExtendedText, Content}; + let source_text = ExtendedText { + description: "SOURCE".to_string(), + value: "Deezer".to_string(), + }; + tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(source_text))); + + let sourceid_text = ExtendedText { + description: "SOURCEID".to_string(), + value: source_id.clone(), + }; + tag.add_frame(id3::frame::Frame::with_content("TXXX", Content::ExtendedText(sourceid_text))); + } + + // Lyrics (USLT frame) + if embed_lyrics { + if let Some(ref lyrics) = metadata.lyrics_unsync { + use id3::frame::{Lyrics, Content}; + let lyrics_frame = Lyrics { + lang: "eng".to_string(), + description: String::new(), + text: lyrics.clone(), + }; + tag.add_frame(id3::frame::Frame::with_content("USLT", Content::Lyrics(lyrics_frame))); + } + } + + // Cover art (APIC frame) + if let Some(cover_bytes) = cover_data { + if !cover_bytes.is_empty() { + let mime_type = detect_mime_type_str(cover_bytes); + let picture = Picture { + mime_type: mime_type.to_string(), + picture_type: PictureType::CoverFront, + description: "Cover".to_string(), + data: cover_bytes.to_vec(), + }; + tag.add_frame(id3::frame::Frame::with_content("APIC", id3::frame::Content::Picture(picture))); + } + } + + // Write tag + tag.write_to_path(path, Version::Id3v24) + .map_err(|e| format!("Failed to write MP3 tags: {}", e)) +} + +/// Tag FLAC file using metaflac crate +fn tag_flac( + path: &str, + metadata: &TaggingMetadata, + cover_data: Option<&[u8]>, + embed_lyrics: bool, +) -> Result<(), String> { + let mut tag = FlacTag::read_from_path(path) + .map_err(|e| format!("Failed to read FLAC file: {}", e))?; + + // Remove all existing vorbis comments to start fresh + let vorbis = tag.vorbis_comments_mut(); + vorbis.comments.clear(); + + // Basic metadata + if let Some(ref title) = metadata.title { + tag.set_vorbis("TITLE", vec![title.clone()]); + } + + if let Some(ref artists) = metadata.artists { + if !artists.is_empty() { + // FLAC supports multiple ARTIST tags + tag.set_vorbis("ARTIST", artists.clone()); + } + } else if let Some(ref artist) = metadata.artist { + tag.set_vorbis("ARTIST", vec![artist.clone()]); + } + + if let Some(ref album) = metadata.album { + tag.set_vorbis("ALBUM", vec![album.clone()]); + } + + if let Some(ref album_artist) = metadata.album_artist { + tag.set_vorbis("ALBUMARTIST", vec![album_artist.clone()]); + } + + // Track and disc numbers + if let Some(track_number) = metadata.track_number { + tag.set_vorbis("TRACKNUMBER", vec![track_number.to_string()]); + } + + if let Some(disc_number) = metadata.disc_number { + tag.set_vorbis("DISCNUMBER", vec![disc_number.to_string()]); + } + + // Year/Date + if let Some(year) = metadata.year { + tag.set_vorbis("DATE", vec![year.to_string()]); + } + + // Genre + if let Some(ref genres) = metadata.genre { + tag.set_vorbis("GENRE", genres.clone()); + } + + // Additional metadata + if let Some(ref isrc) = metadata.isrc { + tag.set_vorbis("ISRC", vec![isrc.clone()]); + } + + if let Some(ref label) = metadata.label { + tag.set_vorbis("PUBLISHER", vec![label.clone()]); + } + + if let Some(bpm) = metadata.bpm { + tag.set_vorbis("BPM", vec![bpm.to_string()]); + } + + if let Some(ref copyright) = metadata.copyright { + tag.set_vorbis("COPYRIGHT", vec![copyright.clone()]); + } + + if let Some(ref barcode) = metadata.barcode { + tag.set_vorbis("BARCODE", vec![barcode.clone()]); + } + + if let Some(explicit) = metadata.explicit { + tag.set_vorbis("ITUNESADVISORY", vec![if explicit { "1" } else { "0" }.to_string()]); + } + + if let Some(ref replay_gain) = metadata.replay_gain { + tag.set_vorbis("REPLAYGAIN_TRACK_GAIN", vec![replay_gain.clone()]); + } + + if let Some(ref source_id) = metadata.source_id { + tag.set_vorbis("SOURCE", vec!["Deezer".to_string()]); + tag.set_vorbis("SOURCEID", vec![source_id.clone()]); + } + + // Lyrics + if embed_lyrics { + if let Some(ref lyrics) = metadata.lyrics_unsync { + tag.set_vorbis("LYRICS", vec![lyrics.clone()]); + } + } + + // Cover art + if let Some(cover_bytes) = cover_data { + if !cover_bytes.is_empty() { + let mime_type = detect_mime_type_str(cover_bytes); + tag.remove_picture_type(metaflac::block::PictureType::CoverFront); + tag.add_picture(mime_type, metaflac::block::PictureType::CoverFront, cover_bytes.to_vec()); + } + } + + // Write tag + tag.save() + .map_err(|e| format!("Failed to write FLAC tags: {}", e)) +} + +/// Detect MIME type from image data header +fn detect_mime_type_str(data: &[u8]) -> &'static str { + if data.len() < 4 { + return "image/jpeg"; + } + + if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF { + "image/jpeg" + } else if data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 { + "image/png" + } else { + "image/jpeg" + } +} diff --git a/src/lib/services/deezer/addToQueue.ts b/src/lib/services/deezer/addToQueue.ts index fabb12c..ab1bad4 100644 --- a/src/lib/services/deezer/addToQueue.ts +++ b/src/lib/services/deezer/addToQueue.ts @@ -5,7 +5,6 @@ import { deezerAPI } from '$lib/services/deezer'; import { addToQueue } from '$lib/stores/downloadQueue'; -import { parseLyricsToLRC, parseLyricsToSYLT, parseLyricsText } from './tagger'; /** * Fetch track metadata and add to download queue @@ -39,10 +38,20 @@ export async function addDeezerTrackToQueue(trackId: string): Promise { // Parse lyrics if available let lyrics = undefined; if (lyricsData) { + // Parse LRC format (synced lyrics) + let syncLrc = ''; + if (lyricsData.LYRICS_SYNC_JSON) { + for (const line of lyricsData.LYRICS_SYNC_JSON) { + const text = line.line || ''; + const timestamp = line.lrc_timestamp || '[00:00.00]'; + syncLrc += `${timestamp}${text}\n`; + } + } + lyrics = { - sync: parseLyricsToLRC(lyricsData), - unsync: parseLyricsText(lyricsData), - syncID3: parseLyricsToSYLT(lyricsData) + sync: syncLrc || undefined, + unsync: lyricsData.LYRICS_TEXT || undefined, + syncID3: undefined // No longer needed, handled by Rust tagger }; } diff --git a/src/lib/services/deezer/downloader.ts b/src/lib/services/deezer/downloader.ts index 1cb29f2..74e93dc 100644 --- a/src/lib/services/deezer/downloader.ts +++ b/src/lib/services/deezer/downloader.ts @@ -6,7 +6,7 @@ import { fetch } from '@tauri-apps/plugin-http'; import { writeFile, mkdir, remove, rename, exists } from '@tauri-apps/plugin-fs'; import { generateBlowfishKey, decryptChunk } from './crypto'; import { generateTrackPath } from './paths'; -import { tagMP3 } from './tagger'; +import { tagAudioFile } from './tagger'; import { downloadCover, saveCoverToAlbumFolder } from './imageDownload'; import { settings } from '$lib/stores/settings'; import { get } from 'svelte/store'; @@ -106,22 +106,9 @@ export async function downloadTrack( } } - // Apply tags (currently MP3 only) - let finalData = decryptedData; - if (format === 'MP3_320' || format === 'MP3_128') { - console.log('Tagging MP3 file...'); - finalData = await tagMP3( - decryptedData, - track, - appSettings.embedCoverArt ? coverData : undefined, - appSettings.embedLyrics - ); - } - // TODO: Add FLAC tagging when library is ready - - // Write tagged file to temp - console.log('Writing tagged file to temp...'); - await writeFile(paths.tempPath, finalData); + // Write untagged file to temp first + console.log('Writing untagged file to temp...'); + await writeFile(paths.tempPath, decryptedData); // Move to final location const finalPath = `${paths.filepath}/${paths.filename}`; @@ -135,6 +122,21 @@ export async function downloadTrack( await rename(paths.tempPath, finalPath); + // Apply tags (works for both MP3 and FLAC) + console.log('Tagging audio file...'); + try { + await tagAudioFile( + finalPath, + track, + appSettings.embedCoverArt ? coverData : undefined, + appSettings.embedLyrics + ); + console.log('Tagging complete!'); + } catch (error) { + console.error('Failed to tag audio file:', error); + // Non-fatal error - file is still downloaded, just not tagged + } + // Save LRC sidecar file if enabled if (appSettings.saveLrcFile && track.lyrics?.sync) { try { diff --git a/src/lib/services/deezer/tagger.ts b/src/lib/services/deezer/tagger.ts index 5b3bd3f..aec86a8 100644 --- a/src/lib/services/deezer/tagger.ts +++ b/src/lib/services/deezer/tagger.ts @@ -1,207 +1,106 @@ /** - * Audio file tagging module - * Embeds metadata, lyrics, and cover art into audio files + * Audio file tagging interface + * Handles metadata tagging for MP3 and FLAC files via Rust backend */ -import { ID3Writer } from 'browser-id3-writer'; +import { invoke } from '@tauri-apps/api/core'; import type { DeezerTrack } from '$lib/types/deezer'; /** - * Tag MP3 file with metadata, lyrics, and cover art - * @param audioData - Decrypted audio data + * Metadata structure that matches Rust TaggingMetadata + */ +export interface TaggingMetadata { + title?: string; + artist?: string; + artists?: string[]; + album?: string; + albumArtist?: string; + trackNumber?: number; + discNumber?: number; + year?: number; + genre?: string[]; + duration?: number; + bpm?: number; + isrc?: string; + label?: string; + barcode?: string; + copyright?: string; + explicit?: boolean; + replayGain?: string; + sourceId?: string; + lyricsUnsync?: string; +} + +/** + * Convert DeezerTrack to TaggingMetadata for Rust + */ +export function convertToTaggingMetadata(track: DeezerTrack): TaggingMetadata { + const metadata: TaggingMetadata = { + title: track.title, + artist: track.artist, + artists: track.artists, + album: track.album, + albumArtist: track.albumArtist, + trackNumber: track.trackNumber, + discNumber: track.discNumber, + isrc: track.isrc, + bpm: track.bpm, + copyright: track.copyright, + explicit: track.explicit, + replayGain: track.replayGain, + label: track.label, + barcode: track.barcode, + sourceId: track.id.toString(), + duration: track.duration, + }; + + // Extract year from releaseDate (format: YYYY-MM-DD) + if (track.releaseDate) { + const year = parseInt(track.releaseDate.split('-')[0]); + if (!isNaN(year)) { + metadata.year = year; + } + } + + // Genre + if (track.genre && track.genre.length > 0) { + metadata.genre = track.genre; + } + + // Lyrics (unsynced text) + if (track.lyrics?.unsync) { + metadata.lyricsUnsync = track.lyrics.unsync; + } + + return metadata; +} + +/** + * Tag an audio file with metadata, cover art, and lyrics + * @param filePath - Path to the audio file * @param track - Track metadata * @param coverData - Optional cover art image data * @param embedLyrics - Whether to embed lyrics - * @returns Tagged audio data as Uint8Array */ -export async function tagMP3( - audioData: Uint8Array, +export async function tagAudioFile( + filePath: string, track: DeezerTrack, coverData?: Uint8Array, embedLyrics: boolean = true -): Promise { - const writer = new ID3Writer(audioData.buffer); +): Promise { + const metadata = convertToTaggingMetadata(track); - // Basic tags - if (track.title) { - writer.setFrame('TIT2', track.title); - } + // Convert Uint8Array to regular array for JSON serialization + const coverArray = coverData ? Array.from(coverData) : undefined; - if (track.artists && track.artists.length > 0) { - writer.setFrame('TPE1', track.artists); - } - - if (track.album) { - writer.setFrame('TALB', track.album); - } - - if (track.albumArtist) { - writer.setFrame('TPE2', track.albumArtist); - } - - // Track and disc numbers - if (track.trackNumber) { - writer.setFrame('TRCK', track.trackNumber.toString()); - } - - if (track.discNumber) { - writer.setFrame('TPOS', track.discNumber.toString()); - } - - // Additional metadata - if (track.genre && track.genre.length > 0) { - writer.setFrame('TCON', track.genre); - } - - if (track.releaseDate) { - const year = track.releaseDate.split('-')[0]; - if (year) { - writer.setFrame('TYER', parseInt(year)); - } - } - - if (track.duration) { - writer.setFrame('TLEN', track.duration * 1000); - } - - if (track.bpm) { - writer.setFrame('TBPM', track.bpm); - } - - if (track.label) { - writer.setFrame('TPUB', track.label); - } - - if (track.isrc) { - writer.setFrame('TSRC', track.isrc); - } - - if (track.barcode) { - writer.setFrame('TXXX', { - description: 'BARCODE', - value: track.barcode + try { + await invoke('tag_audio_file', { + path: filePath, + metadata, + coverData: coverArray, + embedLyrics, }); + } catch (error) { + throw new Error(`Failed to tag audio file: ${error}`); } - - if (track.explicit !== undefined) { - writer.setFrame('TXXX', { - description: 'ITUNESADVISORY', - value: track.explicit ? '1' : '0' - }); - } - - if (track.replayGain) { - writer.setFrame('TXXX', { - description: 'REPLAYGAIN_TRACK_GAIN', - value: track.replayGain - }); - } - - if (track.copyright) { - writer.setFrame('TCOP', track.copyright); - } - - // Source tags - writer.setFrame('TXXX', { - description: 'SOURCE', - value: 'Deezer' - }); - - writer.setFrame('TXXX', { - description: 'SOURCEID', - value: track.id.toString() - }); - - // Lyrics - if (embedLyrics && track.lyrics) { - // Unsynced lyrics (USLT frame) - if (track.lyrics.unsync) { - writer.setFrame('USLT', { - description: '', - lyrics: track.lyrics.unsync, - language: 'eng' - }); - } - - // Synced lyrics (SYLT frame) - if (track.lyrics.syncID3 && track.lyrics.syncID3.length > 0) { - writer.setFrame('SYLT', { - type: 1, - text: track.lyrics.syncID3, - timestampFormat: 2 - }); - } - } - - // Cover art (APIC frame) - if (coverData && coverData.length > 0) { - writer.setFrame('APIC', { - type: 3, - data: coverData.buffer, - description: 'cover' - }); - } - - const taggedBuffer = writer.addTag(); - return new Uint8Array(taggedBuffer); -} - -/** - * Parse Deezer lyrics to LRC format - * @param lyricsData - Lyrics data from Deezer API - * @returns LRC formatted string - */ -export function parseLyricsToLRC(lyricsData: any): string { - if (!lyricsData || !lyricsData.LYRICS_SYNC_JSON) { - return ''; - } - - const syncLyricsJson = lyricsData.LYRICS_SYNC_JSON; - let lrc = ''; - - for (const line of syncLyricsJson) { - const text = line.line || ''; - const timestamp = line.lrc_timestamp || '[00:00.00]'; - lrc += `${timestamp}${text}\n`; - } - - return lrc; -} - -/** - * Parse Deezer lyrics to ID3 SYLT format - * @param lyricsData - Lyrics data from Deezer API - * @returns Array of [text, milliseconds] tuples - */ -export function parseLyricsToSYLT(lyricsData: any): Array<[string, number]> { - if (!lyricsData || !lyricsData.LYRICS_SYNC_JSON) { - return []; - } - - const syncLyricsJson = lyricsData.LYRICS_SYNC_JSON; - const sylt: Array<[string, number]> = []; - - for (const line of syncLyricsJson) { - const text = line.line || ''; - const milliseconds = parseInt(line.milliseconds || '0'); - - if (text || milliseconds > 0) { - sylt.push([text, milliseconds]); - } - } - - return sylt; -} - -/** - * Get plain text lyrics - * @param lyricsData - Lyrics data from Deezer API - * @returns Plain text lyrics - */ -export function parseLyricsText(lyricsData: any): string { - if (!lyricsData) { - return ''; - } - - return lyricsData.LYRICS_TEXT || ''; }