v1.6.0: more options around retrying after errors

main
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]]
name = "screencap-bot"
version = "1.5.1"
version = "1.6.0"
dependencies = [
"anyhow",
"dotenvy",

View File

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

View File

@ -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

View File

@ -9,22 +9,36 @@ use std::{
use anyhow::{anyhow, Context};
pub struct Config {
pub capture_images: bool,
pub capture_audio_duration: Option<f64>,
pub struct CaptureConfig {
pub images: bool,
pub audio: Option<f64>,
pub subtitle_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 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<u8>,
}
const VAR_PREFIX: &str = "SCREENCAP_BOT_";
fn get_var(name: &str) -> anyhow::Result<Option<String>> {
@ -108,22 +122,33 @@ pub fn load() -> anyhow::Result<Config> {
}
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(),
})
}

View File

@ -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<R: Rng>(
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<R: Rng>(
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<R: Rng>(
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<R: Rng>(
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<R: Rng>(
}
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,

View File

@ -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<P: AsRef<Path>>(
source: &P,
subtitle_lang: Option<&str>,
audio_lang: Option<&str>,
capture_config: &CaptureConfig,
) -> anyhow::Result<MediaInfo> {
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<P: AsRef<Path>>(
.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)