// 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(¤tly_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 ")) } }