songrec 0.7.3

An open-source Shazam client for Linux, written in Rust.
use log::{error, info, warn};
use std::error::Error;
use std::sync::{Arc, Mutex};

use chrono::Local;
use gettextrs::gettext;

#[cfg(all(target_os = "linux", feature = "mpris"))]
use mpris_server::PlaybackStatus;

use crate::core::http_task::http_task;
use crate::core::microphone_thread::microphone_thread;
use crate::core::processing_thread::processing_thread;
use crate::core::thread_messages::{
    spawn_big_thread, GUIMessage, MicrophoneMessage, ProcessingMessage,
};

use crate::core::preferences::{Preferences, PreferencesInterface};
#[cfg(all(target_os = "linux", feature = "mpris"))]
use crate::plugins::mpris_player::{get_player, update_song};
use crate::utils::csv_song_history::SongHistoryRecord;

pub enum CLIOutputType {
    SongName,
    JSON,
    CSV,
}

pub struct CLIParameters {
    pub enable_mpris: bool,
    pub list_devices: bool,
    pub recognize_once: bool,
    pub audio_device: Option<String>,
    pub request_interval: u64,
    pub input_file: Option<String>,
    pub output_type: CLIOutputType,
}

pub fn cli_main(parameters: CLIParameters) -> Result<(), Box<dyn Error>> {
    let (gui_tx, gui_rx) = async_channel::unbounded();
    let (microphone_tx, microphone_rx) = async_channel::unbounded();
    let (processing_tx, processing_rx) = async_channel::unbounded();
    let (http_tx, http_rx) = async_channel::unbounded();

    let gui_tx_2 = gui_tx.clone();
    let gui_tx_3 = gui_tx.clone();
    let processing_tx_2 = processing_tx.clone();
    let microphone_tx_2 = microphone_tx.clone();
    let microphone_tx_3 = microphone_tx.clone();

    let preferences_interface = Arc::new(Mutex::new(PreferencesInterface {
        preferences_file_path: None,
        preferences: Preferences::with_interval(parameters.request_interval),
    }));

    spawn_big_thread(move || {
        microphone_thread(
            microphone_rx,
            microphone_tx_2,
            processing_tx_2,
            gui_tx_2,
            preferences_interface,
            true,
        );
    });

    spawn_big_thread(move || {
        processing_thread(processing_rx, http_tx, gui_tx_3);
    });

    glib::spawn_future_local(http_task(http_rx, gui_tx, microphone_tx_3));

    let main_loop = glib::MainLoop::new(None, false);
    let loop_inner = main_loop.clone();

    glib::spawn_future_local(async move {
        // Recognize once if an input file is provided

        let do_recognize_once = parameters.recognize_once || parameters.input_file.is_some();

        // Do not enable mpris if recognizing one song

        #[cfg(all(target_os = "linux", feature = "mpris"))]
        let mpris_obj = {
            let do_enable_mpris = parameters.enable_mpris && !do_recognize_once;
            if do_enable_mpris {
                get_player(false).await
            } else {
                None
            }
        };
        #[cfg(all(target_os = "linux", feature = "mpris"))]
        let mut last_cover_path = None;

        let mut last_track: Option<String> = None;

        let audio_dev_name = parameters.audio_device.as_ref().map(|dev| dev.to_string());
        let input_file_name = parameters.input_file.as_ref().map(|dev| dev.to_string());

        if let Some(ref filename) = parameters.input_file {
            processing_tx
                .try_send(ProcessingMessage::ProcessAudioFile(filename.to_string()))
                .unwrap();
        }

        let mut csv_writer = csv::Writer::from_writer(std::io::stdout());

        while let Ok(gui_message) = gui_rx.recv().await {
            match gui_message {
                GUIMessage::DevicesList(device_names) => {
                    // no need to start a microphone if recognizing from file
                    if input_file_name.is_some() {
                        continue;
                    }
                    for device in device_names.iter() {
                        info!(
                            "{} {} ({})",
                            gettext("Available device:"),
                            device.inner_name,
                            device.display_name
                        );
                    }
                    if parameters.list_devices {
                        loop_inner.quit();
                        break;
                    }
                    let dev_name = if let Some(dev) = &audio_dev_name {
                        let mut found: bool = false;
                        for device in device_names.iter() {
                            if &device.inner_name == dev || &device.display_name == dev {
                                found = true;
                                break;
                            }
                        }
                        if !found {
                            error!("{}", gettext("Exiting: audio device not found"));
                            break;
                        }
                        dev
                    } else {
                        if device_names.is_empty() {
                            error!("{}", gettext("Exiting: no audio devices found!"));
                            break;
                        }
                        &device_names[0].inner_name
                    };
                    info!("{}", gettext("Using device %s").replace("%s", dev_name));
                    microphone_tx
                        .try_send(MicrophoneMessage::MicrophoneRecordStart(
                            dev_name.to_owned(),
                        ))
                        .unwrap();
                }
                GUIMessage::NetworkStatus(reachable) => {
                    #[cfg(all(target_os = "linux", feature = "mpris"))]
                    {
                        let mpris_status = if reachable {
                            PlaybackStatus::Playing
                        } else {
                            PlaybackStatus::Paused
                        };
                        if let Some(ref player) = mpris_obj {
                            if let Err(error) = player.set_playback_status(mpris_status).await {
                                error!("Could not set MPRIS playback status: {:?}", error);
                            }
                        }
                    }

                    if !reachable {
                        if input_file_name.is_some() {
                            error!("{}", gettext("Error: Network unreachable"));
                            break;
                        } else {
                            warn!("{}", gettext("Warning: Network unreachable"));
                        }
                    }
                }
                GUIMessage::ErrorMessage(string) => {
                    if string != gettext("No match for this song") || input_file_name.is_some() {
                        error!("{} {}", gettext("Error:"), string);
                    }
                    if input_file_name.is_some() {
                        break;
                    }
                }
                GUIMessage::MicrophoneRecording if !do_recognize_once => {
                    info!("{}", gettext("Recording started!"));
                }
                GUIMessage::SongRecognized(message) => {
                    let track_key = Some(message.track_key.clone());
                    let song_name = format!("{} - {}", message.artist_name, message.song_name);

                    if last_track != track_key {
                        // TODO re-implement this with new lib
                        #[cfg(all(target_os = "linux", feature = "mpris"))]
                        if let Some(ref player) = mpris_obj {
                            update_song(player, &message, &mut last_cover_path).await;
                        }

                        last_track = track_key;
                        match parameters.output_type {
                            CLIOutputType::JSON => {
                                println!("{}", message.shazam_json);
                            }
                            CLIOutputType::CSV => {
                                csv_writer
                                    .serialize(SongHistoryRecord {
                                        song_name,
                                        album: Some(message.album_name.unwrap_or_default()),
                                        track_key: Some(message.track_key),
                                        release_year: Some(
                                            message.release_year.unwrap_or_default(),
                                        ),
                                        genre: Some(message.genre.unwrap_or_default()),
                                        recognition_date: Local::now().format("%c").to_string(),
                                    })
                                    .unwrap();
                                csv_writer.flush().unwrap();
                            }
                            CLIOutputType::SongName => {
                                println!("{}", song_name);
                            }
                        };
                    }
                    if do_recognize_once {
                        break;
                    }
                }
                _ => {}
            }
        }

        gui_rx.close();
        loop_inner.quit();
    });

    main_loop.run();

    Ok(())
}