new endpoint for media files by job token

This commit is contained in:
Hanashi committed 2026-08-04 23:10:59 -04:00
1 parent 36cfba0000
commit cedbea71b2
11 files changed
+486

No files matched your search

@@ -0,0 +1,179 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.token as `token: tokens::tokens::media_files::MediaFileToken`,\n\n m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,\n media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,\n media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,\n\n m.maybe_origin_filename,\n m.maybe_duration_millis as `maybe_duration_millis: u64`,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nWHERE\n m.maybe_source_job_token = ?\n AND m.maybe_creator_user_token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token: tokens::tokens::media_files::MediaFileToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "media_class: enums::by_table::media_files::media_file_class::MediaFileClass",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 2,
"name": "media_type: enums::by_table::media_files::media_file_type::MediaFileType",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 3,
"name": "maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 4,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
"flags": "MULTIPLE_KEY | BINARY",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 5,
"name": "public_bucket_directory_hash",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 6,
"name": "maybe_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 7,
"name": "maybe_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 8,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
"flags": "BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 9,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 10,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 11,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 1020
}
},
{
"ordinal": 12,
"name": "maybe_duration_millis: u64",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 13,
"name": "created_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 14,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP | ON_UPDATE_NOW",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
false,
false
]
},
"hash": "cd04f4f7b48ae9da53b6a210111f8e3edff94dee30263e22b45d5e29dbe9245a"
}
@@ -0,0 +1,61 @@
//! Types for `GET /v1/media_files/by_job/{job_token}` — list the media files
//! produced by an inference job.
use chrono::{DateTime, Utc};
use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_type::MediaFileType;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use crate::common::responses::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::common::responses::media_links::MediaLinks;
#[derive(Serialize, Deserialize, ToSchema)]
pub struct ListMediaFilesByJobSuccessResponse {
pub success: bool,
/// The job's output files, oldest first.
pub media_files: Vec<JobMediaFileInfo>,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct JobMediaFileInfo {
pub token: MediaFileToken,
/// The coarse-grained class of media file: image, video, etc.
pub media_class: MediaFileClass,
/// Type of media will dictate which fields are populated and what
/// the frontend should display (eg. video player vs audio player).
/// This is closer in meaning to a "mime type".
pub media_type: MediaFileType,
/// If the file was generated as part of a batch, this is the token for the batch.
pub maybe_batch_token: Option<BatchGenerationToken>,
/// The foreign key to the prompt used to generate the media, if applicable.
pub maybe_prompt_token: Option<PromptToken>,
/// Rich CDN links to the media, including thumbnails, previews, and more.
pub media_links: MediaLinks,
/// Information about the cover image. Many media files do not require a cover image,
/// e.g. image files, video files with thumbnails, audio files, etc.
/// 3D files require them.
pub cover_image: MediaFileCoverImageDetails,
/// The original filename for uploaded files, if they were provided.
/// In the future we'll provide our own internal optional filenames.
pub maybe_original_filename: Option<String>,
/// Duration for audio and video files, if available.
/// Measured in milliseconds.
pub maybe_duration_millis: Option<u64>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -0,0 +1 @@
pub mod list_media_files_by_job;
@@ -1,4 +1,5 @@
pub mod delete_media_file;
pub mod job;
pub mod list;
pub mod list_batch_generated_media_files;
pub mod list_session_project_media_files;
@@ -0,0 +1,110 @@
use std::marker::PhantomData;
use chrono::{DateTime, Utc};
use sqlx::{Executor, MySql};
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_type::MediaFileType;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
/// A job has a bounded number of outputs (the largest batches are single
/// digits), so the list is capped rather than paginated.
const MAX_RESULTS: u32 = 100;
pub struct JobMediaFileRecord {
pub token: MediaFileToken,
pub media_class: MediaFileClass,
pub media_type: MediaFileType,
pub maybe_batch_token: Option<BatchGenerationToken>,
pub maybe_prompt_token: Option<PromptToken>,
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_file_cover_image_public_bucket_hash: Option<String>,
pub maybe_file_cover_image_public_bucket_prefix: Option<String>,
pub maybe_file_cover_image_public_bucket_extension: Option<String>,
pub maybe_origin_filename: Option<String>,
pub maybe_duration_millis: Option<u64>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
pub struct ListMediaFilesBySourceJobArgs<'a, 'c, E>
where
E: 'a + Executor<'c, Database = MySql>,
{
/// The inference job whose outputs to list.
pub job_token: &'a InferenceJobToken,
/// Only return files created by this user (the session user). Enforces that
/// callers can only list the outputs of their own jobs.
pub creator_user_token: &'a UserToken,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
}
/// List the media files produced by an inference job (`maybe_source_job_token`),
/// oldest first. Deliberately lean: no user/model/stats joins — just the file
/// fields plus a self-join for the cover image.
pub async fn list_media_files_by_source_job<'a, 'c: 'a, E>(
args: ListMediaFilesBySourceJobArgs<'a, 'c, E>,
) -> Result<Vec<JobMediaFileRecord>, sqlx::Error>
where
E: 'a + Executor<'c, Database = MySql>,
{
sqlx::query_as!(
JobMediaFileRecord,
r#"
SELECT
m.token as `token: tokens::tokens::media_files::MediaFileToken`,
m.media_class as `media_class: enums::by_table::media_files::media_file_class::MediaFileClass`,
m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,
m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,
m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
media_file_cover_image.public_bucket_directory_hash as maybe_file_cover_image_public_bucket_hash,
media_file_cover_image.maybe_public_bucket_prefix as maybe_file_cover_image_public_bucket_prefix,
media_file_cover_image.maybe_public_bucket_extension as maybe_file_cover_image_public_bucket_extension,
m.maybe_origin_filename,
m.maybe_duration_millis as `maybe_duration_millis: u64`,
m.created_at,
m.updated_at
FROM media_files AS m
LEFT OUTER JOIN media_files as media_file_cover_image
ON media_file_cover_image.token = m.maybe_cover_image_media_file_token
WHERE
m.maybe_source_job_token = ?
AND m.maybe_creator_user_token = ?
AND m.user_deleted_at IS NULL
AND m.mod_deleted_at IS NULL
AND NOT m.is_intermediate_system_file
ORDER BY m.id ASC
LIMIT ?
"#,
args.job_token,
args.creator_user_token,
MAX_RESULTS,
)
.fetch_all(args.mysql_executor)
.await
}
@@ -1,6 +1,7 @@
pub mod _keys;
pub mod count_untried_jobs_of_type;
pub mod list_available_generic_inference_jobs;
pub mod list_media_files_by_source_job;
pub mod mark_generic_inference_job_completely_failed;
pub mod mark_generic_inference_job_failure;
pub mod mark_job_failed_by_token;
@@ -356,6 +356,7 @@ use artcraft_api_defs::credits::get_session_credits::*;
use artcraft_api_defs::subscriptions::get_session_subscription::*;
use artcraft_api_defs::media_file::list_batch_generated_media_files::*;
use artcraft_api_defs::media_file::list::by_type::list_session_common::*;
use artcraft_api_defs::media_file::job::list_media_files_by_job::*;
use artcraft_api_defs::media_file::list::by_type::list_session_mesh_media_files::*;
use artcraft_api_defs::media_file::list::by_type::list_session_splat_media_files::*;
use artcraft_api_defs::media_file::list_session_project_media_files::*;
@@ -495,6 +496,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::media_files::edit::set_media_file_cover_image_handler::set_media_file_cover_image_handler,
crate::http_server::endpoints::media_files::get::batch_get_media_files_handler::batch_get_media_files_handler,
crate::http_server::endpoints::media_files::get::get_media_file_handler::get_media_file_handler,
crate::http_server::endpoints::media_files::job::list_media_files_by_job_handler::list_media_files_by_job_handler,
crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::list_featured_media_files_handler,
crate::http_server::endpoints::media_files::list::list_media_files_by_batch_token_handler::list_media_files_by_batch_token_handler,
crate::http_server::endpoints::media_files::list::list_media_files_for_user_handler::list_media_files_for_user_handler,
@@ -791,6 +793,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
// Common response structs
JobDetailsLivePortraitRequest,
JobMediaFileInfo,
JobDetailsLipsyncRequest,
MediaFileLivePortraitDetails,
MediaFileModelDetails,
@@ -1125,6 +1128,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
ListUserImpersonationRequestsSuccessResponse,
ListMediaFilesByBatchPathInfo,
ListMediaFilesByBatchSuccessResponse,
ListMediaFilesByJobSuccessResponse,
ListMediaFilesForUserPathInfo,
ListMediaFilesForUserQueryParams,
ListMediaFilesForUserSuccessResponse,
@@ -0,0 +1,122 @@
use std::marker::PhantomData;
use std::sync::Arc;
use actix_web::web::{Json, Path};
use actix_web::{web, HttpRequest};
use log::{error, warn};
use artcraft_api_defs::media_file::job::list_media_files_by_job::{
JobMediaFileInfo, ListMediaFilesByJobSuccessResponse,
};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use mysql_queries::queries::generic_inference::job::list_media_files_by_source_job::{
list_media_files_by_source_job, ListMediaFilesBySourceJobArgs,
};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use utoipa::ToSchema;
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::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::user_lookup::api_or_web_session::require_api_or_web_session::require_api_or_web_session;
use crate::state::server_state::ServerState;
#[derive(Deserialize, ToSchema)]
pub struct ListMediaFilesByJobPathInfo {
job_token: InferenceJobToken,
}
/// List the media files produced by an inference job.
/// Authenticates as either a web-session (cookie) user or an API-key (`Authorization` header)
/// user, and only returns files from the caller's own jobs.
#[utoipa::path(
get,
tag = "Media Files",
path = "/v1/media_files/by_job/{job_token}",
responses(
(status = 200, description = "Success", body = ListMediaFilesByJobSuccessResponse),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Server error"),
),
params(
("path" = ListMediaFilesByJobPathInfo, description = "Path for Request")
)
)]
pub async fn list_media_files_by_job_handler(
http_request: HttpRequest,
path: Path<ListMediaFilesByJobPathInfo>,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListMediaFilesByJobSuccessResponse>, CommonWebError> {
let job_token = &path.job_token;
let mut mysql_connection = server_state.mysql_pool
.acquire()
.await
.map_err(|err| {
error!("MySql pool error: {:?}", err);
CommonWebError::from_error(err)
})?;
let session = require_api_or_web_session(
&http_request,
&server_state.session_checker,
&server_state.avt_cookie_manager,
&mut *mysql_connection,
).await?;
// NB: Jobs another user owns (or unknown job tokens) simply return an empty
// list — the query filters on the session user's ownership.
let records = list_media_files_by_source_job(ListMediaFilesBySourceJobArgs {
job_token,
creator_user_token: &session.user_token,
mysql_executor: &mut *mysql_connection,
phantom: PhantomData,
})
.await
.map_err(|err| {
warn!("List media files by job {:?} query error: {:?}", job_token, err);
CommonWebError::from_error(err)
})?;
let media_domain = get_media_domain(&http_request);
let media_files = records.into_iter()
.map(|record| {
let public_bucket_path = MediaFileBucketPath::from_object_hash(
&record.public_bucket_directory_hash,
record.maybe_public_bucket_prefix.as_deref(),
record.maybe_public_bucket_extension.as_deref(),
);
JobMediaFileInfo {
media_links: MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path,
),
cover_image: MediaFileCoverImageDetailsBuilder::from_optional_db_fields(
&record.token,
media_domain,
server_state.server_environment,
record.maybe_file_cover_image_public_bucket_hash.as_deref(),
record.maybe_file_cover_image_public_bucket_prefix.as_deref(),
record.maybe_file_cover_image_public_bucket_extension.as_deref(),
),
token: record.token,
media_class: record.media_class,
media_type: record.media_type,
maybe_batch_token: record.maybe_batch_token,
maybe_prompt_token: record.maybe_prompt_token,
maybe_original_filename: record.maybe_origin_filename,
maybe_duration_millis: record.maybe_duration_millis,
created_at: record.created_at,
updated_at: record.updated_at,
}
})
.collect::<Vec<_>>();
Ok(Json(ListMediaFilesByJobSuccessResponse {
success: true,
media_files,
}))
}
@@ -0,0 +1 @@
pub mod list_media_files_by_job_handler;
@@ -3,6 +3,7 @@ pub mod delete;
pub mod edit;
pub mod get;
pub mod helpers;
pub mod job;
pub mod list;
pub mod search;
pub mod upload;
@@ -12,6 +12,7 @@ use crate::http_server::endpoints::media_files::edit::set_media_file_cover_image
use crate::http_server::endpoints::media_files::edit::update_media_file_handler::update_media_file_handler;
use crate::http_server::endpoints::media_files::get::batch_get_media_files_handler::batch_get_media_files_handler;
use crate::http_server::endpoints::media_files::get::get_media_file_handler::get_media_file_handler;
use crate::http_server::endpoints::media_files::job::list_media_files_by_job_handler::list_media_files_by_job_handler;
use crate::http_server::endpoints::media_files::list::list_batch_generated_redux_media_files_handler::list_batch_generated_redux_media_files_handler;
use crate::http_server::endpoints::media_files::list::list_featured_media_files_handler::list_featured_media_files_handler;
use crate::http_server::endpoints::media_files::list::list_media_files_by_batch_token_handler::list_media_files_by_batch_token_handler;
@@ -98,6 +99,10 @@ pub fn add_media_file_routes<T, B> (app: App<T>) -> App<T>
.route(web::get().to(list_media_files_by_batch_token_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/by_job/{job_token}")
.route(web::get().to(list_media_files_by_job_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
.service(web::resource("/list_featured")
.route(web::get().to(list_featured_media_files_handler))
.route(web::head().to(|| HttpResponse::Ok()))