v1.6.0: more options around retrying after errors
This commit is contained in:
parent
5694c1269e
commit
b0b371a279
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1220,7 +1220,7 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
|||
|
||||
[[package]]
|
||||
name = "screencap-bot"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dotenvy",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
63
src/main.rs
63
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<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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue