From b0b371a279a54fdb8040cb41f9a20de0e50ca4db Mon Sep 17 00:00:00 2001 From: xenofem Date: Sun, 6 Aug 2023 19:52:52 -0400 Subject: [PATCH] v1.6.0: more options around retrying after errors --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 6 +++++ src/config.rs | 75 ++++++++++++++++++++++++++++++++++----------------- src/main.rs | 63 +++++++++++++++++++++---------------------- src/media.rs | 9 ++++--- 6 files changed, 94 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa3e92e..52fc94f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,7 +1220,7 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "screencap-bot" -version = "1.5.1" +version = "1.6.0" dependencies = [ "anyhow", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 508eac1..38f9b45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "screencap-bot" -version = "1.5.1" +version = "1.6.0" edition = "2021" authors = ["xenofem "] license = "MIT" diff --git a/README.md b/README.md index 4878ebb..a1809e7 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,12 @@ directory: - `SCREENCAP_BOT_18PLUS`: whether posts should be flagged as containing 18+ content (default: `false`). this can be overridden for individual shows, see below. +- `SCREENCAP_BOT_RETRY_DELAY`: if taking a capture or creating a post + fails, wait this number of seconds before retrying (default: 30) +- `SCREENCAP_BOT_MAX_RETRIES`: maximum number of consecutive times to + retry after a failure before exiting with an error. This can be set + to 0 to never retry, or set to -1 to retry indefinitely. (default: + 5) - `RUST_LOG`: log levels, for the app as a whole and/or for specific submodules/libraries. See [`env_logger`](https://docs.rs/env_logger/latest/env_logger/)'s diff --git a/src/config.rs b/src/config.rs index 08f68bb..ad114f9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,22 +9,36 @@ use std::{ use anyhow::{anyhow, Context}; -pub struct Config { - pub capture_images: bool, - pub capture_audio_duration: Option, +pub struct CaptureConfig { + pub images: bool, + pub audio: Option, pub subtitle_language: Option, pub audio_language: Option, - pub shows_file: PathBuf, +} + +pub struct CohostIdentityConfig { + pub email: String, + pub password: String, + pub page: String, +} + +pub struct PostMetadataConfig { pub global_tags: Vec, - pub post_interval: Duration, - pub cohost_email: String, - pub cohost_password: String, - pub cohost_page: String, - pub cohost_draft: bool, - pub cohost_cw: bool, + pub draft: bool, + pub cw: bool, pub eighteen_plus: bool, } +pub struct Config { + pub capture: CaptureConfig, + pub cohost: CohostIdentityConfig, + pub metadata: PostMetadataConfig, + pub shows_file: PathBuf, + pub post_interval: Duration, + pub retry_delay: Duration, + pub max_retries: Option, +} + const VAR_PREFIX: &str = "SCREENCAP_BOT_"; fn get_var(name: &str) -> anyhow::Result> { @@ -108,22 +122,33 @@ pub fn load() -> anyhow::Result { } Ok(Config { - capture_images, - capture_audio_duration, - subtitle_language: get_language_code_var("SUBTITLE_LANGUAGE", || { - Some(String::from("eng")) - })?, - audio_language: get_language_code_var("AUDIO_LANGUAGE", || None)?, + capture: CaptureConfig { + images: capture_images, + audio: capture_audio_duration, + subtitle_language: get_language_code_var("SUBTITLE_LANGUAGE", || { + Some(String::from("eng")) + })?, + audio_language: get_language_code_var("AUDIO_LANGUAGE", || None)?, + }, + cohost: CohostIdentityConfig { + email: require_var("COHOST_EMAIL")?, + password: require_var("COHOST_PASSWORD")?, + page: require_var("COHOST_PAGE")?, + }, + metadata: PostMetadataConfig { + global_tags: get_var("GLOBAL_TAGS")? + .map(|s| s.split(',').map(String::from).collect()) + .unwrap_or_default(), + draft: parse_var("COHOST_DRAFT")?.unwrap_or(false), + cw: parse_var("COHOST_CW")?.unwrap_or(capture_images), + eighteen_plus: parse_var("18PLUS")?.unwrap_or(false), + }, shows_file: parse_var("SHOWS_FILE")?.unwrap_or_else(|| PathBuf::from("./shows.yaml")), - 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_default()), - cohost_email: require_var("COHOST_EMAIL")?, - cohost_password: require_var("COHOST_PASSWORD")?, - cohost_page: require_var("COHOST_PAGE")?, - cohost_draft: parse_var("COHOST_DRAFT")?.unwrap_or(false), - cohost_cw: parse_var("COHOST_CW")?.unwrap_or(capture_images), - eighteen_plus: parse_var("18PLUS")?.unwrap_or(false), + retry_delay: Duration::from_secs(parse_var("RETRY_DELAY")?.unwrap_or(30)), + max_retries: parse_var::<_, i8>("MAX_RETRIES")? + .unwrap_or(5) + .try_into() + .ok(), }) } diff --git a/src/main.rs b/src/main.rs index 685b107..fc832c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ use std::time::Duration; use anyhow::{anyhow, Context}; use config::Config; -use lazy_static::lazy_static; use log::{debug, error, info}; use rand::{ distributions::{Distribution, Standard, WeightedIndex}, @@ -15,10 +14,6 @@ mod config; mod media; mod shows; -lazy_static! { - static ref RETRY_INTERVAL: Duration = Duration::from_secs(30); -} - #[tokio::main] async fn main() -> anyhow::Result<()> { dotenvy::dotenv().ok(); @@ -43,28 +38,36 @@ async fn main() -> anyhow::Result<()> { let dist = WeightedIndex::new(shows.iter().map(|s| s.weight)) .context("Failed to load show weights")?; - info!("Logging into cohost as {}", conf.cohost_email); - let session = eggbug::Session::login(&conf.cohost_email, &conf.cohost_password) + info!("Logging into cohost as {}", conf.cohost.email); + let session = eggbug::Session::login(&conf.cohost.email, &conf.cohost.password) .await .context("Failed to login to cohost")?; + let mut retry_count = 0; + loop { let result = post_random_capture(&conf, &shows, &session, &dist, &mut rng) .await .context("Failed to post a random capture"); - if conf.post_interval == Duration::ZERO { - return result; - } - - let delay = match result { - Ok(()) => conf.post_interval, - Err(e) => { - error!("{}", e); - *RETRY_INTERVAL + match result { + Ok(()) => { + if conf.post_interval == Duration::ZERO { + return result; + } + retry_count = 0; + tokio::time::sleep(conf.post_interval).await; } - }; - tokio::time::sleep(delay).await; + Err(ref e) => { + error!("{:#}", e); + if conf.max_retries.map(|m| retry_count < m) == Some(false) { + return result.context("Retry limit exceeded"); + } + info!("retrying in {} seconds", conf.retry_delay.as_secs()); + retry_count += 1; + tokio::time::sleep(conf.retry_delay).await; + } + } } } @@ -89,12 +92,8 @@ async fn post_random_capture( info!("Selected: {} - {}", descriptor, file.display()); - let media_info = media::get_media_info( - file, - conf.subtitle_language.as_deref(), - conf.audio_language.as_deref(), - ) - .with_context(|| format!("Failed to get info for media file {}", file.display()))?; + let media_info = media::get_media_info(file, &conf.capture) + .with_context(|| format!("Failed to get info for media file {}", file.display()))?; debug!( "Media duration: {}", format_timestamp(media_info.duration_secs, None, true) @@ -104,7 +103,7 @@ async fn post_random_capture( media_info.subtitle_stream_index ); - let max_timestamp = match conf.capture_audio_duration { + let max_timestamp = match conf.capture.audio { Some(d) => media_info.duration_secs - d, None => media_info.duration_secs, }; @@ -123,7 +122,7 @@ async fn post_random_capture( let mut attachments = Vec::new(); let display_timestamp = format_timestamp(timestamp, Some(media_info.duration_secs), false); - if conf.capture_images { + if conf.capture.images { let image_data = media::take_screencap(file, timestamp, media_info.subtitle_stream_index) .await .context("Failed to take screencap")?; @@ -148,7 +147,7 @@ async fn post_random_capture( attachments.push(image_attachment); } - if let Some(duration) = conf.capture_audio_duration { + if let Some(duration) = conf.capture.audio { let audio_data = media::take_audio_clip(file, timestamp, duration, media_info.audio_stream_index) .await @@ -168,21 +167,21 @@ async fn post_random_capture( } let mut tags = show.tags.clone(); - tags.extend_from_slice(&conf.global_tags); + tags.extend_from_slice(&conf.metadata.global_tags); let chost_id = session .create_post( - &conf.cohost_page, + &conf.cohost.page, &mut eggbug::Post { - content_warnings: if conf.cohost_cw { + content_warnings: if conf.metadata.cw { vec![descriptor] } else { vec![] }, attachments, tags, - draft: conf.cohost_draft, - adult_content: show.eighteen_plus.unwrap_or(conf.eighteen_plus), + draft: conf.metadata.draft, + adult_content: show.eighteen_plus.unwrap_or(conf.metadata.eighteen_plus), headline: String::new(), markdown: String::new(), metadata: None, diff --git a/src/media.rs b/src/media.rs index 867690e..1cb95bc 100644 --- a/src/media.rs +++ b/src/media.rs @@ -12,6 +12,8 @@ use tokio::{fs, process::Command}; use std::path::Path; +use crate::config::CaptureConfig; + lazy_static! { static ref SUBTITLE_FORBID_REGEX: Regex = Regex::new("(?i)sign|song").unwrap(); } @@ -39,15 +41,14 @@ fn indexed_streams( pub fn get_media_info>( source: &P, - subtitle_lang: Option<&str>, - audio_lang: Option<&str>, + capture_config: &CaptureConfig, ) -> anyhow::Result { let ctx = input(source).context("Failed to load media file")?; let duration_secs = ctx.duration() as f64 / f64::from(ffmpeg_next::ffi::AV_TIME_BASE); debug!("{:?}", ctx.metadata()); - let subtitle_stream_index = subtitle_lang.and_then(|lang| { + let subtitle_stream_index = capture_config.subtitle_language.as_ref().and_then(|lang| { indexed_streams(&ctx, Type::Subtitle) .filter(|(_, stream)| { let metadata = stream.metadata(); @@ -67,7 +68,7 @@ pub fn get_media_info>( .map(|(idx, _)| idx) }); - let audio_stream_index = audio_lang.and_then(|lang| { + let audio_stream_index = capture_config.audio_language.as_ref().and_then(|lang| { indexed_streams(&ctx, Type::Audio) .find(|(_, stream)| stream.metadata().get("language") == Some(lang)) .map(|(idx, _)| idx)