rustifydl 0.2.41

A fast, no-fuss Spotify downloader built in Rust.
Documentation
//! Metadata tagging utilities for RustifyDL.
//!
//! This module writes clean, player-friendly tags to downloaded audio files.
//!
//! Tag strategy by format:
//! - MP3: Write ID3v2.3 for maximum compatibility.
//! - FLAC/OGG/OPUS: Write native Vorbis Comments.
//! - MP4/M4A: Write iTunes/MP4 `ilst` atoms.
//! - WAV: Where supported, write RIFF INFO.
//!
//! Artwork is embedded as the front cover when the container allows it.

use std::path::PathBuf;

use lofty::{
    config::WriteOptions,
    file::{AudioFile, TaggedFileExt},
    picture::{MimeType, Picture, PictureType},
    read_from_path,
    tag::{Accessor, Tag},
};
use reqwest;
use spotify_rs::{ClientCredsClient, model::track::Track};

use crate::DownloadOptions;

/// Try to detect the image MIME type from raw bytes.
///
/// Falls back to JPEG when unknown.
fn detect_image_mime_type(bytes: &[u8]) -> MimeType {
    if bytes.len() < 4 {
        return MimeType::Jpeg;
    }

    if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
        return MimeType::Jpeg;
    }

    if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
        return MimeType::Png;
    }

    MimeType::Jpeg
}

/// Write metadata tags and artwork to the given song file.
///
/// Behavior:
/// - Fetches any missing context (e.g., album) from Spotify.
/// - Builds a fresh tag and saves using the native container format.
/// - Embeds front cover artwork and sets artist/album/track/disc/genre/year.
///
/// Returns an error if the file cannot be tagged or network requests fail.
pub async fn metadata(
    song: &String,
    track: &Track,
    options: &DownloadOptions,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let spotify =
        ClientCredsClient::authenticate(options.client_id.clone(), options.client_secret.clone())
            .await?;

    let path = PathBuf::from(format!(
        "{}/{}.{}",
        options.output_dir,
        song,
        options.format.clone()
    ));
    let mut tagged_file = read_from_path(&path)?;

    let tag_type = tagged_file.primary_tag_type();
    let mut tag = Tag::new(tag_type);

    tag.set_title(track.name.clone());
    tag.set_artist(
        track
            .artists
            .iter()
            .map(|artist| artist.name.as_str())
            .collect::<Vec<_>>()
            .join(", "),
    );
    tag.set_album(track.album.name.clone());
    let album = spotify_rs::album(track.album.id.clone())
        .get(&spotify)
        .await?;
    if !album.genres.is_empty() {
        tag.set_genre(
            album
                .genres
                .iter()
                .map(|genre| genre.as_str())
                .collect::<Vec<_>>()
                .join(", "),
        );
    } else {
        let song_artist = spotify_rs::get_artist(&album.artists[0].id, &spotify).await?;
        tag.set_genre(
            song_artist
                .genres
                .iter()
                .map(|genre| genre.as_str())
                .collect::<Vec<_>>()
                .join(", "),
        );
    }

    tag.set_disk(track.disc_number);

    let image_url = &album.images[0].url;
    let image_bytes = reqwest::get(image_url).await?.bytes().await?.to_vec();

    let mime_type = detect_image_mime_type(&image_bytes);

    let front_cover = Picture::new_unchecked(
        PictureType::CoverFront,
        Some(mime_type),
        Some("Cover".to_string()),
        image_bytes,
    );
    tag.push_picture(front_cover);
    tag.set_track(track.track_number);
    tag.set_track_total(album.total_tracks);
    tag.set_year(album.release_date[..4].parse::<u32>().unwrap_or(0));

    tagged_file.insert_tag(tag);

    // Use ID3v2.3 only for MP3; otherwise rely on native tags.
    let mut write_options = WriteOptions::new().remove_others(true);
    if options.format.eq_ignore_ascii_case("mp3") {
        write_options = write_options.use_id3v23(true);
    }

    tagged_file
        .save_to_path(path.clone(), write_options)
        .expect("ERROR: Failed to write the tag!");

    Ok(())
}