v1.6.0: more options around retrying after errors

This commit is contained in:
xenofem 2023-08-06 19:52:52 -04:00
parent 5694c1269e
commit b0b371a279
6 changed files with 94 additions and 63 deletions

2
Cargo.lock generated
View file

@ -1220,7 +1220,7 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "screencap-bot" name = "screencap-bot"
version = "1.5.1" version = "1.6.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dotenvy", "dotenvy",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "screencap-bot" name = "screencap-bot"
version = "1.5.1" version = "1.6.0"
edition = "2021" edition = "2021"
authors = ["xenofem <xenofem@xeno.science>"] authors = ["xenofem <xenofem@xeno.science>"]
license = "MIT" license = "MIT"

View file

@ -52,6 +52,12 @@ directory:
- `SCREENCAP_BOT_18PLUS`: whether posts should be flagged as - `SCREENCAP_BOT_18PLUS`: whether posts should be flagged as
containing 18+ content (default: `false`). this can be overridden containing 18+ content (default: `false`). this can be overridden
for individual shows, see below. 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 - `RUST_LOG`: log levels, for the app as a whole and/or for specific
submodules/libraries. See submodules/libraries. See
[`env_logger`](https://docs.rs/env_logger/latest/env_logger/)'s [`env_logger`](https://docs.rs/env_logger/latest/env_logger/)'s

View file

@ -9,22 +9,36 @@ use std::{
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
pub struct Config { pub struct CaptureConfig {
pub capture_images: bool, pub images: bool,
pub capture_audio_duration: Option<f64>, pub audio: Option<f64>,
pub subtitle_language: Option<String>, pub subtitle_language: Option<String>,
pub audio_language: Option<String>, pub audio_language: Option<String>,
pub shows_file: PathBuf, }
pub struct CohostIdentityConfig {
pub email: String,
pub password: String,
pub page: String,
}
pub struct PostMetadataConfig {
pub global_tags: Vec<String>, pub global_tags: Vec<String>,
pub post_interval: Duration, pub draft: bool,
pub cohost_email: String, pub cw: bool,
pub cohost_password: String,
pub cohost_page: String,
pub cohost_draft: bool,
pub cohost_cw: bool,
pub eighteen_plus: 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<u8>,
}
const VAR_PREFIX: &str = "SCREENCAP_BOT_"; const VAR_PREFIX: &str = "SCREENCAP_BOT_";
fn get_var(name: &str) -> anyhow::Result<Option<String>> { fn get_var(name: &str) -> anyhow::Result<Option<String>> {
@ -108,22 +122,33 @@ pub fn load() -> anyhow::Result<Config> {
} }
Ok(Config { Ok(Config {
capture_images, capture: CaptureConfig {
capture_audio_duration, images: capture_images,
subtitle_language: get_language_code_var("SUBTITLE_LANGUAGE", || { audio: capture_audio_duration,
Some(String::from("eng")) subtitle_language: get_language_code_var("SUBTITLE_LANGUAGE", || {
})?, Some(String::from("eng"))
audio_language: get_language_code_var("AUDIO_LANGUAGE", || None)?, })?,
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")), 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()), post_interval: Duration::from_secs(parse_var("POST_INTERVAL")?.unwrap_or_default()),
cohost_email: require_var("COHOST_EMAIL")?, retry_delay: Duration::from_secs(parse_var("RETRY_DELAY")?.unwrap_or(30)),
cohost_password: require_var("COHOST_PASSWORD")?, max_retries: parse_var::<_, i8>("MAX_RETRIES")?
cohost_page: require_var("COHOST_PAGE")?, .unwrap_or(5)
cohost_draft: parse_var("COHOST_DRAFT")?.unwrap_or(false), .try_into()
cohost_cw: parse_var("COHOST_CW")?.unwrap_or(capture_images), .ok(),
eighteen_plus: parse_var("18PLUS")?.unwrap_or(false),
}) })
} }

View file

@ -2,7 +2,6 @@ use std::time::Duration;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use config::Config; use config::Config;
use lazy_static::lazy_static;
use log::{debug, error, info}; use log::{debug, error, info};
use rand::{ use rand::{
distributions::{Distribution, Standard, WeightedIndex}, distributions::{Distribution, Standard, WeightedIndex},
@ -15,10 +14,6 @@ mod config;
mod media; mod media;
mod shows; mod shows;
lazy_static! {
static ref RETRY_INTERVAL: Duration = Duration::from_secs(30);
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
@ -43,28 +38,36 @@ async fn main() -> anyhow::Result<()> {
let dist = WeightedIndex::new(shows.iter().map(|s| s.weight)) let dist = WeightedIndex::new(shows.iter().map(|s| s.weight))
.context("Failed to load show weights")?; .context("Failed to load show weights")?;
info!("Logging into cohost as {}", conf.cohost_email); info!("Logging into cohost as {}", conf.cohost.email);
let session = eggbug::Session::login(&conf.cohost_email, &conf.cohost_password) let session = eggbug::Session::login(&conf.cohost.email, &conf.cohost.password)
.await .await
.context("Failed to login to cohost")?; .context("Failed to login to cohost")?;
let mut retry_count = 0;
loop { loop {
let result = post_random_capture(&conf, &shows, &session, &dist, &mut rng) let result = post_random_capture(&conf, &shows, &session, &dist, &mut rng)
.await .await
.context("Failed to post a random capture"); .context("Failed to post a random capture");
if conf.post_interval == Duration::ZERO { match result {
return result; Ok(()) => {
} if conf.post_interval == Duration::ZERO {
return result;
let delay = match result { }
Ok(()) => conf.post_interval, retry_count = 0;
Err(e) => { tokio::time::sleep(conf.post_interval).await;
error!("{}", e);
*RETRY_INTERVAL
} }
}; Err(ref e) => {
tokio::time::sleep(delay).await; 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<R: Rng>(
info!("Selected: {} - {}", descriptor, file.display()); info!("Selected: {} - {}", descriptor, file.display());
let media_info = media::get_media_info( let media_info = media::get_media_info(file, &conf.capture)
file, .with_context(|| format!("Failed to get info for media file {}", file.display()))?;
conf.subtitle_language.as_deref(),
conf.audio_language.as_deref(),
)
.with_context(|| format!("Failed to get info for media file {}", file.display()))?;
debug!( debug!(
"Media duration: {}", "Media duration: {}",
format_timestamp(media_info.duration_secs, None, true) format_timestamp(media_info.duration_secs, None, true)
@ -104,7 +103,7 @@ async fn post_random_capture<R: Rng>(
media_info.subtitle_stream_index 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, Some(d) => media_info.duration_secs - d,
None => media_info.duration_secs, None => media_info.duration_secs,
}; };
@ -123,7 +122,7 @@ async fn post_random_capture<R: Rng>(
let mut attachments = Vec::new(); let mut attachments = Vec::new();
let display_timestamp = format_timestamp(timestamp, Some(media_info.duration_secs), false); 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) let image_data = media::take_screencap(file, timestamp, media_info.subtitle_stream_index)
.await .await
.context("Failed to take screencap")?; .context("Failed to take screencap")?;
@ -148,7 +147,7 @@ async fn post_random_capture<R: Rng>(
attachments.push(image_attachment); attachments.push(image_attachment);
} }
if let Some(duration) = conf.capture_audio_duration { if let Some(duration) = conf.capture.audio {
let audio_data = let audio_data =
media::take_audio_clip(file, timestamp, duration, media_info.audio_stream_index) media::take_audio_clip(file, timestamp, duration, media_info.audio_stream_index)
.await .await
@ -168,21 +167,21 @@ async fn post_random_capture<R: Rng>(
} }
let mut tags = show.tags.clone(); 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 let chost_id = session
.create_post( .create_post(
&conf.cohost_page, &conf.cohost.page,
&mut eggbug::Post { &mut eggbug::Post {
content_warnings: if conf.cohost_cw { content_warnings: if conf.metadata.cw {
vec![descriptor] vec![descriptor]
} else { } else {
vec![] vec![]
}, },
attachments, attachments,
tags, tags,
draft: conf.cohost_draft, draft: conf.metadata.draft,
adult_content: show.eighteen_plus.unwrap_or(conf.eighteen_plus), adult_content: show.eighteen_plus.unwrap_or(conf.metadata.eighteen_plus),
headline: String::new(), headline: String::new(),
markdown: String::new(), markdown: String::new(),
metadata: None, metadata: None,

View file

@ -12,6 +12,8 @@ use tokio::{fs, process::Command};
use std::path::Path; use std::path::Path;
use crate::config::CaptureConfig;
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();
} }
@ -39,15 +41,14 @@ fn indexed_streams(
pub fn get_media_info<P: AsRef<Path>>( pub fn get_media_info<P: AsRef<Path>>(
source: &P, source: &P,
subtitle_lang: Option<&str>, capture_config: &CaptureConfig,
audio_lang: Option<&str>,
) -> anyhow::Result<MediaInfo> { ) -> anyhow::Result<MediaInfo> {
let ctx = input(source).context("Failed to load media file")?; 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); let duration_secs = ctx.duration() as f64 / f64::from(ffmpeg_next::ffi::AV_TIME_BASE);
debug!("{:?}", ctx.metadata()); 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) indexed_streams(&ctx, Type::Subtitle)
.filter(|(_, stream)| { .filter(|(_, stream)| {
let metadata = stream.metadata(); let metadata = stream.metadata();
@ -67,7 +68,7 @@ pub fn get_media_info<P: AsRef<Path>>(
.map(|(idx, _)| idx) .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) indexed_streams(&ctx, Type::Audio)
.find(|(_, stream)| stream.metadata().get("language") == Some(lang)) .find(|(_, stream)| stream.metadata().get("language") == Some(lang))
.map(|(idx, _)| idx) .map(|(idx, _)| idx)