about summary refs log tree commit diff stats
path: root/sys/nixpkgs/pkgs/ytc/src/main.rs
blob: 5c7849b8d40169ad700a59c017719e080f6f9974 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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 let Some(id) = id {
                if code == 0 {
                    println!("\x1b[32;1mMarking {} as watched!\x1b[0m", id);
                    fs::remove_file(&path)?;
                    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)
}