Files
shark/src-tauri/src/tagger.rs
Markury d774aba0d4 feat(dz): add cache clearing and database reset functionality
Add ability to fully clear cached online library by deleting and recreating the database file.
Integrate new Clear Cache option in settings UI, which restarts the app after clearing.
Remove unused artist/album fields from cache and UI. Add process plugin for relaunch.
2025-10-02 13:40:13 -04:00

359 lines
10 KiB
Rust

use id3::{
frame::{Picture, PictureType},
Tag as ID3Tag, TagLike, Version,
};
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<String>,
pub artist: Option<String>,
pub artists: Option<Vec<String>>,
pub album: Option<String>,
pub album_artist: Option<String>,
pub track_number: Option<u32>,
pub disc_number: Option<u32>,
pub year: Option<i32>,
pub genre: Option<Vec<String>>,
pub duration: Option<u32>, // in seconds
pub bpm: Option<u32>,
pub isrc: Option<String>,
pub label: Option<String>,
pub barcode: Option<String>,
pub copyright: Option<String>,
pub explicit: Option<bool>,
pub replay_gain: Option<String>,
pub source_id: Option<String>,
pub lyrics_unsync: Option<String>,
}
/// 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::{Content, ExtendedText};
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::{Content, ExtendedText};
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::{Content, ExtendedText};
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::{Content, ExtendedText};
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::{Content, Lyrics};
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"
}
}