librmo 0.4.4

A library to manage media files and play them
Documentation
use super::structs::MediaFile;
use crate::app::settings::get_db_connection;
use crate::entity::{
    library::{self, Model as LibraryModel},
    media::{self, ActiveModel as MediaAModel, Model as MediaModel},
};
use crate::error::error::{CustomError, QueryError};
use crate::media_checker::albums::validate_albums;

use log::info;
use sea_orm::entity::*;
use sea_orm::{
    ActiveModelTrait, ActiveValue, DatabaseConnection, EntityTrait, Iterable, ModelTrait,
    QueryFilter, Set,
};
use sea_query;
use std::str::FromStr;
use std::sync::mpsc::Sender;
use std::time::{Instant, UNIX_EPOCH};
use walkdir::WalkDir;

/// MusicManager is the link between your music sources and filtering out the valid records
///
/// Create via ::new
///
/// Run via .execute
pub struct MusicManager {
    /// Consume messages back from the MusicManager for progress through scanning your library
    sender: Option<Sender<String>>,
}

impl MusicManager {
    /// Creates a new [`MusicManager`].
    ///
    /// Provide an optional message channel for learning the progress through scanning your library
    pub fn new(tx: Option<Sender<String>>) -> Self {
        Self { sender: tx }
    }

    /// Removes a library by a provided id, will validate the data in the RMO database to also remove linked Albums
    ///
    /// # Errors
    ///
    /// This function will return an error if the provided ID cannot be found or the ID is an invalid i32
    #[deprecated(note = "Use remove_library_by_id instead")]
    pub async fn remove_library(library_id: &str) -> Result<(), CustomError> {
        info!("MusicManager::remove_library");
        let db = get_db_connection().await?;

        let library = library::Entity::find_by_id(i32::from_str(library_id).unwrap())
            .one(&db)
            .await?;
        let mut ret_val: Result<(), CustomError> = Ok(());
        match library {
            Some(x) => {
                x.delete(&db).await?;
            }
            None => {
                ret_val = Err(CustomError::QueryErr(QueryError {
                    query: format!("Failed to find library with id {}", library_id),
                }));
            }
        }
        validate_albums().await?;
        ret_val
    }

    /// Removes a library by a provided id, will validate the data in the RMO database to also remove linked Albums
    ///
    /// # Errors
    ///
    /// This function will return an error if the provided ID cannot be found
    pub async fn remove_library_by_id(library_id: i32) -> Result<(), CustomError> {
        info!("MusicManager::remove_library");
        let db = get_db_connection().await?;

        let library = library::Entity::find_by_id(library_id).one(&db).await?;
        let mut ret_val: Result<(), CustomError> = Ok(());
        match library {
            Some(x) => {
                x.delete(&db).await?;
            }
            None => {
                ret_val = Err(CustomError::QueryErr(QueryError {
                    query: format!("Failed to find library with id {}", library_id),
                }));
            }
        }
        validate_albums().await?;
        ret_val
    }

    /// Use to get all the currently defined library records in the RMO database
    pub async fn get_media_by_library(library_id: i32) -> Result<Vec<MediaModel>, CustomError> {
        info!("MusicManager::get_media_by_library");
        let db = get_db_connection().await?;
        let libraries = media::Entity::find()
            .filter(media::Column::LibraryId.eq(library_id))
            .all(&db)
            .await
            .expect("get_media_by_library::Failed to retrieve all libraries");
        Ok(libraries)
    }

    /// With a new set of MediaFiles and a library id deletes all records that are not in the new set by matching path
    async fn cleanup_media(
        new_set: &Vec<MediaFile>,
        library_id: i32,
        db: &DatabaseConnection,
    ) -> Result<(), CustomError> {
        // First get all the media we have for this library
        // Check if any of the current records do not exist in the walked dir
        // delete them
        let media: Vec<MediaModel> = MusicManager::get_media_by_library(library_id).await?;
        let res_paths: Vec<String> = new_set.iter().map(|x| x.path.clone()).collect();
        let removed_media: Vec<MediaModel> = media
            .into_iter()
            .filter(|x| !res_paths.contains(&x.path))
            .collect();

        for r in removed_media.iter() {
            media::Entity::delete_by_id(r.id).exec(db).await?;
        }
        validate_albums().await?;
        Ok(())
    }

    /// Finds all the files contained in each root directory of defined libraries.
    ///
    /// Sends out a message every 100 files scanned or on the last file
    pub async fn execute(&self) -> Result<(), CustomError> {
        info!("MusicManager.execute");
        let _before = Instant::now();
        let db = get_db_connection().await?;

        let libraries = MusicManager::get_libraries().await?;
        info!(
            "MusicManager.execute: Libraries to scan: {}",
            libraries.len()
        );
        for l in libraries.iter() {
            self.send(format!(
                "MusicManager.execute: Scanning library: {}",
                l.name
            ));
            let res = MusicManager::walk_dir(&l.path);

            MusicManager::cleanup_media(&res, l.id, &db).await?;

            let mut media_inserts = vec![];
            let mut i = 0;
            let tot_files = res.len();
            for r in res.iter() {
                i += 1;
                if i % 100 == 0 || i == tot_files {
                    let message = format!("Scanning file - {}/{}", i, tot_files);
                    info!("{}", message);
                    self.send(message);
                }
                let media = media::ActiveModel {
                    id: ActiveValue::not_set(),
                    path: ActiveValue::set(r.path.to_owned()),
                    mtime: ActiveValue::set(
                        r.mtime
                            .duration_since(UNIX_EPOCH)
                            .unwrap()
                            .as_secs()
                            .to_string(),
                    ),
                    library_id: ActiveValue::set(l.id),
                    file_type: ActiveValue::set(r.file_type.to_string()),
                };
                media_inserts.push(media);
            }

            for chunk in media_inserts.chunks(
                (u16::MAX / (<library::Entity as EntityTrait>::Column::iter().count() as u16 * 5))
                    as usize,
            ) {
                MusicManager::insert_media(chunk.to_vec(), &db).await?;
            }
            let _ = MusicManager::calculate_library_files(l.id.clone(), res.len(), &db).await;
            self.send(format!(
                "MusicManager.execute: Completed library: {}",
                l.name
            ));
        }
        info!(
            "MusicManager.execute: Timing Complete in {:.2?}",
            _before.elapsed()
        );
        self.send(format!("Complete MusicManager Scan"));
        Ok(())
    }

    async fn calculate_library_files(
        library_id: i32,
        tot_files: usize,
        db: &DatabaseConnection,
    ) -> Result<(), CustomError> {
        let updated_library = library::Entity::find_by_id(library_id).one(db).await?;
        let mut updated_library: library::ActiveModel = match updated_library {
            Some(x) => x.into(),
            None => {
                return Err(CustomError::QueryErr(QueryError {
                    query: format!("Failed to find library with id {}", library_id),
                }))
            }
        };
        updated_library.files = Set(tot_files.to_string());
        updated_library.update(db).await?;
        Ok(())
    }

    pub async fn insert_media(
        media: Vec<MediaAModel>,
        db: &DatabaseConnection,
    ) -> Result<String, CustomError> {
        let _ = media::Entity::insert_many(media)
            .on_empty_do_nothing()
            .on_conflict(
                sea_query::OnConflict::column(media::Column::Path)
                    .update_column(media::Column::Mtime)
                    .to_owned(),
            )
            .exec(db)
            .await?;
        Ok("Ok".to_string())
    }

    fn send(&self, message: String) {
        match self.sender.as_ref() {
            Some(x) => {
                let _ = x.send(message);
            }
            None => {}
        };
    }

    fn walk_dir(root_path: &String) -> Vec<MediaFile> {
        let files = WalkDir::new(root_path)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|file| file.path().is_file());
        let mut media_files: Vec<MediaFile> = Vec::new();
        for f in files {
            let media_file = MediaFile::build(&f);
            match media_file {
                Err(_) => continue,
                Ok(x) => media_files.push(x),
            }
        }
        media_files
    }

    pub async fn get_libraries() -> Result<Vec<LibraryModel>, CustomError> {
        let db = get_db_connection().await?;
        let libraries = library::Entity::find().all(&db).await?;

        Ok(libraries)
    }
    pub async fn get_media() -> Result<Vec<MediaModel>, CustomError> {
        let db = get_db_connection().await?;
        let media = media::Entity::find().all(&db).await?;

        Ok(media)
    }

    pub async fn get_filtered_media() -> Result<Vec<MediaModel>, CustomError> {
        let db = get_db_connection().await?;
        let media = media::Entity::find()
            .filter(
                media::Column::FileType
                    .contains("MPEG")
                    .or(media::Column::FileType.contains("XFLAC")),
            )
            .all(&db)
            .await
            .expect("get_filtered_media::Failed to find relevant media");
        Ok(media)
    }

    /// Insert a new library record or overwrite an existing records path with the same name
    pub async fn insert_library(name: &str, path: &str) -> Result<(), CustomError> {
        let db = get_db_connection().await?;
        let library = library::ActiveModel {
            id: ActiveValue::not_set(),
            name: ActiveValue::set(name.to_owned()),
            path: ActiveValue::set(path.to_owned()),
            files: ActiveValue::set(0.to_string()),
        };
        let _ = library::Entity::insert(library)
            .on_conflict(
                sea_query::OnConflict::column(library::Column::Name)
                    .update_column(library::Column::Path)
                    .to_owned(),
            )
            .exec(&db)
            .await;
        Ok(())
    }
}