aboutsummaryrefslogtreecommitdiffstats
path: root/src/comments/mod.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 10:49:23 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 11:28:43 +0200
commit1debeb77f7986de1b659dcfdc442de6415e1d9f5 (patch)
tree4df3e7c3f6a2d1ec116e4088c5ace7f143a8b05f /src/comments/mod.rs
downloadyt-1debeb77f7986de1b659dcfdc442de6415e1d9f5.zip
chore: Initial Commit
This repository was migrated out of my nixos-config.
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 "))
+ }
+}