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)
}