use anyhow::{bail, Context}; use serde::Deserialize; use url::Url; pub mod constants; pub mod downloader; #[derive(Deserialize)] pub struct YtccListData { pub url: String, pub title: String, pub description: String, pub publish_date: String, pub watch_date: Option, pub duration: String, pub thumbnail_url: String, pub extractor_hash: String, pub id: u32, pub playlists: Vec, } impl std::fmt::Display for YtccListData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!( f, r#"pick {} "{}" "{}" "{}" "{}" "{}"{}"#, self.id, self.title.replace('"', "'"), self.publish_date, self.playlists .iter() .map(|p| p.name.replace('"', "'")) .collect::>() .join(", "), Duration::from(self.duration.trim()), self.url.replace('"', "'"), "\n" ) } } #[derive(Deserialize)] pub struct YtccPlaylistData { pub name: String, pub url: String, pub reverse: bool, } pub enum LineCommand { Pick, Drop, Watch, } impl std::str::FromStr for LineCommand { type Err = anyhow::Error; fn from_str(v: &str) -> Result::Err> { match v { "pick" | "p" => Ok(Self::Pick), "drop" | "d" => Ok(Self::Drop), "watch" | "w" => Ok(Self::Watch), other => bail!("'{}' is not a recognized command!", other), } } } pub struct Line { pub cmd: LineCommand, pub id: u32, pub url: Url, } /// We expect that each line is correctly formatted, and simply use default ones if they are not impl From<&str> for Line { fn from(v: &str) -> Self { let buf: Vec<_> = v.split_whitespace().collect(); let url: Url = Url::parse( buf.last() .expect("This should always exists") .trim_matches('"'), ) .expect("This parsing should work,as the url is generated"); Line { cmd: buf .get(0) .unwrap_or(&"pick") .parse() .unwrap_or(LineCommand::Pick), id: buf.get(1).unwrap_or(&"0").parse().unwrap_or(0), url, } } } pub struct Duration { time: u32, } impl From<&str> for Duration { fn from(v: &str) -> Self { let buf: Vec<_> = v.split(':').take(2).collect(); Self { time: (buf[0].parse::().expect("Should be a number") * 60) + buf[1].parse::().expect("Should be a number"), } } } impl std::fmt::Display for Duration { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { const SECOND: u32 = 1; const MINUTE: u32 = 60 * SECOND; const HOUR: u32 = 60 * MINUTE; let base_hour = self.time - (self.time % HOUR); let base_min = (self.time % HOUR) - ((self.time % HOUR) % MINUTE); let base_sec = ((self.time % HOUR) % MINUTE) - (((self.time % HOUR) % MINUTE) % SECOND); let h = base_hour / HOUR; let m = base_min / MINUTE; let s = base_sec / SECOND; if self.time == 0 { write!(f, "[No Duration]") } else if h > 0 { write!(f, "[{h}h {m}m]") } else { write!(f, "[{m}m {s}s]") } } } #[cfg(test)] mod test { use crate::Duration; #[test] fn test_display_duration_1h() { let dur = Duration { time: 60 * 60 }; assert_eq!("[1h 0m]".to_owned(), dur.to_string()); } #[test] fn test_display_duration_30min() { let dur = Duration { time: 60 * 30 }; assert_eq!("[30m 0s]".to_owned(), dur.to_string()); } } pub fn ytcc_drop(id: u32) -> anyhow::Result<()> { let mut ytcc = std::process::Command::new("ytcc"); ytcc.args(["mark", &format!("{}", id)]); if !ytcc.status().context("Failed to run ytcc")?.success() { bail!("`ytcc mark {}` failed to execute", id) } Ok(()) }