ytmusic-cli 0.3.0

A terminal YouTube Music player built with Rust, Ratatui, mpv, yt-dlp, and rs-ytmusic-api
use std::{path::PathBuf, process::Stdio};

use anyhow::Context;
use tokio::process::Command;

use crate::types::track::Track;

const AUDIO_EXTS: &[&str; 5] = &["m4a", "mp3", "opus", "webm", "ogg"];
const DEFAULT_AUDIO_FORMAT: &str = "m4a";

#[derive(Debug, Clone)]
pub struct CacheService {
    dir: PathBuf,
    audio_format: String,
}

impl CacheService {
    pub fn new() -> anyhow::Result<Self> {
        let music_dir = dirs::audio_dir()
            .or_else(dirs::home_dir)
            .context("failed to resolve home or music directory")?;
        let dir = music_dir.join("ytmusic.cli");

        std::fs::create_dir_all(&dir)
            .with_context(|| format!("failed to create cache directory: {}", dir.display()))?;

        Ok(Self {
            dir,
            audio_format: DEFAULT_AUDIO_FORMAT.to_string(),
        })
    }

    pub fn find(&self, video_id: &str) -> Option<PathBuf> {
        AUDIO_EXTS
            .iter()
            .map(|ext| self.dir.join(format!("{video_id}.{ext}")))
            .find(|path| path.exists())
    }

    pub fn enrich(&self, mut track: Track) -> Track {
        track.cached_path = self.find(&track.video_id);
        track
    }

    pub async fn cache_track(&self, track: &Track) -> anyhow::Result<()> {
        let output_template = self.dir.join("%(id)s.%(ext)s");
        Command::new("yt-dlp")
            .arg("-f")
            .arg("bestaudio")
            .arg("-x")
            .arg("--audio-format")
            .arg(&self.audio_format)
            .arg("--audio-quality")
            .arg("0")
            .arg("--embed-metadata")
            .arg("--embed-thumbnail")
            .arg("--convert-thumbnails")
            .arg("jpg")
            .arg("-o")
            .arg(output_template)
            .arg(track.url())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn()
            .context("failed to spawn yt-dlp")?;
        Ok(())
    }
}