aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/sources/yt/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/sources/yt/src/lib.rs')
-rw-r--r--pkgs/sources/yt/src/lib.rs185
1 files changed, 185 insertions, 0 deletions
diff --git a/pkgs/sources/yt/src/lib.rs b/pkgs/sources/yt/src/lib.rs
new file mode 100644
index 00000000..b089c1a2
--- /dev/null
+++ b/pkgs/sources/yt/src/lib.rs
@@ -0,0 +1,185 @@
+use anyhow::{bail, Context};
+use downloader::Downloadable;
+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<f64>,
+ pub duration: String,
+ pub thumbnail_url: Option<String>,
+ pub extractor_hash: String,
+ pub id: u32,
+ pub playlists: Vec<YtccPlaylistData>,
+}
+
+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::<Vec<String>>()
+ .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,
+ Url,
+}
+
+impl std::str::FromStr for LineCommand {
+ type Err = anyhow::Error;
+ fn from_str(v: &str) -> Result<Self, <Self as std::str::FromStr>::Err> {
+ match v {
+ "pick" | "p" => Ok(Self::Pick),
+ "drop" | "d" => Ok(Self::Drop),
+ "watch" | "w" => Ok(Self::Watch),
+ "url" | "u" => Ok(Self::Url),
+ 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::<u32>().expect("Should be a number") * 60)
+ + buf[1].parse::<u32>().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;
+
+ 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(())
+}
+
+pub fn filter_line(line: &str) -> anyhow::Result<Option<Downloadable>> {
+ // Filter out comments and empty lines
+ if line.starts_with('#') || line.trim().is_empty() {
+ return Ok(None);
+ }
+
+ let line = Line::from(line);
+ match line.cmd {
+ LineCommand::Pick => Ok(None),
+ LineCommand::Drop => ytcc_drop(line.id)
+ .with_context(|| format!("Failed to drop: {}", line.id))
+ .map(|_| None),
+ LineCommand::Watch => Ok(Some(Downloadable {
+ id: Some(line.id),
+ url: line.url,
+ })),
+ LineCommand::Url => {
+ let mut firefox = std::process::Command::new("firefox");
+ firefox.args(["-P", "timesinks.youtube"]);
+ firefox.arg(line.url.as_str());
+ let _handle = firefox.spawn().context("Failed to run firefox")?;
+ Ok(None)
+ }
+ }
+}