cohost posting
This commit is contained in:
parent
6873e14f37
commit
1bc7b0ec2f
|
@ -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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
59
src/main.rs
59
src/main.rs
|
@ -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 {
|
||||||
|
|
|
@ -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()) {
|
22
src/video.rs
22
src/video.rs
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue