nukeop/nuclear

View on GitHub
packages/scanner/src/metadata.rs

Summary

Maintainability
Test Coverage
use std::fs::File;

use derive_builder::Builder;
use id3::TagLike;
use metaflac;
use symphonia::core::{
    formats::FormatOptions,
    io::MediaSourceStream,
    meta::{MetadataOptions, StandardTagKey},
    probe::Hint,
};

use crate::{
    error::MetadataError,
    thumbnails::{FlacThumbnailGenerator, Mp3ThumbnailGenerator},
    thumbnails::{Mp4ThumbnailGenerator, ThumbnailGenerator},
};

#[derive(Default, Debug, Clone, Builder)]
#[builder(setter(strip_option))]
pub struct AudioMetadata {
    pub artist: Option<String>,
    pub title: Option<String>,
    pub album: Option<String>,
    pub duration: Option<u32>,
    pub disc: Option<u32>,
    pub position: Option<u32>,
    pub year: Option<String>,
    pub thumbnail: Option<String>,
}

impl AudioMetadata {
    pub fn new() -> Self {
        Self {
            artist: None,
            title: None,
            album: None,
            duration: None,
            disc: None,
            position: None,
            year: None,
            thumbnail: None,
        }
    }
}

pub trait MetadataExtractor {
    fn extract_metadata(
        &self,
        path: &str,
        thumbnails_dir: &str,
    ) -> Result<AudioMetadata, MetadataError>;
}

#[derive(Debug, Clone)]

pub struct Mp3MetadataExtractor;
impl MetadataExtractor for Mp3MetadataExtractor {
    fn extract_metadata(
        &self,
        path: &str,
        thumbnails_dir: &str,
    ) -> Result<AudioMetadata, MetadataError> {
        let tag = id3::Tag::read_from_path(path).unwrap();
        let mut metadata = AudioMetadata::new();

        metadata.artist = tag.artist().map(|s| s.to_string());
        metadata.title = tag.title().map(|s| s.to_string());
        metadata.album = tag.album().map(|s| s.to_string());
        let duration = mp3_duration::from_path(&path).map(|duration| duration.as_secs() as u32);

        match duration {
            Ok(duration) => metadata.duration = Some(duration),
            Err(_) => metadata.duration = None,
        }
        metadata.position = tag.track();
        metadata.disc = tag.disc();
        metadata.year = tag.year().map(|y| y.to_string());

        metadata.thumbnail = Mp3ThumbnailGenerator::generate_thumbnail(
            &path,
            metadata.album.as_deref(),
            thumbnails_dir,
        );

        Ok(metadata)
    }
}

pub struct FlacMetadataExtractor;
impl FlacMetadataExtractor {
    fn extract_string_metadata(
        tag: &metaflac::Tag,
        key: &str,
        fallback_key: Option<&str>,
    ) -> Option<String> {
        tag.get_vorbis(key)
            .and_then(|mut iter| iter.next())
            .map(|s| s.to_string())
            .or_else(|| {
                fallback_key.and_then(|key| {
                    tag.get_vorbis(key)
                        .and_then(|mut iter| iter.next())
                        .map(|s| s.to_string())
                })
            })
    }

    fn extract_numeric_metadata<T: std::str::FromStr>(tag: &metaflac::Tag, key: &str) -> Option<T> {
        tag.get_vorbis(key)
            .and_then(|mut iter| iter.next())
            .and_then(|s| s.parse::<T>().ok())
    }
}

impl MetadataExtractor for FlacMetadataExtractor {
    fn extract_metadata(
        &self,
        path: &str,
        thumbnails_dir: &str,
    ) -> Result<AudioMetadata, MetadataError> {
        let tag = metaflac::Tag::read_from_path(path).unwrap();
        let mut metadata = AudioMetadata::new();
        metadata.artist = Self::extract_string_metadata(&tag, "ARTIST", Some("ALBUMARTIST"));
        metadata.title = Self::extract_string_metadata(&tag, "TITLE", None);
        metadata.album = Self::extract_string_metadata(&tag, "ALBUM", None);
        let total_samples = tag.get_streaminfo().unwrap().total_samples;
        let sample_rate = tag.get_streaminfo().unwrap().sample_rate;
        metadata.duration = Some(total_samples as u32 / (sample_rate as u32));
        metadata.position = Self::extract_numeric_metadata(&tag, "TRACKNUMBER");
        metadata.disc = Self::extract_numeric_metadata(&tag, "DISCNUMBER");
        metadata.year = Self::extract_numeric_metadata(&tag, "DATE");
        metadata.thumbnail = FlacThumbnailGenerator::generate_thumbnail(
            &path,
            metadata.album.as_deref(),
            thumbnails_dir,
        );

        Ok(metadata)
    }
}

pub struct OggMetadataExtractor;

impl OggMetadataExtractor {}

impl MetadataExtractor for OggMetadataExtractor {
    fn extract_metadata(
        &self,
        path: &str,
        _thumbnails_dir: &str, // Thumbnail generation is not handled here
    ) -> Result<AudioMetadata, MetadataError> {
        let file = File::open(path);
        if file.is_err() {
            return Err(MetadataError::new(
                format!("Could not open file {}", path).as_str(),
            ));
        }

        let file = File::open(path);

        if let Err(_) = file {
            return Err(MetadataError::new(
                format!("Could not open file {}", path).as_str(),
            ));
        }

        let mss = MediaSourceStream::new(Box::new(file.unwrap()), Default::default());

        let meta_opts: MetadataOptions = Default::default();
        let fmt_opts: FormatOptions = Default::default();

        let mut hint = Hint::new();
        hint.with_extension("ogg");

        let mut probed = symphonia::default::get_probe()
            .format(&hint, mss, &fmt_opts, &meta_opts)
            .expect("unsupported format");
        let mut metadata = AudioMetadata::new();

        let track = probed.format.default_track().unwrap();
        let time_base = track.codec_params.time_base.unwrap();
        let duration = track
            .codec_params
            .n_frames
            .map(|frames| track.codec_params.start_ts + frames);

        metadata.duration = duration.map(|d| ((d as u32) * time_base.numer / time_base.denom));

        if let Some(meta) = probed.format.metadata().current() {
            for tag in meta.tags().iter() {
                if tag.is_known() {
                    match tag.std_key {
                        Some(StandardTagKey::TrackTitle) => {
                            metadata.title = Some(tag.value.to_string());
                        }
                        Some(StandardTagKey::Artist) => {
                            metadata.artist = Some(tag.value.to_string());
                        }
                        Some(StandardTagKey::Album) => {
                            metadata.album = Some(tag.value.to_string());
                        }
                        Some(StandardTagKey::TrackNumber) => {
                            metadata.position = Some(tag.value.to_string().parse::<u32>().unwrap());
                        }
                        Some(StandardTagKey::DiscNumber) => {
                            metadata.disc = Some(tag.value.to_string().parse::<u32>().unwrap());
                        }
                        Some(StandardTagKey::Date) => {
                            metadata.year = Some(tag.value.to_string());
                        }
                        _ => {}
                    }
                }
            }
        }
        Ok(metadata)
    }
}

pub struct Mp4MetadataExtractor;

impl Mp4MetadataExtractor {}

impl MetadataExtractor for Mp4MetadataExtractor {
    fn extract_metadata(
        &self,
        path: &str,
        thumbnails_dir: &str,
    ) -> Result<AudioMetadata, MetadataError> {
        let tag = mp4ameta::Tag::read_from_path(path).unwrap();

        let mut metadata = AudioMetadata::new();

        metadata.artist = tag.artist().map(|s| s.to_string());
        metadata.title = tag.title().map(|s| s.to_string());
        metadata.album = tag.album().map(|s| s.to_string());
        metadata.duration = tag.duration().map(|d| d.as_secs() as u32);
        metadata.position = tag.track_number().map(|n| n as u32);
        metadata.disc = tag.disc_number().map(|n| n as u32);
        metadata.year = tag.year().map(|y: &str| y.parse().unwrap());

        metadata.thumbnail = Mp4ThumbnailGenerator::generate_thumbnail(
            &path,
            metadata.album.as_deref(),
            thumbnails_dir,
        );

        Ok(metadata)
    }
}