selene-core 0.2.0

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{
    collections::{HashMap, HashSet},
    path::{Path, PathBuf},
    sync::Arc,
};

use barber::{ProgressBar, ProgressRenderer};
use lunar_lib::formatter::{FormatTable, format_str};
use rayon::{
    ThreadPoolBuilder,
    iter::{IntoParallelRefIterator, ParallelIterator},
};

use crate::{
    config::common::common_config,
    database::{DatabaseEntry, DatabaseError, db_transaction},
    errors::ExtractError,
    ffmpeg::{ffprobe_format_tags, loudnorm::LoudnormAnalysis},
    library::{
        artist::{Artist, ArtistGroup, add_from_artists, extract_from_featuring},
        cover_art::{CoverArt, has_video_stream},
        import::ExtractResult,
        track::{
            Track, TrackId,
            track_meta::{AlbumReference, TrackMeta},
        },
    },
    media_container::MediaContainer,
    utils::{hash_file, pair_extension},
};

#[must_use] 
pub fn scan_for_extract<'a>(
    known_tracks: &'a [TrackId],
    sources: &'a HashMap<TrackId, PathBuf>,
) -> Vec<&'a Path> {
    sources
        .iter()
        .filter_map(|(id, path)| {
            if known_tracks.contains(id) {
                None
            } else {
                Some(path.as_path())
            }
        })
        .collect()
}

pub fn extract(
    files: &[&Path],
    analyze_loudnorm: bool,
    progress_renderer: Arc<dyn ProgressRenderer>,
) -> Result<(), ExtractError> {
    if files.is_empty() {
        return Ok(());
    }

    let progress_bar = ProgressBar::new(0, files.len(), progress_renderer);
    progress_bar.set_label("Extracting metadata from files...");

    let max_threads = (rayon::current_num_threads() as f32 * 0.33).ceil() as usize;

    let thread_pool = ThreadPoolBuilder::new()
        .num_threads(max_threads)
        .build()
        .unwrap();

    thread_pool.install(|| {
        files
            .par_iter()
            .try_for_each(|source| -> Result<(), ExtractError> {
                let ExtractResult {
                    track,
                    album,
                    artists,
                } = extract_metadata(source, analyze_loudnorm)?;

                db_transaction(|cas_tx| -> Result<(), DatabaseError> {
                    track.tx_insert(cas_tx)?;

                    if let Some(album) = &album {
                        album.tx_patch(cas_tx)?;
                    }
                    for artist in &artists {
                        artist.tx_patch(cas_tx)?;
                    }

                    Ok(())
                })?;

                progress_bar.set_label(&format!(
                    "Extracted metadata from '{path}'",
                    path = source.display()
                ));
                progress_bar.increment();

                Ok(())
            })
    })?;

    progress_bar.flush();
    Ok(())
}

/// Analyzes and extracts the input file metadata into an [`ExtractResult`]
pub fn extract_metadata(
    source_file: impl AsRef<Path>,
    loudnorm: bool,
) -> Result<ExtractResult, ExtractError> {
    let source_file = source_file.as_ref();

    let src_container = MediaContainer::from_file(source_file.to_path_buf())?;
    let (container, codec) = src_container
        .transcode_to()
        .ok_or(ExtractError::InvalidContainer)?;

    let mut all_artists: HashSet<Artist> = HashSet::new();

    let tags = ffprobe_format_tags(source_file)?;

    let track_num = tags.extract_track_num().0;
    let disc_num = tags.extract_disc_num().0;
    let date = tags.extract_date();

    let mut track_artists = tags.extract_track_artists();

    let title = if let Some(title) = &tags.title {
        let (title, other_artists) = extract_from_featuring(title);

        track_artists.extend(other_artists);
        Some(title.to_string())
    } else {
        None
    };

    all_artists.extend(track_artists.clone());

    let lyric_data = tags.extract_lyric_data();
    let album = tags.extract_album(&track_artists).map(|(album, artists)| {
        all_artists.extend(artists.clone());
        album
    });

    let metadata = TrackMeta {
        album: album
            .as_ref()
            .map(|a| AlbumReference::new(a.id(), track_num, disc_num)),
        artists: ArtistGroup::from_artists(&track_artists),
        date,
        genre: tags.genre,
        lyric_data,
        other: tags.other,
        title,
    };

    let relative_path = {
        let mut format_table = FormatTable::new();
        format_table.extend_from_taggable(&metadata);
        add_from_artists(&mut format_table, &track_artists, "track");
        if let Some(album) = &album {
            format_table.extend_from_taggable(album);
        }

        let mut path = format_str(
            &common_config().track_name_config.format_string,
            &format_table,
        )?;

        path.push('.');
        path.push_str(pair_extension(&container, &codec).unwrap());
        PathBuf::from(path)
    };

    let mut track = Track::new(
        hash_file(source_file)?,
        src_container,
        metadata,
        relative_path,
    );

    if loudnorm {
        track.loudnorm_analysis = Some(LoudnormAnalysis::from_file(source_file)?);
    }

    if let Some(source) = has_video_stream(source_file).then_some(source_file.to_path_buf()) {
        let cover_art = CoverArt::from_file(&source)?;
        track.cover_art = Some(cover_art);
    }

    let result = ExtractResult {
        track,
        album,
        artists: all_artists.into_iter().collect(),
    };

    Ok(result)
}