cohost posting

This commit is contained in:
xenofem 2023-06-30 15:51:06 -04:00
parent 6873e14f37
commit 1bc7b0ec2f
4 changed files with 109 additions and 48 deletions

View file

@ -1,17 +1,35 @@
use std::{collections::HashMap, env, fs, path::PathBuf}; use std::{env::{self, VarError}, path::PathBuf, str::FromStr, fmt::Debug, time::Duration};
pub type Shows = HashMap<String, PathBuf>;
pub struct Config { pub struct Config {
pub shows: Shows, pub shows_file: PathBuf,
pub global_tags: Vec<String>,
pub post_interval: Duration,
pub cohost_email: String,
pub cohost_password: String,
pub cohost_page: String,
}
const VAR_PREFIX: &'static str = "SCREENCAP_BOT_";
fn get_var(name: &str) -> Result<String, VarError> {
env::var(VAR_PREFIX.to_string() + name)
}
fn parse_var<E: Debug, T: FromStr<Err = E>>(name: &str) -> Result<T, VarError> {
get_var(name).map(|s| s.parse().expect(&format!("Failed to parse {}{}", VAR_PREFIX, name)))
}
fn expect_var(name: &str) -> String {
get_var(name).expect(&format!("{}{} must be set", VAR_PREFIX, name))
} }
pub fn load() -> Config { pub fn load() -> Config {
let shows_file = env::var("SCREENCAP_BOT_SHOWS_FILE").unwrap_or(String::from("./shows.yaml"));
Config { Config {
shows: serde_yaml::from_reader( shows_file: parse_var("SHOWS_FILE").unwrap_or(PathBuf::from("./shows.yaml")),
fs::File::open(shows_file).expect("Failed to open shows file"), global_tags: get_var("GLOBAL_TAGS").map(|s| s.split(",").map(String::from).collect()).unwrap_or_default(),
) post_interval: Duration::from_secs(parse_var("POST_INTERVAL").unwrap_or(6*3600)),
.expect("Failed to parse YAML from shows file"), cohost_email: expect_var("COHOST_EMAIL"),
cohost_password: expect_var("COHOST_PASSWORD"),
cohost_page: expect_var("COHOST_PAGE"),
} }
} }

View file

@ -1,34 +1,36 @@
use std::path::Path;
use enumeration::enumerate_shows;
use log::{debug, error, info}; use log::{debug, error, info};
use rand::{distributions::Standard, seq::IteratorRandom, Rng}; use rand::{distributions::Standard, seq::IteratorRandom, Rng};
mod config; mod config;
mod enumeration; mod shows;
mod video; mod video;
#[tokio::main] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() {
dotenvy::dotenv().ok();
env_logger::init(); env_logger::init();
ffmpeg_next::init().unwrap(); ffmpeg_next::init().unwrap();
dotenvy::dotenv().ok();
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let conf = config::load(); let conf = config::load();
let enumerated_shows = enumerate_shows(conf.shows);
info!("Loading shows from {}", conf.shows_file.display());
let shows = shows::load(conf.shows_file);
info!("Logging into cohost as {}", conf.cohost_email);
let session = eggbug::Session::login(&conf.cohost_email, &conf.cohost_password).await.expect("Failed to login to cohost");
loop { loop {
let (show, episodes) = enumerated_shows let (title, show) = shows
.iter() .iter()
.choose(&mut rng) .choose(&mut rng)
.expect("No shows found!"); .expect("No shows found!");
let (num, file) = episodes.iter().choose(&mut rng).unwrap(); let (num, file) = show.episodes.iter().choose(&mut rng).unwrap();
let descriptor = format!( let descriptor = format!(
"{}{}", "{}{}",
show, title,
if let Some(n) = num { if let Some(n) = num {
format!(" episode {}", n) format!(" episode {}", n)
} else { } else {
@ -51,18 +53,43 @@ async fn main() -> std::io::Result<()> {
let timestamp = video_info.duration_secs * rng.sample::<f64, _>(Standard); let timestamp = video_info.duration_secs * rng.sample::<f64, _>(Standard);
let formatted_timestamp = format_timestamp(timestamp, Some(video_info.duration_secs)); let formatted_timestamp = format_timestamp(timestamp, Some(video_info.duration_secs));
info!("Taking screencap at {}", formatted_timestamp); info!("Taking screencap at {}", formatted_timestamp);
if let Err(e) = video::take_screencap(
match video::take_screencap(
file, file,
timestamp, timestamp,
video_info.subtitle_stream_index, video_info.subtitle_stream_index,
&Path::new("./out.png"), ).await {
) { Err(e) => { error!("Failed to take screencap: {}", e); }
error!("Failed to take screencap: {}", e); Ok(img_data) => {
let attachment = eggbug::Attachment::new(
img_data,
format!("{} @{}.png", descriptor, formatted_timestamp),
String::from("image/png"),
).with_alt_text(format!("Screencap of {} at {}", descriptor, formatted_timestamp));
let mut tags = show.tags.clone();
tags.extend_from_slice(&conf.global_tags);
match session.create_post(
&conf.cohost_page,
&mut eggbug::Post {
content_warnings: vec![descriptor],
attachments: vec![attachment],
tags,
draft: false,
adult_content: false,
headline: String::new(),
markdown: String::new(),
}
).await {
Ok(id) => info!("Created post {}", id),
Err(e) => error!("Failed to create post: {}", e)
}
}
} }
break; tokio::time::sleep(conf.post_interval).await;
} }
Ok(())
} }
fn format_timestamp(timestamp: f64, total_duration: Option<f64>) -> String { fn format_timestamp(timestamp: f64, total_duration: Option<f64>) -> String {

View file

@ -8,35 +8,47 @@ use std::{
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::{debug, error, trace, warn}; use log::{debug, error, trace, warn};
use regex::Regex; use regex::Regex;
use serde::Deserialize;
use crate::config::Shows;
lazy_static! { lazy_static! {
static ref NUMBER_REGEX: Regex = Regex::new("[0-9]+").unwrap(); static ref NUMBER_REGEX: Regex = Regex::new("[0-9]+").unwrap();
} }
#[derive(Deserialize)]
pub struct ShowSpec {
pub path: PathBuf,
pub tags: Vec<String>,
}
/// [episode number, or None for a standalone movie] -> [video file path] /// [episode number, or None for a standalone movie] -> [video file path]
type Enumeration = HashMap<Option<u32>, PathBuf>; type Episodes = HashMap<Option<u32>, PathBuf>;
/// [show name] -> [enumeration] pub struct Show {
type EnumeratedShows = HashMap<String, Enumeration>; pub episodes: Episodes,
pub tags: Vec<String>,
}
pub fn enumerate_shows(shows: Shows) -> EnumeratedShows { pub fn load(shows_file: PathBuf) -> HashMap<String, Show> {
shows let show_specs: HashMap<String, ShowSpec> = serde_yaml::from_reader(
fs::File::open(shows_file).expect("Failed to open shows file"),
)
.expect("Failed to parse YAML from shows file");
show_specs
.into_iter() .into_iter()
.filter_map(|(name, path)| { .filter_map(|(name, show)| {
debug!("Enumerating {} from {}", name, path.display()); debug!("Enumerating show {}: {}", name, show.path.display());
enumerate_show(path) enumerate_show(show.path)
.map_err(|e| { .map_err(|e| {
error!("Error processing {}: {}", name, e); error!("Error processing {}: {}", name, e);
}) })
.ok() .ok()
.map(|en| (name, en)) .map(|eps| (name, Show { episodes: eps, tags: show.tags }))
}) })
.collect() .collect()
} }
fn enumerate_show(path: PathBuf) -> std::io::Result<Enumeration> { fn enumerate_show(path: PathBuf) -> std::io::Result<Episodes> {
let metadata = fs::metadata(&path)?; let metadata = fs::metadata(&path)?;
if metadata.is_file() { if metadata.is_file() {
debug!("{} is a file, standalone", path.display()); debug!("{} is a file, standalone", path.display());
@ -52,7 +64,7 @@ fn enumerate_show(path: PathBuf) -> std::io::Result<Enumeration> {
} }
} }
fn enumerate_dir(path: PathBuf) -> std::io::Result<Enumeration> { fn enumerate_dir(path: PathBuf) -> std::io::Result<Episodes> {
let files: Vec<(PathBuf, String)> = fs::read_dir(&path)? let files: Vec<(PathBuf, String)> = fs::read_dir(&path)?
.map(|entry| { .map(|entry| {
let entry = entry?; let entry = entry?;
@ -108,7 +120,7 @@ fn enumerate_dir(path: PathBuf) -> std::io::Result<Enumeration> {
"No valid prefixes found", "No valid prefixes found",
))?; ))?;
let mut result: Enumeration = HashMap::new(); let mut result: Episodes = HashMap::new();
for (num, path) in best_enumeration.into_iter() { for (num, path) in best_enumeration.into_iter() {
debug!("Episode {}: {}", num, path.display()); debug!("Episode {}: {}", num, path.display());
if let Some(dup) = result.insert(Some(num), path.to_path_buf()) { if let Some(dup) = result.insert(Some(num), path.to_path_buf()) {

View file

@ -5,8 +5,9 @@ use ffmpeg_next::{
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use tempfile::tempdir; use tempfile::tempdir;
use tokio::{fs, process::Command};
use std::{ffi::OsStr, io::ErrorKind, os::unix::fs, path::Path, process::Command}; use std::{io::ErrorKind, path::Path};
lazy_static! { lazy_static! {
static ref SUBTITLE_FORBID_REGEX: Regex = Regex::new("(?i)sign|song").unwrap(); static ref SUBTITLE_FORBID_REGEX: Regex = Regex::new("(?i)sign|song").unwrap();
@ -60,12 +61,11 @@ pub fn get_video_info<P: AsRef<Path>>(
}) })
} }
pub fn take_screencap<P: AsRef<Path>, Q: AsRef<OsStr>>( pub async fn take_screencap<P: AsRef<Path>>(
source: &P, source: &P,
timestamp_secs: f64, timestamp_secs: f64,
subtitle_stream_index: Option<usize>, subtitle_stream_index: Option<usize>,
dest: &Q, ) -> std::io::Result<Vec<u8>> {
) -> std::io::Result<()> {
let ext = source.as_ref().extension().map(|s| s.to_str()).flatten(); let ext = source.as_ref().extension().map(|s| s.to_str()).flatten();
if ext != Some("mkv") && ext != Some("mp4") { if ext != Some("mkv") && ext != Some("mp4") {
return Err(std::io::Error::new( return Err(std::io::Error::new(
@ -73,9 +73,12 @@ pub fn take_screencap<P: AsRef<Path>, Q: AsRef<OsStr>>(
"unexpected file extension", "unexpected file extension",
)); ));
} }
let tmp_dir = tempdir()?; let tmp_dir = tempdir()?;
let link_path = tmp_dir.path().join(format!("input.{}", ext.unwrap())); let link_path = tmp_dir.path().join(format!("in.{}", ext.unwrap()));
fs::symlink(source, &link_path)?; fs::symlink(source, &link_path).await?;
let dest_path = tmp_dir.path().join("out.png");
let mut cmd = Command::new("ffmpeg"); let mut cmd = Command::new("ffmpeg");
cmd.arg("-ss") cmd.arg("-ss")
@ -95,13 +98,14 @@ pub fn take_screencap<P: AsRef<Path>, Q: AsRef<OsStr>>(
cmd.args(["-vframes", "1"]) cmd.args(["-vframes", "1"])
.args(["-loglevel", "quiet"]) .args(["-loglevel", "quiet"])
.arg("-y") .arg("-y")
.arg(&dest); .arg(&dest_path);
if !cmd.status()?.success() { if !cmd.status().await?.success() {
return Err(std::io::Error::new( return Err(std::io::Error::new(
ErrorKind::Other, ErrorKind::Other,
"ffmpeg command failed", "ffmpeg command failed",
)); ));
} }
Ok(())
fs::read(&dest_path).await
} }