// 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::{
io::Write,
mem,
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;
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 "))
}
}