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]]
|
[[package]]
|
||||||
name = "screencap-bot"
|
name = "screencap-bot"
|
||||||
version = "1.5.1"
|
version = "1.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
audio: capture_audio_duration,
|
||||||
subtitle_language: get_language_code_var("SUBTITLE_LANGUAGE", || {
|
subtitle_language: get_language_code_var("SUBTITLE_LANGUAGE", || {
|
||||||
Some(String::from("eng"))
|
Some(String::from("eng"))
|
||||||
})?,
|
})?,
|
||||||
audio_language: get_language_code_var("AUDIO_LANGUAGE", || None)?,
|
audio_language: get_language_code_var("AUDIO_LANGUAGE", || None)?,
|
||||||
shows_file: parse_var("SHOWS_FILE")?.unwrap_or_else(|| PathBuf::from("./shows.yaml")),
|
},
|
||||||
|
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")?
|
global_tags: get_var("GLOBAL_TAGS")?
|
||||||
.map(|s| s.split(',').map(String::from).collect())
|
.map(|s| s.split(',').map(String::from).collect())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
post_interval: Duration::from_secs(parse_var("POST_INTERVAL")?.unwrap_or_default()),
|
draft: parse_var("COHOST_DRAFT")?.unwrap_or(false),
|
||||||
cohost_email: require_var("COHOST_EMAIL")?,
|
cw: parse_var("COHOST_CW")?.unwrap_or(capture_images),
|
||||||
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),
|
eighteen_plus: parse_var("18PLUS")?.unwrap_or(false),
|
||||||
|
},
|
||||||
|
shows_file: parse_var("SHOWS_FILE")?.unwrap_or_else(|| PathBuf::from("./shows.yaml")),
|
||||||
|
post_interval: Duration::from_secs(parse_var("POST_INTERVAL")?.unwrap_or_default()),
|
||||||
|
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(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
55
src/main.rs
55
src/main.rs
|
@ -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");
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
if conf.post_interval == Duration::ZERO {
|
if conf.post_interval == Duration::ZERO {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
retry_count = 0;
|
||||||
let delay = match result {
|
tokio::time::sleep(conf.post_interval).await;
|
||||||
Ok(()) => conf.post_interval,
|
}
|
||||||
Err(e) => {
|
Err(ref e) => {
|
||||||
error!("{}", e);
|
error!("{:#}", e);
|
||||||
*RETRY_INTERVAL
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
tokio::time::sleep(delay).await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,11 +92,7 @@ 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,
|
|
||||||
conf.subtitle_language.as_deref(),
|
|
||||||
conf.audio_language.as_deref(),
|
|
||||||
)
|
|
||||||
.with_context(|| format!("Failed to get info for media file {}", file.display()))?;
|
.with_context(|| format!("Failed to get info for media file {}", file.display()))?;
|
||||||
debug!(
|
debug!(
|
||||||
"Media duration: {}",
|
"Media duration: {}",
|
||||||
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue