about summary refs log blame commit diff stats
path: root/sys/nixpkgs/pkgs/ytc/src/main.rs
blob: 552e85d318a586733744a35bff69b906df07dd4c (plain) (tree)
















































































































































                                                                                         
                                




                                                                

                                        
                                                                            





























                                                                              
use std::{
    env,
    fs::{self, canonicalize},
    io::{stderr, stdout},
    os::unix::fs::symlink,
    path::PathBuf,
    process::Command as StdCmd,
};

use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
use downloader::Downloader;
use log::debug;
use serde::Deserialize;

mod downloader;

const STATUS_PATH: &str = "ytcc/running";

/// A helper for downloading and playing youtube videos
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
    #[command(subcommand)]
    /// The subcommand to execute
    pub subcommand: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
    #[clap(value_parser)]
    /// Work based of ytcc ids
    Id {
        #[clap(value_parser)]
        /// A list of ids to play
        ids: Vec<u32>,
    },
    #[clap(value_parser)]
    /// Work based of raw youtube urls
    Url {
        #[clap(value_parser)]
        /// A list of urls to play
        urls: Vec<String>,
    },
}

struct PlayThing {
    url: String,
    id: Option<u32>,
}

#[derive(Deserialize)]
struct YtccListData {
    url: String,
    #[allow(unused)]
    title: String,
    #[allow(unused)]
    description: String,
    #[allow(unused)]
    publish_date: String,
    #[allow(unused)]
    watch_date: Option<String>,
    #[allow(unused)]
    duration: String,
    #[allow(unused)]
    thumbnail_url: String,
    #[allow(unused)]
    extractor_hash: String,
    id: u32,
    #[allow(unused)]
    playlists: Vec<YtccPlaylistData>,
}
#[derive(Deserialize)]
struct YtccPlaylistData {
    #[allow(unused)]
    name: String,
    #[allow(unused)]
    url: String,
    #[allow(unused)]
    reverse: bool,
}

fn main() -> Result<()> {
    let args = Args::parse();
    cli_log::init_cli_log!();

    let playspec: Vec<PlayThing> = match args.subcommand {
        Command::Id { ids } => {
            let mut output = Vec::with_capacity(ids.len());
            for id in ids {
                debug!("Adding {}", id);
                let mut ytcc = StdCmd::new("ytcc");
                ytcc.args([
                    "--output",
                    "json",
                    "list",
                    "--attributes",
                    "url",
                    "--ids",
                    id.to_string().as_str(),
                ]);
                let json = serde_json::from_slice::<Vec<YtccListData>>(
                    &ytcc.output().context("Failed to get url from id")?.stdout,
                )
                .context("Failed to deserialize json output")?;

                if json.len() == 0 {
                    bail!("Could not find a video with id: {}", id);
                }
                assert_eq!(json.len(), 1);
                let json = json.first().expect("Has only one element");

                debug!("Id resolved to: '{}'", &json.url);

                output.push(PlayThing {
                    url: json.url.clone(),
                    id: Some(json.id),
                })
            }
            output
        }
        Command::Url { urls } => urls
            .into_iter()
            .map(|url| PlayThing { url, id: None })
            .collect(),
    };

    debug!("Initializing downloader");
    let mut downloader = Downloader::new(playspec)?;

    while let Some((path, id)) = downloader.next() {
        debug!("Next path to play is: '{}'", path.display());
        let mut info_json = canonicalize(&path).context("Failed to canoncialize path")?;
        info_json.set_extension("info.json");

        if status_path()?.is_symlink() {
            fs::remove_file(status_path()?).context("Failed to delete old status file")?;
        } else {
            bail!(
                "The status path ('{}') is not a symlink!",
                status_path()?.display()
            );
        }

        symlink(info_json, status_path()?).context("Failed to symlink")?;

        let mut mpv = StdCmd::new("mpv");
        // mpv.stdout(stdout());
        mpv.stderr(stderr());
        mpv.args(["--speed=2.7", "--volume=75"]);
        mpv.arg(&path);

        let status = mpv.status().context("Failed to run mpv")?;
        if let Some(code) = status.code() {
            if code == 0 {
                fs::remove_file(&path)?;
                if let Some(id) = id {
                    println!("\x1b[32;1mMarking {} as watched!\x1b[0m", id);
                    let mut ytcc = StdCmd::new("ytcc");
                    ytcc.stdout(stdout());
                    ytcc.stderr(stderr());
                    ytcc.args(["mark"]);
                    ytcc.arg(id.to_string());
                    let status = ytcc.status().context("Failed to run ytcc")?;
                    if let Some(code) = status.code() {
                        if code != 0 {
                            bail!("Ytcc failed with status: {}", code);
                        }
                    }
                }
            }
            debug!("Mpv exited with: {}", code);
        }
    }
    downloader.drop()?;

    Ok(())
}

fn status_path() -> Result<PathBuf> {
    let out: PathBuf = format!(
        "{}/{}",
        env::var("XDG_RUNTIME_DIR").expect("This should always exist"),
        STATUS_PATH
    )
    .into();
    fs::create_dir_all(&out.parent().expect("Parent should exist"))?;
    Ok(out)
}