about summary refs log tree commit diff stats
path: root/src/comments/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/comments/mod.rs')
-rw-r--r--src/comments/mod.rs197
1 files changed, 197 insertions, 0 deletions
diff --git a/src/comments/mod.rs b/src/comments/mod.rs
new file mode 100644
index 0000000..eba391e
--- /dev/null
+++ b/src/comments/mod.rs
@@ -0,0 +1,197 @@
+// 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,
+    fs::{self},
+    io::Write,
+    mem,
+    path::PathBuf,
+    process::{Command, Stdio},
+};
+
+use anyhow::{bail, Context, Result};
+use comment::{CommentExt, Comments};
+use regex::Regex;
+use yt_dlp::wrapper::info_json::{Comment, InfoJson, Parent};
+
+use crate::{
+    app::App,
+    storage::video_database::{
+        getters::{get_currently_playing_video, get_video_info_json},
+        Video,
+    },
+};
+
+mod comment;
+mod display;
+
+fn get_runtime_path(component: &'static str) -> anyhow::Result<PathBuf> {
+    let out: PathBuf = format!(
+        "{}/{}",
+        env::var("XDG_RUNTIME_DIR").expect("This should always exist"),
+        component
+    )
+    .into();
+    fs::create_dir_all(out.parent().expect("Parent should exist"))?;
+    Ok(out)
+}
+
+const STATUS_PATH: &str = "ytcc/running";
+pub fn status_path() -> anyhow::Result<PathBuf> {
+    get_runtime_path(STATUS_PATH)
+}
+
+pub async fn get_comments(app: &App) -> Result<Comments> {
+    let currently_playing_video: Video =
+        if let Some(video) = get_currently_playing_video(&app).await? {
+            video
+        } else {
+            bail!("Could not find a currently playing video!");
+        };
+
+    let mut info_json: InfoJson = get_video_info_json(&currently_playing_video)
+        .await?
+        .expect("A currently *playing* must be cached. And thus the info.json should be available");
+
+    let base_comments = mem::take(&mut info_json.comments).expect("A video should have comments");
+    drop(info_json);
+
+    let mut comments = Comments::new();
+    base_comments.into_iter().for_each(|c| {
+        if let Parent::Id(id) = &c.parent {
+            comments.insert(&(id.clone()), CommentExt::from(c));
+        } else {
+            comments.push(CommentExt::from(c));
+        }
+    });
+
+    comments.vec.iter_mut().for_each(|comment| {
+       let replies = mem::take(&mut comment.replies);
+       let mut output_replies: Vec<CommentExt>  = vec![];
+
+       let re = Regex::new(r"\u{200b}?(@[^\t\s]+)\u{200b}?").unwrap();
+       for reply in replies {
+           if let Some(replyee_match) =  re.captures(&reply.value.text){
+               let full_match = replyee_match.get(0).expect("This always exists");
+               let text = reply.
+                   value.
+                   text[0..full_match.start()]
+                   .to_owned()
+                   +
+                   &reply
+                   .value
+                   .text[full_match.end()..];
+               let text: &str = text.trim().trim_matches('\u{200b}');
+
+               let replyee = replyee_match.get(1).expect("This should exist").as_str();
+
+
+               if let Some(parent) = output_replies
+                   .iter_mut()
+                   // .rev()
+                   .flat_map(|com| &mut com.replies)
+                   .flat_map(|com| &mut com.replies)
+                   .flat_map(|com| &mut com.replies)
+                   .filter(|com| com.value.author == replyee)
+                   .last()
+               {
+                   parent.replies.push(CommentExt::from(Comment {
+                       text: text.to_owned(),
+                       ..reply.value
+                   }))
+               } else if let Some(parent) = output_replies
+                   .iter_mut()
+                   // .rev()
+                   .flat_map(|com| &mut com.replies)
+                   .flat_map(|com| &mut com.replies)
+                   .filter(|com| com.value.author == replyee)
+                   .last()
+               {
+                   parent.replies.push(CommentExt::from(Comment {
+                       text: text.to_owned(),
+                       ..reply.value
+                   }))
+               } else if let Some(parent) = output_replies
+                   .iter_mut()
+                   // .rev()
+                   .flat_map(|com| &mut com.replies)
+                   .filter(|com| com.value.author == replyee)
+                   .last()
+               {
+                   parent.replies.push(CommentExt::from(Comment {
+                       text: text.to_owned(),
+                       ..reply.value
+                   }))
+               } else if let Some(parent) = output_replies.iter_mut()
+                   // .rev()
+                   .filter(|com| com.value.author == replyee)
+                   .last()
+               {
+                   parent.replies.push(CommentExt::from(Comment {
+                       text: text.to_owned(),
+                       ..reply.value
+                   }))
+               } else {
+                   eprintln!(
+                   "Failed to find a parent for ('{}') both directly and via replies! The reply text was:\n'{}'\n",
+                   replyee,
+                   reply.value.text
+               );
+                   output_replies.push(reply);
+               }
+           } else {
+               output_replies.push(reply);
+           }
+       }
+       comment.replies = output_replies;
+    });
+
+    Ok(comments)
+}
+
+pub async fn comments(app: &App) -> Result<()> {
+    let comments = get_comments(app).await?;
+
+    let mut less = Command::new("less")
+        .args(["--raw-control-chars"])
+        .stdin(Stdio::piped())
+        .stderr(Stdio::inherit())
+        .spawn()
+        .context("Failed to run less")?;
+
+    let mut child = Command::new("fmt")
+        .args(["--uniform-spacing", "--split-only", "--width=90"])
+        .stdin(Stdio::piped())
+        .stderr(Stdio::inherit())
+        .stdout(less.stdin.take().expect("Should be open"))
+        .spawn()
+        .context("Failed to run fmt")?;
+
+    let mut stdin = child.stdin.take().context("Failed to open stdin")?;
+    std::thread::spawn(move || {
+        stdin
+            .write_all(comments.render(true).as_bytes())
+            .expect("Should be able to write to stdin of fmt");
+    });
+
+    let _ = less.wait().context("Failed to await less")?;
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod test {
+    #[test]
+    fn test_string_replacement() {
+        let s = "A \n\nB\n\nC".to_owned();
+        assert_eq!("A \n  \n  B\n  \n  C", s.replace('\n', "\n  "))
+    }
+}