diff options
Diffstat (limited to '')
-rw-r--r-- | old/url.old/downloader.rs | 224 | ||||
-rw-r--r-- | old/url.old/mod.rs | 25 | ||||
-rw-r--r-- | old/ytc/main.rs | 85 | ||||
-rw-r--r-- | old/yts/main.rs | 99 |
4 files changed, 433 insertions, 0 deletions
diff --git a/old/url.old/downloader.rs b/old/url.old/downloader.rs new file mode 100644 index 0000000..b30b03c --- /dev/null +++ b/old/url.old/downloader.rs @@ -0,0 +1,224 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{ + fs::{self, canonicalize}, + io::{stderr, stdout, Read}, + mem, + os::unix::fs::symlink, + path::PathBuf, + process::Command, + sync::mpsc::{self, Receiver, Sender}, + thread::{self, JoinHandle}, +}; + +use anyhow::{bail, Context, Result}; +use log::{debug, error, warn}; +use url::Url; + +use crate::constants::{status_path, CONCURRENT, DOWNLOAD_DIR, MPV_FLAGS, YT_DLP_FLAGS}; + +#[derive(Debug)] +pub struct Downloadable { + pub url: Url, + pub id: Option<u32>, +} + +impl std::fmt::Display for Downloadable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{}|{}", + self.url.as_str().replace('|', ";"), + self.id.unwrap_or(0), + ) + } +} + +pub struct Downloader { + sent: usize, + download_thread: JoinHandle<Result<()>>, + orx: Receiver<(PathBuf, Option<u32>)>, + itx: Option<Sender<Downloadable>>, + playspec: Vec<Downloadable>, +} + +impl Downloader { + pub fn new(mut playspec: Vec<Downloadable>) -> anyhow::Result<Downloader> { + let (itx, irx): (Sender<Downloadable>, Receiver<Downloadable>) = mpsc::channel(); + let (otx, orx) = mpsc::channel(); + + let jh = thread::spawn(move || -> Result<()> { + while let Ok(pt) = irx.recv() { + debug!("Got '{}' to be downloaded", pt); + let path = download_url(&pt.url) + .with_context(|| format!("Failed to download url: '{}'", &pt.url))?; + otx.send((path, pt.id)).expect("Should not be dropped"); + } + debug!("Finished Downloading everything"); + Ok(()) + }); + + playspec.reverse(); + let mut output = Downloader { + sent: 0, + download_thread: jh, + orx, + itx: Some(itx), + playspec, + }; + + if output.playspec.len() <= CONCURRENT as usize { + output.add(output.playspec.len() as u32)?; + } else { + output.add(CONCURRENT)?; + } + Ok(output) + } + + pub fn add(&mut self, number_to_add: u32) -> Result<()> { + debug!("Adding {} to be downloaded concurrently", number_to_add); + for _ in 0..number_to_add { + let pt = self.playspec.pop().expect("This call should be guarded"); + self.itx.as_ref().expect("Should still be valid").send(pt)?; + self.sent += 1; + } + Ok(()) + } + + /// Return the next video already downloaded, will block until the download is complete + pub fn next(&mut self) -> Option<(PathBuf, Option<u32>)> { + debug!("Requesting next output"); + match self.orx.recv() { + Ok(ok) => { + debug!("Output downloaded to: {}", ok.0.display()); + if !self.playspec.is_empty() { + self.add(1).ok()?; + } else { + debug!( + "Done sending videos to be downloaded, downoladed: {} videos", + self.sent + ); + let itx = mem::take(&mut self.itx); + drop(itx) + } + debug!("Returning: {}|{}", ok.0.display(), ok.1.unwrap_or(0)); + Some(ok) + } + Err(err) => { + debug!("Received error while listening: {}", err); + None + } + } + } + + pub fn drop(self) -> anyhow::Result<()> { + // Check that we really downloaded everything + assert_eq!(self.playspec.len(), 0); + match self.download_thread.join() { + Ok(ok) => ok, + Err(err) => panic!("Failed to join downloader thread: '{:#?}'", err), + } + } + + pub fn consume(mut self) -> anyhow::Result<()> { + while let Some((path, id)) = self.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 if !status_path()?.exists() { + debug!( + "The status path at '{}' does not exists", + status_path()?.display() + ); + } else { + bail!( + "The status path ('{}') is not a symlink but exists!", + status_path()?.display() + ); + } + + symlink(info_json, status_path()?).context("Failed to symlink")?; + + let mut mpv = Command::new("mpv"); + mpv.stdout(stdout()); + mpv.stderr(stderr()); + mpv.args(MPV_FLAGS); + // TODO: Set the title to the name of the video, not the path <2024-02-09> + // mpv.arg(format!("--title=")) + mpv.arg(&path); + + let status = mpv.status().context("Failed to run mpv")?; + if status.success() { + fs::remove_file(&path)?; + if let Some(id) = id { + println!("\x1b[32;1mMarking {} as watched!\x1b[0m", id); + let mut ytcc = std::process::Command::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: '{}'", status); + } else { + warn!("mpv exited with: '{}'", status); + } + } + self.drop()?; + Ok(()) + } +} + +fn download_url(url: &Url) -> Result<PathBuf> { + let output_file = tempfile::NamedTempFile::new().context("Failed to create tempfile")?; + output_file + .as_file() + .set_len(0) + .context("Failed to truncate temp-file")?; + if !Into::<PathBuf>::into(DOWNLOAD_DIR).exists() { + fs::create_dir_all(DOWNLOAD_DIR) + .with_context(|| format!("Failed to create download dir at: {}", DOWNLOAD_DIR))? + } + let mut yt_dlp = Command::new("yt-dlp"); + yt_dlp.current_dir(DOWNLOAD_DIR); + yt_dlp.stdout(stdout()); + yt_dlp.stderr(stderr()); + yt_dlp.args(YT_DLP_FLAGS); + yt_dlp.args([ + "--output", + "%(channel)s/%(title)s.%(ext)s", + url.as_str(), + "--print-to-file", + "after_move:filepath", + ]); + yt_dlp.arg(output_file.path().as_os_str()); + + let status = yt_dlp.status().context("Failed to run yt-dlp")?; + if !status.success() { + error!("yt-dlp execution failed with error: '{}'", status); + } + + let mut path = String::new(); + output_file + .as_file() + .read_to_string(&mut path) + .context("Failed to read output file temp file")?; + let path = path.trim(); + Ok(path.into()) +} diff --git a/old/url.old/mod.rs b/old/url.old/mod.rs new file mode 100644 index 0000000..cff6310 --- /dev/null +++ b/old/url.old/mod.rs @@ -0,0 +1,25 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use anyhow::Result; +use url::Url; + +use self::downloader::{Downloadable, Downloader}; + +mod downloader; + +pub fn download(urls: Vec<Url>) -> Result<()> { + let downloadables = urls + .into_iter() + .map(|url| Downloadable { url, id: None }) + .collect(); + let downloader = Downloader::new(downloadables)?; + downloader.consume() +} diff --git a/old/ytc/main.rs b/old/ytc/main.rs new file mode 100644 index 0000000..e1359f9 --- /dev/null +++ b/old/ytc/main.rs @@ -0,0 +1,85 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{env, process::Command as StdCmd}; + +use anyhow::{bail, Context, Result}; +use clap::Parser; +use log::debug; +use url::Url; +use yt::{ + downloader::{Downloadable, Downloader}, + YtccListData, +}; + +use crate::args::{Args, Command}; + +fn main() -> Result<()> { + let args = Args::parse(); + cli_log::init_cli_log!(); + + let playspec: Vec<Downloadable> = 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", + "--watched", + "--unwatched", + "--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.is_empty() { + 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(Downloadable { + url: Url::parse(&json.url)?, + id: Some(json.id), + }) + } + output + } + Command::Url { urls } => { + let mut output = Vec::with_capacity(urls.len()); + for url in urls { + output.push(Downloadable { + url: Url::parse(&url).context("Failed to parse url")?, + id: None, + }) + } + output + } + }; + + debug!("Initializing downloader"); + let downloader = Downloader::new(playspec)?; + + downloader + .consume() + .context("Failed to consume downloader")?; + + Ok(()) +} diff --git a/old/yts/main.rs b/old/yts/main.rs new file mode 100644 index 0000000..cd4ef35 --- /dev/null +++ b/old/yts/main.rs @@ -0,0 +1,99 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use anyhow::{bail, Context, Result}; +use clap::Parser; +use std::{ + env, + io::{BufRead, BufReader, Write}, + process::Command as StdCmd, +}; +use tempfile::NamedTempFile; +use yt::{constants::HELP_STR, filter_line, YtccListData}; + +use crate::args::{Args, Command, OrderCommand}; + +fn main() -> Result<()> { + let args = Args::parse(); + cli_log::init_cli_log!(); + + let ordering = match args.subcommand.unwrap_or(Command::Order { + command: OrderCommand::Date { + desc: true, + asc: false, + }, + }) { + Command::Order { command } => match command { + OrderCommand::Date { desc, asc } => { + if desc { + vec!["--order-by".into(), "publish_date".into(), "desc".into()] + } else if asc { + vec!["--order-by".into(), "publish_date".into(), "asc".into()] + } else { + vec!["--order-by".into(), "publish_date".into(), "desc".into()] + } + } + OrderCommand::Raw { value } => [vec!["--order-by".into()], value].concat(), + }, + }; + + let json_map = { + let mut ytcc = StdCmd::new("ytcc"); + ytcc.args(["--output", "json", "list"]); + ytcc.args(ordering); + + serde_json::from_slice::<Vec<YtccListData>>( + &ytcc.output().context("Failed to json from ytcc")?.stdout, + ) + .context("Failed to deserialize json output")? + }; + + let mut edit_file = NamedTempFile::new().context("Failed to get tempfile")?; + + json_map.iter().for_each(|line| { + let line = line.to_string(); + edit_file + .write_all(line.as_bytes()) + .expect("This write should not fail"); + }); + + write!(&edit_file, "{}", HELP_STR)?; + edit_file.flush().context("Failed to flush edit file")?; + + let read_file = edit_file.reopen()?; + + let mut nvim = StdCmd::new("nvim"); + nvim.arg(edit_file.path()); + + let status = nvim.status().context("Falied to run nvim")?; + if !status.success() { + bail!("Nvim exited with error status: {}", status) + } + + let mut watching = Vec::new(); + let reader = BufReader::new(&read_file); + for line in reader.lines() { + let line = line.context("Failed to read line")?; + + if let Some(downloadable) = + filter_line(&line).with_context(|| format!("Failed to process line: '{}'", line))? + { + watching.push(downloadable); + } + } + + let watching: String = watching + .iter() + .map(|d| d.to_string()) + .collect::<Vec<String>>() + .join("\n"); + println!("{}", &watching); + Ok(()) +} |