cohost posting
This commit is contained in:
		
							parent
							
								
									6873e14f37
								
							
						
					
					
						commit
						1bc7b0ec2f
					
				
					 4 changed files with 109 additions and 48 deletions
				
			
		|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue