mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 05:48:06 -05:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cc7a9b434 | ||
|
|
6b5f296048 | ||
|
|
9dc237f48a | ||
|
|
2e1ce43cc0 | ||
|
|
695d6b11a5 |
Generated
-9
@@ -3876,13 +3876,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error_utils"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errors"
|
||||
version = "0.0.1"
|
||||
@@ -10179,7 +10172,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"bootstrap",
|
||||
"bucket_paths",
|
||||
"chrono",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"easyenv",
|
||||
@@ -11241,7 +11233,6 @@ dependencies = [
|
||||
"elasticsearch",
|
||||
"elasticsearch_schema",
|
||||
"enums",
|
||||
"error_utils",
|
||||
"errors",
|
||||
"fal_client",
|
||||
"ffmpeg_utils",
|
||||
|
||||
@@ -59,7 +59,6 @@ members = [
|
||||
"crates/lib/data/primitives",
|
||||
"crates/lib/data/web_base64",
|
||||
"crates/lib/easyenv", # NB: Vendored version of https://github.com/echelon/easyenv.rs that we'll improve.
|
||||
"crates/lib/error_utils",
|
||||
"crates/lib/errors",
|
||||
"crates/lib/ffmpeg_utils",
|
||||
"crates/lib/files/file_to_base64_url",
|
||||
@@ -237,7 +236,6 @@ cookie_store = { path = "crates/lib/cookie_store" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
easyenv = { path = "crates/lib/easyenv" }
|
||||
enums = { path = "crates/schema/public/enums" }
|
||||
error_utils = { path = "crates/lib/error_utils" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
fal_client = { path = "crates/api_clients/fal_client" }
|
||||
ffmpeg_utils = { path = "crates/lib/ffmpeg_utils" }
|
||||
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
|
||||
use crate::error::seedance2pro_error::Seedance2ProError;
|
||||
use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError;
|
||||
use crate::requests::get_user_auth_details::request_types::*;
|
||||
use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::utils::common_headers::FIREFOX_USER_AGENT;
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
|
||||
// --- Args & response ---
|
||||
|
||||
pub struct GetUserAuthDetailsArgs<'a> {
|
||||
pub session: &'a Seedance2ProSession,
|
||||
|
||||
/// Override the default host (kinovi.ai).
|
||||
pub host_override: Option<KinoviHost>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GetUserAuthDetailsResponse {
|
||||
pub email: String,
|
||||
pub credits: u64,
|
||||
pub available_credits: u64,
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
/// The tRPC `input` query param for `auth.user` — a static null payload.
|
||||
const AUTH_USER_INPUT: &str =
|
||||
r#"{"0":{"json":null,"meta":{"values":["undefined"],"v":1}}}"#;
|
||||
|
||||
pub async fn get_user_auth_details(
|
||||
args: GetUserAuthDetailsArgs<'_>,
|
||||
) -> Result<GetUserAuthDetailsResponse, Seedance2ProError> {
|
||||
let host = resolve_host(args.host_override.as_ref());
|
||||
let base_url = host.api_base_url();
|
||||
let url = format!("{}/api/trpc/auth.user", base_url);
|
||||
|
||||
info!("Fetching user auth details...");
|
||||
|
||||
let client = Client::builder()
|
||||
.emulation(Emulation::Firefox143)
|
||||
.build()
|
||||
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
|
||||
|
||||
let cookie = args.session.cookies.as_str();
|
||||
let referer = format!("{}/pricing", base_url);
|
||||
|
||||
let request = client.get(&url)
|
||||
.query(&[("batch", "1"), ("input", AUTH_USER_INPUT)])
|
||||
.header("User-Agent", FIREFOX_USER_AGENT)
|
||||
.header("Accept", "*/*")
|
||||
.header("Accept-Language", "en-US,en;q=0.9")
|
||||
.header("Accept-Encoding", "gzip, deflate, br, zstd")
|
||||
.header("Referer", &referer)
|
||||
.header("content-type", "application/json")
|
||||
.header("x-trpc-source", "client")
|
||||
.header("Connection", "keep-alive")
|
||||
.header("Cookie", cookie)
|
||||
.header("Sec-Fetch-Dest", "empty")
|
||||
.header("Sec-Fetch-Mode", "cors")
|
||||
.header("Sec-Fetch-Site", "same-origin")
|
||||
.header("Priority", "u=4")
|
||||
.header("TE", "trailers")
|
||||
.build()
|
||||
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
|
||||
|
||||
let response = client.execute(request)
|
||||
.await
|
||||
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
|
||||
|
||||
let status = response.status();
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| Seedance2ProGenericApiError::WreqError(err))?;
|
||||
|
||||
info!("Get user auth details response status: {}", status);
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(Seedance2ProGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: status,
|
||||
body: response_body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
let batch_response: Vec<BatchResponseItem> = serde_json::from_str(&response_body)
|
||||
.map_err(|err| Seedance2ProGenericApiError::SerdeResponseParseErrorWithBody(
|
||||
err,
|
||||
response_body.clone(),
|
||||
))?;
|
||||
|
||||
let json = batch_response
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Seedance2ProGenericApiError::UncategorizedBadResponse(
|
||||
"Empty batch response array".to_string()
|
||||
))?
|
||||
.result
|
||||
.data
|
||||
.json;
|
||||
|
||||
info!("User: {}, credits: {}, available: {}", json.email, json.credits, json.available_credits);
|
||||
|
||||
Ok(GetUserAuthDetailsResponse {
|
||||
email: json.email,
|
||||
credits: json.credits,
|
||||
available_credits: json.available_credits,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use crate::test_utils::get_test_cookies::get_test_cookies;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use errors::AnyhowResult;
|
||||
use log::LevelFilter;
|
||||
|
||||
fn test_session() -> AnyhowResult<Seedance2ProSession> {
|
||||
let cookies = get_test_cookies()?;
|
||||
Ok(Seedance2ProSession::from_cookies_string(cookies))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually test — requires real cookies
|
||||
async fn test_get_user_auth_details() -> AnyhowResult<()> {
|
||||
setup_test_logging(LevelFilter::Trace);
|
||||
let session = test_session()?;
|
||||
let result = get_user_auth_details(GetUserAuthDetailsArgs {
|
||||
session: &session,
|
||||
host_override: None,
|
||||
}).await?;
|
||||
println!("Email: {}", result.email);
|
||||
println!("Credits: {}", result.credits);
|
||||
println!("Available credits: {}", result.available_credits);
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect output.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod get_user_auth_details;
|
||||
mod request_types;
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct BatchResponseItem {
|
||||
pub result: BatchResponseResult,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct BatchResponseResult {
|
||||
pub data: BatchResponseData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(super) struct BatchResponseData {
|
||||
pub json: RawUserAuthDetails,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct RawUserAuthDetails {
|
||||
pub email: String,
|
||||
pub credits: u64,
|
||||
pub available_credits: u64,
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
pub mod generate_character;
|
||||
pub mod generate_video;
|
||||
pub mod get_user_auth_details;
|
||||
pub mod kinovi_host;
|
||||
pub mod poll_characters;
|
||||
pub mod poll_orders;
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::requests::kinovi_host::{KinoviHost, resolve_host};
|
||||
use crate::requests::poll_orders::failure_reason::FailureReason;
|
||||
use crate::requests::poll_orders::request_types::*;
|
||||
use crate::utils::common_headers::FIREFOX_USER_AGENT;
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::info;
|
||||
use wreq::Client;
|
||||
use wreq_util::Emulation;
|
||||
@@ -95,9 +94,6 @@ pub struct OrderStatus {
|
||||
|
||||
/// ISO 8601 creation timestamp (e.g. `"2026-02-19T01:20:50.398Z"`).
|
||||
pub created_at: String,
|
||||
|
||||
/// Parsed `created_at` as a `DateTime<Utc>`. `None` if the raw string could not be parsed.
|
||||
pub created_at_utc: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
@@ -180,10 +176,6 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
|
||||
(None, TaskStatus::Failed) => Some(FailureReason::from_reason("(no reason)")),
|
||||
_ => None,
|
||||
};
|
||||
let created_at_utc = DateTime::parse_from_rfc3339(&o.created_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.ok();
|
||||
|
||||
OrderStatus {
|
||||
order_id: o.order_id,
|
||||
task_status,
|
||||
@@ -195,7 +187,6 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
|
||||
}).collect(),
|
||||
fail_reason,
|
||||
created_at: o.created_at,
|
||||
created_at_utc,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "error_utils"
|
||||
edition = "2021"
|
||||
version = "0.0.1"
|
||||
authors = [
|
||||
"Brandon Thomas <bt@brand.io>",
|
||||
"Brandon Thomas <echelon@gmail.com>",
|
||||
]
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "error_utils"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
regex.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
pub mod try_error_name;
|
||||
@@ -1,157 +0,0 @@
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Regex that matches CamelCase type names with an optional `::Variant` suffix.
|
||||
/// E.g. `CommonWebError::InvalidRequest`, `EnvError`, `KinoviError::RequestError`.
|
||||
static ERROR_NAME_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"[A-Z][0-9a-z]+(?:[A-Z][0-9a-z]+)*(?:::[A-Z][0-9a-z]+(?:[A-Z][0-9a-z]+)*)?")
|
||||
.expect("error name regex should compile")
|
||||
});
|
||||
|
||||
/// Try to extract a human-readable error title from the causal error's Debug
|
||||
/// representation. Looks for CamelCase type names like `EnvError::NotUnicode`.
|
||||
pub fn try_error_name(error: &dyn std::error::Error) -> Option<String> {
|
||||
let debug_str = format!("{:?}", error);
|
||||
try_extract_error_name(&debug_str)
|
||||
}
|
||||
|
||||
/// Extract the leading CamelCase error name (with optional `::Variant`) from
|
||||
/// a Debug-formatted error string. Returns `None` if no match is found.
|
||||
///
|
||||
/// ```text
|
||||
/// "CommonWebError::InvalidRequest(\"bad\")" → Some("CommonWebError::InvalidRequest")
|
||||
/// "EnvError::ParseError { reason: \"foo\" }" → Some("EnvError::ParseError")
|
||||
/// "just a plain string" → None
|
||||
/// ```
|
||||
pub (crate) fn try_extract_error_name(debug_str: &str) -> Option<String> {
|
||||
ERROR_NAME_RE.find(debug_str)
|
||||
.map(|m| m.as_str().to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// --- try_extract_error_name tests ---
|
||||
|
||||
#[test]
|
||||
fn extract_just_error_name() {
|
||||
assert_eq!(
|
||||
try_extract_error_name("JustErrorName"),
|
||||
Some("JustErrorName".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_error_with_variant_and_string_payload() {
|
||||
assert_eq!(
|
||||
try_extract_error_name(r#"CommonWebError::InvalidRequest("Invalid request")"#),
|
||||
Some("CommonWebError::InvalidRequest".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_error_with_unit_variant() {
|
||||
assert_eq!(
|
||||
try_extract_error_name("EnvError::NotUnicode"),
|
||||
Some("EnvError::NotUnicode".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_error_with_struct_payload() {
|
||||
assert_eq!(
|
||||
try_extract_error_name(r#"EnvError::ParseError { reason: "foo" }"#),
|
||||
Some("EnvError::ParseError".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_error_with_tuple_payload() {
|
||||
assert_eq!(
|
||||
try_extract_error_name("KinoviError::RequestError(asdf)"),
|
||||
Some("KinoviError::RequestError".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_error_with_nested_errors() {
|
||||
assert_eq!(
|
||||
try_extract_error_name(
|
||||
"ComplexError::SomeError(SomeError::SomeVariant(SomeInnerError::SomeInnerVariant))"
|
||||
),
|
||||
Some("ComplexError::SomeError".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_error_with_struct_containing_nested() {
|
||||
assert_eq!(
|
||||
try_extract_error_name(
|
||||
"WrappedError::Variant { inner: SomeInnerError::SomeInnerVariant }"
|
||||
),
|
||||
Some("WrappedError::Variant".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_match_for_lowercase_string() {
|
||||
assert_eq!(
|
||||
try_extract_error_name("just a plain error message with no type name"),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_match_for_empty_string() {
|
||||
assert_eq!(try_extract_error_name(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_word_camel_case() {
|
||||
assert_eq!(
|
||||
try_extract_error_name("Error"),
|
||||
Some("Error".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
// --- try_error_title tests (with real error types) ---
|
||||
|
||||
#[test]
|
||||
fn title_from_io_error() {
|
||||
let err: Box<dyn std::error::Error> = Box::new(
|
||||
std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"),
|
||||
);
|
||||
let title = try_error_name(err.as_ref());
|
||||
assert!(title.is_some());
|
||||
}
|
||||
|
||||
/// A simple custom error for testing.
|
||||
#[derive(Debug)]
|
||||
enum TestAppError {
|
||||
DatabaseTimeout,
|
||||
InvalidInput(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TestAppError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TestAppError {}
|
||||
|
||||
#[test]
|
||||
fn title_from_custom_unit_variant() {
|
||||
let err = TestAppError::DatabaseTimeout;
|
||||
let title = try_error_name(&err);
|
||||
assert_eq!(title, Some("DatabaseTimeout".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_from_custom_tuple_variant() {
|
||||
let err = TestAppError::InvalidInput("bad data".to_string());
|
||||
let title = try_error_name(&err);
|
||||
assert_eq!(title, Some("InvalidInput".to_string()));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@ use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
|
||||
pub struct PendingSeedance2ProJob {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
/// Database column `maybe_external_third_party_id` is Kinovi's `order_id`.
|
||||
/// The external order_id from seedance2-pro.com.
|
||||
pub order_id: String,
|
||||
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
|
||||
@@ -41,7 +41,6 @@ tokens.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] }
|
||||
|
||||
@@ -2,10 +2,3 @@
|
||||
|
||||
# Development
|
||||
SERVER_ENVIRONMENT='development'
|
||||
|
||||
# Paging
|
||||
ENABLE_PAGING=true
|
||||
|
||||
# Doesn't make sense for jobs:
|
||||
# ENABLE_PAGING_FOR_500S=true
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Duration;
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use concurrency::relaxed_atomic_bool::RelaxedAtomicBool;
|
||||
use jobs_common::job_stats::JobStats;
|
||||
@@ -8,7 +5,6 @@ use pager::client::pager::Pager;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use server_environment::ServerEnvironment;
|
||||
use sqlx::MySqlPool;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobDependencies {
|
||||
@@ -32,21 +28,9 @@ pub struct JobDependencies {
|
||||
/// when the order list is very long.
|
||||
pub maybe_pages_per_batch: Option<u32>,
|
||||
|
||||
/// If set, stop paginating backwards through orders once we encounter
|
||||
/// an order older than this duration. This prevents endlessly scanning
|
||||
/// ancient orders that will never match a pending job.
|
||||
pub maybe_max_job_age: Option<Duration>,
|
||||
|
||||
/// Page an alert when available Kinovi credits fall below this threshold.
|
||||
pub credits_alert_threshold: u64,
|
||||
|
||||
/// Set to `true` from another thread to trigger graceful shutdown.
|
||||
pub application_shutdown: RelaxedAtomicBool,
|
||||
|
||||
/// Notified when `application_shutdown` is set. Allows sleeping tasks
|
||||
/// to wake up immediately instead of waiting for the full sleep duration.
|
||||
pub shutdown_notify: Arc<Notify>,
|
||||
|
||||
/// Pager client for sending alerts.
|
||||
pub pager: Pager,
|
||||
}
|
||||
|
||||
+1
-4
@@ -29,10 +29,7 @@ pub async fn character_polling_main_loop(deps: JobDependencies) {
|
||||
warn!("Character poll iteration took {:.1}s (threshold: {}s)", elapsed.as_secs_f64(), POLL_ALERT_THRESHOLD.as_secs());
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_millis(deps.poll_interval_millis)) => {}
|
||||
_ = deps.shutdown_notify.notified() => {}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(deps.poll_interval_millis)).await;
|
||||
}
|
||||
|
||||
warn!("Character polling main loop is shut down.");
|
||||
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
use std::cmp::min;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{error, info, warn};
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
use seedance2pro_client::requests::get_user_auth_details::get_user_auth_details::{
|
||||
get_user_auth_details, GetUserAuthDetailsArgs,
|
||||
};
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
const CREDITS_CHECK_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
pub async fn credits_checking_main_loop(deps: JobDependencies) {
|
||||
let mut consecutive_failures: u32 = 0;
|
||||
|
||||
while !deps.application_shutdown.get() {
|
||||
match check_credits(&deps).await {
|
||||
Ok(()) => {
|
||||
consecutive_failures = 0;
|
||||
}
|
||||
Err(err) => {
|
||||
consecutive_failures += 1;
|
||||
error!("Credits check failed ({} consecutive): {:?}", consecutive_failures, err);
|
||||
|
||||
if consecutive_failures >= 2 {
|
||||
let notification = NotificationDetailsBuilder::from_title(
|
||||
"Kinovi credits check failing".to_string())
|
||||
.set_description(Some(format!(
|
||||
"Credits check has failed {} times in a row. Last error: {:?}",
|
||||
consecutive_failures, err,
|
||||
)))
|
||||
.set_urgency(Some(NotificationUrgency::Medium))
|
||||
.build();
|
||||
|
||||
if let Err(pager_err) = deps.pager.enqueue_page(notification) {
|
||||
error!("Failed to enqueue credits check failure alert: {:?}", pager_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(CREDITS_CHECK_INTERVAL) => {}
|
||||
_ = deps.shutdown_notify.notified() => {}
|
||||
}
|
||||
}
|
||||
|
||||
warn!("Credits checking main loop is shut down.");
|
||||
}
|
||||
|
||||
async fn check_credits(deps: &JobDependencies) -> anyhow::Result<()> {
|
||||
let response = get_user_auth_details(GetUserAuthDetailsArgs {
|
||||
session: &deps.seedance2pro_session,
|
||||
host_override: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| anyhow::anyhow!("get_user_auth_details failed: {:?}", err))?;
|
||||
|
||||
let credits = min(response.credits, response.available_credits);
|
||||
let threshold = deps.credits_alert_threshold;
|
||||
|
||||
info!(
|
||||
"Kinovi credits: {} (API credits: {}, API credits available: {}) (alert threshold: {})",
|
||||
credits,
|
||||
response.credits,
|
||||
response.available_credits,
|
||||
threshold,
|
||||
);
|
||||
|
||||
if credits < threshold {
|
||||
warn!(
|
||||
"Kinovi credits are low! {} available credits (alert threshold: {})",
|
||||
credits, threshold,
|
||||
);
|
||||
|
||||
let notification = NotificationDetailsBuilder::from_title(
|
||||
"Kinovi billing credits low !".to_string())
|
||||
.set_description(Some(format!(
|
||||
"Available credits: {}. Alert threshold: {}.",
|
||||
credits, threshold,
|
||||
)))
|
||||
.set_urgency(Some(NotificationUrgency::High))
|
||||
.build();
|
||||
|
||||
if let Err(pager_err) = deps.pager.enqueue_page(notification) {
|
||||
error!("Failed to enqueue low credits alert: {:?}", pager_err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod credits_checking_main_loop;
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod character_polling_job;
|
||||
pub mod credits_checking_job;
|
||||
pub mod video_polling_job;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod alert_on_error;
|
||||
pub mod video_polling_main_loop;
|
||||
pub mod process_job;
|
||||
pub mod process_orders_batch;
|
||||
pub mod process_page_batch;
|
||||
|
||||
+5
-37
@@ -12,22 +12,17 @@ use crate::jobs::video_polling_job::process_job::process_successful_job::process
|
||||
///
|
||||
/// Removes matched order_ids from `job_by_order_id` so they are not
|
||||
/// processed again in subsequent batches.
|
||||
pub async fn process_orders_batch(
|
||||
pub async fn process_page_batch(
|
||||
deps: &JobDependencies,
|
||||
kinovi_orders: &[OrderStatus],
|
||||
orders: &[OrderStatus],
|
||||
job_by_order_id: &mut HashMap<String, PendingSeedance2ProJob>,
|
||||
pages_in_current_batch: u32,
|
||||
) {
|
||||
log_batch_summary(&kinovi_orders, pages_in_current_batch);
|
||||
|
||||
let pending_job_count = job_by_order_id.len();
|
||||
|
||||
let mut batch_succeeded = 0u32;
|
||||
let mut batch_failed = 0u32;
|
||||
let mut batch_in_progress = 0u32;
|
||||
let mut batch_matched = 0u32;
|
||||
|
||||
for order in kinovi_orders {
|
||||
for order in orders {
|
||||
let job = match job_by_order_id.remove(&order.order_id) {
|
||||
Some(j) => j,
|
||||
None => continue, // Not one of our pending jobs.
|
||||
@@ -69,14 +64,8 @@ pub async fn process_orders_batch(
|
||||
}
|
||||
|
||||
info!(
|
||||
"Batch processing done: \
|
||||
{} kinovi orders in polled batch, \
|
||||
{} pending db jobs in batch at start, \
|
||||
{} kinovi orders in batch matched pending jobs \
|
||||
(orders succeeded={}, orders failed={}, orders in_progress={}), \
|
||||
{} pending db jobs in batch remaining",
|
||||
kinovi_orders.len(),
|
||||
pending_job_count,
|
||||
"Batch processing done: {} orders in batch, {} matched pending jobs (succeeded={}, failed={}, in_progress={}), {} pending jobs remaining",
|
||||
orders.len(),
|
||||
batch_matched,
|
||||
batch_succeeded,
|
||||
batch_failed,
|
||||
@@ -84,24 +73,3 @@ pub async fn process_orders_batch(
|
||||
job_by_order_id.len(),
|
||||
);
|
||||
}
|
||||
|
||||
fn log_batch_summary(orders: &[OrderStatus], pages_in_batch: u32) {
|
||||
let mut succeeded = 0u32;
|
||||
let mut failed = 0u32;
|
||||
let mut in_progress = 0u32;
|
||||
let mut unknown = 0u32;
|
||||
|
||||
for order in orders {
|
||||
match &order.task_status {
|
||||
TaskStatus::Completed => succeeded += 1,
|
||||
TaskStatus::Failed => failed += 1,
|
||||
TaskStatus::Pending | TaskStatus::Processing => in_progress += 1,
|
||||
TaskStatus::Unknown(_) => unknown += 1,
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Processing batch of {} Kinovi order pages, {} total orders (succeeded={}, failed={}, in_progress={}, unknown={})",
|
||||
pages_in_batch, orders.len(), succeeded, failed, in_progress, unknown
|
||||
);
|
||||
}
|
||||
+63
-119
@@ -1,34 +1,27 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use chrono::Utc;
|
||||
use log::{error, info, warn};
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_video_jobs::{list_pending_seedance2pro_video_jobs, PendingSeedance2ProJob};
|
||||
use mysql_queries::queries::generic_inference::seedance2pro::list_pending_seedance2pro_video_jobs::list_pending_seedance2pro_video_jobs;
|
||||
use seedance2pro_client::requests::poll_orders::poll_orders::{poll_orders, OrderStatus, PollOrdersArgs, TaskStatus};
|
||||
|
||||
use crate::jobs::video_polling_job::alert_on_error::alert_pager_and_return_err;
|
||||
use crate::jobs::video_polling_job::process_orders_batch::process_orders_batch;
|
||||
use crate::jobs::video_polling_job::process_page_batch::process_page_batch;
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
const POLL_ALERT_THRESHOLD: Duration = Duration::from_mins(6);
|
||||
const POLL_ALERT_THRESHOLD: Duration = Duration::from_secs(600);
|
||||
|
||||
pub async fn video_polling_main_loop(job_dependencies: JobDependencies) {
|
||||
while !job_dependencies.application_shutdown.get() {
|
||||
let start = Instant::now();
|
||||
|
||||
//
|
||||
// Run a single polling iteration and alert if it takes too long
|
||||
//
|
||||
|
||||
let result = run_poll_iteration(&job_dependencies).await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Error in poll iteration: {:?}", err);
|
||||
let _ = alert_pager_and_return_err::<()>(&job_dependencies.pager, "Kinovi poll iteration error", err, None);
|
||||
let _ = alert_pager_and_return_err::<()>(&job_dependencies.pager, "Seedance2Pro poll iteration error", err, None);
|
||||
let _ = job_dependencies.job_stats.increment_failure_count();
|
||||
}
|
||||
|
||||
@@ -36,7 +29,7 @@ pub async fn video_polling_main_loop(job_dependencies: JobDependencies) {
|
||||
warn!("Poll iteration took {:.1}s (threshold: {}s)", elapsed.as_secs_f64(), POLL_ALERT_THRESHOLD.as_secs());
|
||||
|
||||
let notification = NotificationDetailsBuilder::from_title(
|
||||
"Kinovi poll iteration slow".to_string())
|
||||
"Seedance2Pro poll iteration slow".to_string())
|
||||
.set_description(Some(format!(
|
||||
"Poll iteration took {:.1} seconds, exceeding the 10-minute threshold.",
|
||||
elapsed.as_secs_f64(),
|
||||
@@ -49,70 +42,39 @@ pub async fn video_polling_main_loop(job_dependencies: JobDependencies) {
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_millis(job_dependencies.poll_interval_millis)) => {}
|
||||
_ = job_dependencies.shutdown_notify.notified() => {}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(job_dependencies.poll_interval_millis)).await;
|
||||
}
|
||||
|
||||
warn!("Kinovi job runner main loop is shut down.");
|
||||
warn!("Seedance2Pro job runner main loop is shut down.");
|
||||
}
|
||||
|
||||
async fn run_poll_iteration(deps: &JobDependencies) -> anyhow::Result<()> {
|
||||
// 1. Query all (limit 25,000) non-terminal Seedance2Pro jobs from DB.
|
||||
// This is all non-(complete_success, complete_failure) jobs.
|
||||
// 1. Query all non-terminal Seedance2Pro jobs from DB.
|
||||
let pending_jobs = match list_pending_seedance2pro_video_jobs(&deps.mysql_pool).await {
|
||||
Ok(jobs) => jobs,
|
||||
Err(err) => {
|
||||
error!("Failed to list pending database jobs: {:?}", err);
|
||||
return alert_pager_and_return_err(&deps.pager, "Jobs DB query failed", err.into(), None);
|
||||
error!("Failed to list pending seedance2pro jobs: {:?}", err);
|
||||
return alert_pager_and_return_err(&deps.pager, "Seedance2Pro DB query failed", err.into(), None);
|
||||
}
|
||||
};
|
||||
|
||||
let total_pending_jobs = pending_jobs.len();
|
||||
|
||||
if pending_jobs.is_empty() {
|
||||
info!("No pending database jobs found in the database.");
|
||||
info!("No pending Seedance2Pro jobs.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Found {} pending database job(s).", pending_jobs.len());
|
||||
|
||||
let result = website_polling_loop(&deps, pending_jobs).await?;
|
||||
|
||||
info!(
|
||||
"Database + Kinovi poll iteration complete: \
|
||||
{} total database jobs, \
|
||||
{} total Kinovi pages, \
|
||||
{} total Kinovi orders seen.",
|
||||
total_pending_jobs,
|
||||
result.total_pages_seen,
|
||||
result.total_orders_seen
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct WebsitePollingResult {
|
||||
total_pages_seen: u32,
|
||||
total_orders_seen: u32,
|
||||
}
|
||||
|
||||
async fn website_polling_loop(
|
||||
deps: &&JobDependencies,
|
||||
pending_jobs: Vec<PendingSeedance2ProJob>
|
||||
) -> anyhow::Result<WebsitePollingResult> {
|
||||
info!("Found {} pending Seedance2Pro job(s).", pending_jobs.len());
|
||||
|
||||
// Build a lookup: order_id -> job. This is mutated as batches are processed
|
||||
// so that each order is handled at most once.
|
||||
let mut job_by_order_id: HashMap<String, _> = pending_jobs
|
||||
.into_iter()
|
||||
.map(|job| (job.order_id.clone(), job))
|
||||
.collect();
|
||||
.into_iter()
|
||||
.map(|job| (job.order_id.clone(), job))
|
||||
.collect();
|
||||
|
||||
// 2. Poll orders from Kinovi API, optionally processing in chunks.
|
||||
// 2. Poll orders from seedance2pro API, optionally processing in chunks.
|
||||
let mut cursor: Option<u64> = None;
|
||||
let mut total_pages_seen: u32 = 0;
|
||||
let mut total_page_number: u32 = 0;
|
||||
let mut total_orders_seen: u32 = 0;
|
||||
|
||||
// Accumulates orders for the current batch (when chunking is enabled)
|
||||
@@ -126,9 +88,12 @@ async fn website_polling_loop(
|
||||
break;
|
||||
}
|
||||
|
||||
total_page_number += 1;
|
||||
pages_in_current_batch += 1;
|
||||
|
||||
info!(
|
||||
"Beginning request for Kinovi page {}... (cursor: {:?}, total orders seen thus far: {})",
|
||||
total_pages_seen, cursor, total_orders_seen
|
||||
"Beginning request for page {}... (cursor={:?}, total_orders_so_far={})",
|
||||
total_page_number, cursor, total_orders_seen
|
||||
);
|
||||
|
||||
let response = match poll_orders(PollOrdersArgs {
|
||||
@@ -138,85 +103,42 @@ async fn website_polling_loop(
|
||||
}).await {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
warn!("Error polling Kinovi orders: {:?}", err);
|
||||
warn!("Error polling seedance2pro orders: {:?}", err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"Kinovi API polling failed",
|
||||
"Seedance2Pro API poll failed",
|
||||
anyhow::anyhow!("poll_orders failed: {:?}", err),
|
||||
None,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let page_orders_count = response.orders.len() as u32;
|
||||
|
||||
total_pages_seen += 1;
|
||||
total_orders_seen += page_orders_count;
|
||||
|
||||
pages_in_current_batch += 1;
|
||||
|
||||
let maybe_last_order_created_at = response.orders
|
||||
.iter()
|
||||
.filter(|order| order.created_at_utc.is_some())
|
||||
.last()
|
||||
.and_then(|order| order.created_at_utc);
|
||||
|
||||
info!(
|
||||
"Done polling Kinovi orders page {}. \
|
||||
Got {} orders on this page (oldest on page created at {:?}).",
|
||||
total_pages_seen,
|
||||
page_orders_count,
|
||||
maybe_last_order_created_at,
|
||||
);
|
||||
|
||||
// Check if the last (oldest) order in this page exceeds the max age threshold.
|
||||
// Orders are returned newest-first, so the last order is the oldest.
|
||||
let mut exceeded_max_age = false;
|
||||
|
||||
if let Some(ref max_age) = deps.maybe_max_job_age {
|
||||
if let Some(last_order_created_at) = maybe_last_order_created_at {
|
||||
let order_age = Utc::now() - last_order_created_at;
|
||||
let too_old = order_age > *max_age;
|
||||
if too_old {
|
||||
info!(
|
||||
"Last order on Kinovi order page {} is {} hours old \
|
||||
(staleness threshold: {} hours). Stopping Kinovi pagination.",
|
||||
total_pages_seen,
|
||||
order_age.num_hours(),
|
||||
max_age.num_hours()
|
||||
);
|
||||
exceeded_max_age = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
let page_count = response.orders.len() as u32;
|
||||
info!("Done polling page {}. Got {} orders on this page.", total_page_number, page_count);
|
||||
|
||||
total_orders_seen += page_count;
|
||||
batch_orders.extend(response.orders);
|
||||
|
||||
cursor = response.next_cursor;
|
||||
|
||||
let reached_end = cursor.is_none() || exceeded_max_age;
|
||||
|
||||
let batch_is_full = deps.maybe_pages_per_batch
|
||||
.map(|batch_page_limit| pages_in_current_batch >= batch_page_limit)
|
||||
.unwrap_or(false);
|
||||
let reached_end = cursor.is_none();
|
||||
|
||||
// Determine if we should process the current batch now.
|
||||
let should_process_batch_now = reached_end || batch_is_full;
|
||||
let should_process_batch = reached_end
|
||||
|| deps.maybe_pages_per_batch
|
||||
.map(|limit| pages_in_current_batch >= limit)
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_process_batch_now && !batch_orders.is_empty() {
|
||||
process_orders_batch(
|
||||
deps,
|
||||
&batch_orders,
|
||||
&mut job_by_order_id,
|
||||
pages_in_current_batch
|
||||
).await;
|
||||
if should_process_batch && !batch_orders.is_empty() {
|
||||
log_batch_summary(&batch_orders, pages_in_current_batch);
|
||||
|
||||
process_page_batch(deps, &batch_orders, &mut job_by_order_id).await;
|
||||
|
||||
batch_orders.clear();
|
||||
pages_in_current_batch = 0;
|
||||
|
||||
// If all pending jobs have been matched, no need to keep paging.
|
||||
if job_by_order_id.is_empty() {
|
||||
info!("All pending database jobs have been matched to Kinovi orders and processed. Stopping pagination early.");
|
||||
info!("All pending jobs have been matched. Stopping pagination early.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -226,9 +148,31 @@ async fn website_polling_loop(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(WebsitePollingResult {
|
||||
total_pages_seen,
|
||||
total_orders_seen
|
||||
})
|
||||
info!(
|
||||
"Poll iteration complete: {} total pages, {} total orders seen.",
|
||||
total_page_number, total_orders_seen
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn log_batch_summary(orders: &[OrderStatus], pages_in_batch: u32) {
|
||||
let mut succeeded = 0u32;
|
||||
let mut failed = 0u32;
|
||||
let mut in_progress = 0u32;
|
||||
let mut unknown = 0u32;
|
||||
|
||||
for order in orders {
|
||||
match &order.task_status {
|
||||
TaskStatus::Completed => succeeded += 1,
|
||||
TaskStatus::Failed => failed += 1,
|
||||
TaskStatus::Pending | TaskStatus::Processing => in_progress += 1,
|
||||
TaskStatus::Unknown(_) => unknown += 1,
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Processing batch of {} pages, {} orders (succeeded={}, failed={}, in_progress={}, unknown={})",
|
||||
pages_in_batch, orders.len(), succeeded, failed, in_progress, unknown
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,11 @@
|
||||
|
||||
#[macro_use] extern crate serde_derive;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use log::{info, warn};
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use bootstrap::bootstrap::{bootstrap, BootstrapArgs};
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
@@ -29,7 +27,6 @@ use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
|
||||
|
||||
use crate::http_server::run_http_server::{launch_http_server, CreateServerArgs};
|
||||
use crate::jobs::character_polling_job::character_polling_main_loop::character_polling_main_loop;
|
||||
use crate::jobs::credits_checking_job::credits_checking_main_loop::credits_checking_main_loop;
|
||||
use crate::jobs::video_polling_job::video_polling_main_loop::video_polling_main_loop;
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
use crate::startup::build_pager::build_pager;
|
||||
@@ -46,7 +43,6 @@ const ENV_REGION_NAME: &str = "REGION_NAME";
|
||||
const ENV_PUBLIC_BUCKET_NAME: &str = "PUBLIC_BUCKET_NAME";
|
||||
const ENV_S3_ENDPOINT: &str = "S3_COMPATIBLE_ENDPOINT_URL";
|
||||
const ENV_SEEDANCE2PRO_COOKIES : &str = "SEEDANCE2PRO_COOKIES";
|
||||
const ENV_MAX_JOB_AGE_THRESHOLD_HOURS: &str = "MAX_JOB_AGE_THRESHOLD_HOURS";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> AnyhowResult<()> {
|
||||
@@ -117,20 +113,6 @@ async fn main() -> AnyhowResult<()> {
|
||||
info!("Batch page count: {}", count);
|
||||
}
|
||||
|
||||
let maybe_max_job_age = easyenv::try_get_env_num_optional::<i64>(ENV_MAX_JOB_AGE_THRESHOLD_HOURS)?
|
||||
.and_then(chrono::Duration::try_hours);
|
||||
|
||||
if let Some(ref duration) = maybe_max_job_age {
|
||||
info!("Max job age threshold: {} hours", duration.num_hours());
|
||||
}
|
||||
|
||||
let credits_alert_threshold: u64 = easyenv::get_env_num(
|
||||
"CREDITS_ALERT_THRESHOLD",
|
||||
10_000,
|
||||
)?;
|
||||
|
||||
info!("Credits alert threshold: {}", credits_alert_threshold);
|
||||
|
||||
let (pager, pager_worker) = build_pager(server_environment, &container_environment.hostname);
|
||||
|
||||
info!("Spawning pager worker.");
|
||||
@@ -144,7 +126,6 @@ async fn main() -> AnyhowResult<()> {
|
||||
});
|
||||
|
||||
let application_shutdown = RelaxedAtomicBool::new(false);
|
||||
let shutdown_notify = Arc::new(Notify::new());
|
||||
let job_stats = JobStats::new();
|
||||
|
||||
let pager_for_shutdown = pager.clone();
|
||||
@@ -163,10 +144,7 @@ async fn main() -> AnyhowResult<()> {
|
||||
job_stats,
|
||||
poll_interval_millis,
|
||||
maybe_pages_per_batch,
|
||||
maybe_max_job_age,
|
||||
credits_alert_threshold,
|
||||
application_shutdown: application_shutdown.clone(),
|
||||
shutdown_notify: shutdown_notify.clone(),
|
||||
pager,
|
||||
};
|
||||
|
||||
@@ -183,14 +161,12 @@ async fn main() -> AnyhowResult<()> {
|
||||
|
||||
// Listen for SIGTERM / Ctrl-C to trigger graceful shutdown.
|
||||
let application_shutdown_for_signal = application_shutdown.clone();
|
||||
let shutdown_notify_for_signal = shutdown_notify.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match tokio::signal::ctrl_c().await {
|
||||
Ok(()) => {
|
||||
info!("Received shutdown signal. Shutting down...");
|
||||
application_shutdown_for_signal.set(true);
|
||||
shutdown_notify_for_signal.notify_waiters();
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Error listening for shutdown signal: {:?}", err);
|
||||
@@ -198,10 +174,9 @@ async fn main() -> AnyhowResult<()> {
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn all polling loops as concurrent tasks.
|
||||
// Spawn both polling loops as concurrent tasks.
|
||||
let video_deps = job_dependencies.clone();
|
||||
let character_deps = job_dependencies.clone();
|
||||
let credits_deps = job_dependencies;
|
||||
let character_deps = job_dependencies;
|
||||
|
||||
let video_handle = tokio::spawn(async move {
|
||||
video_polling_main_loop(video_deps).await;
|
||||
@@ -211,12 +186,8 @@ async fn main() -> AnyhowResult<()> {
|
||||
character_polling_main_loop(character_deps).await;
|
||||
});
|
||||
|
||||
let credits_handle = tokio::spawn(async move {
|
||||
credits_checking_main_loop(credits_deps).await;
|
||||
});
|
||||
|
||||
// Wait for all to finish (they exit when application_shutdown is set).
|
||||
let _ = tokio::join!(video_handle, character_handle, credits_handle);
|
||||
// Wait for both to finish (they exit when application_shutdown is set).
|
||||
let _ = tokio::join!(video_handle, character_handle);
|
||||
|
||||
info!("Shutting down pager worker...");
|
||||
pager_for_shutdown.shutdown_worker();
|
||||
|
||||
@@ -50,7 +50,6 @@ datetimes = { path = "../../../lib/data/datetimes" }
|
||||
easyenv.workspace = true
|
||||
elasticsearch_schema = { path = "../../../schema/database/elasticsearch_schema" }
|
||||
enums.workspace = true
|
||||
error_utils.workspace = true
|
||||
errors.workspace = true
|
||||
fal_client.workspace = true
|
||||
filesys.workspace = true
|
||||
|
||||
@@ -603,8 +603,10 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
AppStateSubscriptionProductKey,
|
||||
AppStateUserInfo,
|
||||
AppStateUserLocale,
|
||||
BatchGetInferenceJobStatusError,
|
||||
BatchGetInferenceJobStatusQueryParams,
|
||||
BatchGetInferenceJobStatusSuccessResponse,
|
||||
BatchGetMediaFilesError,
|
||||
BatchGetMediaFilesModelInfo,
|
||||
CreatePromptRequest,
|
||||
CreatePromptResponse,
|
||||
@@ -837,11 +839,13 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
GenerateVeo2Duration,
|
||||
GenerateVeo2ImageToVideoRequest,
|
||||
GenerateVeo2ImageToVideoResponse,
|
||||
GetInferenceJobStatusError,
|
||||
GetInferenceJobStatusPathInfo,
|
||||
GetInferenceJobStatusSuccessResponse,
|
||||
GetIsFeaturedItemError,
|
||||
GetIsFeaturedItemPathInfo,
|
||||
GetIsFeaturedItemSuccessResponse,
|
||||
GetMediaFileError,
|
||||
GetMediaFileModelInfo,
|
||||
GetMediaFileModeratorFields,
|
||||
GetMediaFilePathInfo,
|
||||
@@ -880,18 +884,23 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ListDatasetsByUserError,
|
||||
ListDatasetsByUserPathInfo,
|
||||
ListDatasetsByUserSuccessResponse,
|
||||
ListFeaturedMediaFilesError,
|
||||
ListFeaturedMediaFilesQueryParams,
|
||||
ListFeaturedMediaFilesSuccessResponse,
|
||||
ListFeaturedWeightsError,
|
||||
ListFeaturedWeightsQueryParams,
|
||||
ListFeaturedWeightsSuccessResponse,
|
||||
ListMediaFilesByBatchError,
|
||||
ListMediaFilesByBatchPathInfo,
|
||||
ListMediaFilesByBatchSuccessResponse,
|
||||
ListMediaFilesError,
|
||||
ListMediaFilesForUserError,
|
||||
ListMediaFilesForUserPathInfo,
|
||||
ListMediaFilesForUserQueryParams,
|
||||
ListMediaFilesForUserSuccessResponse,
|
||||
ListMediaFilesQueryParams,
|
||||
ListMediaFilesSuccessResponse,
|
||||
ListPinnedMediaFilesError,
|
||||
ListPinnedMediaFilesSuccessResponse,
|
||||
ListPinnedWeightsError,
|
||||
ListPinnedWeightsSuccessResponse,
|
||||
|
||||
+1
-1
@@ -239,7 +239,7 @@ async fn upload_to_kinovi(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error downloading media file from CDN: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?
|
||||
.to_vec();
|
||||
|
||||
|
||||
+1
-1
@@ -683,7 +683,7 @@ async fn upload_to_seedance2pro(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Error downloading media file from CDN: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?
|
||||
.to_vec();
|
||||
|
||||
|
||||
+54
-16
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
|
||||
@@ -10,9 +10,12 @@ use crate::http_server::endpoints::inference_job::utils::extractors::extract_liv
|
||||
use crate::http_server::endpoints::inference_job::utils::extractors::extract_polymorphic_inference_args::extract_polymorphic_inference_args;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use actix_web_lab::extract::Query;
|
||||
use artcraft_api_defs::common::responses::job_details::{JobDetailsLipsyncRequest, JobDetailsLivePortraitRequest};
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
@@ -23,7 +26,7 @@ use enums::by_table::generic_inference_jobs::frontend_failure_category::Frontend
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use log::{error, warn};
|
||||
use log::error;
|
||||
use mysql_queries::queries::generic_inference::web::batch_get_inference_job_status::batch_get_inference_job_status;
|
||||
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
|
||||
use redis::Commands;
|
||||
@@ -140,6 +143,37 @@ pub struct BatchResultDetailsResponse {
|
||||
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum BatchGetInferenceJobStatusError {
|
||||
ServerError,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl ResponseError for BatchGetInferenceJobStatusError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
BatchGetInferenceJobStatusError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
BatchGetInferenceJobStatusError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
Self::ServerError => "server error".to_string(),
|
||||
Self::NotFound => "not found".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for BatchGetInferenceJobStatusError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get job statuses for a batch of multiple job tokens.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -147,7 +181,7 @@ pub struct BatchResultDetailsResponse {
|
||||
path = "/v1/jobs/batch",
|
||||
responses(
|
||||
(status = 200, body = BatchGetInferenceJobStatusSuccessResponse),
|
||||
(status = 500),
|
||||
(status = 500, body = BatchGetInferenceJobStatusError),
|
||||
),
|
||||
params(
|
||||
BatchGetInferenceJobStatusQueryParams
|
||||
@@ -156,8 +190,8 @@ pub struct BatchResultDetailsResponse {
|
||||
pub async fn batch_get_inference_job_status_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<BatchGetInferenceJobStatusQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<BatchGetInferenceJobStatusSuccessResponse>, AdvancedCommonWebError> {
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<BatchGetInferenceJobStatusSuccessResponse>, BatchGetInferenceJobStatusError> {
|
||||
|
||||
let tokens = query.tokens.iter()
|
||||
.map(|token| token.trim())
|
||||
@@ -171,27 +205,31 @@ pub async fn batch_get_inference_job_status_handler(
|
||||
if tokens.is_empty() {
|
||||
// NB: MediaDomain doesn't matter since it's an empty list.
|
||||
return Ok(records_to_response(
|
||||
Vec::new(),
|
||||
server_state.server_environment,
|
||||
Vec::new(),
|
||||
server_state.server_environment,
|
||||
MediaDomain::Storyteller
|
||||
));
|
||||
}
|
||||
|
||||
// NB: Since this is publicly exposed, we don't query sensitive data.
|
||||
let records = batch_get_inference_job_status(
|
||||
let maybe_records = batch_get_inference_job_status(
|
||||
&tokens,
|
||||
&server_state.mysql_pool
|
||||
).await
|
||||
.map_err(|err| {
|
||||
warn!("Batch job query error: {:?}", err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
).await;
|
||||
|
||||
let records = match maybe_records {
|
||||
Ok(records) => records,
|
||||
Err(err) => {
|
||||
error!("tts job query error: {:?}", err);
|
||||
return Err(BatchGetInferenceJobStatusError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
warn!("Redis pool error: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
error!("redis error: {:?}", e);
|
||||
BatchGetInferenceJobStatusError::ServerError
|
||||
})?;
|
||||
|
||||
// TODO(bt,2024-04-22): Look up the extra redis statuses per item.
|
||||
|
||||
+47
-14
@@ -1,6 +1,6 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
|
||||
@@ -9,9 +9,12 @@ use crate::http_server::endpoints::inference_job::utils::extractors::extract_liv
|
||||
use crate::http_server::endpoints::inference_job::utils::extractors::extract_polymorphic_inference_args::extract_polymorphic_inference_args;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::filter_model_name::maybe_filter_model_name;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::common::responses::job_details::{JobDetailsLipsyncRequest, JobDetailsLivePortraitRequest};
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
@@ -21,7 +24,7 @@ use enums::by_table::generic_inference_jobs::frontend_failure_category::Frontend
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use log::{error, warn};
|
||||
use log::error;
|
||||
use mysql_queries::queries::generic_inference::web::get_inference_job_status::get_inference_job_status;
|
||||
use mysql_queries::queries::generic_inference::web::job_status::GenericInferenceJobStatus;
|
||||
use redis::{Commands, RedisResult};
|
||||
@@ -131,6 +134,37 @@ pub struct ResultDetailsResponse {
|
||||
pub maybe_successfully_completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum GetInferenceJobStatusError {
|
||||
ServerError,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl ResponseError for GetInferenceJobStatusError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GetInferenceJobStatusError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
GetInferenceJobStatusError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
Self::ServerError => "server error".to_string(),
|
||||
Self::NotFound => "not found".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for GetInferenceJobStatusError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the status for a single job.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -141,20 +175,19 @@ pub struct ResultDetailsResponse {
|
||||
),
|
||||
responses(
|
||||
(status = 200, body = GetInferenceJobStatusSuccessResponse),
|
||||
(status = 404),
|
||||
(status = 500),
|
||||
(status = 500, body = GetInferenceJobStatusError),
|
||||
),
|
||||
)]
|
||||
pub async fn get_inference_job_status_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetInferenceJobStatusPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<GetInferenceJobStatusSuccessResponse>, AdvancedCommonWebError> {
|
||||
server_state: web::Data<Arc<ServerState>>) -> Result<Json<GetInferenceJobStatusSuccessResponse>, GetInferenceJobStatusError>
|
||||
{
|
||||
if path.token.as_str().trim() == "None" {
|
||||
// NB: A bunch of Python clients use our API and can fail in this manner.
|
||||
// This was a large traffic driver during the 2023-03-08 outage.
|
||||
// Presumably, it's this client: https://github.com/shards-7/fakeyou.py
|
||||
return Err(AdvancedCommonWebError::NotFound);
|
||||
// Presumably it's this client: https://github.com/shards-7/fakeyou.py
|
||||
return Err(GetInferenceJobStatusError::NotFound);
|
||||
}
|
||||
|
||||
// NB: Since this is publicly exposed, we don't query sensitive data.
|
||||
@@ -165,18 +198,18 @@ pub async fn get_inference_job_status_handler(
|
||||
|
||||
let record = match maybe_status {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => return Err(AdvancedCommonWebError::NotFound),
|
||||
Ok(None) => return Err(GetInferenceJobStatusError::NotFound),
|
||||
Err(err) => {
|
||||
warn!("Job query error for token {}: {:?}", path.token.as_str(), err);
|
||||
return Err(AdvancedCommonWebError::from_anyhow_error(err));
|
||||
error!("tts job query error: {:?}", err);
|
||||
return Err(GetInferenceJobStatusError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let mut redis = server_state.redis_pool
|
||||
.get()
|
||||
.map_err(|e| {
|
||||
warn!("Redis pool error: {:?}", e);
|
||||
AdvancedCommonWebError::from_error(e)
|
||||
error!("redis error: {:?}", e);
|
||||
GetInferenceJobStatusError::ServerError
|
||||
})?;
|
||||
|
||||
// TODO(bt,2023-05-21): Make async.
|
||||
|
||||
+47
-7
@@ -1,8 +1,11 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use actix_web_lab::extract::Query;
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
@@ -24,7 +27,6 @@ use tokens::tokens::model_weights::ModelWeightToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
|
||||
@@ -32,6 +34,7 @@ use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
|
||||
use crate::http_server::endpoints::media_files::common_responses::live_portrait::MediaFileLivePortraitDetails;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
// =============== Request ===============
|
||||
@@ -194,6 +197,39 @@ pub struct BatchGetMediaFilesModelInfo {
|
||||
pub maybe_weight_creator: Option<UserDetailsLight>,
|
||||
}
|
||||
|
||||
// =============== Error Response ===============
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum BatchGetMediaFilesError {
|
||||
ServerError,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl ResponseError for BatchGetMediaFilesError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
BatchGetMediaFilesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
BatchGetMediaFilesError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
BatchGetMediaFilesError::ServerError => "server error".to_string(),
|
||||
BatchGetMediaFilesError::NotFound => "not found".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for BatchGetMediaFilesError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Handler ===============
|
||||
|
||||
/// List many media files at the same time by supplying their tokens. You'll have to provide the tokens.
|
||||
@@ -203,8 +239,8 @@ pub struct BatchGetMediaFilesModelInfo {
|
||||
path = "/v1/media_files/batch",
|
||||
responses(
|
||||
(status = 200, description = "Found", body = BatchGetMediaFilesSuccessResponse),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Server error"),
|
||||
(status = 404, description = "Not found", body = BatchGetMediaFilesError),
|
||||
(status = 500, description = "Server error", body = BatchGetMediaFilesError),
|
||||
),
|
||||
params(
|
||||
BatchGetMediaFilesQueryParams,
|
||||
@@ -214,11 +250,15 @@ pub async fn batch_get_media_files_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<BatchGetMediaFilesQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<BatchGetMediaFilesSuccessResponse>, AdvancedCommonWebError> {
|
||||
) -> Result<Json<BatchGetMediaFilesSuccessResponse>, BatchGetMediaFilesError> {
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
BatchGetMediaFilesError::ServerError
|
||||
})?;
|
||||
|
||||
let mut show_deleted_results = false;
|
||||
let mut is_moderator = false;
|
||||
@@ -248,7 +288,7 @@ pub async fn batch_get_media_files_handler(
|
||||
Ok(media_files) => media_files,
|
||||
Err(e) => {
|
||||
warn!("query error: {:?}", e);
|
||||
return Err(AdvancedCommonWebError::from_anyhow_error(e));
|
||||
return Err(BatchGetMediaFilesError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+52
-14
@@ -1,6 +1,6 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_domain::MediaDomain;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
@@ -10,9 +10,12 @@ use crate::http_server::endpoints::media_files::common_responses::live_portrait:
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::bucket_urls::bucket_url_from_media_path::bucket_url_from_media_path;
|
||||
use crate::http_server::web_utils::bucket_urls::bucket_url_from_str_path::bucket_url_from_str_path;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -199,6 +202,37 @@ pub struct GetMediaFileModelInfo {
|
||||
pub maybe_weight_creator: Option<UserDetailsLight>,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum GetMediaFileError {
|
||||
ServerError,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl ResponseError for GetMediaFileError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
GetMediaFileError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
GetMediaFileError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
GetMediaFileError::ServerError => "server error".to_string(),
|
||||
GetMediaFileError::NotFound => "not found".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using derive_more::Display since Clion doesn't understand it.
|
||||
impl fmt::Display for GetMediaFileError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a single media file by token.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -206,8 +240,8 @@ pub struct GetMediaFileModelInfo {
|
||||
path = "/v1/media_files/file/{token}",
|
||||
responses(
|
||||
(status = 200, description = "Found", body = GetMediaFileSuccessResponse),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Server error"),
|
||||
(status = 404, description = "Not found", body = GetMediaFileError),
|
||||
(status = 500, description = "Server error", body = GetMediaFileError),
|
||||
),
|
||||
params(
|
||||
("path" = GetMediaFilePathInfo, description = "Path for Request")
|
||||
@@ -217,13 +251,17 @@ pub async fn get_media_file_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<GetMediaFilePathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<GetMediaFileSuccessResponse>, AdvancedCommonWebError> {
|
||||
) -> Result<Json<GetMediaFileSuccessResponse>, GetMediaFileError> {
|
||||
let media_file_token = path.into_inner().token;
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
GetMediaFileError::ServerError
|
||||
})?;
|
||||
|
||||
let mut show_deleted_results = false;
|
||||
let mut is_moderator = false;
|
||||
@@ -267,7 +305,7 @@ async fn modern_media_file_lookup(
|
||||
show_deleted_results: bool,
|
||||
server_state: &ServerState,
|
||||
media_domain: MediaDomain,
|
||||
) -> Result<GetMediaFileSuccessResponse, AdvancedCommonWebError> {
|
||||
) -> Result<GetMediaFileSuccessResponse, GetMediaFileError> {
|
||||
|
||||
let is_mod = show_deleted_results;
|
||||
|
||||
@@ -280,9 +318,9 @@ async fn modern_media_file_lookup(
|
||||
let result = match result {
|
||||
Err(e) => {
|
||||
warn!("query error: {:?}", e);
|
||||
return Err(AdvancedCommonWebError::from_anyhow_error(e));
|
||||
return Err(GetMediaFileError::ServerError);
|
||||
}
|
||||
Ok(None) => return Err(AdvancedCommonWebError::NotFound),
|
||||
Ok(None) => return Err(GetMediaFileError::NotFound),
|
||||
Ok(Some(result)) => result,
|
||||
};
|
||||
|
||||
@@ -330,7 +368,7 @@ async fn modern_media_file_lookup(
|
||||
public_bucket_url: bucket_url_from_media_path(&public_bucket_path, media_domain, server_state.server_environment)
|
||||
.map_err(|err| {
|
||||
warn!("error creating URL: {:?}", err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
GetMediaFileError::ServerError
|
||||
})?,
|
||||
public_bucket_path: public_bucket_path
|
||||
.get_full_object_path_str()
|
||||
@@ -415,7 +453,7 @@ async fn emulate_media_file_with_legacy_tts_result_lookup(
|
||||
show_deleted_results: bool,
|
||||
server_state: &ServerState,
|
||||
media_domain: MediaDomain,
|
||||
) -> Result<GetMediaFileSuccessResponse, AdvancedCommonWebError> {
|
||||
) -> Result<GetMediaFileSuccessResponse, GetMediaFileError> {
|
||||
|
||||
let result = select_tts_result_by_token(
|
||||
&media_file_token.as_str(),
|
||||
@@ -426,9 +464,9 @@ async fn emulate_media_file_with_legacy_tts_result_lookup(
|
||||
let result = match result {
|
||||
Err(e) => {
|
||||
warn!("query error (legacy TTS): {:?}", e);
|
||||
return Err(AdvancedCommonWebError::from_anyhow_error(e));
|
||||
return Err(GetMediaFileError::ServerError);
|
||||
}
|
||||
Ok(None) => return Err(AdvancedCommonWebError::NotFound),
|
||||
Ok(None) => return Err(GetMediaFileError::NotFound),
|
||||
Ok(Some(result)) => result,
|
||||
};
|
||||
|
||||
@@ -468,7 +506,7 @@ async fn emulate_media_file_with_legacy_tts_result_lookup(
|
||||
public_bucket_url: bucket_url_from_str_path(&public_bucket_path, media_domain, server_state.server_environment)
|
||||
.map_err(|err| {
|
||||
warn!("error creating URL: {:?}", err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
GetMediaFileError::ServerError
|
||||
})?,
|
||||
public_bucket_path,
|
||||
cover_image: MediaFileCoverImageDetails::from_legacy_token_str(&result.tts_result_token),
|
||||
|
||||
+14
-5
@@ -1,14 +1,19 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details_builder::MediaFileCoverImageDetailsBuilder;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::common_responses::user_details_lite_builder::UserDetailsLightBuilder;
|
||||
use crate::http_server::endpoints::media_files::get::get_media_file_handler::GetMediaFilePathInfo;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use actix_web_lab::extract::Query;
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
use artcraft_api_defs::common::responses::simple_entity_stats::SimpleEntityStats;
|
||||
@@ -40,7 +45,7 @@ pub async fn list_batch_generated_redux_media_files_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<ListBatchGeneratedReduxMediaFilesPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<ListBatchGeneratedReduxMediaFilesSuccessResponse>, AdvancedCommonWebError> {
|
||||
) -> Result<Json<ListBatchGeneratedReduxMediaFilesSuccessResponse>, CommonWebError> {
|
||||
let mut mysql_connection = server_state.mysql_pool
|
||||
.acquire()
|
||||
.await?;
|
||||
@@ -48,7 +53,11 @@ pub async fn list_batch_generated_redux_media_files_handler(
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session_from_connection(&http_request, &mut mysql_connection)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
CommonWebError::ServerError
|
||||
})?;
|
||||
|
||||
let mut show_deleted_results = false;
|
||||
let mut is_moderator = false;
|
||||
@@ -74,7 +83,7 @@ pub async fn list_batch_generated_redux_media_files_handler(
|
||||
Ok(batch) => batch.media_files,
|
||||
Err(e) => {
|
||||
warn!("query error: {:?}", e);
|
||||
return Err(AdvancedCommonWebError::from_anyhow_error(e));
|
||||
return Err(CommonWebError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+45
-10
@@ -1,3 +1,5 @@
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Query;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use actix_web_lab::__reexports::tracing::info;
|
||||
@@ -21,7 +23,6 @@ use time::ext::InstantExt;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
|
||||
@@ -181,6 +182,27 @@ pub struct FeaturedMediaFile {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum ListFeaturedMediaFilesError {
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ListFeaturedMediaFilesError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for ListFeaturedMediaFilesError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ListFeaturedMediaFilesError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
ListFeaturedMediaFilesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List media files that were featured or promoted by the staff (paginated). This will power our global asset drawers and highlight reels.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -189,15 +211,15 @@ pub struct FeaturedMediaFile {
|
||||
params(ListFeaturedMediaFilesQueryParams),
|
||||
responses(
|
||||
(status = 200, description = "List Featured Media Files", body = ListFeaturedMediaFilesSuccessResponse),
|
||||
(status = 401, description = "Not authorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
(status = 401, description = "Not authorized", body = ListFeaturedMediaFilesError),
|
||||
(status = 500, description = "Server error", body = ListFeaturedMediaFilesError),
|
||||
),
|
||||
)]
|
||||
pub async fn list_featured_media_files_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<ListFeaturedMediaFilesQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, AdvancedCommonWebError> {
|
||||
) -> Result<HttpResponse, ListFeaturedMediaFilesError> {
|
||||
|
||||
let cache_start = Instant::now();
|
||||
|
||||
@@ -224,14 +246,22 @@ pub async fn list_featured_media_files_handler(
|
||||
};
|
||||
|
||||
let cursor_next = if let Some(id) = results_page.last_id {
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)?;
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)
|
||||
.map_err(|e| {
|
||||
warn!("crypto error: {:?}", e);
|
||||
ListFeaturedMediaFilesError::ServerError
|
||||
})?;
|
||||
Some(cursor)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let cursor_previous = if let Some(id) = results_page.first_id {
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)?;
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)
|
||||
.map_err(|e| {
|
||||
warn!("crypto error: {:?}", e);
|
||||
ListFeaturedMediaFilesError::ServerError
|
||||
})?;
|
||||
Some(cursor)
|
||||
} else {
|
||||
None
|
||||
@@ -319,7 +349,8 @@ pub async fn list_featured_media_files_handler(
|
||||
}
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)?;
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| ListFeaturedMediaFilesError::ServerError)?;
|
||||
|
||||
let mut response_builder = HttpResponse::Ok();
|
||||
|
||||
@@ -337,7 +368,7 @@ async fn database_lookup(
|
||||
query: &ListFeaturedMediaFilesQueryParams,
|
||||
server_state: &ServerState,
|
||||
maybe_cloudflare_header: Option<&str>,
|
||||
) -> Result<FeaturedMediaFileListPage, AdvancedCommonWebError> {
|
||||
) -> Result<FeaturedMediaFileListPage, ListFeaturedMediaFilesError> {
|
||||
|
||||
// TODO(bt,2023-12-04): Enforce real maximums and defaults
|
||||
let limit = query.page_size.unwrap_or(25);
|
||||
@@ -346,7 +377,11 @@ async fn database_lookup(
|
||||
let cursor_is_reversed = query.cursor_is_reversed.unwrap_or(false);
|
||||
|
||||
let cursor = if let Some(cursor) = query.cursor.as_deref() {
|
||||
let cursor = server_state.sort_key_crypto.decrypt_id(cursor)?;
|
||||
let cursor = server_state.sort_key_crypto.decrypt_id(cursor)
|
||||
.map_err(|e| {
|
||||
warn!("crypto error: {:?}", e);
|
||||
ListFeaturedMediaFilesError::ServerError
|
||||
})?;
|
||||
Some(cursor as usize)
|
||||
} else {
|
||||
None
|
||||
@@ -376,7 +411,7 @@ async fn database_lookup(
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await.map_err(|err| {
|
||||
error!("DB error: {:?}", err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
ListFeaturedMediaFilesError::ServerError
|
||||
})?;
|
||||
|
||||
let query_duration = Instant::now().signed_duration_since(query_start);
|
||||
|
||||
+39
-6
@@ -1,7 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Json, Path, Query};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -21,7 +23,6 @@ use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
|
||||
@@ -29,6 +30,7 @@ use crate::http_server::common_responses::pagination_page::PaginationPage;
|
||||
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
|
||||
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::allowed_studio_access::allowed_studio_access;
|
||||
|
||||
@@ -138,6 +140,33 @@ pub struct MediaFilesByBatchListItem {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum ListMediaFilesByBatchError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for ListMediaFilesByBatchError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ListMediaFilesByBatchError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
ListMediaFilesByBatchError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ListMediaFilesByBatchError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// List media files that were generated together in a "batch", e.g. stable diffusion outputs.
|
||||
///
|
||||
/// This uses a special "batch token" that these jobs return. Media files created together will also
|
||||
@@ -151,7 +180,7 @@ pub struct MediaFilesByBatchListItem {
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List Media Files by Batch", body = ListMediaFilesByBatchSuccessResponse),
|
||||
(status = 500, description = "Server error"),
|
||||
(status = 500, description = "Server error", body = ListMediaFilesByBatchError),
|
||||
),
|
||||
)]
|
||||
pub async fn list_media_files_by_batch_token_handler(
|
||||
@@ -159,12 +188,16 @@ pub async fn list_media_files_by_batch_token_handler(
|
||||
path: Path<ListMediaFilesByBatchPathInfo>,
|
||||
query: Query<ListMediaFilesByBatchQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<ListMediaFilesByBatchSuccessResponse>, AdvancedCommonWebError>
|
||||
) -> Result<Json<ListMediaFilesByBatchSuccessResponse>, ListMediaFilesByBatchError>
|
||||
{
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
ListMediaFilesByBatchError::ServerError
|
||||
})?;
|
||||
|
||||
// NB: Temporary rollout flag for certain file types (BVH, etc).
|
||||
let mut is_allowed_studio_access = allowed_studio_access(
|
||||
@@ -193,7 +226,7 @@ pub async fn list_media_files_by_batch_token_handler(
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
warn!("Query error: {:?}", e);
|
||||
return Err(AdvancedCommonWebError::from_anyhow_error(e));
|
||||
return Err(ListMediaFilesByBatchError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+49
-9
@@ -1,6 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path, Query};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Path, Query};
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
@@ -20,7 +22,6 @@ use mysql_queries::queries::media_files::list::list_media_files_for_user::{list_
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
|
||||
@@ -31,6 +32,7 @@ use crate::http_server::endpoints::media_files::helpers::get_scoped_engine_categ
|
||||
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_classes::get_scoped_media_classes;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_types::get_scoped_media_types;
|
||||
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::allowed_studio_access::allowed_studio_access;
|
||||
|
||||
@@ -173,6 +175,33 @@ pub struct MediaFileForUserListItem {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum ListMediaFilesForUserError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for ListMediaFilesForUserError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ListMediaFilesForUserError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
ListMediaFilesForUserError::ServerError => "server error".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ListMediaFilesForUserError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// List all of a user's media files (paginated).
|
||||
///
|
||||
/// This endpoint uses the session to automatically show all files for the given user, but only
|
||||
@@ -186,7 +215,7 @@ pub struct MediaFileForUserListItem {
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "List Featured Media Files", body = ListMediaFilesForUserSuccessResponse),
|
||||
(status = 500, description = "Server error"),
|
||||
(status = 500, description = "Server error", body = ListMediaFilesForUserError),
|
||||
),
|
||||
)]
|
||||
pub async fn list_media_files_for_user_handler(
|
||||
@@ -194,12 +223,16 @@ pub async fn list_media_files_for_user_handler(
|
||||
path: Path<ListMediaFilesForUserPathInfo>,
|
||||
query: Query<ListMediaFilesForUserQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<ListMediaFilesForUserSuccessResponse>, AdvancedCommonWebError>
|
||||
) -> Result<HttpResponse, ListMediaFilesForUserError>
|
||||
{
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
ListMediaFilesForUserError::ServerError
|
||||
})?;
|
||||
|
||||
let mut is_author = false;
|
||||
let mut is_mod = false;
|
||||
@@ -254,7 +287,7 @@ pub async fn list_media_files_for_user_handler(
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
warn!("Query error: {:?}", e);
|
||||
return Err(AdvancedCommonWebError::from_anyhow_error(e));
|
||||
return Err(ListMediaFilesForUserError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -339,12 +372,19 @@ pub async fn list_media_files_for_user_handler(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(ListMediaFilesForUserSuccessResponse {
|
||||
let response = ListMediaFilesForUserSuccessResponse {
|
||||
success: true,
|
||||
results,
|
||||
pagination: PaginationPage {
|
||||
pagination: PaginationPage{
|
||||
current: results_page.current_page,
|
||||
total_page_count: results_page.total_page_count,
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| ListMediaFilesForUserError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
|
||||
+68
-14
@@ -1,6 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Query};
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::Query;
|
||||
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse};
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
@@ -9,18 +11,17 @@ use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationT
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::view_as::ViewAs;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
|
||||
use log::warn;
|
||||
use mysql_queries::queries::media_files::list::list_media_files::{list_media_files, ListMediaFilesArgs};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
|
||||
@@ -32,6 +33,7 @@ use crate::http_server::endpoints::media_files::helpers::get_scoped_engine_categ
|
||||
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_classes::get_scoped_media_classes;
|
||||
use crate::http_server::endpoints::media_files::helpers::get_scoped_media_types::get_scoped_media_types;
|
||||
use crate::http_server::web_utils::bucket_urls::bucket_url_string_from_media_path::bucket_url_string_from_media_path;
|
||||
use crate::http_server::web_utils::response_error_helpers::to_simple_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
use crate::util::allowed_explore_media_access::allowed_explore_media_access;
|
||||
|
||||
@@ -172,6 +174,36 @@ pub struct MediaFileListItem {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum ListMediaFilesError {
|
||||
ServerError,
|
||||
NotAuthorized,
|
||||
}
|
||||
|
||||
impl ResponseError for ListMediaFilesError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ListMediaFilesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ListMediaFilesError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let error_reason = match self {
|
||||
ListMediaFilesError::ServerError => "server error".to_string(),
|
||||
ListMediaFilesError::NotAuthorized => "not authorized".to_string(),
|
||||
};
|
||||
|
||||
to_simple_json_error(&error_reason, self.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ListMediaFilesError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// List all media files in the system globally, across all users (paginated).
|
||||
///
|
||||
/// This powers "explore files" page.
|
||||
@@ -182,26 +214,29 @@ pub struct MediaFileListItem {
|
||||
params(ListMediaFilesQueryParams),
|
||||
responses(
|
||||
(status = 200, description = "List Featured Media Files", body = ListMediaFilesSuccessResponse),
|
||||
(status = 401, description = "Not authorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
(status = 500, description = "Server error", body = ListMediaFilesError),
|
||||
),
|
||||
)]
|
||||
pub async fn list_media_files_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<ListMediaFilesQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<ListMediaFilesSuccessResponse>, AdvancedCommonWebError>
|
||||
) -> Result<HttpResponse, ListMediaFilesError>
|
||||
{
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
ListMediaFilesError::ServerError
|
||||
})?;
|
||||
|
||||
// ==================== FEATURE FLAG CHECK ==================== //
|
||||
|
||||
if !allowed_explore_media_access(maybe_user_session.as_ref()) {
|
||||
warn!("Explore media access is not permitted for user");
|
||||
return Err(AdvancedCommonWebError::NotAuthorized);
|
||||
return Err(ListMediaFilesError::NotAuthorized);
|
||||
}
|
||||
|
||||
let mut is_mod = false;
|
||||
@@ -220,7 +255,11 @@ pub async fn list_media_files_handler(
|
||||
let cursor_is_reversed = query.cursor_is_reversed.unwrap_or(false);
|
||||
|
||||
let cursor = if let Some(cursor) = query.cursor.as_deref() {
|
||||
let cursor = server_state.sort_key_crypto.decrypt_id(cursor)?;
|
||||
let cursor = server_state.sort_key_crypto.decrypt_id(cursor)
|
||||
.map_err(|e| {
|
||||
warn!("crypto error: {:?}", e);
|
||||
ListMediaFilesError::ServerError
|
||||
})?;
|
||||
Some(cursor as usize)
|
||||
} else {
|
||||
None
|
||||
@@ -254,19 +293,27 @@ pub async fn list_media_files_handler(
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
warn!("Query error: {:?}", e);
|
||||
return Err(AdvancedCommonWebError::from_anyhow_error(e));
|
||||
return Err(ListMediaFilesError::ServerError);
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_next = if let Some(id) = results_page.last_id {
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)?;
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)
|
||||
.map_err(|e| {
|
||||
warn!("crypto error: {:?}", e);
|
||||
ListMediaFilesError::ServerError
|
||||
})?;
|
||||
Some(cursor)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let cursor_previous = if let Some(id) = results_page.first_id {
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)?;
|
||||
let cursor = server_state.sort_key_crypto.encrypt_id(id as u64)
|
||||
.map_err(|e| {
|
||||
warn!("crypto error: {:?}", e);
|
||||
ListMediaFilesError::ServerError
|
||||
})?;
|
||||
Some(cursor)
|
||||
} else {
|
||||
None
|
||||
@@ -340,7 +387,7 @@ pub async fn list_media_files_handler(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(ListMediaFilesSuccessResponse {
|
||||
let response = ListMediaFilesSuccessResponse {
|
||||
success: true,
|
||||
results,
|
||||
pagination: PaginationCursors {
|
||||
@@ -348,5 +395,12 @@ pub async fn list_media_files_handler(
|
||||
maybe_previous: cursor_previous,
|
||||
cursor_is_reversed,
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| ListMediaFilesError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
|
||||
+38
-10
@@ -1,7 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use actix_web::web::Json;
|
||||
use artcraft_api_defs::common::responses::media_links::MediaLinks;
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -19,7 +20,6 @@ use redis::Commands;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::common_responses::media::media_file_cover_image_details::MediaFileCoverImageDetails;
|
||||
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
|
||||
use crate::http_server::common_responses::media_file_origin_details::MediaFileOriginDetails;
|
||||
@@ -122,6 +122,27 @@ pub struct PinnedMediaFile {
|
||||
/// The key we store pinned media file tokens under
|
||||
const REDIS_KEY : &str = "pinned_media_files_list";
|
||||
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum ListPinnedMediaFilesError {
|
||||
NotAuthorized,
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ListPinnedMediaFilesError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for ListPinnedMediaFilesError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
ListPinnedMediaFilesError::NotAuthorized => StatusCode::UNAUTHORIZED,
|
||||
ListPinnedMediaFilesError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List media files that were pinned by the staff (paginated).
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -129,25 +150,25 @@ const REDIS_KEY : &str = "pinned_media_files_list";
|
||||
path = "/v1/media_files/list_pinned",
|
||||
responses(
|
||||
(status = 200, description = "List Featured Media Files", body = ListPinnedMediaFilesSuccessResponse),
|
||||
(status = 401, description = "Not authorized"),
|
||||
(status = 500, description = "Server error"),
|
||||
(status = 401, description = "Not authorized", body = ListPinnedMediaFilesError),
|
||||
(status = 500, description = "Server error", body = ListPinnedMediaFilesError),
|
||||
),
|
||||
)]
|
||||
pub async fn list_pinned_media_files_handler(
|
||||
http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<Json<ListPinnedMediaFilesSuccessResponse>, AdvancedCommonWebError> {
|
||||
) -> Result<HttpResponse, impl ResponseError> {
|
||||
|
||||
let mut redis = server_state.redis_pool.get()
|
||||
.map_err(|err| {
|
||||
error!("Could not obtain redis: {err}");
|
||||
AdvancedCommonWebError::server_error_with_message("pinned media files error")
|
||||
ListPinnedMediaFilesError::ServerError
|
||||
})?;
|
||||
|
||||
let token_list : Option<String> = redis.get(REDIS_KEY)
|
||||
.map_err(|err| {
|
||||
error!("Could not get redis result: {err}");
|
||||
AdvancedCommonWebError::server_error_with_message("pinned media files error")
|
||||
ListPinnedMediaFilesError::ServerError
|
||||
})?;
|
||||
|
||||
let media_file_tokens = token_list
|
||||
@@ -171,14 +192,14 @@ pub async fn list_pinned_media_files_handler(
|
||||
Ok(media_files) => media_files,
|
||||
Err(e) => {
|
||||
warn!("Query error: {:?}", e);
|
||||
return Err(AdvancedCommonWebError::from_anyhow_error(e));
|
||||
return Err(ListPinnedMediaFilesError::ServerError);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let media_domain = get_media_domain(&http_request);
|
||||
|
||||
Ok(Json(ListPinnedMediaFilesSuccessResponse {
|
||||
let response = ListPinnedMediaFilesSuccessResponse {
|
||||
success: true,
|
||||
results: media_files.into_iter()
|
||||
.map(|m| {
|
||||
@@ -244,5 +265,12 @@ pub async fn list_pinned_media_files_handler(
|
||||
updated_at: m.updated_at,
|
||||
}
|
||||
}).collect::<Vec<_>>(),
|
||||
}))
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response)
|
||||
.map_err(|e| ListPinnedMediaFilesError::ServerError)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body))
|
||||
}
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ pub async fn fal_webhook_handler(
|
||||
// Step 4 & 5: Branch on the inner payload type.
|
||||
let result = match hydrated_contents {
|
||||
HydratedWebhookContents::Success(success_data) => {
|
||||
handle_successful_fal_webhook(&server_state, request_id, &success_data, &server_state.pager).await
|
||||
handle_successful_fal_webhook(&server_state, request_id, &success_data).await
|
||||
}
|
||||
HydratedWebhookContents::Error(error_data) => {
|
||||
handle_failed_fal_webhook(
|
||||
|
||||
+2
-3
@@ -4,7 +4,7 @@ use http_server_common::response::response_success_helpers::SimpleGenericJsonSuc
|
||||
use log::{info, warn};
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::get_inference_job_by_fal_id;
|
||||
use mysql_queries::queries::generic_inference::fal::mark_fal_generic_inference_job_successfully_done::{mark_fal_generic_inference_job_successfully_done, MarkJobArgs};
|
||||
use pager::client::pager::Pager;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
@@ -18,7 +18,6 @@ pub async fn handle_successful_fal_webhook(
|
||||
server_state: &ServerState,
|
||||
request_id: &str,
|
||||
success_data: &WebhookSuccessData,
|
||||
pager: &Pager,
|
||||
) -> Result<Json<SimpleGenericJsonSuccess>, AdvancedCommonWebError> {
|
||||
|
||||
let db_result = get_inference_job_by_fal_id(
|
||||
@@ -54,7 +53,7 @@ pub async fn handle_successful_fal_webhook(
|
||||
|
||||
if let Some(ref images_data) = extracted.images {
|
||||
info!("Handling images payload for request_id {} / job {:?}", request_id, job.job_token);
|
||||
let (media_token, batch_token) = process_images_payload(images_data, &job, server_state, pager).await?;
|
||||
let (media_token, batch_token) = process_images_payload(images_data, &job, server_state).await?;
|
||||
if maybe_media_token.is_none() {
|
||||
maybe_media_token = media_token;
|
||||
}
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ pub async fn process_image_payload(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download image from {}: {:?}", image_url, err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
if file_bytes.len() <= 10 {
|
||||
@@ -48,7 +48,7 @@ pub async fn process_image_payload(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download image on retry from {}: {:?}", image_url, err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
+3
-52
@@ -10,13 +10,10 @@ use fal_client::webhook_api::hydrated::hydrated_webhook_contents::ImagesData;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use images::encoding::webp_bytes_to_png_bytes::webp_bytes_to_png_bytes;
|
||||
use images::image_info::image_info::ImageInfo;
|
||||
use log::{error, info, warn};
|
||||
use log::{info, warn};
|
||||
use mimetypes::mimetype_info::file_extension::FileExtension;
|
||||
use mysql_queries::queries::generic_inference::fal::get_inference_job_by_fal_id::FalJobDetails;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
use pager::client::pager::Pager;
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
@@ -26,7 +23,6 @@ pub async fn process_images_payload(
|
||||
images_data: &[ImagesData],
|
||||
job: &FalJobDetails,
|
||||
server_state: &ServerState,
|
||||
pager: &Pager,
|
||||
) -> Result<(Option<MediaFileToken>, Option<BatchGenerationToken>), AdvancedCommonWebError> {
|
||||
|
||||
let mut maybe_media_token = None;
|
||||
@@ -39,57 +35,12 @@ pub async fn process_images_payload(
|
||||
maybe_batch_token = Some(BatchGenerationToken::generate());
|
||||
}
|
||||
|
||||
// NB: Fal has been failing for some images in batches of many images.
|
||||
// Rather than fail the entire batch, let's skip the failure(s) and notify ourselves.
|
||||
let mut success_count = 0;
|
||||
let mut maybe_error = None;
|
||||
|
||||
for (i, image) in images_data.iter().enumerate() {
|
||||
info!("Uploading image {} of {}: {:?}", i + 1, images_data.len(), image.url);
|
||||
|
||||
let result = upload_image(job, server_state, image, maybe_batch_token.as_ref()).await;
|
||||
|
||||
let media_token = match result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
maybe_error = Some(err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for image in images_data {
|
||||
let media_token = upload_image(job, server_state, image, maybe_batch_token.as_ref()).await?;
|
||||
|
||||
if maybe_media_token.is_none() {
|
||||
maybe_media_token = Some(media_token); // Set the first media token
|
||||
}
|
||||
|
||||
success_count += 1;
|
||||
}
|
||||
|
||||
if success_count == 0 {
|
||||
if let Some(err) = maybe_error {
|
||||
return Err(AdvancedCommonWebError::from_error(err));
|
||||
} else {
|
||||
// NB: Branch should be unreachable.
|
||||
return Err(AdvancedCommonWebError::server_error_with_message("none of the images could be processed"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(err) = maybe_error {
|
||||
// Even if some images were downloaded, let's still page about any failures.
|
||||
let notification = NotificationDetailsBuilder::from_boxed_error(err.into())
|
||||
.set_title(format!("Failure to download all images from FAL webhook: {} out of {} succeeded", success_count, images_data.len()))
|
||||
.set_description(Some(format!(
|
||||
"We uploaded all of the images we could and marked the job as a success, \
|
||||
but the user may need assistance with the remaining images and/or reimbursement.\n\
|
||||
**Internal Job Token**: {}\n\
|
||||
**Fal ID**: {}\n",
|
||||
job.job_token.as_str(),
|
||||
job.external_third_party_id)))
|
||||
.set_urgency(Some(NotificationUrgency::Medium))
|
||||
.build();
|
||||
|
||||
if let Err(pager_err) = pager.enqueue_page(notification) {
|
||||
error!("Failed to enqueue FAL webhook parse error alert: {:?}", pager_err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((maybe_media_token, maybe_batch_token))
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ pub async fn process_model_glb_payload(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download mesh from {}: {:?}", mesh_url, err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
if file_bytes.len() <= 10 {
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ pub async fn process_model_mesh_payload(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download mesh from {}: {:?}", mesh_url, err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
if file_bytes.len() <= 10 {
|
||||
@@ -46,7 +46,7 @@ pub async fn process_model_mesh_payload(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download mesh on retry from {}: {:?}", mesh_url, err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ pub async fn process_video_payload(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download video from {}: {:?}", video_url, err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
|
||||
if file_bytes.len() <= 10 {
|
||||
@@ -50,7 +50,7 @@ pub async fn process_video_payload(
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to download video on retry from {}: {:?}", video_url, err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
+1
-8
@@ -1,4 +1,3 @@
|
||||
use error_utils::try_error_name::try_error_name;
|
||||
use log::{debug, warn};
|
||||
|
||||
use pager::client::pager::Pager;
|
||||
@@ -22,13 +21,7 @@ pub(crate) fn check_advanced_common_web_error(
|
||||
return true;
|
||||
}
|
||||
|
||||
let maybe_error_name = error.cause()
|
||||
.and_then(|cause| try_error_name(cause));
|
||||
|
||||
let title = match maybe_error_name {
|
||||
Some(name) => format!("{} on {} {}", name, method, path),
|
||||
None => format!("UncaughtServerError on {} {}", method, path),
|
||||
};
|
||||
let title = format!("UncaughtServerError on {} {}", method, path);
|
||||
|
||||
// Clone the Arc'd causal error so the notification owns a reference to it.
|
||||
let cause_arc = error.clone_cause_arc();
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
use bytes::Bytes;
|
||||
use log::warn;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use wreq::{Client, StatusCode};
|
||||
use errors::AnyhowResult;
|
||||
use wreq::Client;
|
||||
|
||||
const USER_AGENT: &str = "storyteller-web/1.0";
|
||||
|
||||
// TODO(bt, 2025-06-03): Don't load the entire file into memory!
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DownloadFileToBytesError {
|
||||
WreqError(wreq::Error),
|
||||
BadResponseError {
|
||||
status_code: StatusCode,
|
||||
body: Bytes,
|
||||
url: String,
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads a (binary) file to memory. Good for images, etc. Not great for large files.
|
||||
pub async fn http_download_url_to_bytes(url: &str) -> Result<Bytes, DownloadFileToBytesError> {
|
||||
pub async fn http_download_url_to_bytes(url: &str) -> AnyhowResult<Bytes> {
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
@@ -30,44 +18,7 @@ pub async fn http_download_url_to_bytes(url: &str) -> Result<Bytes, DownloadFile
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
if !status.is_success() {
|
||||
warn!("Error downloading URL (status={}): {:?}", status.as_u16(), url);
|
||||
|
||||
return Err(DownloadFileToBytesError::BadResponseError {
|
||||
status_code: status,
|
||||
body: bytes,
|
||||
url: url.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
impl Error for DownloadFileToBytesError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
DownloadFileToBytesError::WreqError(e) => Some(e),
|
||||
DownloadFileToBytesError::BadResponseError { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DownloadFileToBytesError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DownloadFileToBytesError::WreqError(e) => write!(f, "Wreq error: {}", e),
|
||||
DownloadFileToBytesError::BadResponseError { status_code, body, url } => {
|
||||
write!(f, "Bad response (status={}) fetching {} : {:?}", status_code.as_u16(), url, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<wreq::Error> for DownloadFileToBytesError {
|
||||
fn from(e: wreq::Error) -> Self {
|
||||
DownloadFileToBytesError::WreqError(e)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user