Compare commits

...
Author SHA1 Message Date
Brandon Thomas 977b2ce5b6 better website hero text 2026-10-01 20:21:56 -04:00
BFlat e92253c5b4 Add Craft Apps marketing pages to the website (#45)
## Summary

Marketing pages on getartcraft.com for the five Craft Apps: PhotoCraft,
DrawCraft, FilmCraft, LightCraft and PrintCraft. Everything is driven by
one data file, `src/lib/crafting-apps.ts`.

- **`/apps` hub:** lineup cards for all five apps, shared principles,
and Discord calls to action.
- **`/apps/<slug>`:**
  - Hero screenshot with Discord, GitHub and share links.
  - Six highlights and a screenshot gallery.
- "Get it": a Discord waitlist for installers, next to build-from-source
steps with a copy button.
  - Cards linking to the sibling apps.
- **Nav:** a "Craft Apps" link after a divider following Support, and a
Discord button on desktop.
- **Footer:** a "Craft Apps" column.
- **Sitemap:** entries for the hub and all five app pages.
- **Press kit:** all 20 app screenshots, with full-resolution downloads.

## Branding
- **Colors:** each app has its own accent (`.app-*` in `globals.css`),
AA-contrast in light and dark.
- **Wordmarks:** text only, with "Craft" set in the app's color. These
pages don't use the serif accent font.
- **Share previews:** a 1200×630 image per app plus one for the hub, and
`SoftwareApplication` JSON-LD on each app page.
- **Adobe:** the copy never names Adobe products.

## Notes
- No installers exist yet in any of the five repos, so each page sends
visitors to Discord and to building from source. When an app ships a
release, adding `downloads` to its entry in `crafting-apps.ts` replaces
that block with download buttons.
- Screenshots come from each repo's `docs/images`, show only
public-domain artwork, and are converted to WebP at 1600px.
- **Small refactors:**
- New shared `DiscordButton`, now also used by the community strip and
the press kit.
  - New `CopyButton` in the UI kit.
  - `SITE_URL`/`siteUrl` moved into `lib/links.ts`.
  - New `kicker`/`mediaTag` props on `CampaignHero`.

## Testing
- A clean `npm ci && npm run build`, the same build Netlify runs,
passes: TypeScript is clean, `/apps` is static, and all five app pages
prerender as static HTML.
- `next start` serves all six routes plus their images, and an unknown
slug returns 404.
- Each page's Open Graph and Twitter tags render with the per-app image.
- Checked visually at 1440px and 1024px (the nav fits) and at a 390px
mobile viewport.
2026-10-01 22:56:48 +07:00
Brandon Thomas 847edc81e5 Merge pull request #44 from storytold/bt-no-more-gif-thumbnails
webp instead of gif thumbnails
2026-09-28 01:15:13 -04:00
Brandon Thomas 2e596cc4b3 ffmpeg fallback on mac 2026-09-28 01:07:45 -04:00
Brandon Thomas c3d72fb321 Generate WebP thumbnails for videos instead of Gif (except: MacOS 10.15)
MacOS Catalina 10.15 released October 7, 2019 can't do gifs in
safari/webkit.
2026-09-28 00:52:07 -04:00
Brandon Thomas cd27599c0a Merge pull request #43 from storytold/bt-additional-cors-tests
additional cors tests to prevent regressions
2026-09-27 23:43:50 -04:00
Brandon Thomas 46fd907182 additional cors tests to prevent regressions 2026-09-27 23:43:06 -04:00
Brandon Thomas 5bdc19f573 Merge pull request #42 from storytold/bt-domain-config-2
tests
2026-09-27 22:57:15 -04:00
Brandon Thomas 6b34f23b43 tests 2026-09-27 22:55:36 -04:00
Brandon Thomas ddf151794e Merge pull request #41 from storytold/bt-domain-config
domain config
2026-09-27 22:46:20 -04:00
170 changed files with 3807 additions and 371 deletions

No files matched your search

@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\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.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\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.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -75,6 +75,16 @@
},
{
"ordinal": 7,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 8,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -84,7 +94,7 @@
}
},
{
"ordinal": 8,
"ordinal": 9,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -212,6 +222,7 @@
true,
true,
true,
true,
false,
true,
true,
@@ -219,5 +230,5 @@
false
]
},
"hash": "a52cc2d6e86607c430173553230b3794a4c1ada07274a975c04128e47d3b7f83"
"hash": "251082fa59374723719147af19e3411300291f1c7de0acffd078dff73f7fcf40"
}
@@ -1,6 +1,6 @@
{
"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_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n ",
"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_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n ",
"describe": {
"columns": [
{
@@ -255,6 +255,16 @@
},
{
"ordinal": 25,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 26,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -264,7 +274,7 @@
}
},
{
"ordinal": 26,
"ordinal": 27,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -303,9 +313,10 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "971471f349bcd22364b16b5d6c68e807ed897101eec1e8a62a7b8fab3359fa0c"
"hash": "29f205860e3a790cc0b4b4b8bbd28bbc92a9df27ba62cc2688be1aa195b1fcf2"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,\n pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension\n\nFROM prompt_context_items pci\nJOIN media_files m\nON\n pci.media_token = m.token\nWHERE\n pci.prompt_token = ?\nORDER BY pci.id ASC\n ",
"query": "\nSELECT\n pci.media_token as `media_token: tokens::tokens::media_files::MediaFileToken`,\n pci.context_semantic_type as `context_semantic_type: enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n m.created_at as `created_at: DateTime<Utc>`\n\nFROM prompt_context_items pci\nJOIN media_files m\nON\n pci.media_token = m.token\nWHERE\n pci.prompt_token = ?\nORDER BY pci.id ASC\n ",
"describe": {
"columns": [
{
@@ -52,6 +52,26 @@
"char_set": 224,
"max_size": 64
}
},
{
"ordinal": 5,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 6,
"name": "created_at: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
}
],
"parameters": {
@@ -62,8 +82,10 @@
false,
false,
true,
true
true,
true,
false
]
},
"hash": "094eb099cde85835db0662c2700c6b82644b51bf9b3609391ed4d2945fe5e98f"
"hash": "2e53d51429bf380b141a8a680d1fc17990ab02f822cae4d8def2d1126e513549"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -95,6 +95,16 @@
},
{
"ordinal": 9,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 10,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "is_intermediate_system_file: bool",
"type_info": {
"type": "Tiny",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -214,7 +224,7 @@
}
},
{
"ordinal": 21,
"ordinal": 22,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -224,7 +234,7 @@
}
},
{
"ordinal": 22,
"ordinal": 23,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -250,6 +260,7 @@
true,
true,
true,
true,
false,
false,
false,
@@ -263,5 +274,5 @@
false
]
},
"hash": "c0ba9f9aa3184b1cb3d714173b31e530e201a6c24afd29ad534cf7f3954c7316"
"hash": "41b47b3e0874676651bd7de2c6c3bd2c25db56c51ce6e16810f4a7f56dfb48ab"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n jobs.failure_reason,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n jobs.failure_reason,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n media_files.maybe_thumbnail_version as `maybe_media_file_thumbnail_version?: u8`,\n media_files.created_at as `maybe_media_file_created_at?: DateTime<Utc>`,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
"describe": {
"columns": [
{
@@ -265,6 +265,26 @@
},
{
"ordinal": 26,
"name": "maybe_media_file_thumbnail_version?: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 27,
"name": "maybe_media_file_created_at?: DateTime<Utc>",
"type_info": {
"type": "Timestamp",
"flags": "MULTIPLE_KEY | BINARY | TIMESTAMP",
"char_set": 63,
"max_size": 19
}
},
{
"ordinal": 28,
"name": "maybe_assigned_worker",
"type_info": {
"type": "VarString",
@@ -274,7 +294,7 @@
}
},
{
"ordinal": 27,
"ordinal": 29,
"name": "maybe_assigned_cluster",
"type_info": {
"type": "VarString",
@@ -284,7 +304,7 @@
}
},
{
"ordinal": 28,
"ordinal": 30,
"name": "is_keepalive_required",
"type_info": {
"type": "Tiny",
@@ -294,7 +314,7 @@
}
},
{
"ordinal": 29,
"ordinal": 31,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -304,7 +324,7 @@
}
},
{
"ordinal": 30,
"ordinal": 32,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -314,7 +334,7 @@
}
},
{
"ordinal": 31,
"ordinal": 33,
"name": "maybe_first_started_at",
"type_info": {
"type": "Timestamp",
@@ -324,7 +344,7 @@
}
},
{
"ordinal": 32,
"ordinal": 34,
"name": "maybe_successfully_completed_at",
"type_info": {
"type": "Timestamp",
@@ -334,7 +354,7 @@
}
},
{
"ordinal": 33,
"ordinal": 35,
"name": "database_clock",
"type_info": {
"type": "Datetime",
@@ -376,6 +396,8 @@
true,
true,
true,
true,
true,
false,
false,
false,
@@ -384,5 +406,5 @@
false
]
},
"hash": "6948edca3f06643605695665df51cc94f897e2cb9a2d6906910d58d4d56ee73e"
"hash": "4b3fa0dcfb916ba7af04ae35c69a2b75d1f4a4858e8f94f87d74c69fccc32079"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\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.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\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.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -213,11 +223,12 @@
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "d8f4ce4e1026c71a7be8fd87f00f6f8858c401701da1ca3316eed1ed971d4bb2"
"hash": "503c8faa461462b3e2f8cae3cb702922ad54a8ddbdb01d1e7fc785ca1b7dd4c4"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\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.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\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.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -213,11 +223,12 @@
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "11cc7b91730784e9e8c8f073aeaf51a1f1660584e379b9a5be3aea370d84f692"
"hash": "770f5c0672653e82c40deade6ba7b1e769558d8ebcf40d736f798f30c7a75c68"
}
@@ -1,6 +1,6 @@
{
"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 ",
"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 m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\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": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_duration_millis: u64",
"type_info": {
"type": "Long",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -171,9 +181,10 @@
true,
true,
true,
true,
false,
false
]
},
"hash": "cd04f4f7b48ae9da53b6a210111f8e3edff94dee30263e22b45d5e29dbe9245a"
"hash": "79f08e53fdacb526524810f481d22ca169b9f35e8874ed898b63cd2ba4b7453d"
}
@@ -1,6 +1,6 @@
{
"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_engine_category as `maybe_engine_category: enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory`,\n m.maybe_animation_type as `maybe_animation_type: enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_style_transfer_source_media_file_token as `maybe_style_transfer_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n m.maybe_scene_source_media_file_token as `maybe_scene_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n model_weights.token as `maybe_model_weights_token: tokens::tokens::model_weights::ModelWeightToken`,\n model_weights.title as maybe_model_weights_title,\n model_weights.weights_type as `maybe_model_weights_type: enums::by_table::model_weights::weights_types::WeightsType`,\n model_weights.weights_category as `maybe_model_weights_category: enums::by_table::model_weights::weights_category::WeightsCategory`,\n\n model_weight_cover_image.public_bucket_directory_hash as maybe_model_cover_image_public_bucket_hash,\n model_weight_cover_image.maybe_public_bucket_prefix as maybe_model_cover_image_public_bucket_prefix,\n model_weight_cover_image.maybe_public_bucket_extension as maybe_model_cover_image_public_bucket_extension,\n\n model_weight_creator.token as `maybe_model_weight_creator_user_token: tokens::tokens::users::UserToken`,\n model_weight_creator.username as maybe_model_weight_creator_username,\n model_weight_creator.display_name as maybe_model_weight_creator_display_name,\n model_weight_creator.email_gravatar_hash as maybe_model_weight_creator_gravatar_hash,\n\n m.extra_file_modification_info,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n entity_stats.ratings_positive_count as maybe_ratings_positive_count,\n entity_stats.ratings_negative_count as maybe_ratings_negative_count,\n entity_stats.bookmark_count as maybe_bookmark_count,\n\n featured_items.entity_token IS NOT NULL AS is_featured,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN model_weights\n ON m.maybe_origin_model_token = model_weights.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN media_files as model_weight_cover_image\n ON model_weight_cover_image.token = model_weights.maybe_cover_image_media_file_token\nLEFT OUTER JOIN users as model_weight_creator\n ON model_weight_creator.token = model_weights.creator_user_token\nLEFT OUTER JOIN entity_stats\n ON entity_stats.entity_type = \"media_file\"\n AND entity_stats.entity_token = m.token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nLEFT OUTER JOIN featured_items\n ON featured_items.entity_type = 'media_file'\n AND featured_items.entity_token = m.token\n AND featured_items.deleted_at IS NULL\nWHERE\n m.token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n ",
"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_engine_category as `maybe_engine_category: enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory`,\n m.maybe_animation_type as `maybe_animation_type: enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_style_transfer_source_media_file_token as `maybe_style_transfer_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n m.maybe_scene_source_media_file_token as `maybe_scene_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n model_weights.token as `maybe_model_weights_token: tokens::tokens::model_weights::ModelWeightToken`,\n model_weights.title as maybe_model_weights_title,\n model_weights.weights_type as `maybe_model_weights_type: enums::by_table::model_weights::weights_types::WeightsType`,\n model_weights.weights_category as `maybe_model_weights_category: enums::by_table::model_weights::weights_category::WeightsCategory`,\n\n model_weight_cover_image.public_bucket_directory_hash as maybe_model_cover_image_public_bucket_hash,\n model_weight_cover_image.maybe_public_bucket_prefix as maybe_model_cover_image_public_bucket_prefix,\n model_weight_cover_image.maybe_public_bucket_extension as maybe_model_cover_image_public_bucket_extension,\n\n model_weight_creator.token as `maybe_model_weight_creator_user_token: tokens::tokens::users::UserToken`,\n model_weight_creator.username as maybe_model_weight_creator_username,\n model_weight_creator.display_name as maybe_model_weight_creator_display_name,\n model_weight_creator.email_gravatar_hash as maybe_model_weight_creator_gravatar_hash,\n\n m.extra_file_modification_info,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n entity_stats.ratings_positive_count as maybe_ratings_positive_count,\n entity_stats.ratings_negative_count as maybe_ratings_negative_count,\n entity_stats.bookmark_count as maybe_bookmark_count,\n\n featured_items.entity_token IS NOT NULL AS is_featured,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN model_weights\n ON m.maybe_origin_model_token = model_weights.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN media_files as model_weight_cover_image\n ON model_weight_cover_image.token = model_weights.maybe_cover_image_media_file_token\nLEFT OUTER JOIN users as model_weight_creator\n ON model_weight_creator.token = model_weights.creator_user_token\nLEFT OUTER JOIN entity_stats\n ON entity_stats.entity_type = \"media_file\"\n AND entity_stats.entity_token = m.token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nLEFT OUTER JOIN featured_items\n ON featured_items.entity_type = 'media_file'\n AND featured_items.entity_token = m.token\n AND featured_items.deleted_at IS NULL\nWHERE\n m.token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n ",
"describe": {
"columns": [
{
@@ -415,6 +415,16 @@
},
{
"ordinal": 41,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 42,
"name": "maybe_ratings_positive_count",
"type_info": {
"type": "Long",
@@ -424,7 +434,7 @@
}
},
{
"ordinal": 42,
"ordinal": 43,
"name": "maybe_ratings_negative_count",
"type_info": {
"type": "Long",
@@ -434,7 +444,7 @@
}
},
{
"ordinal": 43,
"ordinal": 44,
"name": "maybe_bookmark_count",
"type_info": {
"type": "Long",
@@ -444,7 +454,7 @@
}
},
{
"ordinal": 44,
"ordinal": 45,
"name": "is_featured",
"type_info": {
"type": "LongLong",
@@ -454,7 +464,7 @@
}
},
{
"ordinal": 45,
"ordinal": 46,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -464,7 +474,7 @@
}
},
{
"ordinal": 46,
"ordinal": 47,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -522,10 +532,11 @@
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "ad217c26087b2ebed6f2c9f79b16ead63727107dfce3366f3c1d12bc9d565c2c"
"hash": "7a499a8fabb4633ef1c26a2df29851a706d9b6d5e99d2dc57a42f3939c7c5ecd"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.media_class = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1\n FROM folder_media_files fmf\n JOIN folders f\n ON f.token = fmf.folder_token\n AND f.maybe_deleted_at IS NULL\n WHERE fmf.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.media_class = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1\n FROM folder_media_files fmf\n JOIN folders f\n ON f.token = fmf.folder_token\n AND f.maybe_deleted_at IS NULL\n WHERE fmf.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "3681e931cc87714d71c95da44d6dd92568ce58aea252164ab3d5f8e96491169c"
"hash": "96d03f60d11b2303a8fe8ac16e17cd1ee58fec404049ee3f906ed7b5fcc9c3ed"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n fmf.created_at as `added_to_folder_at: DateTime<Utc>`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n mf.is_intermediate_system_file as `is_intermediate_system_file: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM folder_media_files fmf\nJOIN media_files mf\n ON mf.token = fmf.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE fmf.folder_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -95,6 +95,16 @@
},
{
"ordinal": 9,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 10,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "is_intermediate_system_file: bool",
"type_info": {
"type": "Tiny",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -214,7 +224,7 @@
}
},
{
"ordinal": 21,
"ordinal": 22,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -224,7 +234,7 @@
}
},
{
"ordinal": 22,
"ordinal": 23,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -250,6 +260,7 @@
true,
true,
true,
true,
false,
false,
false,
@@ -263,5 +274,5 @@
false
]
},
"hash": "8aca9bc976b980ea1a3b3052bc8a45dd4d8db55c20fd1aede2a4415cc1395cf3"
"hash": "9bf5ccbfaaa7c1c4e9c4af4b3044b307976825383c9a737a22a877c89cd6db09"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\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.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\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.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.maybe_project_type = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id > ?\nORDER BY m.id ASC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -213,11 +223,12 @@
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "6c7adbc8171bc3fdfb6109193ffb0a55413fa0942746d65037c2b1d39b361bd0"
"hash": "9c8c0ea8510cd66175014e4cf4ffaf403c27256ae1e39f06f1bc430dbcea5e62"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1 FROM media_file_tags mft\n WHERE mft.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1 FROM media_file_tags mft\n WHERE mft.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "aa1e693babe6997e75935ec893e6eed46351ec72ce02a9144fd11d2fcfd37fdf"
"hash": "9f116e00964272b8389a766674177e7882a16ad6459785551bc2093e27404f80"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\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.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\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.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -75,6 +75,16 @@
},
{
"ordinal": 7,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 8,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -84,7 +94,7 @@
}
},
{
"ordinal": 8,
"ordinal": 9,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -212,6 +222,7 @@
true,
true,
true,
true,
false,
true,
true,
@@ -219,5 +230,5 @@
false
]
},
"hash": "73540435892b7b4dbc11c6c63712abdb18fd8b3db2bcd56b8518c968b8169de7"
"hash": "b0d2f025aa7d137c0a9f50d0af4bf863e1d0741d9a34111bdfaaf18f10ec11f4"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n m.id as `id: u64`,\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.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n m.id as `id: u64`,\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.maybe_project_type as `project_type!: enums::by_table::media_files::media_file_project_type::MediaFileProjectType`,\n m.media_type as `media_type: enums::by_table::media_files::media_file_type::MediaFileType`,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.maybe_title,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m FORCE INDEX (fk_maybe_creator_user_token)\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\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_creator_user_token = ?\n AND m.media_class = 'project'\n AND m.maybe_project_type IS NOT NULL\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n AND NOT m.is_intermediate_system_file\n AND m.id < ?\nORDER BY m.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_creator_username",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_creator_display_name",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "maybe_creator_gravatar_hash",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "maybe_file_cover_image_public_bucket_hash",
"type_info": {
"type": "VarString",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_file_cover_image_public_bucket_prefix",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_file_cover_image_public_bucket_extension",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "creator_set_visibility: enums::common::visibility::Visibility",
"type_info": {
"type": "String",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -213,11 +223,12 @@
true,
true,
true,
true,
false,
true,
false,
false
]
},
"hash": "97e4bc3ab037126e3f4e213cdb7535c2ad233809fbeb6607c34fb02ec45cfe47"
"hash": "d96bca438343b0a5a3f202ae48b51670ed3e660cdfaa0100b47893249d5d0c11"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND EXISTS (\n SELECT 1 FROM media_file_tags mft\n WHERE mft.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND EXISTS (\n SELECT 1 FROM media_file_tags mft\n WHERE mft.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "76bcf698f40d852558f071a6909b05b1c960c1cf236831ae009f90de46d3aefb"
"hash": "dabd15fbe5fd3cc9f0ce921d9c804ea25956fc36bb7e677d29b869b31f2c6c1d"
}
@@ -1,6 +1,6 @@
{
"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_engine_category as `maybe_engine_category: enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory`,\n m.maybe_animation_type as `maybe_animation_type: enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_style_transfer_source_media_file_token as `maybe_style_transfer_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n m.maybe_scene_source_media_file_token as `maybe_scene_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n model_weights.token as `maybe_model_weights_token: tokens::tokens::model_weights::ModelWeightToken`,\n model_weights.title as maybe_model_weights_title,\n model_weights.weights_type as `maybe_model_weights_type: enums::by_table::model_weights::weights_types::WeightsType`,\n model_weights.weights_category as `maybe_model_weights_category: enums::by_table::model_weights::weights_category::WeightsCategory`,\n\n model_weight_cover_image.public_bucket_directory_hash as maybe_model_cover_image_public_bucket_hash,\n model_weight_cover_image.maybe_public_bucket_prefix as maybe_model_cover_image_public_bucket_prefix,\n model_weight_cover_image.maybe_public_bucket_extension as maybe_model_cover_image_public_bucket_extension,\n\n model_weight_creator.token as `maybe_model_weight_creator_user_token: tokens::tokens::users::UserToken`,\n model_weight_creator.username as maybe_model_weight_creator_username,\n model_weight_creator.display_name as maybe_model_weight_creator_display_name,\n model_weight_creator.email_gravatar_hash as maybe_model_weight_creator_gravatar_hash,\n\n m.extra_file_modification_info,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n entity_stats.ratings_positive_count as maybe_ratings_positive_count,\n entity_stats.ratings_negative_count as maybe_ratings_negative_count,\n entity_stats.bookmark_count as maybe_bookmark_count,\n\n featured_items.entity_token IS NOT NULL AS is_featured,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN model_weights\n ON m.maybe_origin_model_token = model_weights.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN media_files as model_weight_cover_image\n ON model_weight_cover_image.token = model_weights.maybe_cover_image_media_file_token\nLEFT OUTER JOIN users as model_weight_creator\n ON model_weight_creator.token = model_weights.creator_user_token\nLEFT OUTER JOIN entity_stats\n ON entity_stats.entity_type = \"media_file\"\n AND entity_stats.entity_token = m.token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nLEFT OUTER JOIN featured_items\n ON featured_items.entity_type = 'media_file'\n AND featured_items.entity_token = m.token\n AND featured_items.deleted_at IS NULL\nWHERE\n m.token = ?\n ",
"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_engine_category as `maybe_engine_category: enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory`,\n m.maybe_animation_type as `maybe_animation_type: enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType`,\n\n m.maybe_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_style_transfer_source_media_file_token as `maybe_style_transfer_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n m.maybe_scene_source_media_file_token as `maybe_scene_source_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n model_weights.token as `maybe_model_weights_token: tokens::tokens::model_weights::ModelWeightToken`,\n model_weights.title as maybe_model_weights_title,\n model_weights.weights_type as `maybe_model_weights_type: enums::by_table::model_weights::weights_types::WeightsType`,\n model_weights.weights_category as `maybe_model_weights_category: enums::by_table::model_weights::weights_category::WeightsCategory`,\n\n model_weight_cover_image.public_bucket_directory_hash as maybe_model_cover_image_public_bucket_hash,\n model_weight_cover_image.maybe_public_bucket_prefix as maybe_model_cover_image_public_bucket_prefix,\n model_weight_cover_image.maybe_public_bucket_extension as maybe_model_cover_image_public_bucket_extension,\n\n model_weight_creator.token as `maybe_model_weight_creator_user_token: tokens::tokens::users::UserToken`,\n model_weight_creator.username as maybe_model_weight_creator_username,\n model_weight_creator.display_name as maybe_model_weight_creator_display_name,\n model_weight_creator.email_gravatar_hash as maybe_model_weight_creator_gravatar_hash,\n\n m.extra_file_modification_info,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n entity_stats.ratings_positive_count as maybe_ratings_positive_count,\n entity_stats.ratings_negative_count as maybe_ratings_negative_count,\n entity_stats.bookmark_count as maybe_bookmark_count,\n\n featured_items.entity_token IS NOT NULL AS is_featured,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN model_weights\n ON m.maybe_origin_model_token = model_weights.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN media_files as model_weight_cover_image\n ON model_weight_cover_image.token = model_weights.maybe_cover_image_media_file_token\nLEFT OUTER JOIN users as model_weight_creator\n ON model_weight_creator.token = model_weights.creator_user_token\nLEFT OUTER JOIN entity_stats\n ON entity_stats.entity_type = \"media_file\"\n AND entity_stats.entity_token = m.token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nLEFT OUTER JOIN featured_items\n ON featured_items.entity_type = 'media_file'\n AND featured_items.entity_token = m.token\n AND featured_items.deleted_at IS NULL\nWHERE\n m.token = ?\n ",
"describe": {
"columns": [
{
@@ -415,6 +415,16 @@
},
{
"ordinal": 41,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 42,
"name": "maybe_ratings_positive_count",
"type_info": {
"type": "Long",
@@ -424,7 +434,7 @@
}
},
{
"ordinal": 42,
"ordinal": 43,
"name": "maybe_ratings_negative_count",
"type_info": {
"type": "Long",
@@ -434,7 +444,7 @@
}
},
{
"ordinal": 43,
"ordinal": 44,
"name": "maybe_bookmark_count",
"type_info": {
"type": "Long",
@@ -444,7 +454,7 @@
}
},
{
"ordinal": 44,
"ordinal": 45,
"name": "is_featured",
"type_info": {
"type": "LongLong",
@@ -454,7 +464,7 @@
}
},
{
"ordinal": 45,
"ordinal": 46,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -464,7 +474,7 @@
}
},
{
"ordinal": 46,
"ordinal": 47,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -522,10 +532,11 @@
true,
true,
true,
true,
false,
false,
false
]
},
"hash": "784ca68c6cb61d481b605bfe6ee3248a5b44499038c71b57fc9ea6c4722d170f"
"hash": "e6c3c16d96339ad84b65302f3610b4b798e7ea16c2ae9d03d6b706cb42989990"
}
@@ -1,6 +1,6 @@
{
"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_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n AND m.user_deleted_at IS NULL\n AND m.mod_deleted_at IS NULL\n ",
"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_mime_type,\n\n users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n users.username as maybe_creator_username,\n users.display_name as maybe_creator_display_name,\n users.email_gravatar_hash as maybe_creator_gravatar_hash,\n\n m.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n\n m.maybe_batch_token as `maybe_batch_token: tokens::tokens::batch_generations::BatchGenerationToken`,\n\n m.maybe_title,\n m.maybe_text_transcript,\n\n m.maybe_origin_filename,\n\n m.maybe_duration_millis,\n\n m.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n prompts.maybe_other_args as maybe_other_prompt_args,\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.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n\n m.is_user_upload,\n m.is_intermediate_system_file,\n\n m.public_bucket_directory_hash,\n m.maybe_public_bucket_prefix,\n m.maybe_public_bucket_extension,\n m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n m.created_at,\n m.updated_at\n\nFROM media_files AS m\nLEFT OUTER JOIN users\n ON m.maybe_creator_user_token = users.token\nLEFT OUTER JOIN media_files as media_file_cover_image\n ON media_file_cover_image.token = m.maybe_cover_image_media_file_token\nLEFT OUTER JOIN prompts\n ON prompts.token = m.maybe_prompt_token\nWHERE\n m.maybe_batch_token = ?\n ",
"describe": {
"columns": [
{
@@ -255,6 +255,16 @@
},
{
"ordinal": 25,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 26,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -264,7 +274,7 @@
}
},
{
"ordinal": 26,
"ordinal": 27,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -303,9 +313,10 @@
false,
true,
true,
true,
false,
false
]
},
"hash": "f75fedb86a93ac2542315ddb7ef0d46aa42ecc25a3cf9856219114064183337a"
"hash": "ede1fc9893cb4309a89acc15a567b660faba1bb7eb0bf012605a9dfa15a53229"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1\n FROM folder_media_files fmf\n JOIN folders f\n ON f.token = fmf.folder_token\n AND f.maybe_deleted_at IS NULL\n WHERE fmf.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_files mf\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND NOT EXISTS (\n SELECT 1\n FROM folder_media_files fmf\n JOIN folders f\n ON f.token = fmf.folder_token\n AND f.maybe_deleted_at IS NULL\n WHERE fmf.media_file_token = mf.token\n )\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "6f077c5d35b19319aba4e9f4c59610822334fd304ba9133ba8e099056c426f2a"
"hash": "f392e28b5f4287625ccb1ee0668089f9049940368da50b7f0e0b8370f112305c"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_file_tags mft\nJOIN media_files mf\n ON mf.token = mft.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mft.tag_token = ?\n AND mft.user_token = ?\n AND mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"query": "\nSELECT\n mf.id as `media_file_id: u64`,\n\n mf.token as `media_file_token: MediaFileToken`,\n mf.media_class as `media_class: MediaFileClass`,\n mf.media_type as `media_type: MediaFileType`,\n\n mf.maybe_batch_token as `maybe_batch_token: BatchGenerationToken`,\n\n mf.public_bucket_directory_hash,\n mf.maybe_public_bucket_prefix,\n mf.maybe_public_bucket_extension,\n mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n\n cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,\n cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,\n cover.maybe_public_bucket_extension as `maybe_cover_public_bucket_extension?`,\n\n mf.creator_set_visibility as `creator_set_visibility: Visibility`,\n mf.is_user_upload as `is_user_upload: bool`,\n\n mf.maybe_title,\n mf.maybe_prompt_token as `maybe_prompt_token: PromptToken`,\n mf.maybe_origin_filename,\n\n mf.maybe_duration_millis,\n mf.maybe_frame_width,\n mf.maybe_frame_height,\n\n mf.created_at,\n mf.updated_at\nFROM media_file_tags mft\nJOIN media_files mf\n ON mf.token = mft.media_file_token\nLEFT JOIN media_files cover\n ON cover.token = mf.maybe_cover_image_media_file_token\nWHERE mft.tag_token = ?\n AND mft.user_token = ?\n AND mf.maybe_creator_user_token = ?\n AND mf.user_deleted_at IS NULL\n AND mf.mod_deleted_at IS NULL\n AND NOT mf.is_intermediate_system_file\n AND mf.id < ?\nORDER BY mf.id DESC\nLIMIT ?\n ",
"describe": {
"columns": [
{
@@ -85,6 +85,16 @@
},
{
"ordinal": 8,
"name": "maybe_thumbnail_version: u8",
"type_info": {
"type": "Tiny",
"flags": "UNSIGNED",
"char_set": 63,
"max_size": 3
}
},
{
"ordinal": 9,
"name": "maybe_cover_public_bucket_directory_hash?",
"type_info": {
"type": "VarString",
@@ -94,7 +104,7 @@
}
},
{
"ordinal": 9,
"ordinal": 10,
"name": "maybe_cover_public_bucket_prefix?",
"type_info": {
"type": "VarString",
@@ -104,7 +114,7 @@
}
},
{
"ordinal": 10,
"ordinal": 11,
"name": "maybe_cover_public_bucket_extension?",
"type_info": {
"type": "VarString",
@@ -114,7 +124,7 @@
}
},
{
"ordinal": 11,
"ordinal": 12,
"name": "creator_set_visibility: Visibility",
"type_info": {
"type": "String",
@@ -124,7 +134,7 @@
}
},
{
"ordinal": 12,
"ordinal": 13,
"name": "is_user_upload: bool",
"type_info": {
"type": "Tiny",
@@ -134,7 +144,7 @@
}
},
{
"ordinal": 13,
"ordinal": 14,
"name": "maybe_title",
"type_info": {
"type": "VarString",
@@ -144,7 +154,7 @@
}
},
{
"ordinal": 14,
"ordinal": 15,
"name": "maybe_prompt_token: PromptToken",
"type_info": {
"type": "VarString",
@@ -154,7 +164,7 @@
}
},
{
"ordinal": 15,
"ordinal": 16,
"name": "maybe_origin_filename",
"type_info": {
"type": "VarString",
@@ -164,7 +174,7 @@
}
},
{
"ordinal": 16,
"ordinal": 17,
"name": "maybe_duration_millis",
"type_info": {
"type": "Long",
@@ -174,7 +184,7 @@
}
},
{
"ordinal": 17,
"ordinal": 18,
"name": "maybe_frame_width",
"type_info": {
"type": "Long",
@@ -184,7 +194,7 @@
}
},
{
"ordinal": 18,
"ordinal": 19,
"name": "maybe_frame_height",
"type_info": {
"type": "Long",
@@ -194,7 +204,7 @@
}
},
{
"ordinal": 19,
"ordinal": 20,
"name": "created_at",
"type_info": {
"type": "Timestamp",
@@ -204,7 +214,7 @@
}
},
{
"ordinal": 20,
"ordinal": 21,
"name": "updated_at",
"type_info": {
"type": "Timestamp",
@@ -229,6 +239,7 @@
true,
true,
true,
true,
false,
false,
true,
@@ -241,5 +252,5 @@
false
]
},
"hash": "f36c7238231353882b5b2ac39616dcdc6adebf7c7e1198d9c2af4674ba5d982c"
"hash": "fbdbb627018a7664499834149d5f571457ba4313b1666152197cdbea92c8068c"
}
@@ -15,7 +15,8 @@ pub struct MediaLinks {
/// video previews, which, in turn, have their own thumbnail templates.)
pub maybe_thumbnail_template: Option<String>,
/// Video preview images (still and animated gif) for mp4 video files.
/// Video preview images (still, animated webp or gif, and a Catalina gif
/// fallback) for mp4 video files.
/// These are only set for video media files.
/// These are not set for "cover images" that are gif or webms, but rather video files.
pub maybe_video_previews: Option<VideoPreviews>,
@@ -26,7 +27,9 @@ pub struct VideoPreviews {
/// A static single frame preview image of the video.
pub still: Url,
/// An animated gif preview of the video.
/// An animated preview of the video: an animated webp for videos with
/// thumbnail version 2 (2026-09+), otherwise a gif. Clients that can't
/// render animated webp should use `maybe_catalina_fallback_animated_gif`.
pub animated: Url,
/// A template used to construct the still thumbnail URL.
@@ -36,4 +39,21 @@ pub struct VideoPreviews {
/// A template used to construct the animated thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
pub animated_thumbnail_template: String,
/// An animated gif preview of the video, for clients that can't render the
/// webp in `animated`: ArtCraft desktop on macOS 10.15 (Catalina) runs on a
/// WKWebView without animated webp support. Since thumbnail version 2
/// (2026-09) the thumbnail job writes this gif beside the webp; for older
/// videos it's the same file as `animated`. See `video_thumbnail_suffixes`
/// in `bucket_paths` for the version history.
///
/// Always set by the server. Optional (and defaulted) only so newer clients
/// can still read responses from older servers.
#[serde(default)]
pub maybe_catalina_fallback_animated_gif: Option<Url>,
/// A template used to construct the Catalina fallback gif thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
#[serde(default)]
pub maybe_catalina_fallback_animated_gif_thumbnail_template: Option<String>,
}
@@ -68,7 +68,8 @@ pub struct FolderThumbnail {
/// which, in turn, have their own thumbnail templates.)
pub maybe_thumbnail_template: Option<String>,
/// Video preview images (still and animated gif) for mp4 video files.
/// Video preview images (still, animated webp or gif, and a Catalina gif
/// fallback) for mp4 video files.
/// These are only set for video media files.
pub maybe_video_previews: Option<FolderThumbnailVideoPreviews>,
}
@@ -81,7 +82,9 @@ pub struct FolderThumbnailVideoPreviews {
/// A static single frame preview image of the video.
pub still: Url,
/// An animated gif preview of the video.
/// An animated preview of the video: an animated webp for videos with
/// thumbnail version 2 (2026-09+), otherwise a gif. Clients that can't
/// render animated webp should use `maybe_catalina_fallback_animated_gif`.
pub animated: Url,
/// A template used to construct the still thumbnail URL.
@@ -91,4 +94,21 @@ pub struct FolderThumbnailVideoPreviews {
/// A template used to construct the animated thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
pub animated_thumbnail_template: String,
/// An animated gif preview of the video, for clients that can't render the
/// webp in `animated`: ArtCraft desktop on macOS 10.15 (Catalina) runs on a
/// WKWebView without animated webp support. Since thumbnail version 2
/// (2026-09) the thumbnail job writes this gif beside the webp; for older
/// videos it's the same file as `animated`. See `video_thumbnail_suffixes`
/// in `bucket_paths` for the version history.
///
/// Always set by the server. Optional (and defaulted) only so newer clients
/// can still read responses from older servers.
#[serde(default)]
pub maybe_catalina_fallback_animated_gif: Option<Url>,
/// A template used to construct the Catalina fallback gif thumbnail URL.
/// Replace the string `{WIDTH}` with the desired width.
#[serde(default)]
pub maybe_catalina_fallback_animated_gif_thumbnail_template: Option<String>,
}
@@ -0,0 +1,328 @@
//! Regression tests pinning every exact origin the CORS config allowed before
//! first-party domains moved to the `domains` crate (commit a1147a98d2).
//!
//! The origins are string literals on purpose: they check the `domains`
//! constants against the real values instead of against themselves. Each row
//! is checked against its module in isolation (so a module can't pass because
//! another module allows the same origin) and against the full config.
//!
//! The table was generated from the `allowed_origin("...")` calls at
//! a1147a98d2. When you add or remove an origin, update this table too.
use actix_cors::Cors;
use server_environment::ServerEnvironment;
use crate::configs::artcraft_2d::add_artcraft_2d;
use crate::configs::artcraft_3d::add_artcraft_3d;
use crate::configs::artcraft_admin_dashboard::add_artcraft_admin_dashboard;
use crate::configs::artcraft_desktop::add_artcraft_desktop;
use crate::configs::artcraft_webapp::add_artcraft_webapp;
use crate::configs::artcraft_website::add_artcraft_website;
use crate::configs::fakeyou::add_fakeyou;
use crate::configs::fakeyou::add_fakeyou_dev_proxy;
use crate::configs::legacy::add_legacy_storyteller_stream;
use crate::configs::legacy::add_legacy_trumped;
use crate::configs::legacy::add_legacy_vocodes;
use crate::configs::legacy::add_power_stream;
use crate::configs::realseedance::add_realseedance;
use crate::configs::storyteller::add_storyteller;
use crate::configs::storyteller::add_storyteller_dev_proxy;
use crate::configs::storyteller_board::add_storyteller_board;
use crate::configs::storyteller_render::add_storyteller_render;
use crate::configs::storyteller_studio::add_storyteller_studio;
use crate::configs::tauri::add_tauri;
use crate::cors::build_cors_config;
use crate::testing::{assert_exact_https_origin, is_origin_allowed, isolated_cors};
use Env::{Both, DevelopmentOnly, ProductionOnly};
type AddOrigins = fn(Cors, bool) -> Cors;
/// Which environments an origin is configured for.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Env {
Both,
ProductionOnly,
DevelopmentOnly,
}
const ALLOWLIST: &[(AddOrigins, Env, &str)] = &[
// add_artcraft_2d
(add_artcraft_2d, Both, "https://2d.storyteller.ai"),
(add_artcraft_2d, Both, "https://2d.getartcraft.com"),
(add_artcraft_2d, Both, "http://localhost:5741"),
(add_artcraft_2d, Both, "http://localhost:5742"),
(add_artcraft_2d, Both, "http://localhost:5743"),
// add_artcraft_3d
(add_artcraft_3d, Both, "https://3d.storyteller.ai"),
(add_artcraft_3d, Both, "https://3d.getartcraft.com"),
(add_artcraft_3d, Both, "http://localhost:5173"),
(add_artcraft_3d, Both, "http://localhost:5174"),
(add_artcraft_3d, Both, "http://localhost:5175"),
(add_artcraft_3d, Both, "https://macaroni-1.tailce84f.ts.net"),
(add_artcraft_3d, Both, "https://halide.tailce84f.ts.net"),
(add_artcraft_3d, Both, "https://brandons-macbook-pro.taild62114.ts.net"),
// add_artcraft_admin_dashboard
(add_artcraft_admin_dashboard, Both, "https://artcraft-dashboard.netlify.app"),
// add_artcraft_desktop
(add_artcraft_desktop, Both, "https://desktop.getartcraft.com"),
// add_artcraft_webapp
(add_artcraft_webapp, Both, "https://app.getartcraft.com"),
// add_artcraft_website
(add_artcraft_website, Both, "https://getartcraft.com"),
(add_artcraft_website, Both, "https://www.getartcraft.com"),
(add_artcraft_website, Both, "https://artcraft.ai"),
(add_artcraft_website, Both, "https://www.artcraft.ai"),
(add_artcraft_website, Both, "http://localhost:4200"),
(add_artcraft_website, Both, "http://localhost:4201"),
(add_artcraft_website, Both, "http://localhost:4202"),
// add_fakeyou
(add_fakeyou, Both, "https://engine.fakeyou.com"),
(add_fakeyou, ProductionOnly, "https://api.fakeyou.com"),
(add_fakeyou, ProductionOnly, "https://fakeyou.com"),
(add_fakeyou, ProductionOnly, "https://staging.fakeyou.com"),
(add_fakeyou, DevelopmentOnly, "http://dev.fakeyou.com"),
(add_fakeyou, DevelopmentOnly, "http://dev.fakeyou.com:7000"),
(add_fakeyou, DevelopmentOnly, "http://dev.fakeyou.com:7001"),
(add_fakeyou, DevelopmentOnly, "https://dev.fakeyou.com"),
(add_fakeyou, DevelopmentOnly, "https://dev.fakeyou.com:7000"),
(add_fakeyou, DevelopmentOnly, "https://dev.fakeyou.com:7001"),
// add_fakeyou_dev_proxy
(add_fakeyou_dev_proxy, Both, "http://devproxy.fakeyou.com"),
(add_fakeyou_dev_proxy, Both, "http://devproxy.fakeyou.com:5173"),
(add_fakeyou_dev_proxy, Both, "http://devproxy.fakeyou.com:7000"),
(add_fakeyou_dev_proxy, Both, "http://devproxy.fakeyou.com:7001"),
(add_fakeyou_dev_proxy, Both, "http://devproxy.fakeyou.com:7002"),
(add_fakeyou_dev_proxy, Both, "https://devproxy.fakeyou.com"),
(add_fakeyou_dev_proxy, Both, "https://devproxy.fakeyou.com:5173"),
(add_fakeyou_dev_proxy, Both, "https://devproxy.fakeyou.com:7000"),
(add_fakeyou_dev_proxy, Both, "https://devproxy.fakeyou.com:7001"),
(add_fakeyou_dev_proxy, Both, "https://devproxy.fakeyou.com:7002"),
// add_legacy_storyteller_stream
(add_legacy_storyteller_stream, ProductionOnly, "http://api.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "http://obs.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "http://storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "http://ws.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "https://api.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "https://obs.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "https://storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "https://ws.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "http://staging.obs.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "http://staging.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "https://staging.obs.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "https://staging.storyteller.stream"),
(add_legacy_storyteller_stream, ProductionOnly, "http://create.storyteller.ai"),
(add_legacy_storyteller_stream, ProductionOnly, "http://obs.storyteller.ai"),
(add_legacy_storyteller_stream, ProductionOnly, "http://ws.storyteller.ai"),
(add_legacy_storyteller_stream, ProductionOnly, "https://create.storyteller.ai"),
(add_legacy_storyteller_stream, ProductionOnly, "https://obs.storyteller.ai"),
(add_legacy_storyteller_stream, ProductionOnly, "https://ws.storyteller.ai"),
// add_legacy_trumped
(add_legacy_trumped, ProductionOnly, "https://trumped.com"),
(add_legacy_trumped, DevelopmentOnly, "http://dev.trumped.com"),
(add_legacy_trumped, DevelopmentOnly, "https://dev.trumped.com"),
// add_legacy_vocodes
(add_legacy_vocodes, ProductionOnly, "https://api.vo.codes"),
(add_legacy_vocodes, ProductionOnly, "https://vo.codes"),
(add_legacy_vocodes, ProductionOnly, "https://vocodes.com"),
(add_legacy_vocodes, DevelopmentOnly, "http://dev.api.vo.codes"),
(add_legacy_vocodes, DevelopmentOnly, "http://dev.vo.codes"),
(add_legacy_vocodes, DevelopmentOnly, "https://dev.api.vo.codes"),
(add_legacy_vocodes, DevelopmentOnly, "https://dev.vo.codes"),
// add_power_stream
(add_power_stream, ProductionOnly, "https://dash.power.stream"),
(add_power_stream, ProductionOnly, "https://power.stream"),
(add_power_stream, DevelopmentOnly, "http://dev.dash.power.stream"),
(add_power_stream, DevelopmentOnly, "http://dev.power.stream"),
(add_power_stream, DevelopmentOnly, "https://dev.dash.power.stream"),
(add_power_stream, DevelopmentOnly, "https://dev.power.stream"),
// add_realseedance
(add_realseedance, Both, "https://realseedance.com"),
(add_realseedance, Both, "https://www.realseedance.com"),
// add_storyteller
(add_storyteller, ProductionOnly, "https://engine.storyteller.ai"),
(add_storyteller, ProductionOnly, "https://studio.storyteller.ai"),
(add_storyteller, ProductionOnly, "https://api.storyteller.ai"),
(add_storyteller, ProductionOnly, "https://storyteller.ai"),
(add_storyteller, ProductionOnly, "https://staging.storyteller.ai"),
(add_storyteller, DevelopmentOnly, "http://dev.storyteller.ai"),
(add_storyteller, DevelopmentOnly, "http://dev.storyteller.ai:5173"),
(add_storyteller, DevelopmentOnly, "http://dev.storyteller.ai:7000"),
(add_storyteller, DevelopmentOnly, "http://dev.storyteller.ai:7001"),
(add_storyteller, DevelopmentOnly, "http://dev.storyteller.ai:7002"),
(add_storyteller, DevelopmentOnly, "https://dev.storyteller.ai"),
(add_storyteller, DevelopmentOnly, "https://dev.storyteller.ai:5173"),
(add_storyteller, DevelopmentOnly, "https://dev.storyteller.ai:7000"),
(add_storyteller, DevelopmentOnly, "https://dev.storyteller.ai:7001"),
(add_storyteller, DevelopmentOnly, "https://dev.storyteller.ai:7002"),
// add_storyteller_dev_proxy
(add_storyteller_dev_proxy, Both, "http://devproxy.storyteller.ai"),
(add_storyteller_dev_proxy, Both, "http://devproxy.storyteller.ai:5173"),
(add_storyteller_dev_proxy, Both, "http://devproxy.storyteller.ai:7000"),
(add_storyteller_dev_proxy, Both, "http://devproxy.storyteller.ai:7001"),
(add_storyteller_dev_proxy, Both, "http://devproxy.storyteller.ai:7002"),
(add_storyteller_dev_proxy, Both, "https://devproxy.storyteller.ai"),
(add_storyteller_dev_proxy, Both, "https://devproxy.storyteller.ai:5173"),
(add_storyteller_dev_proxy, Both, "https://devproxy.storyteller.ai:7000"),
(add_storyteller_dev_proxy, Both, "https://devproxy.storyteller.ai:7001"),
(add_storyteller_dev_proxy, Both, "https://devproxy.storyteller.ai:7002"),
// add_storyteller_board
(add_storyteller_board, Both, "https://memeboard.ai"),
(add_storyteller_board, Both, "https://dingboard.ai"),
(add_storyteller_board, Both, "https://board.storyteller.ai"),
(add_storyteller_board, Both, "http://localhost:5173/"),
// add_storyteller_render
(add_storyteller_render, Both, "https://render.storyteller.ai"),
// add_storyteller_studio
(add_storyteller_studio, Both, "https://studio.storyteller.ai"),
(add_storyteller_studio, Both, "https://studio-staging.studio.storyteller.ai"),
(add_storyteller_studio, Both, "https://studio-testing.studio.storyteller.ai"),
(add_storyteller_studio, Both, "http://localhost:5173"),
(add_storyteller_studio, Both, "https://animate.storyteller.ai"),
// add_tauri
(add_tauri, Both, "http://tauri.localhost"),
(add_tauri, Both, "tauri://localhost"),
];
/// The origins whose `allowed_origin(...)` literal became a `domains` constant.
const MIGRATED_ORIGINS: &[&str] = &[
"https://getartcraft.com",
"https://www.getartcraft.com",
"https://app.getartcraft.com",
"https://desktop.getartcraft.com",
"https://2d.getartcraft.com",
"https://3d.getartcraft.com",
"https://artcraft.ai",
"https://www.artcraft.ai",
"https://fakeyou.com",
"https://api.fakeyou.com",
"https://engine.fakeyou.com",
"https://staging.fakeyou.com",
"https://storyteller.ai",
"https://api.storyteller.ai",
"https://engine.storyteller.ai",
"https://staging.storyteller.ai",
"https://studio.storyteller.ai",
"https://studio-staging.studio.storyteller.ai",
"https://studio-testing.studio.storyteller.ai",
"https://animate.storyteller.ai",
"https://board.storyteller.ai",
"https://render.storyteller.ai",
"https://2d.storyteller.ai",
"https://3d.storyteller.ai",
];
mod isolated_module_tests {
use super::*;
#[actix_rt::test]
async fn each_module_allows_its_origins_in_production() {
let failures = allowed_failures(true, |env| env != DevelopmentOnly, false).await;
assert!(failures.is_empty(), "production origins not allowed by their module: {:#?}", failures);
}
#[actix_rt::test]
async fn each_module_allows_its_origins_in_development() {
let failures = allowed_failures(false, |env| env != ProductionOnly, false).await;
assert!(failures.is_empty(), "development origins not allowed by their module: {:#?}", failures);
}
#[actix_rt::test]
async fn development_only_origins_are_rejected_by_their_module_in_production() {
let failures = allowed_failures(true, |env| env == DevelopmentOnly, true).await;
assert!(failures.is_empty(), "development-only origins allowed in production: {:#?}", failures);
}
#[actix_rt::test]
async fn production_only_origins_are_rejected_by_their_module_in_development() {
let failures = allowed_failures(false, |env| env == ProductionOnly, true).await;
assert!(failures.is_empty(), "production-only origins allowed in development: {:#?}", failures);
}
/// Checks each matching row against its own module's isolated config and
/// returns the origins that were allowed (`expect_rejected`) or not.
async fn allowed_failures(
is_production: bool,
select: impl Fn(Env) -> bool,
expect_rejected: bool,
) -> Vec<&'static str> {
let mut failures = Vec::new();
for &(add_origins, env, origin) in ALLOWLIST {
if !select(env) {
continue;
}
let cors = isolated_cors(add_origins, is_production);
if is_origin_allowed(&cors, origin).await == expect_rejected {
failures.push(origin);
}
}
failures
}
}
mod full_config_tests {
use super::*;
#[actix_rt::test]
async fn production_config_allows_every_production_origin() {
let cors = build_cors_config(ServerEnvironment::Production);
let failures = full_config_failures(&cors, |env| env != DevelopmentOnly, false).await;
assert!(failures.is_empty(), "production origins not allowed: {:#?}", failures);
}
#[actix_rt::test]
async fn development_config_allows_every_development_origin() {
let cors = build_cors_config(ServerEnvironment::Development);
let failures = full_config_failures(&cors, |env| env != ProductionOnly, false).await;
assert!(failures.is_empty(), "development origins not allowed: {:#?}", failures);
}
#[actix_rt::test]
async fn production_config_rejects_development_only_origins() {
let cors = build_cors_config(ServerEnvironment::Production);
let failures = full_config_failures(&cors, |env| env == DevelopmentOnly, true).await;
assert!(failures.is_empty(), "development-only origins allowed in production: {:#?}", failures);
}
/// Every origin that moved to a `domains` constant, checked for lookalikes
/// against the full production config (not just the owning module).
#[actix_rt::test]
async fn production_config_rejects_lookalikes_of_migrated_origins() {
let cors = build_cors_config(ServerEnvironment::Production);
for origin in MIGRATED_ORIGINS {
assert_exact_https_origin(&cors, origin).await;
}
}
async fn full_config_failures(
cors: &Cors,
select: impl Fn(Env) -> bool,
expect_rejected: bool,
) -> Vec<&'static str> {
let mut failures = Vec::new();
for &(_, env, origin) in ALLOWLIST {
if select(env) && is_origin_allowed(cors, origin).await == expect_rejected {
failures.push(origin);
}
}
failures
}
}
@@ -17,3 +17,35 @@ pub fn add_artcraft_2d(cors: Cors, _is_production: bool) -> Cors {
.allowed_origin("http://localhost:5742") // If already started
.allowed_origin("http://localhost:5743") // If already started
}
#[cfg(test)]
mod tests {
use crate::testing::{assert_exact_https_origin, assert_netlify_project, assert_origin_invalid, assert_origin_ok, isolated_cors};
use super::add_artcraft_2d;
#[actix_rt::test]
async fn domains() {
for is_production in [true, false] {
let cors = isolated_cors(add_artcraft_2d, is_production);
assert_exact_https_origin(&cors, "https://2d.getartcraft.com").await;
assert_exact_https_origin(&cors, "https://2d.storyteller.ai").await;
}
}
#[actix_rt::test]
async fn tauri_localhost_ports() {
let cors = isolated_cors(add_artcraft_2d, true);
for origin in ["http://localhost:5741", "http://localhost:5742", "http://localhost:5743"] {
assert_origin_ok(&cors, origin).await;
}
assert_origin_invalid(&cors, "http://localhost:5744").await;
assert_origin_invalid(&cors, "http://localhost:5173").await; // The 3D engine's port
}
#[actix_rt::test]
async fn netlify_project() {
let cors = isolated_cors(add_artcraft_2d, true);
assert_netlify_project(&cors, "storyteller-2d.netlify.app").await;
}
}
@@ -20,3 +20,48 @@ pub fn add_artcraft_3d(cors: Cors, _is_production: bool) -> Cors {
.allowed_origin("https://halide.tailce84f.ts.net") // TODO
.allowed_origin("https://brandons-macbook-pro.taild62114.ts.net") // TODO
}
#[cfg(test)]
mod tests {
use crate::testing::{assert_exact_https_origin, assert_netlify_project, assert_origin_invalid, assert_origin_ok, isolated_cors};
use super::add_artcraft_3d;
#[actix_rt::test]
async fn domains() {
for is_production in [true, false] {
let cors = isolated_cors(add_artcraft_3d, is_production);
assert_exact_https_origin(&cors, "https://3d.getartcraft.com").await;
assert_exact_https_origin(&cors, "https://3d.storyteller.ai").await;
}
}
#[actix_rt::test]
async fn tauri_localhost_ports() {
let cors = isolated_cors(add_artcraft_3d, true);
for origin in ["http://localhost:5173", "http://localhost:5174", "http://localhost:5175"] {
assert_origin_ok(&cors, origin).await;
}
assert_origin_invalid(&cors, "http://localhost:5176").await;
assert_origin_invalid(&cors, "http://localhost:5741").await; // The 2D engine's port
}
#[actix_rt::test]
async fn tailscale_hosts() {
let cors = isolated_cors(add_artcraft_3d, true);
for origin in [
"https://macaroni-1.tailce84f.ts.net",
"https://halide.tailce84f.ts.net",
"https://brandons-macbook-pro.taild62114.ts.net",
] {
assert_exact_https_origin(&cors, origin).await;
}
assert_origin_invalid(&cors, "https://other-machine.tailce84f.ts.net").await;
}
#[actix_rt::test]
async fn netlify_project() {
let cors = isolated_cors(add_artcraft_3d, true);
assert_netlify_project(&cors, "storyteller-3d.netlify.app").await;
}
}
@@ -8,3 +8,18 @@ pub fn add_artcraft_admin_dashboard(cors: Cors, _is_production: bool) -> Cors {
netlify_branch_domain_matches(origin, "artcraft-dashboard.netlify.app")
})
}
#[cfg(test)]
mod tests {
use crate::testing::{assert_netlify_project, isolated_cors};
use super::add_artcraft_admin_dashboard;
#[actix_rt::test]
async fn netlify_project() {
for is_production in [true, false] {
let cors = isolated_cors(add_artcraft_admin_dashboard, is_production);
assert_netlify_project(&cors, "artcraft-dashboard.netlify.app").await;
}
}
}
@@ -11,3 +11,24 @@ pub fn add_artcraft_webapp(cors: Cors, _is_production: bool) -> Cors {
netlify_branch_domain_matches(origin, "artcraft-webapp.netlify.app")
})
}
#[cfg(test)]
mod tests {
use crate::testing::{assert_exact_https_origin, assert_netlify_project, isolated_cors};
use super::add_artcraft_webapp;
#[actix_rt::test]
async fn webapp_domain() {
for is_production in [true, false] {
let cors = isolated_cors(add_artcraft_webapp, is_production);
assert_exact_https_origin(&cors, "https://app.getartcraft.com").await;
}
}
#[actix_rt::test]
async fn netlify_project() {
let cors = isolated_cors(add_artcraft_webapp, true);
assert_netlify_project(&cors, "artcraft-webapp.netlify.app").await;
}
}
@@ -22,3 +22,42 @@ pub fn add_artcraft_website(cors: Cors, _is_production: bool) -> Cors {
netlify_branch_domain_matches(origin, "artcraft-website.netlify.app")
})
}
#[cfg(test)]
mod tests {
use crate::testing::{assert_exact_https_origin, assert_netlify_project, assert_origin_invalid, assert_origin_ok, isolated_cors};
use super::add_artcraft_website;
#[actix_rt::test]
async fn website_domains() {
for is_production in [true, false] {
let cors = isolated_cors(add_artcraft_website, is_production);
for origin in [
"https://getartcraft.com",
"https://www.getartcraft.com",
"https://artcraft.ai",
"https://www.artcraft.ai",
] {
assert_exact_https_origin(&cors, origin).await;
}
}
}
/// Local frontend builds talk to the production API.
#[actix_rt::test]
async fn localhost_development_against_production() {
let cors = isolated_cors(add_artcraft_website, true);
for origin in ["http://localhost:4200", "http://localhost:4201", "http://localhost:4202"] {
assert_origin_ok(&cors, origin).await;
}
assert_origin_invalid(&cors, "http://localhost:4203").await;
assert_origin_invalid(&cors, "https://localhost:4200").await;
}
#[actix_rt::test]
async fn netlify_project() {
let cors = isolated_cors(add_artcraft_website, true);
assert_netlify_project(&cors, "artcraft-website.netlify.app").await;
}
}
@@ -25,3 +25,48 @@ pub fn add_development_only(cors: Cors) -> Cors {
}
})
}
#[cfg(test)]
mod tests {
use actix_cors::Cors;
use crate::testing::{assert_origin_invalid, assert_origin_ok};
use super::add_development_only;
#[actix_rt::test]
async fn any_localhost_is_allowed() {
let cors = development_only_cors();
for origin in [
"http://localhost",
"http://localhost:3000",
"https://localhost:8443",
"http://127.0.0.1",
"http://127.0.0.1:5173",
] {
assert_origin_ok(&cors, origin).await;
}
}
#[actix_rt::test]
async fn non_localhost_is_rejected() {
let cors = development_only_cors();
for origin in [
"https://getartcraft.com",
"http://localhost.evil.example",
"http://evil.localhost.example",
"http://127.0.0.2",
"http://192.168.1.10",
"http://[::1]", // NB: IPv6 loopback isn't allowed today
"not a url",
"null",
] {
assert_origin_invalid(&cors, origin).await;
}
}
fn development_only_cors() -> Cors {
add_development_only(Cors::default())
.block_on_origin_mismatch(true)
}
}
@@ -75,3 +75,35 @@ pub fn add_legacy_trumped(cors: Cors, is_production: bool) -> Cors {
}
}
#[cfg(test)]
mod tests {
use crate::testing::{assert_origin_invalid, assert_origin_ok, isolated_cors};
use super::{add_legacy_storyteller_stream, add_legacy_trumped, add_legacy_vocodes, add_power_stream};
// NB: The full per-environment origin lists are pinned in allowlist_regression_tests.
#[actix_rt::test]
async fn legacy_storyteller_stream_has_no_development_origins() {
let cors = isolated_cors(add_legacy_storyteller_stream, false);
assert_origin_invalid(&cors, "https://storyteller.stream").await;
assert_origin_invalid(&cors, "https://create.storyteller.ai").await;
}
#[actix_rt::test]
async fn production_hosts_are_exact() {
let power_stream = isolated_cors(add_power_stream, true);
assert_origin_ok(&power_stream, "https://power.stream").await;
assert_origin_invalid(&power_stream, "http://power.stream").await;
assert_origin_invalid(&power_stream, "https://power.stream.evil.example").await;
let vocodes = isolated_cors(add_legacy_vocodes, true);
assert_origin_ok(&vocodes, "https://vo.codes").await;
assert_origin_invalid(&vocodes, "https://evil.vo.codes").await;
let trumped = isolated_cors(add_legacy_trumped, true);
assert_origin_ok(&trumped, "https://trumped.com").await;
assert_origin_invalid(&trumped, "https://trumped.com:8443").await;
}
}
@@ -12,3 +12,25 @@ pub fn add_realseedance(cors: Cors, _is_production: bool) -> Cors {
netlify_branch_domain_matches(origin, "real-seedance.netlify.app")
})
}
#[cfg(test)]
mod tests {
use crate::testing::{assert_exact_https_origin, assert_netlify_project, isolated_cors};
use super::add_realseedance;
#[actix_rt::test]
async fn domains() {
for is_production in [true, false] {
let cors = isolated_cors(add_realseedance, is_production);
assert_exact_https_origin(&cors, "https://realseedance.com").await;
assert_exact_https_origin(&cors, "https://www.realseedance.com").await;
}
}
#[actix_rt::test]
async fn netlify_project() {
let cors = isolated_cors(add_realseedance, true);
assert_netlify_project(&cors, "real-seedance.netlify.app").await;
}
}
@@ -13,6 +13,8 @@ pub fn add_storyteller(cors: Cors, is_production: bool) -> Cors {
// Storyteller Engine (Production)
.allowed_origin(STORYTELLER_ENGINE.https_link_without_path())
// Storyteller Studio (Production)
// DO NOT REMOVE: shipped ArtCraft desktop builds send this origin.
// See deployed_desktop_client_origin_tests.rs.
.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
// Storyteller.ai (Production)
.allowed_origin(STORYTELLER_API.https_link_without_path())
@@ -7,7 +7,10 @@ use domains::storyteller::STORYTELLER_STUDIO_STAGING;
use domains::storyteller::STORYTELLER_STUDIO_TESTING;
pub fn add_storyteller_studio(cors: Cors, _is_production: bool) -> Cors {
cors.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
cors
// DO NOT REMOVE: shipped ArtCraft desktop builds send this origin.
// See deployed_desktop_client_origin_tests.rs.
.allowed_origin(STORYTELLER_STUDIO.https_link_without_path())
.allowed_origin(STORYTELLER_STUDIO_STAGING.https_link_without_path())
.allowed_origin(STORYTELLER_STUDIO_TESTING.https_link_without_path())
.allowed_origin_fn(|origin, _req_head| {
@@ -5,6 +5,8 @@ pub fn add_tauri(cors: Cors, _is_production: bool) -> Cors {
// Tauri Windows
.allowed_origin("http://tauri.localhost")
// Tauri Mac
// DO NOT REMOVE: shipped ArtCraft desktop builds send this origin.
// See deployed_desktop_client_origin_tests.rs.
.allowed_origin("tauri://localhost")
}
@@ -0,0 +1,110 @@
//! DO NOT REMOVE THESE ORIGINS: shipped ArtCraft desktop builds depend on them.
//!
//! Old ArtCraft desktop builds in the wild send one of these `Origin` headers:
//!
//! * `https://studio.storyteller.ai`
//! * `tauri://localhost` (macOS Tauri webview)
//!
//! Those builds can't be updated server-side. If the API stops allowing these
//! origins, those installs break: every credentialed request (login, session,
//! generation) fails CORS in the webview.
//!
//! These tests live in their own module on purpose, apart from the Storyteller
//! and Tauri configs, so reworking or deleting those configs (or their tests)
//! can't silently remove this protection. They use string literals, not
//! `domains` constants, so renaming a constant can't change what they check.
use actix_web::dev::Transform;
use actix_web::http::{header, Method, StatusCode};
use actix_web::test::{self, TestRequest};
use server_environment::ServerEnvironment;
use crate::cors::build_cors_config;
const STORYTELLER_STUDIO_ORIGIN: &str = "https://studio.storyteller.ai";
const TAURI_MAC_ORIGIN: &str = "tauri://localhost";
const ENVIRONMENTS: [ServerEnvironment; 2] = [ServerEnvironment::Production, ServerEnvironment::Development];
mod storyteller_studio_origin {
use super::*;
#[actix_rt::test]
async fn credentialed_requests_are_allowed() {
assert_credentialed_requests_allowed(STORYTELLER_STUDIO_ORIGIN).await;
}
#[actix_rt::test]
async fn preflights_are_allowed() {
assert_preflights_allowed(STORYTELLER_STUDIO_ORIGIN).await;
}
}
mod tauri_mac_origin {
use super::*;
#[actix_rt::test]
async fn credentialed_requests_are_allowed() {
assert_credentialed_requests_allowed(TAURI_MAC_ORIGIN).await;
}
#[actix_rt::test]
async fn preflights_are_allowed() {
assert_preflights_allowed(TAURI_MAC_ORIGIN).await;
}
}
/// Simple requests in every environment and method get the origin echoed
/// back with credentials allowed, which is what the webview requires.
async fn assert_credentialed_requests_allowed(origin: &str) {
for environment in ENVIRONMENTS {
let service = build_cors_config(environment)
.new_transform(test::ok_service())
.await
.unwrap();
for method in [Method::GET, Method::POST, Method::PUT, Method::DELETE] {
let request = TestRequest::default()
.method(method.clone())
.uri("/v1/session")
.insert_header((header::ORIGIN, origin))
.to_srv_request();
let response = test::call_service(&service, request).await;
let context = format!(
"Shipped ArtCraft desktop builds send Origin {} ({:?}, {}). DO NOT REMOVE this origin from the CORS config.",
origin, environment, method);
assert_eq!(response.status(), StatusCode::OK, "{}", context);
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).map(|v| v.to_str().unwrap()), Some(origin), "{}", context);
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_CREDENTIALS).map(|v| v.to_str().unwrap()), Some("true"), "{}", context);
}
}
}
/// Preflights for the methods and headers the desktop app uses are accepted.
async fn assert_preflights_allowed(origin: &str) {
for environment in ENVIRONMENTS {
let service = build_cors_config(environment)
.new_transform(test::ok_service())
.await
.unwrap();
for requested_method in ["GET", "POST", "PUT", "DELETE"] {
let request = TestRequest::default()
.method(Method::OPTIONS)
.uri("/v1/session")
.insert_header((header::ORIGIN, origin))
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, requested_method))
.insert_header((header::ACCESS_CONTROL_REQUEST_HEADERS, "content-type,session"))
.to_srv_request();
let response = test::call_service(&service, request).await;
let context = format!(
"Shipped ArtCraft desktop builds send Origin {} ({:?}, preflight {}). DO NOT REMOVE this origin from the CORS config.",
origin, environment, requested_method);
assert_eq!(response.status(), StatusCode::OK, "{}", context);
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).map(|v| v.to_str().unwrap()), Some(origin), "{}", context);
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_CREDENTIALS).map(|v| v.to_str().unwrap()), Some("true"), "{}", context);
}
}
}
+8
View File
@@ -27,3 +27,11 @@ pub (crate) mod util;
#[cfg(test)]
pub (crate) mod testing;
#[cfg(test)]
mod allowlist_regression_tests;
// Origins sent by shipped ArtCraft desktop builds. Kept apart from the configs
// they test so those can't be refactored away with them.
#[cfg(test)]
mod deployed_desktop_client_origin_tests;
@@ -7,6 +7,53 @@ use actix_web::test;
use actix_web::test::TestRequest;
use speculoos::asserting;
/// A config containing ONLY one module's origins, finished the same way as
/// the production config (mismatched origins get a 400). Testing modules in
/// isolation proves each module allows its own origins, rather than passing
/// because some other module happens to allow them too.
pub (crate) fn isolated_cors(add_origins: fn(Cors, bool) -> Cors, is_production: bool) -> Cors {
add_origins(Cors::default(), is_production)
.block_on_origin_mismatch(true)
}
/// Whether `cors` allows a simple request from `origin`, for tests that check
/// many origins and report every failure at once.
pub (crate) async fn is_origin_allowed(cors: &Cors, origin: &str) -> bool {
let status = make_test_request(cors, origin).await.status();
assert!(
status == StatusCode::OK || status == StatusCode::BAD_REQUEST,
"unexpected status {} for origin {}", status, origin);
status == StatusCode::OK
}
/// Assert that `cors` allows the exact `https://` origin but none of its
/// lookalikes: plain http, another port, a suffix attack, or a sub-subdomain.
/// Don't use this for Netlify hosts: their matcher checks the hostname only.
pub (crate) async fn assert_exact_https_origin(cors: &Cors, origin: &str) {
let host = origin.strip_prefix("https://")
.unwrap_or_else(|| panic!("expected an https:// origin, got {}", origin));
assert_origin_ok(cors, origin).await;
assert_origin_invalid(cors, &format!("http://{}", host)).await;
assert_origin_invalid(cors, &format!("https://{}:8443", host)).await;
assert_origin_invalid(cors, &format!("https://{}.evil.example", host)).await;
assert_origin_invalid(cors, &format!("https://evil.{}", host)).await;
}
/// Assert that `cors` allows a Netlify project's main, branch and
/// deploy-preview hosts, and rejects other projects' hosts.
pub (crate) async fn assert_netlify_project(cors: &Cors, netlify_hostname: &str) {
assert_origin_ok(cors, &format!("https://{}", netlify_hostname)).await;
assert_origin_ok(cors, &format!("https://my-branch--{}", netlify_hostname)).await;
assert_origin_ok(cors, &format!("https://deploy-preview-123--{}", netlify_hostname)).await;
assert_origin_invalid(cors, "https://unrelated-project.netlify.app").await;
assert_origin_invalid(cors, "https://deploy-preview-123--unrelated-project.netlify.app").await;
assert_origin_invalid(cors, &format!("https://{}.evil.example", netlify_hostname)).await;
// A hyphen alone isn't a branch separator; Netlify branch hosts use "--".
assert_origin_invalid(cors, &format!("https://evil-{}", netlify_hostname)).await;
}
pub (crate) async fn assert_origin_ok(cors: &Cors, hostname: &str) {
let response = make_test_request(cors, hostname).await;
asserting(&format!("Hostname {} is valid", hostname))
+3
View File
@@ -12,6 +12,9 @@ pub const STORYTELLER_ENGINE: Domain = domain!("engine.storyteller.ai");
pub const STORYTELLER_STAGING: Domain = domain!("staging.storyteller.ai");
/// Storyteller Studio and its deploy environments.
///
/// NB: Shipped ArtCraft desktop builds send `https://studio.storyteller.ai` as
/// their CORS origin, so it must stay allowed (see `actix_cors_configs`).
pub const STORYTELLER_STUDIO: Domain = domain!("studio.storyteller.ai");
pub const STORYTELLER_STUDIO_STAGING: Domain = domain!("studio-staging.studio.storyteller.ai");
pub const STORYTELLER_STUDIO_TESTING: Domain = domain!("studio-testing.studio.storyteller.ai");
@@ -0,0 +1,145 @@
use log::info;
use std::path::Path;
use std::process::Command;
use errors::AnyhowResult;
use crate::ffmpeg::run_thumbnail_ffmpeg::run_thumbnail_ffmpeg;
use crate::ffmpeg::webp_ffmpeg_binary::webp_ffmpeg;
/// Lossy WebP quality (0-100). Started at 60 on 2026-09-28; on the test video
/// this was ~50x smaller than the GIF preview (23 KB vs 1.1 MB). Safe to tune:
/// it only affects newly generated previews.
const WEBP_QUALITY: &str = "60";
pub struct FfmpegVideoWebpPreviewArgs<I: AsRef<Path>, O: AsRef<Path>> {
pub input_video_path: I,
pub output_webp_path: O,
}
/// Extract a short, endlessly looping animated WebP preview from a video file.
///
/// Uses the same frames as the GIF preview (`ffmpeg_video_gif_preview`): the
/// first 5 seconds, resampled to 10 fps, scaled to fit within 360x360
/// (preserving aspect ratio). Runs the ffmpeg chosen by
/// `webp_ffmpeg_binary::resolve_webp_ffmpeg` (call it at startup).
pub fn ffmpeg_video_webp_preview<I: AsRef<Path>, O: AsRef<Path>>(
args: FfmpegVideoWebpPreviewArgs<I, O>,
) -> AnyhowResult<()> {
let mut command = Command::new(webp_ffmpeg()?);
command
.arg("-nostdin")
.arg("-y")
.arg("-loglevel").arg("error")
.arg("-nostats")
.arg("-threads").arg("1")
.arg("-filter_complex_threads").arg("1")
.arg("-ss").arg("0")
.arg("-to").arg("5")
.arg("-i").arg(args.input_video_path.as_ref())
.arg("-threads").arg("1")
.arg("-filter_complex")
.arg("fps=10,scale=360:360:force_original_aspect_ratio=decrease")
.arg("-frames:v").arg("50")
.arg("-an")
.arg("-c:v").arg("libwebp_anim")
.arg("-lossless").arg("0")
.arg("-quality").arg(WEBP_QUALITY)
// NB: The webp muxer plays animations once by default; 0 loops forever like the GIF.
.arg("-loop").arg("0")
.arg(args.output_webp_path.as_ref());
info!("Calling ffmpeg (webp preview)...");
run_thumbnail_ffmpeg(command)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ffmpeg::webp_ffmpeg_binary::resolve_webp_ffmpeg;
use tempdir::TempDir;
use test_utils::test_file_path::test_file_path;
#[test]
fn test_extract_webp_preview_from_mp4() {
if !libwebp_anim_available() {
return;
}
let input_path = test_file_path("test_data/video/mp4/golden_sun_garoh.mp4")
.expect("test video should exist");
let temp_dir = TempDir::new_in("/tmp", "ffmpeg_webp_preview_test")
.expect("should create temp dir");
let output_path = temp_dir.path().join("preview.webp");
ffmpeg_video_webp_preview(FfmpegVideoWebpPreviewArgs {
input_video_path: &input_path,
output_webp_path: &output_path,
}).expect("ffmpeg should succeed");
let bytes = std::fs::read(&output_path).expect("output webp should exist");
// RIFF container holding a WebP file.
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WEBP");
// Animated WebP: an ANIM chunk with a loop count of 0 (loop forever).
let anim = find_chunk(&bytes, b"ANIM").expect("webp should be animated");
let loop_count = u16::from_le_bytes([bytes[anim + 12], bytes[anim + 13]]);
assert_eq!(loop_count, 0, "preview should loop forever");
// More than one frame, and no more than the 50 we asked for.
let frames = count_chunks(&bytes, b"ANMF");
assert!(frames > 1 && frames <= 50, "unexpected frame count: {}", frames);
// 640x480 input fits within 360x360 as 360x270. The VP8X chunk stores the
// canvas width-1 and height-1 as 24-bit little-endian values. (Frames
// after the first may be smaller sub-rectangles, so check the canvas.)
let vp8x = find_chunk(&bytes, b"VP8X").expect("animated webp should have a VP8X header");
let width = u24_le(&bytes[vp8x + 12..vp8x + 15]) + 1;
let height = u24_le(&bytes[vp8x + 15..vp8x + 18]) + 1;
assert_eq!((width, height), (360, 270));
}
/// Resolves like a development job would (including the macOS `ffmpeg-full`
/// fallback). Skips if this machine has no webp-capable ffmpeg at all.
fn libwebp_anim_available() -> bool {
match resolve_webp_ffmpeg(true) {
Ok(_) => true,
Err(err) => {
eprintln!("SKIPPED: {}", err);
false
}
}
}
/// Offset of the first RIFF chunk with this FourCC, walking top-level chunks
/// after the 12-byte RIFF/WEBP header.
fn find_chunk(bytes: &[u8], fourcc: &[u8; 4]) -> Option<usize> {
chunk_offsets(bytes).into_iter().find(|&offset| &bytes[offset..offset + 4] == fourcc)
}
fn count_chunks(bytes: &[u8], fourcc: &[u8; 4]) -> usize {
chunk_offsets(bytes).into_iter().filter(|&offset| &bytes[offset..offset + 4] == fourcc).count()
}
fn chunk_offsets(bytes: &[u8]) -> Vec<usize> {
let mut offsets = Vec::new();
let mut offset = 12;
while offset + 8 <= bytes.len() {
offsets.push(offset);
let size = u32::from_le_bytes([bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]]) as usize;
// Chunk payloads are padded to an even length.
offset += 8 + size + (size & 1);
}
offsets
}
fn u24_le(bytes: &[u8]) -> u32 {
u32::from(bytes[0]) | (u32::from(bytes[1]) << 8) | (u32::from(bytes[2]) << 16)
}
}
@@ -3,4 +3,6 @@ pub mod ffmpeg_transcode_to_mp4;
pub mod ffmpeg_trim_and_resample;
pub mod ffmpeg_video_first_frame_to_jpg_thumbnail;
pub mod ffmpeg_video_gif_preview;
pub mod ffmpeg_video_webp_preview;
mod run_thumbnail_ffmpeg;
pub mod webp_ffmpeg_binary;
@@ -0,0 +1,156 @@
//! Which `ffmpeg` binary to use for animated webp previews.
//!
//! Animated webp needs ffmpeg's `libwebp_anim` encoder. Ubuntu's `ffmpeg`
//! package (production) has it. Homebrew's slimmed `ffmpeg` formula doesn't
//! (as of 2026-09); its full build is the keg-only `ffmpeg-full` formula,
//! which isn't on the PATH. So on macOS development machines we fall back to
//! `ffmpeg-full` when the `ffmpeg` on the PATH can't encode webp.
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use anyhow::anyhow;
use log::{info, warn};
use errors::AnyhowResult;
/// The `ffmpeg` on the PATH.
const PATH_FFMPEG: &str = "ffmpeg";
/// Homebrew's keg-only `ffmpeg-full` (Apple Silicon, then Intel prefixes).
const HOMEBREW_FFMPEG_FULL_CANDIDATES: [&str; 2] = [
"/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg",
"/usr/local/opt/ffmpeg-full/bin/ffmpeg",
];
/// The process-wide decision. Resolved once (the first caller probes the
/// binaries while concurrent callers wait), then shared by every thread.
/// Failures are cached too: a missing encoder won't appear mid-process.
static WEBP_FFMPEG: OnceLock<Result<PathBuf, String>> = OnceLock::new();
/// Resolve (once per process, then cached) the `ffmpeg` binary that can
/// encode animated webp.
///
/// Uses the `ffmpeg` on the PATH if it has `libwebp_anim`. Otherwise, only when
/// `is_development` and on macOS, falls back to Homebrew's `ffmpeg-full` and
/// logs a warning. Call this at startup with the server environment; the first
/// call decides for the whole process, and later calls return the cached
/// decision (whatever `is_development` they pass).
pub fn resolve_webp_ffmpeg(is_development: bool) -> AnyhowResult<&'static Path> {
let decision = WEBP_FFMPEG.get_or_init(|| {
let allow_macos_fallback = is_development && cfg!(target_os = "macos");
let decision = choose_webp_ffmpeg(
has_webp_encoder(Path::new(PATH_FFMPEG)),
allow_macos_fallback,
find_homebrew_ffmpeg_full,
);
match &decision {
Ok(WebpFfmpegChoice::Path) =>
info!("Using the ffmpeg on the PATH for webp previews."),
Ok(WebpFfmpegChoice::HomebrewFallback(binary)) =>
warn!("The ffmpeg on the PATH has no libwebp_anim encoder; falling back to {} for webp previews \
(macOS development only).", binary.display()),
Err(err) =>
warn!("No ffmpeg can encode webp previews: {}", err),
}
decision.map(WebpFfmpegChoice::into_binary)
});
decision.as_deref().map_err(|err| anyhow!("{}", err))
}
/// The binary resolved by [`resolve_webp_ffmpeg`], or production behavior
/// (the PATH only) if nothing resolved it yet.
pub(crate) fn webp_ffmpeg() -> AnyhowResult<&'static Path> {
resolve_webp_ffmpeg(false)
}
#[derive(Debug, PartialEq, Eq)]
enum WebpFfmpegChoice {
Path,
HomebrewFallback(PathBuf),
}
impl WebpFfmpegChoice {
fn into_binary(self) -> PathBuf {
match self {
WebpFfmpegChoice::Path => PathBuf::from(PATH_FFMPEG),
WebpFfmpegChoice::HomebrewFallback(binary) => binary,
}
}
}
/// The decision, separated from probing binaries so it can be tested.
fn choose_webp_ffmpeg(
path_ffmpeg_has_webp: bool,
allow_macos_fallback: bool,
find_fallback: impl FnOnce() -> Option<PathBuf>,
) -> Result<WebpFfmpegChoice, String> {
if path_ffmpeg_has_webp {
return Ok(WebpFfmpegChoice::Path);
}
if !allow_macos_fallback {
return Err("the ffmpeg on the PATH has no libwebp_anim encoder, which animated webp thumbnails need".to_string());
}
find_fallback()
.map(WebpFfmpegChoice::HomebrewFallback)
.ok_or_else(|| "neither the ffmpeg on the PATH nor Homebrew's ffmpeg-full has a libwebp_anim encoder. \
Run `brew install ffmpeg-full`.".to_string())
}
/// The first installed Homebrew `ffmpeg-full` that has the webp encoder.
fn find_homebrew_ffmpeg_full() -> Option<PathBuf> {
HOMEBREW_FFMPEG_FULL_CANDIDATES.iter()
.map(PathBuf::from)
.find(|binary| binary.exists() && has_webp_encoder(binary))
}
/// Whether `binary -encoders` lists `libwebp_anim`. False if it can't run.
fn has_webp_encoder(binary: &Path) -> bool {
Command::new(binary)
.args(["-hide_banner", "-encoders"])
.output()
.map(|output| String::from_utf8_lossy(&output.stdout)
.split_whitespace()
.any(|word| word == "libwebp_anim"))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
const FALLBACK: &str = "/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg";
#[test]
fn uses_path_ffmpeg_when_it_has_webp() {
let choice = choose_webp_ffmpeg(true, true, || panic!("shouldn't look for a fallback"));
assert_eq!(choice, Ok(WebpFfmpegChoice::Path));
}
#[test]
fn falls_back_to_ffmpeg_full_when_allowed() {
let choice = choose_webp_ffmpeg(false, true, || Some(PathBuf::from(FALLBACK)));
assert_eq!(choice, Ok(WebpFfmpegChoice::HomebrewFallback(PathBuf::from(FALLBACK))));
}
#[test]
fn never_falls_back_outside_macos_development() {
let choice = choose_webp_ffmpeg(false, false, || panic!("shouldn't look for a fallback"));
assert!(choice.is_err());
}
#[test]
fn errors_when_the_fallback_is_missing() {
let choice = choose_webp_ffmpeg(false, true, || None);
let err = choice.unwrap_err();
assert!(err.contains("brew install ffmpeg-full"), "{}", err);
}
#[test]
fn choice_binaries() {
assert_eq!(WebpFfmpegChoice::Path.into_binary(), PathBuf::from("ffmpeg"));
assert_eq!(WebpFfmpegChoice::HomebrewFallback(PathBuf::from(FALLBACK)).into_binary(), PathBuf::from(FALLBACK));
}
}
@@ -1,16 +1,176 @@
//! Shared config for thumbnails.
//! Shared config for video thumbnails (the still and animated previews stored
//! beside each video file in the public bucket).
//!
//! # Thumbnail versions (`media_files.maybe_thumbnail_version`)
//!
//! | Version | Since | Files beside `<video>.mp4` |
//! |---------|--------------------------|------------------------------------------------------------|
//! | `NULL` | 2024-02-28 | `-thumb.jpg`, `-thumb.gif` (legacy, or not yet generated) |
//! | `1` | 2026-03-18 | `-thumb.jpg`, `-thumb.gif` |
//! | `2` | 2026-09 (see below) | `-thumb.jpg`, `-thumb.webp`, `-thumb.gif` |
//!
//! * **`NULL`**: No version recorded. Clients have linked `-thumb.gif` and
//! `-thumb.jpg` since 2024-02-28 ("video card thumbnails", e7a427289f), so
//! older rows may have GIF/JPG files from earlier pipelines, or none at all.
//! A `NULL` row may also just be waiting for the thumbnail job. Serving uses
//! the `created_at` cutover in [`animated_preview_format`] to pick GIF or WebP.
//! * **`1`**: Written by `video-thumbnail-job` since it launched on 2026-03-18
//! (#1157). A first-frame JPG and an animated GIF: the first 5 seconds at
//! 10 fps, fit within 360x360.
//! * **`2`**: Same JPG, plus an animated WebP (quality 60) of the same frames,
//! which is typically 6-50x smaller than the GIF. The job still writes the
//! GIF too ("dual writes"); see "macOS Catalina" below. Deployed in late
//! September 2026. TODO(bt): Replace "2026-09" above with the exact deploy date.
//!
//! # macOS Catalina and GIF dual writes
//!
//! Animated WebP needs WebKit on macOS 11 (Big Sur) or newer. ArtCraft desktop
//! (Tauri) uses the system WKWebView, so on macOS 10.15 (Catalina) a WebP in
//! an `<img>` doesn't render. Version 2 therefore keeps writing `-thumb.gif`
//! beside the WebP, and the API exposes it as the Catalina fallback so the
//! desktop app can fall back to it. We may keep the GIF dual writes forever;
//! drop them only once no supported client needs them.
//!
//! # Changing this scheme
//!
//! These suffixes are how files are physically named in the public bucket and
//! are shared between services. Never change or reuse a suffix or a version
//! number. Add a new version instead, and update [`animated_preview_format`].
/// NB: DO NOT CHANGE WITHOUT MIGRATION - THIS IS SHARED BETWEEN SERVICES
/// Video media files have conventional gif thumbnails with this suffix.
/// This is how files are physically named in the public bucket.
pub const VIDEO_ANIMATED_GIF_THUMBNAIL_SUFFIX: &str = "-thumb.gif";
/// NB: DO NOT CHANGE WITHOUT MIGRATION - THIS IS SHARED BETWEEN SERVICES
/// Video media files with thumbnail version 2+ have animated webp thumbnails
/// with this suffix. This is how files are physically named in the public bucket.
pub const VIDEO_ANIMATED_WEBP_THUMBNAIL_SUFFIX: &str = "-thumb.webp";
/// NB: DO NOT CHANGE WITHOUT MIGRATION - THIS IS SHARED BETWEEN SERVICES
/// Video media files have conventional static jpg thumbnails with this suffix.
/// This is how files are physically named in the public bucket.
pub const VIDEO_STATIC_JPG_THUMBNAIL_SUFFIX: &str = "-thumb.jpg";
/// The current thumbnail format is a dual jpg + gif of a set size.
/// We write this as version "1". If we change the thumbnails in the behavior,
/// we may increment this number so that we can backfill.
pub const CURRENT_VIDEO_THUMBNAIL_VERSION : u8 = 1;
/// Version 1 (2026-03-18): first-frame jpg + animated gif.
pub const VIDEO_THUMBNAIL_VERSION_GIF: u8 = 1;
/// Version 2 (2026-09): first-frame jpg + animated webp, plus the animated gif
/// as a dual write for macOS Catalina.
pub const VIDEO_THUMBNAIL_VERSION_WEBP: u8 = 2;
/// The version `video-thumbnail-job` writes for newly processed videos.
pub const CURRENT_VIDEO_THUMBNAIL_VERSION: u8 = VIDEO_THUMBNAIL_VERSION_WEBP;
/// Videos with a `NULL` thumbnail version created at or after this instant
/// (2026-10-10T00:00:00Z) are served WebP previews; older `NULL` rows get GIF.
///
/// TODO(bt): Remove this cutover; it shouldn't live forever. It exists
/// because `NULL` means two things today: "legacy row that only ever had a
/// GIF" and "new row the thumbnail job hasn't reached yet (it will write a
/// WebP)". To remove it:
/// 1. Wait until the thumbnail job's lookback window has passed since
/// 2026-10-10, so every row created after the cutover has been processed.
/// 2. Backfill legacy rows older than the lookback window, eg.
/// `UPDATE media_files SET maybe_thumbnail_version = 1
/// WHERE maybe_thumbnail_version IS NULL AND media_class = 'video'
/// AND created_at < '2026-10-10'`.
/// (Rows inside the lookback window must be left `NULL`, or the job
/// will skip them.)
/// 3. `NULL` then only means "not generated yet", so serve the current
/// format for it: delete this constant and the `created_at` argument to
/// [`animated_preview_format`], and stop selecting `created_at` for it.
pub const NULL_THUMBNAIL_VERSION_WEBP_CUTOVER_UNIX_SECONDS: i64 = 1_791_590_400;
/// The format of a video's animated preview file.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AnimatedPreviewFormat {
Gif,
Webp,
}
impl AnimatedPreviewFormat {
/// The bucket file suffix, eg. `-thumb.webp`.
pub const fn suffix(self) -> &'static str {
match self {
Self::Gif => VIDEO_ANIMATED_GIF_THUMBNAIL_SUFFIX,
Self::Webp => VIDEO_ANIMATED_WEBP_THUMBNAIL_SUFFIX,
}
}
}
/// Which animated preview to serve for a video, from its
/// `media_files.maybe_thumbnail_version` and `created_at` (as Unix seconds).
/// See the module docs for the version history.
pub fn animated_preview_format(
maybe_thumbnail_version: Option<u8>,
created_at_unix_seconds: i64,
) -> AnimatedPreviewFormat {
match maybe_thumbnail_version {
Some(version) if version >= VIDEO_THUMBNAIL_VERSION_WEBP => AnimatedPreviewFormat::Webp,
Some(_) => AnimatedPreviewFormat::Gif,
None if created_at_unix_seconds >= NULL_THUMBNAIL_VERSION_WEBP_CUTOVER_UNIX_SECONDS => AnimatedPreviewFormat::Webp,
None => AnimatedPreviewFormat::Gif,
}
}
#[cfg(test)]
mod tests {
use super::*;
// 2026-10-09T23:59:59Z and 2026-10-10T00:00:00Z
const JUST_BEFORE_CUTOVER: i64 = 1_791_590_399;
const AT_CUTOVER: i64 = 1_791_590_400;
// 2024-05-01T00:00:00Z
const LEGACY_ERA: i64 = 1_714_521_600;
#[test]
fn suffixes_are_stable() {
assert_eq!(VIDEO_ANIMATED_GIF_THUMBNAIL_SUFFIX, "-thumb.gif");
assert_eq!(VIDEO_ANIMATED_WEBP_THUMBNAIL_SUFFIX, "-thumb.webp");
assert_eq!(VIDEO_STATIC_JPG_THUMBNAIL_SUFFIX, "-thumb.jpg");
assert_eq!(AnimatedPreviewFormat::Gif.suffix(), "-thumb.gif");
assert_eq!(AnimatedPreviewFormat::Webp.suffix(), "-thumb.webp");
}
#[test]
fn versions_are_stable() {
assert_eq!(VIDEO_THUMBNAIL_VERSION_GIF, 1);
assert_eq!(VIDEO_THUMBNAIL_VERSION_WEBP, 2);
assert_eq!(CURRENT_VIDEO_THUMBNAIL_VERSION, 2);
}
mod animated_preview_format_tests {
use super::*;
#[test]
fn version_1_is_gif_regardless_of_date() {
assert_eq!(animated_preview_format(Some(1), LEGACY_ERA), AnimatedPreviewFormat::Gif);
assert_eq!(animated_preview_format(Some(1), AT_CUTOVER), AnimatedPreviewFormat::Gif);
}
#[test]
fn version_2_is_webp_regardless_of_date() {
assert_eq!(animated_preview_format(Some(2), LEGACY_ERA), AnimatedPreviewFormat::Webp);
assert_eq!(animated_preview_format(Some(2), AT_CUTOVER), AnimatedPreviewFormat::Webp);
}
#[test]
fn null_before_cutover_is_gif() {
assert_eq!(animated_preview_format(None, LEGACY_ERA), AnimatedPreviewFormat::Gif);
assert_eq!(animated_preview_format(None, JUST_BEFORE_CUTOVER), AnimatedPreviewFormat::Gif);
}
#[test]
fn null_at_or_after_cutover_is_webp() {
assert_eq!(animated_preview_format(None, AT_CUTOVER), AnimatedPreviewFormat::Webp);
assert_eq!(animated_preview_format(None, AT_CUTOVER + 86_400), AnimatedPreviewFormat::Webp);
}
#[test]
fn cutover_is_2026_10_10_utc() {
// 20_736 days from 1970-01-01 to 2026-10-10.
assert_eq!(NULL_THUMBNAIL_VERSION_WEBP_CUTOVER_UNIX_SECONDS, 20_736 * 86_400);
}
}
}
@@ -34,6 +34,8 @@ pub struct FolderMediaFileRow {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
/// Which video preview files exist. See `video_thumbnail_suffixes` in `bucket_paths`.
pub maybe_thumbnail_version: Option<u8>,
// Cover image's bucket fields, from a LEFT JOIN on
// mf.maybe_cover_image_media_file_token. All three are Some-or-None
@@ -102,6 +104,7 @@ SELECT
mf.public_bucket_directory_hash,
mf.maybe_public_bucket_prefix,
mf.maybe_public_bucket_extension,
mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
@@ -150,6 +153,7 @@ LIMIT ?
public_bucket_directory_hash: r.public_bucket_directory_hash,
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
maybe_thumbnail_version: r.maybe_thumbnail_version,
maybe_cover_public_bucket_directory_hash: r.maybe_cover_public_bucket_directory_hash,
maybe_cover_public_bucket_prefix: r.maybe_cover_public_bucket_prefix,
maybe_cover_public_bucket_extension: r.maybe_cover_public_bucket_extension,
@@ -183,6 +187,7 @@ SELECT
mf.public_bucket_directory_hash,
mf.maybe_public_bucket_prefix,
mf.maybe_public_bucket_extension,
mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
@@ -229,6 +234,7 @@ LIMIT ?
public_bucket_directory_hash: r.public_bucket_directory_hash,
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
maybe_thumbnail_version: r.maybe_thumbnail_version,
maybe_cover_public_bucket_directory_hash: r.maybe_cover_public_bucket_directory_hash,
maybe_cover_public_bucket_prefix: r.maybe_cover_public_bucket_prefix,
maybe_cover_public_bucket_extension: r.maybe_cover_public_bucket_extension,
@@ -98,6 +98,7 @@ SELECT
mf.public_bucket_directory_hash,
mf.maybe_public_bucket_prefix,
mf.maybe_public_bucket_extension,
mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
@@ -168,6 +169,7 @@ SELECT
mf.public_bucket_directory_hash,
mf.maybe_public_bucket_prefix,
mf.maybe_public_bucket_extension,
mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
@@ -27,6 +27,7 @@ pub struct JobMediaFileRecord {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub maybe_file_cover_image_public_bucket_hash: Option<String>,
pub maybe_file_cover_image_public_bucket_prefix: Option<String>,
@@ -78,6 +79,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
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,
@@ -117,6 +117,8 @@ SELECT
media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,
media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,
media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,
media_files.maybe_thumbnail_version as maybe_media_file_thumbnail_version,
media_files.created_at as maybe_media_file_created_at,
jobs.assigned_worker as maybe_assigned_worker,
jobs.assigned_cluster as maybe_assigned_cluster,
@@ -246,6 +248,8 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
public_bucket_location_or_hash: public_bucket_hash.to_string(),
maybe_media_file_public_bucket_prefix: record.maybe_media_file_public_bucket_prefix.clone(),
maybe_media_file_public_bucket_extension: record.maybe_media_file_public_bucket_extension.clone(),
maybe_media_file_thumbnail_version: record.maybe_media_file_thumbnail_version,
maybe_media_file_created_at: record.maybe_media_file_created_at,
public_bucket_location_is_hash: bucket_path_is_hash,
maybe_successfully_completed_at: record.maybe_successfully_completed_at,
}
@@ -320,6 +324,8 @@ struct RawGenericInferenceJobStatus {
pub maybe_media_file_public_bucket_directory_hash: Option<String>, // NB: This is the bucket directory hash
pub maybe_media_file_public_bucket_prefix: Option<String>,
pub maybe_media_file_public_bucket_extension: Option<String>,
pub maybe_media_file_thumbnail_version: Option<u8>,
pub maybe_media_file_created_at: Option<DateTime<Utc>>,
pub maybe_assigned_worker: Option<String>,
pub maybe_assigned_cluster: Option<String>,
@@ -377,6 +383,8 @@ impl FromRow<'_, MySqlRow> for RawGenericInferenceJobStatus {
maybe_media_file_public_bucket_directory_hash: row.try_get("maybe_media_file_public_bucket_directory_hash")?,
maybe_media_file_public_bucket_prefix: row.try_get("maybe_media_file_public_bucket_prefix")?,
maybe_media_file_public_bucket_extension: row.try_get("maybe_media_file_public_bucket_extension")?,
maybe_media_file_thumbnail_version: row.try_get("maybe_media_file_thumbnail_version")?,
maybe_media_file_created_at: row.try_get("maybe_media_file_created_at")?,
maybe_assigned_worker: row.try_get("maybe_assigned_worker")?,
maybe_assigned_cluster: row.try_get("maybe_assigned_cluster")?,
maybe_frontend_failure_category: row.try_get("maybe_frontend_failure_category")?,
@@ -75,6 +75,8 @@ SELECT
media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,
media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,
media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,
media_files.maybe_thumbnail_version as `maybe_media_file_thumbnail_version?: u8`,
media_files.created_at as `maybe_media_file_created_at?: DateTime<Utc>`,
jobs.assigned_worker as maybe_assigned_worker,
jobs.assigned_cluster as maybe_assigned_cluster,
@@ -221,6 +223,8 @@ fn raw_record_to_public_result(record: RawGenericInferenceJobStatus) -> GenericI
public_bucket_location_or_hash: public_bucket_hash.to_string(),
maybe_media_file_public_bucket_prefix: record.maybe_media_file_public_bucket_prefix.clone(),
maybe_media_file_public_bucket_extension: record.maybe_media_file_public_bucket_extension.clone(),
maybe_media_file_thumbnail_version: record.maybe_media_file_thumbnail_version,
maybe_media_file_created_at: record.maybe_media_file_created_at,
public_bucket_location_is_hash: bucket_path_is_hash,
maybe_successfully_completed_at: record.maybe_successfully_completed_at,
}
@@ -293,6 +297,8 @@ struct RawGenericInferenceJobStatus {
pub maybe_media_file_public_bucket_directory_hash: Option<String>, // NB: This is the bucket directory hash
pub maybe_media_file_public_bucket_prefix: Option<String>,
pub maybe_media_file_public_bucket_extension: Option<String>,
pub maybe_media_file_thumbnail_version: Option<u8>,
pub maybe_media_file_created_at: Option<DateTime<Utc>>,
pub maybe_assigned_worker: Option<String>,
pub maybe_assigned_cluster: Option<String>,
@@ -79,6 +79,8 @@ pub struct ResultDetails {
pub public_bucket_location_or_hash: String,
pub maybe_media_file_public_bucket_prefix: Option<String>,
pub maybe_media_file_public_bucket_extension: Option<String>,
pub maybe_media_file_thumbnail_version: Option<u8>,
pub maybe_media_file_created_at: Option<DateTime<Utc>>,
/// Whether the location is a full path (for tts) or a hash (for vc) that
/// needs to be reconstructed into a path.
@@ -183,6 +183,8 @@ SELECT
media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,
media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,
media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,
media_files.maybe_thumbnail_version as maybe_media_file_thumbnail_version,
media_files.created_at as maybe_media_file_created_at,
jobs.assigned_worker as maybe_assigned_worker,
jobs.assigned_cluster as maybe_assigned_cluster,
@@ -312,6 +314,8 @@ fn raw_records_to_public_result(records: Vec<RawGenericInferenceJobStatus>) -> V
public_bucket_location_or_hash: public_bucket_hash.to_string(),
maybe_media_file_public_bucket_prefix: record.maybe_media_file_public_bucket_prefix.clone(),
maybe_media_file_public_bucket_extension: record.maybe_media_file_public_bucket_extension.clone(),
maybe_media_file_thumbnail_version: record.maybe_media_file_thumbnail_version,
maybe_media_file_created_at: record.maybe_media_file_created_at,
public_bucket_location_is_hash: bucket_path_is_hash,
maybe_successfully_completed_at: record.maybe_successfully_completed_at,
}
@@ -386,6 +390,8 @@ struct RawGenericInferenceJobStatus {
pub maybe_media_file_public_bucket_directory_hash: Option<String>, // NB: This is the bucket directory hash
pub maybe_media_file_public_bucket_prefix: Option<String>,
pub maybe_media_file_public_bucket_extension: Option<String>,
pub maybe_media_file_thumbnail_version: Option<u8>,
pub maybe_media_file_created_at: Option<DateTime<Utc>>,
pub maybe_assigned_worker: Option<String>,
pub maybe_assigned_cluster: Option<String>,
@@ -443,6 +449,8 @@ impl FromRow<'_, MySqlRow> for RawGenericInferenceJobStatus {
maybe_media_file_public_bucket_directory_hash: row.try_get("maybe_media_file_public_bucket_directory_hash")?,
maybe_media_file_public_bucket_prefix: row.try_get("maybe_media_file_public_bucket_prefix")?,
maybe_media_file_public_bucket_extension: row.try_get("maybe_media_file_public_bucket_extension")?,
maybe_media_file_thumbnail_version: row.try_get("maybe_media_file_thumbnail_version")?,
maybe_media_file_created_at: row.try_get("maybe_media_file_created_at")?,
maybe_assigned_worker: row.try_get("maybe_assigned_worker")?,
maybe_assigned_cluster: row.try_get("maybe_assigned_cluster")?,
maybe_frontend_failure_category: row.try_get("maybe_frontend_failure_category")?,
@@ -1,5 +1,6 @@
use std::marker::PhantomData;
use chrono::{DateTime, Utc};
use sqlx::{Executor, MySql, QueryBuilder};
use enums::by_table::media_files::media_file_class::MediaFileClass;
@@ -15,6 +16,9 @@ pub struct MediaFileThumbnailRow {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
/// Which video preview files exist. See `video_thumbnail_suffixes` in `bucket_paths`.
pub maybe_thumbnail_version: Option<u8>,
pub created_at: DateTime<Utc>,
}
pub struct BatchGetMediaFileThumbnailsByTokensArgs<'e, 'c, E>
@@ -49,7 +53,9 @@ where
media_type, \
public_bucket_directory_hash, \
maybe_public_bucket_prefix, \
maybe_public_bucket_extension \
maybe_public_bucket_extension, \
maybe_thumbnail_version, \
created_at \
FROM media_files \
WHERE user_deleted_at IS NULL \
AND mod_deleted_at IS NULL \
@@ -74,6 +80,8 @@ where
public_bucket_directory_hash: r.public_bucket_directory_hash,
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
maybe_thumbnail_version: r.maybe_thumbnail_version,
created_at: r.created_at,
}).collect())
}
@@ -88,4 +96,6 @@ struct RawRow {
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
maybe_thumbnail_version: Option<u8>,
created_at: DateTime<Utc>,
}
@@ -90,6 +90,7 @@ pub struct MediaFile {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub maybe_ratings_positive_count: Option<u32>,
pub maybe_ratings_negative_count: Option<u32>,
@@ -171,6 +172,7 @@ pub struct MediaFileRaw {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
//pub model_is_mod_approved: bool, // converted
//pub maybe_mod_user_token: Option<String>,
@@ -270,6 +272,7 @@ pub async fn batch_get_media_files(
public_bucket_directory_hash: record.public_bucket_directory_hash,
maybe_public_bucket_prefix: record.maybe_public_bucket_prefix,
maybe_public_bucket_extension: record.maybe_public_bucket_extension,
maybe_thumbnail_version: record.maybe_thumbnail_version,
maybe_ratings_positive_count: record.maybe_ratings_positive_count,
maybe_ratings_negative_count: record.maybe_ratings_negative_count,
maybe_bookmark_count: record.maybe_bookmark_count,
@@ -336,6 +339,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version,
entity_stats.ratings_positive_count as maybe_ratings_positive_count,
entity_stats.ratings_negative_count as maybe_ratings_negative_count,
@@ -427,6 +431,7 @@ impl FromRow<'_, MySqlRow> for MediaFileRaw {
public_bucket_directory_hash: row.try_get("public_bucket_directory_hash")?,
maybe_public_bucket_prefix: row.try_get("maybe_public_bucket_prefix")?,
maybe_public_bucket_extension: row.try_get("maybe_public_bucket_extension")?,
maybe_thumbnail_version: row.try_get("maybe_thumbnail_version")?,
maybe_ratings_positive_count: row.try_get("maybe_ratings_positive_count")?,
maybe_ratings_negative_count: row.try_get("maybe_ratings_negative_count")?,
maybe_bookmark_count: row.try_get("maybe_bookmark_count")?,
@@ -42,6 +42,9 @@ pub struct MediaFilesByTokensRecord {
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
/// Which video preview files exist. See `video_thumbnail_suffixes` in `bucket_paths`.
pub maybe_thumbnail_version: Option<u8>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_username: Option<String>,
pub maybe_creator_display_name: Option<String>,
@@ -145,6 +148,7 @@ async fn get_raw_media_files_by_tokens(
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version,
m.is_user_upload,
m.is_intermediate_system_file,
@@ -211,6 +215,7 @@ async fn get_raw_media_files_by_tokens(
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version,
m.is_user_upload,
m.is_intermediate_system_file,
@@ -315,6 +320,7 @@ fn map_to_media_files(dataset:Vec<RawMediaFileJoinUser>) -> Vec<MediaFilesByToke
public_bucket_directory_hash: media_file.public_bucket_directory_hash,
maybe_public_bucket_prefix: media_file.maybe_public_bucket_prefix,
maybe_public_bucket_extension: media_file.maybe_public_bucket_extension,
maybe_thumbnail_version: media_file.maybe_thumbnail_version,
is_user_upload: i8_to_bool(media_file.is_user_upload),
is_intermediate_system_file: i8_to_bool(media_file.is_intermediate_system_file),
@@ -371,6 +377,7 @@ fn map_to_media_files(dataset:Vec<RawMediaFileJoinUser>) -> Vec<MediaFilesByToke
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub maybe_ratings_positive_count: Option<u32>,
pub maybe_ratings_negative_count: Option<u32>,
@@ -427,6 +434,7 @@ impl FromRow<'_, MySqlRow> for RawMediaFileJoinUser {
public_bucket_directory_hash: row.try_get("public_bucket_directory_hash")?,
maybe_public_bucket_prefix: row.try_get("maybe_public_bucket_prefix")?,
maybe_public_bucket_extension: row.try_get("maybe_public_bucket_extension")?,
maybe_thumbnail_version: row.try_get("maybe_thumbnail_version")?,
maybe_ratings_positive_count: row.try_get("maybe_ratings_positive_count")?,
maybe_ratings_negative_count: row.try_get("maybe_ratings_negative_count")?,
maybe_bookmark_count: row.try_get("maybe_bookmark_count")?,
@@ -96,6 +96,7 @@ pub struct MediaFile {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub maybe_ratings_positive_count: Option<u32>,
pub maybe_ratings_negative_count: Option<u32>,
@@ -184,6 +185,7 @@ pub struct MediaFileRaw {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
//pub model_is_mod_approved: bool, // converted
//pub maybe_mod_user_token: Option<String>,
@@ -296,6 +298,7 @@ pub async fn get_media_file_with_transactor(
public_bucket_directory_hash: record.public_bucket_directory_hash,
maybe_public_bucket_prefix: record.maybe_public_bucket_prefix,
maybe_public_bucket_extension: record.maybe_public_bucket_extension,
maybe_thumbnail_version: record.maybe_thumbnail_version,
maybe_ratings_positive_count: record.maybe_ratings_positive_count,
maybe_ratings_negative_count: record.maybe_ratings_negative_count,
maybe_bookmark_count: record.maybe_bookmark_count,
@@ -373,6 +376,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
entity_stats.ratings_positive_count as maybe_ratings_positive_count,
entity_stats.ratings_negative_count as maybe_ratings_negative_count,
@@ -495,6 +499,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
entity_stats.ratings_positive_count as maybe_ratings_positive_count,
entity_stats.ratings_negative_count as maybe_ratings_negative_count,
@@ -66,6 +66,7 @@ pub struct MediaFile {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@@ -114,6 +115,7 @@ pub struct MediaFileRaw {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@@ -189,6 +191,7 @@ pub async fn list_batch_generated_redux_media_files_with_transactor(
public_bucket_directory_hash: record.public_bucket_directory_hash,
maybe_public_bucket_prefix: record.maybe_public_bucket_prefix,
maybe_public_bucket_extension: record.maybe_public_bucket_extension,
maybe_thumbnail_version: record.maybe_thumbnail_version,
created_at: record.created_at,
updated_at: record.updated_at,
}
@@ -246,6 +249,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
m.created_at,
m.updated_at
@@ -337,6 +341,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
m.created_at,
m.updated_at
@@ -57,6 +57,7 @@ pub struct FeaturedMediaFileListItem {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_username: Option<String>,
@@ -145,6 +146,7 @@ pub async fn list_featured_media_files(args: ListFeaturedMediaFilesArgs<'_>) ->
public_bucket_directory_hash: record.public_bucket_directory_hash,
maybe_public_bucket_prefix: record.maybe_public_bucket_prefix,
maybe_public_bucket_extension: record.maybe_public_bucket_extension,
maybe_thumbnail_version: record.maybe_thumbnail_version,
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_creator_username: record.maybe_creator_username,
maybe_creator_display_name: record.maybe_creator_display_name,
@@ -221,6 +223,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version,
m.maybe_creator_user_token,
u.username as maybe_creator_username,
@@ -463,6 +466,7 @@ struct MediaFileListItemInternal {
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
maybe_thumbnail_version: Option<u8>,
maybe_creator_user_token: Option<UserToken>,
maybe_creator_username: Option<String>,
@@ -526,6 +530,7 @@ impl FromRow<'_, MySqlRow> for MediaFileListItemInternal {
public_bucket_directory_hash: row.try_get("public_bucket_directory_hash")?,
maybe_public_bucket_prefix: row.try_get("maybe_public_bucket_prefix")?,
maybe_public_bucket_extension: row.try_get("maybe_public_bucket_extension")?,
maybe_thumbnail_version: row.try_get("maybe_thumbnail_version")?,
maybe_creator_user_token,
maybe_creator_username: row.try_get("maybe_creator_username")?,
maybe_creator_display_name: row.try_get("maybe_creator_display_name")?,
@@ -54,6 +54,7 @@ pub struct MediaFileListItem {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_username: Option<String>,
@@ -142,6 +143,7 @@ pub async fn list_media_files(args: ListMediaFilesArgs<'_>) -> AnyhowResult<Medi
public_bucket_directory_hash: record.public_bucket_directory_hash,
maybe_public_bucket_prefix: record.maybe_public_bucket_prefix,
maybe_public_bucket_extension: record.maybe_public_bucket_extension,
maybe_thumbnail_version: record.maybe_thumbnail_version,
maybe_creator_user_token: record.maybe_creator_user_token,
maybe_creator_username: record.maybe_creator_username,
maybe_creator_display_name: record.maybe_creator_display_name,
@@ -218,6 +220,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version,
m.maybe_creator_user_token,
u.username as maybe_creator_username,
@@ -443,6 +446,7 @@ struct MediaFileListItemInternal {
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
maybe_thumbnail_version: Option<u8>,
maybe_creator_user_token: Option<UserToken>,
maybe_creator_username: Option<String>,
@@ -506,6 +510,7 @@ impl FromRow<'_, MySqlRow> for MediaFileListItemInternal {
public_bucket_directory_hash: row.try_get("public_bucket_directory_hash")?,
maybe_public_bucket_prefix: row.try_get("maybe_public_bucket_prefix")?,
maybe_public_bucket_extension: row.try_get("maybe_public_bucket_extension")?,
maybe_thumbnail_version: row.try_get("maybe_thumbnail_version")?,
maybe_creator_user_token,
maybe_creator_username: row.try_get("maybe_creator_username")?,
maybe_creator_display_name: row.try_get("maybe_creator_display_name")?,
@@ -49,6 +49,7 @@ pub struct MediaFileListItem {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub creator_set_visibility: Visibility,
@@ -129,6 +130,7 @@ pub async fn list_media_files_by_batch_token(args: ListMediaFileByBatchArgs<'_>)
public_bucket_directory_hash: record.public_bucket_directory_hash,
maybe_public_bucket_prefix: record.maybe_public_bucket_prefix,
maybe_public_bucket_extension: record.maybe_public_bucket_extension,
maybe_thumbnail_version: record.maybe_thumbnail_version,
creator_set_visibility: record.creator_set_visibility,
is_user_upload: i8_to_bool(record.is_user_upload),
is_intermediate_system_file: i8_to_bool(record.is_intermediate_system_file),
@@ -180,6 +182,7 @@ fn select_result_fields() -> String {
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version,
m.creator_set_visibility,
@@ -290,6 +293,7 @@ struct MediaFileListItemInternal {
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
maybe_thumbnail_version: Option<u8>,
creator_set_visibility: Visibility,
@@ -340,6 +344,7 @@ impl FromRow<'_, MySqlRow> for MediaFileListItemInternal {
public_bucket_directory_hash: row.try_get("public_bucket_directory_hash")?,
maybe_public_bucket_prefix: row.try_get("maybe_public_bucket_prefix")?,
maybe_public_bucket_extension: row.try_get("maybe_public_bucket_extension")?,
maybe_thumbnail_version: row.try_get("maybe_thumbnail_version")?,
creator_set_visibility: Visibility::try_from_mysql_row(row, "creator_set_visibility")?,
is_user_upload: row.try_get("is_user_upload")?,
is_intermediate_system_file: row.try_get("is_intermediate_system_file")?,
@@ -25,6 +25,7 @@ const RESULT_FIELDS: &str = r#"
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version,
m.maybe_prompt_token,
@@ -30,6 +30,7 @@ pub struct SessionMediaFileListItem {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_username: Option<String>,
@@ -141,6 +142,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
@@ -206,6 +208,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
@@ -31,6 +31,7 @@ pub struct ProjectMediaFileListItem {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub maybe_creator_user_token: Option<UserToken>,
pub maybe_creator_username: Option<String>,
@@ -148,6 +149,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
@@ -205,6 +207,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
@@ -274,6 +277,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
@@ -331,6 +335,7 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
users.token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
users.username as maybe_creator_username,
@@ -32,6 +32,8 @@ pub struct MediaFileListRow {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
/// Which video preview files exist. See `video_thumbnail_suffixes` in `bucket_paths`.
pub maybe_thumbnail_version: Option<u8>,
// Cover image's bucket fields, from a LEFT JOIN on
// mf.maybe_cover_image_media_file_token. All three are Some-or-None
@@ -38,6 +38,7 @@ pub struct MediaFileListItem {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
pub maybe_thumbnail_version: Option<u8>,
pub maybe_prompt_token: Option<PromptToken>,
@@ -90,6 +91,7 @@ pub(super) struct MediaFileListItemInternal {
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
maybe_thumbnail_version: Option<u8>,
maybe_prompt_token: Option<PromptToken>,
@@ -146,6 +148,7 @@ impl FromRow<'_, MySqlRow> for MediaFileListItemInternal {
public_bucket_directory_hash: row.try_get("public_bucket_directory_hash")?,
maybe_public_bucket_prefix: row.try_get("maybe_public_bucket_prefix")?,
maybe_public_bucket_extension: row.try_get("maybe_public_bucket_extension")?,
maybe_thumbnail_version: row.try_get("maybe_thumbnail_version")?,
maybe_prompt_token: PromptToken::try_from_mysql_row_nullable(row, "maybe_prompt_token")?,
creator_set_visibility: Visibility::try_from_mysql_row(row, "creator_set_visibility")?,
is_user_upload: row.try_get("is_user_upload")?,
@@ -180,6 +183,7 @@ impl From<MediaFileListItemInternal> for MediaFileListItem {
public_bucket_directory_hash: record.public_bucket_directory_hash,
maybe_public_bucket_prefix: record.maybe_public_bucket_prefix,
maybe_public_bucket_extension: record.maybe_public_bucket_extension,
maybe_thumbnail_version: record.maybe_thumbnail_version,
maybe_prompt_token: record.maybe_prompt_token,
creator_set_visibility: record.creator_set_visibility,
is_user_upload: i8_to_bool(record.is_user_upload),
@@ -1,3 +1,4 @@
use chrono::{DateTime, Utc};
use sqlx::mysql::MySqlRow;
use sqlx::pool::PoolConnection;
use sqlx::{FromRow, MySql, QueryBuilder, Row};
@@ -16,6 +17,9 @@ pub struct BatchPromptContextItem {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
/// Which video preview files exist. See `video_thumbnail_suffixes` in `bucket_paths`.
pub maybe_thumbnail_version: Option<u8>,
pub created_at: DateTime<Utc>,
}
impl FromRow<'_, MySqlRow> for BatchPromptContextItem {
@@ -27,6 +31,8 @@ impl FromRow<'_, MySqlRow> for BatchPromptContextItem {
public_bucket_directory_hash: row.try_get("public_bucket_directory_hash")?,
maybe_public_bucket_prefix: row.try_get("maybe_public_bucket_prefix")?,
maybe_public_bucket_extension: row.try_get("maybe_public_bucket_extension")?,
maybe_thumbnail_version: row.try_get("maybe_thumbnail_version")?,
created_at: row.try_get("created_at")?,
})
}
}
@@ -49,7 +55,9 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version,
m.created_at
FROM prompt_context_items pci
JOIN media_files m ON pci.media_token = m.token
@@ -14,6 +14,9 @@ pub struct PromptContextItem {
pub public_bucket_directory_hash: String,
pub maybe_public_bucket_prefix: Option<String>,
pub maybe_public_bucket_extension: Option<String>,
/// Which video preview files exist. See `video_thumbnail_suffixes` in `bucket_paths`.
pub maybe_thumbnail_version: Option<u8>,
pub created_at: DateTime<Utc>,
}
struct RawPromptContextItem {
@@ -22,6 +25,8 @@ struct RawPromptContextItem {
public_bucket_directory_hash: String,
maybe_public_bucket_prefix: Option<String>,
maybe_public_bucket_extension: Option<String>,
maybe_thumbnail_version: Option<u8>,
created_at: DateTime<Utc>,
}
pub async fn list_prompt_context_items(
@@ -37,7 +42,9 @@ SELECT
m.public_bucket_directory_hash,
m.maybe_public_bucket_prefix,
m.maybe_public_bucket_extension
m.maybe_public_bucket_extension,
m.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
m.created_at as `created_at: DateTime<Utc>`
FROM prompt_context_items pci
JOIN media_files m
@@ -61,6 +68,8 @@ ORDER BY pci.id ASC
public_bucket_directory_hash: item.public_bucket_directory_hash,
maybe_public_bucket_prefix: item.maybe_public_bucket_prefix,
maybe_public_bucket_extension: item.maybe_public_bucket_extension,
maybe_thumbnail_version: item.maybe_thumbnail_version,
created_at: item.created_at,
}
}).collect::<Vec<PromptContextItem>>())
}
@@ -61,6 +61,7 @@ SELECT
mf.public_bucket_directory_hash,
mf.maybe_public_bucket_prefix,
mf.maybe_public_bucket_extension,
mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
@@ -54,6 +54,7 @@ SELECT
mf.public_bucket_directory_hash,
mf.maybe_public_bucket_prefix,
mf.maybe_public_bucket_extension,
mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
@@ -54,6 +54,7 @@ SELECT
mf.public_bucket_directory_hash,
mf.maybe_public_bucket_prefix,
mf.maybe_public_bucket_extension,
mf.maybe_thumbnail_version as `maybe_thumbnail_version: u8`,
cover.public_bucket_directory_hash as `maybe_cover_public_bucket_directory_hash?`,
cover.maybe_public_bucket_prefix as `maybe_cover_public_bucket_prefix?`,
@@ -60,6 +60,8 @@ fn select_result_fields() -> &'static str {
media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_hash,
media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,
media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,
media_files.maybe_thumbnail_version as maybe_media_file_thumbnail_version,
media_files.created_at as maybe_media_file_created_at,
media_file_creator.token as maybe_media_file_creator_user_token,
media_file_creator.username as maybe_media_file_creator_username,
media_file_creator.display_name as maybe_media_file_creator_display_name,
@@ -22,6 +22,8 @@ pub struct UserBookmark {
pub maybe_media_file_public_bucket_hash: Option<String>,
pub maybe_media_file_public_bucket_prefix: Option<String>,
pub maybe_media_file_public_bucket_extension: Option<String>,
pub maybe_media_file_thumbnail_version: Option<u8>,
pub maybe_media_file_created_at: Option<DateTime<Utc>>,
pub maybe_media_file_creator_user_token: Option<UserToken>,
pub maybe_media_file_creator_username: Option<String>,
@@ -108,6 +110,8 @@ pub struct RawUserBookmarkRecord {
pub(crate) maybe_media_file_public_bucket_hash: Option<String>,
pub(crate) maybe_media_file_public_bucket_prefix: Option<String>,
pub(crate) maybe_media_file_public_bucket_extension: Option<String>,
pub(crate) maybe_media_file_thumbnail_version: Option<u8>,
pub(crate) maybe_media_file_created_at: Option<DateTime<Utc>>,
pub(crate) maybe_media_file_cover_image_public_bucket_hash: Option<String>,
pub(crate) maybe_media_file_cover_image_public_bucket_prefix: Option<String>,
@@ -148,6 +152,8 @@ impl RawUserBookmarkRecord {
maybe_media_file_public_bucket_hash: self.maybe_media_file_public_bucket_hash,
maybe_media_file_public_bucket_prefix: self.maybe_media_file_public_bucket_prefix,
maybe_media_file_public_bucket_extension: self.maybe_media_file_public_bucket_extension,
maybe_media_file_thumbnail_version: self.maybe_media_file_thumbnail_version,
maybe_media_file_created_at: self.maybe_media_file_created_at,
maybe_media_file_creator_user_token: self.maybe_media_file_creator_user_token,
maybe_media_file_creator_username: self.maybe_media_file_creator_username,
maybe_media_file_creator_display_name: self.maybe_media_file_creator_display_name,
@@ -248,6 +254,8 @@ impl FromRow<'_, MySqlRow> for RawUserBookmarkRecord {
maybe_media_file_public_bucket_hash: row.try_get("maybe_media_file_public_bucket_hash")?,
maybe_media_file_public_bucket_prefix: row.try_get("maybe_media_file_public_bucket_prefix")?,
maybe_media_file_public_bucket_extension: row.try_get("maybe_media_file_public_bucket_extension")?,
maybe_media_file_thumbnail_version: row.try_get("maybe_media_file_thumbnail_version")?,
maybe_media_file_created_at: row.try_get("maybe_media_file_created_at")?,
maybe_media_file_cover_image_public_bucket_hash: row.try_get("maybe_media_file_cover_image_public_bucket_hash")?,
maybe_media_file_cover_image_public_bucket_prefix: row.try_get("maybe_media_file_cover_image_public_bucket_prefix")?,
maybe_media_file_cover_image_public_bucket_extension: row.try_get("maybe_media_file_cover_image_public_bucket_extension")?,
@@ -6,9 +6,10 @@ use log::{error, info};
use tempdir::TempDir;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use bucket_paths::path_conventions::video_thumbnail_suffixes::{CURRENT_VIDEO_THUMBNAIL_VERSION, VIDEO_ANIMATED_GIF_THUMBNAIL_SUFFIX, VIDEO_STATIC_JPG_THUMBNAIL_SUFFIX};
use bucket_paths::path_conventions::video_thumbnail_suffixes::{CURRENT_VIDEO_THUMBNAIL_VERSION, VIDEO_ANIMATED_GIF_THUMBNAIL_SUFFIX, VIDEO_ANIMATED_WEBP_THUMBNAIL_SUFFIX, VIDEO_STATIC_JPG_THUMBNAIL_SUFFIX};
use ffmpeg_utils::ffmpeg::ffmpeg_video_first_frame_to_jpg_thumbnail::{ffmpeg_video_first_frame_to_jpg_thumbnail, FfmpegVideoFirstFrameToJpgThumbnailArgs};
use ffmpeg_utils::ffmpeg::ffmpeg_video_gif_preview::{ffmpeg_video_gif_preview, FfmpegVideoGifPreviewArgs};
use ffmpeg_utils::ffmpeg::ffmpeg_video_webp_preview::{ffmpeg_video_webp_preview, FfmpegVideoWebpPreviewArgs};
use mysql_queries::queries::media_files::thumbnails::list_video_media_files_without_thumbnails_for_job::VideoMediaFileWithoutThumbnail;
use mysql_queries::queries::media_files::thumbnails::update_video_media_file_with_thumbnail::update_video_media_file_with_thumbnail;
@@ -24,6 +25,10 @@ pub struct DownloadedFile {
}
/// Download the source video from the bucket, generate thumbnails, and upload them.
///
/// Writes thumbnail version 2 (see `video_thumbnail_suffixes`): a first-frame
/// jpg, an animated webp, and an animated gif. The gif is a dual write kept for
/// ArtCraft desktop on macOS Catalina, which can't render animated webp.
pub async fn process_single_media_file(
deps: &JobDependencies,
media_file: &VideoMediaFileWithoutThumbnail,
@@ -91,7 +96,7 @@ pub async fn process_single_media_file(
info!("Uploaded JPG thumbnail to {}", jpg_object_path);
// Generate gif thumbnail
// Generate gif thumbnail (dual write for macOS Catalina; see `video_thumbnail_suffixes`)
let gif_path = downloaded.temp_dir.path().join("thumbnail.gif");
let stage_started_at = Instant::now();
@@ -123,6 +128,39 @@ pub async fn process_single_media_file(
info!("Uploaded GIF preview to {}", gif_object_path);
// Generate webp thumbnail
let webp_path = downloaded.temp_dir.path().join("thumbnail.webp");
let stage_started_at = Instant::now();
let source = Arc::clone(&downloaded);
let destination = webp_path.clone();
let result = tokio::task::spawn_blocking(move || {
ffmpeg_video_webp_preview(FfmpegVideoWebpPreviewArgs {
input_video_path: &source.file_path,
output_webp_path: &destination,
})
}).await.map_err(anyhow::Error::from).and_then(|result| result);
timing.log_stage("webp_generation", stage_started_at, result.is_ok());
if let Err(err) = result {
error!("Failed to generate WebP preview for {}: {:?}", media_file.token.as_str(), err);
return alert_pager_and_return_err(&deps.pager, "WebP preview generation failed", err.into());
}
info!("Generated WebP preview for {}", media_file.token.as_str());
let webp_object_path = format!("{video_object_path}{VIDEO_ANIMATED_WEBP_THUMBNAIL_SUFFIX}");
let stage_started_at = Instant::now();
// NB: Explicit content type; the jpg and gif uploads above use the bucket default.
let result = deps.public_bucket_client.upload_filename_with_content_type(&webp_object_path, &webp_path, "image/webp").await;
timing.log_stage("webp_upload", stage_started_at, result.is_ok());
if let Err(err) = result {
error!("Failed to upload WebP preview for {}: {:?}", media_file.token.as_str(), err);
return alert_pager_and_return_err(&deps.pager, "WebP preview upload failed", err);
}
info!("Uploaded WebP preview to {}", webp_object_path);
info!("Marking thumbnail job for {:?} done", media_file.token);
// Mark the media file as having a thumbnail in the database.
@@ -21,6 +21,7 @@ use cloud_storage::legacy_bucket_client::LegacyBucketClient;
use concurrency::relaxed_atomic_bool::RelaxedAtomicBool;
use shared_env_var_config::logging::DEFAULT_RUST_LOG;
use errors::AnyhowResult;
use ffmpeg_utils::ffmpeg::webp_ffmpeg_binary::resolve_webp_ffmpeg;
use jobs_common::job_stats::JobStats;
use server_environment::ServerEnvironment;
use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
@@ -74,6 +75,11 @@ async fn main() -> AnyhowResult<()> {
)
.ok_or(anyhow!("invalid server environment"))?;
// Thumbnail version 2 writes animated webp previews; fail fast if no ffmpeg can.
// (In development on macOS, this may fall back to Homebrew's ffmpeg-full.)
let webp_ffmpeg = resolve_webp_ffmpeg(server_environment.is_development())?;
info!("Webp previews will use ffmpeg at: {}", webp_ffmpeg.display());
// Bucket setup
let access_key = easyenv::get_env_string_required(ENV_ACCESS_KEY)?;
let secret_key = easyenv::get_env_string_required(ENV_SECRET_KEY)?;
@@ -6,6 +6,7 @@ use server_environment::ServerEnvironment;
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;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
/// Build the `MediaLinks` + `MediaFileCoverImageDetails` pair for one
/// media-file list row. Shared by every list endpoint that returns
@@ -26,6 +27,7 @@ pub fn build_media_links_and_cover(
media_domain,
server_environment,
&bucket_path,
VideoThumbnailInfo::new(row.maybe_thumbnail_version, row.created_at),
);
let cover_image = MediaFileCoverImageDetails::from_optional_db_fields(
@@ -2,10 +2,10 @@ use crate::http_server::common_responses::media::cdn_link::{get_cdn_host, new_cd
use crate::http_server::common_responses::media::media_domain::MediaDomain;
use artcraft_api_defs::common::responses::media_links::{MediaLinks, VideoPreviews};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use bucket_paths::path_conventions::video_thumbnail_suffixes::{VIDEO_ANIMATED_GIF_THUMBNAIL_SUFFIX, VIDEO_STATIC_JPG_THUMBNAIL_SUFFIX};
use bucket_paths::path_conventions::video_thumbnail_suffixes::{animated_preview_format, AnimatedPreviewFormat, VIDEO_STATIC_JPG_THUMBNAIL_SUFFIX};
use chrono::{DateTime, Utc};
use server_environment::ServerEnvironment;
use url::Url;
use utoipa::ToSchema;
// TODO(bt,2024-09-05): Worth reducing the quality at all?
const QUALITY : u8 = 95;
@@ -16,28 +16,48 @@ const QUALITY : u8 = 95;
/// This migrates the constructors to the `artcraft_api_defs` crate
pub struct MediaLinksBuilder {}
/// The `media_files` columns that decide which video preview files exist.
/// Required for every media file (not just videos) so no read site can build
/// video previews without them. See `video_thumbnail_suffixes` in
/// `bucket_paths` for the thumbnail version history.
#[derive(Copy, Clone, Debug)]
pub struct VideoThumbnailInfo {
/// `media_files.maybe_thumbnail_version`
pub maybe_thumbnail_version: Option<u8>,
/// `media_files.created_at` (decides `NULL` versions around the cutover)
pub created_at: DateTime<Utc>,
}
impl VideoThumbnailInfo {
pub fn new(maybe_thumbnail_version: Option<u8>, created_at: DateTime<Utc>) -> Self {
Self { maybe_thumbnail_version, created_at }
}
}
impl MediaLinksBuilder {
pub fn from_media_path_and_env(
domain: MediaDomain,
server_environment: ServerEnvironment,
bucket_path: &MediaFileBucketPath,
thumbnail_info: VideoThumbnailInfo,
) -> MediaLinks {
let rooted_path = bucket_path.get_full_object_path_str();
Self::from_rooted_path_and_env(domain, server_environment, rooted_path)
Self::from_rooted_path_and_env(domain, server_environment, rooted_path, thumbnail_info)
}
pub fn from_rooted_path_and_env(
domain: MediaDomain,
server_environment: ServerEnvironment,
rooted_path: &str,
thumbnail_info: VideoThumbnailInfo,
) -> MediaLinks {
let mut cdn_url = new_cdn_url(domain, server_environment);
cdn_url.set_path(rooted_path);
MediaLinks {
cdn_url,
maybe_thumbnail_template: thumbnail_template(domain, server_environment, rooted_path),
maybe_video_previews: VideoPreviewsBuilder::from_rooted_path(domain, server_environment, rooted_path),
maybe_video_previews: VideoPreviewsBuilder::from_rooted_path(domain, server_environment, rooted_path, thumbnail_info),
}
}
}
@@ -49,30 +69,47 @@ impl VideoPreviewsBuilder {
domain: MediaDomain,
server_environment: ServerEnvironment,
rooted_path: &str,
thumbnail_info: VideoThumbnailInfo,
) -> Option<VideoPreviews> {
if !rooted_path.ends_with(".mp4") {
return None;
}
let animated = PreviewType::Animated(animated_preview_format(
thumbnail_info.maybe_thumbnail_version,
thumbnail_info.created_at.timestamp()));
// Written for every thumbnail version (dual write since version 2).
let catalina_fallback = PreviewType::Animated(AnimatedPreviewFormat::Gif);
Some(VideoPreviews {
still: video_preview(domain, server_environment, rooted_path, PreviewType::Jpg),
animated: video_preview(domain, server_environment, rooted_path, PreviewType::Gif),
still_thumbnail_template: video_preview_thumbnail_template(domain, server_environment, rooted_path, PreviewType::Jpg),
animated_thumbnail_template: video_preview_thumbnail_template(domain, server_environment, rooted_path, PreviewType::Gif),
still: video_preview(domain, server_environment, rooted_path, PreviewType::Still),
animated: video_preview(domain, server_environment, rooted_path, animated),
still_thumbnail_template: video_preview_thumbnail_template(domain, server_environment, rooted_path, PreviewType::Still),
animated_thumbnail_template: video_preview_thumbnail_template(domain, server_environment, rooted_path, animated),
maybe_catalina_fallback_animated_gif: Some(video_preview(domain, server_environment, rooted_path, catalina_fallback)),
maybe_catalina_fallback_animated_gif_thumbnail_template: Some(video_preview_thumbnail_template(domain, server_environment, rooted_path, catalina_fallback)),
})
}
}
#[derive(Copy, Clone)]
enum PreviewType {
Gif,
Jpg
/// First-frame jpg.
Still,
/// Animated gif or webp.
Animated(AnimatedPreviewFormat),
}
/// Returns a jpeg or gif preview of the video.
fn video_preview(media_domain: MediaDomain, server_environment: ServerEnvironment, rooted_path: &str, thumbnail_type: PreviewType) -> Url {
let rooted_path = match thumbnail_type {
PreviewType::Gif => format!("{rooted_path}{VIDEO_ANIMATED_GIF_THUMBNAIL_SUFFIX}"),
PreviewType::Jpg => format!("{rooted_path}{VIDEO_STATIC_JPG_THUMBNAIL_SUFFIX}"),
};
impl PreviewType {
fn suffix(self) -> &'static str {
match self {
PreviewType::Still => VIDEO_STATIC_JPG_THUMBNAIL_SUFFIX,
PreviewType::Animated(format) => format.suffix(),
}
}
}
/// Returns a still or animated preview of the video.
fn video_preview(media_domain: MediaDomain, server_environment: ServerEnvironment, rooted_path: &str, preview_type: PreviewType) -> Url {
let rooted_path = format!("{rooted_path}{}", preview_type.suffix());
let mut url = new_cdn_url(media_domain, server_environment);
url.set_path(&rooted_path);
url
@@ -96,12 +133,15 @@ fn thumbnail_template(media_domain: MediaDomain, server_environment: ServerEnvir
}
/// Returns a thumbnail template for video
fn video_preview_thumbnail_template(media_domain: MediaDomain, server_environment: ServerEnvironment, rooted_path: &str, thumbnail_type: PreviewType) -> String {
///
/// TODO(bt,2026-09-28): Verify Cloudflare's `/cdn-cgi/image` resizing keeps
/// animated webp previews (thumbnail version 2) animated, at a few widths,
/// once real version 2 thumbnails exist in production. Cloudflare's docs
/// didn't confirm animated webp support when this was written. If it doesn't
/// work, serve webp previews without `/cdn-cgi/image` (they're already small).
fn video_preview_thumbnail_template(media_domain: MediaDomain, server_environment: ServerEnvironment, rooted_path: &str, preview_type: PreviewType) -> String {
let host = get_cdn_host(media_domain, server_environment);
let rooted_path = match thumbnail_type {
PreviewType::Gif => format!("{rooted_path}{VIDEO_ANIMATED_GIF_THUMBNAIL_SUFFIX}"),
PreviewType::Jpg => format!("{rooted_path}{VIDEO_STATIC_JPG_THUMBNAIL_SUFFIX}"),
};
let rooted_path = format!("{rooted_path}{}", preview_type.suffix());
// NB: Development doesn't support cdn-cgi image resizing.
match server_environment {
ServerEnvironment::Development => format!("{host}{rooted_path}"),
@@ -113,8 +153,9 @@ fn video_preview_thumbnail_template(media_domain: MediaDomain, server_environmen
#[cfg(test)]
mod tests {
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::common_responses::media::media_links_builder::{MediaLinksBuilder, VideoThumbnailInfo};
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use chrono::{DateTime, TimeZone, Utc};
use server_environment::ServerEnvironment;
const DEV_CDN: &str = "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev";
@@ -132,7 +173,7 @@ mod tests {
#[test]
fn wav_file() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.wav");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.wav", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/foo/bar.wav"));
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
@@ -140,7 +181,7 @@ mod tests {
#[test]
fn glb_file() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.glb");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.glb", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/foo/bar.glb"));
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
@@ -148,7 +189,7 @@ mod tests {
#[test]
fn jpg_image() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.jpg");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.jpg", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/foo/bar.jpg"));
assert_eq!(links.maybe_thumbnail_template, Some(format!("{PROD_CDN}/cdn-cgi/image/width={{WIDTH}},quality=95/foo/bar.jpg")));
assert_eq!(links.maybe_video_previews, None);
@@ -156,7 +197,7 @@ mod tests {
#[test]
fn mp4_video() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.mp4");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.mp4", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/foo/bar.mp4"));
assert_eq!(links.maybe_thumbnail_template, None);
let video_previews = links.maybe_video_previews.expect("should have previews");
@@ -169,7 +210,7 @@ mod tests {
#[test]
fn mp4_video_media_path() {
let media_path = MediaFileBucketPath::from_object_hash("t6cnyw4g3e8k7carkk2bvrt6nd3fycjv", Some("storyteller_"), Some(".mp4"));
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path);
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path, version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4"));
let video_previews = links.maybe_video_previews.expect("should have previews");
assert_eq!(video_previews.still.as_str(), format!("{PROD_CDN}/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg"));
@@ -181,7 +222,7 @@ mod tests {
#[test]
fn wav_media_path() {
let media_path = MediaFileBucketPath::from_object_hash("94a27nmbd0bqmd10tg0pp3hz45zytf67", Some("fakeyou_"), Some(".wav"));
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path);
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path, version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav"));
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
@@ -190,7 +231,7 @@ mod tests {
#[test]
fn png_media_path() {
let media_path = MediaFileBucketPath::from_object_hash("37mb3gh8fmj85y21thvbv08bzv24atjt", Some("upload_"), Some(".png"));
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path);
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path, version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png"));
assert_eq!(links.maybe_thumbnail_template, Some(format!("{PROD_CDN}/cdn-cgi/image/width={{WIDTH}},quality=95/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png")));
assert_eq!(links.maybe_video_previews, None);
@@ -204,7 +245,7 @@ mod tests {
#[test]
fn wav_file() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.wav");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.wav", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{DEV_CDN}/foo/bar.wav"));
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
@@ -212,7 +253,7 @@ mod tests {
#[test]
fn jpg_image() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.jpg");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.jpg", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{DEV_CDN}/foo/bar.jpg"));
// NB: Development doesn't support cdn-cgi, so serve the full image.
assert_eq!(links.maybe_thumbnail_template, Some(format!("{DEV_CDN}/foo/bar.jpg")));
@@ -221,7 +262,7 @@ mod tests {
#[test]
fn mp4_video() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.mp4");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.mp4", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{DEV_CDN}/foo/bar.mp4"));
assert_eq!(links.maybe_thumbnail_template, None);
let video_previews = links.maybe_video_previews.expect("should have previews");
@@ -235,7 +276,7 @@ mod tests {
#[test]
fn mp4_video_media_path() {
let media_path = MediaFileBucketPath::from_object_hash("t6cnyw4g3e8k7carkk2bvrt6nd3fycjv", Some("storyteller_"), Some(".mp4"));
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path);
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path, version_1());
assert_eq!(links.cdn_url.as_str(), format!("{DEV_CDN}/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4"));
let video_previews = links.maybe_video_previews.expect("should have previews");
assert_eq!(video_previews.still.as_str(), format!("{DEV_CDN}/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg"));
@@ -258,7 +299,7 @@ mod tests {
#[test]
fn wav_file() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.wav");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.wav", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/foo/bar.wav"));
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
@@ -266,7 +307,7 @@ mod tests {
#[test]
fn glb_file() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.glb");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.glb", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/foo/bar.glb"));
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
@@ -274,7 +315,7 @@ mod tests {
#[test]
fn jpg_image() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.jpg");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.jpg", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/foo/bar.jpg"));
assert_eq!(links.maybe_thumbnail_template, Some(format!("{PROD_CDN}/cdn-cgi/image/width={{WIDTH}},quality=95/foo/bar.jpg")));
assert_eq!(links.maybe_video_previews, None);
@@ -282,7 +323,7 @@ mod tests {
#[test]
fn mp4_video() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.mp4");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.mp4", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/foo/bar.mp4"));
assert_eq!(links.maybe_thumbnail_template, None);
let video_previews = links.maybe_video_previews.expect("should have previews");
@@ -295,7 +336,7 @@ mod tests {
#[test]
fn mp4_video_media_path() {
let media_path = MediaFileBucketPath::from_object_hash("t6cnyw4g3e8k7carkk2bvrt6nd3fycjv", Some("storyteller_"), Some(".mp4"));
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path);
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path, version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4"));
let video_previews = links.maybe_video_previews.expect("should have previews");
assert_eq!(video_previews.still.as_str(), format!("{PROD_CDN}/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg"));
@@ -307,7 +348,7 @@ mod tests {
#[test]
fn wav_media_path() {
let media_path = MediaFileBucketPath::from_object_hash("94a27nmbd0bqmd10tg0pp3hz45zytf67", Some("fakeyou_"), Some(".wav"));
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path);
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path, version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/media/9/4/a/2/7/94a27nmbd0bqmd10tg0pp3hz45zytf67/fakeyou_94a27nmbd0bqmd10tg0pp3hz45zytf67.wav"));
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
@@ -316,7 +357,7 @@ mod tests {
#[test]
fn png_media_path() {
let media_path = MediaFileBucketPath::from_object_hash("37mb3gh8fmj85y21thvbv08bzv24atjt", Some("upload_"), Some(".png"));
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path);
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path, version_1());
assert_eq!(links.cdn_url.as_str(), format!("{PROD_CDN}/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png"));
assert_eq!(links.maybe_thumbnail_template, Some(format!("{PROD_CDN}/cdn-cgi/image/width={{WIDTH}},quality=95/media/3/7/m/b/3/37mb3gh8fmj85y21thvbv08bzv24atjt/upload_37mb3gh8fmj85y21thvbv08bzv24atjt.png")));
assert_eq!(links.maybe_video_previews, None);
@@ -330,7 +371,7 @@ mod tests {
#[test]
fn wav_file() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.wav");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.wav", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{DEV_CDN}/foo/bar.wav"));
assert_eq!(links.maybe_thumbnail_template, None);
assert_eq!(links.maybe_video_previews, None);
@@ -338,7 +379,7 @@ mod tests {
#[test]
fn jpg_image() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.jpg");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.jpg", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{DEV_CDN}/foo/bar.jpg"));
assert_eq!(links.maybe_thumbnail_template, Some(format!("{DEV_CDN}/foo/bar.jpg")));
assert_eq!(links.maybe_video_previews, None);
@@ -346,7 +387,7 @@ mod tests {
#[test]
fn mp4_video() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.mp4");
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, "/foo/bar.mp4", version_1());
assert_eq!(links.cdn_url.as_str(), format!("{DEV_CDN}/foo/bar.mp4"));
assert_eq!(links.maybe_thumbnail_template, None);
let video_previews = links.maybe_video_previews.expect("should have previews");
@@ -359,7 +400,7 @@ mod tests {
#[test]
fn mp4_video_media_path() {
let media_path = MediaFileBucketPath::from_object_hash("t6cnyw4g3e8k7carkk2bvrt6nd3fycjv", Some("storyteller_"), Some(".mp4"));
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path);
let links = MediaLinksBuilder::from_media_path_and_env(DOMAIN, ENV, &media_path, version_1());
assert_eq!(links.cdn_url.as_str(), format!("{DEV_CDN}/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4"));
let video_previews = links.maybe_video_previews.expect("should have previews");
assert_eq!(video_previews.still.as_str(), format!("{DEV_CDN}/media/t/6/c/n/y/t6cnyw4g3e8k7carkk2bvrt6nd3fycjv/storyteller_t6cnyw4g3e8k7carkk2bvrt6nd3fycjv.mp4-thumb.jpg"));
@@ -369,4 +410,108 @@ mod tests {
}
}
}
/// Picking the animated preview by thumbnail version. See
/// `video_thumbnail_suffixes` in `bucket_paths` for the version history.
mod thumbnail_versions {
use super::*;
const DOMAIN : MediaDomain = MediaDomain::FakeYou;
const ENV : ServerEnvironment = ServerEnvironment::Production;
const VIDEO : &str = "/foo/bar.mp4";
#[test]
fn version_1_serves_gif() {
assert_animated_suffix(info(Some(1), after_cutover()), "-thumb.gif");
}
#[test]
fn version_2_serves_webp() {
assert_animated_suffix(info(Some(2), before_cutover()), "-thumb.webp");
}
#[test]
fn null_before_cutover_serves_gif() {
assert_animated_suffix(info(None, before_cutover()), "-thumb.gif");
}
#[test]
fn null_at_cutover_serves_webp() {
assert_animated_suffix(info(None, cutover()), "-thumb.webp");
}
#[test]
fn still_is_jpg_for_every_version() {
for thumbnail_info in [info(None, before_cutover()), info(None, cutover()), info(Some(1), cutover()), info(Some(2), cutover())] {
let previews = previews(thumbnail_info);
assert_eq!(previews.still.as_str(), format!("{PROD_CDN}/foo/bar.mp4-thumb.jpg"));
assert_eq!(previews.still_thumbnail_template, format!("{PROD_CDN}/cdn-cgi/image/width={{WIDTH}},quality=95/foo/bar.mp4-thumb.jpg"));
}
}
#[test]
fn catalina_fallback_is_always_the_gif() {
for thumbnail_info in [info(None, before_cutover()), info(None, cutover()), info(Some(1), cutover()), info(Some(2), cutover())] {
let previews = previews(thumbnail_info);
assert_eq!(
previews.maybe_catalina_fallback_animated_gif.as_ref().map(|url| url.as_str()),
Some(format!("{PROD_CDN}/foo/bar.mp4-thumb.gif").as_str()));
assert_eq!(
previews.maybe_catalina_fallback_animated_gif_thumbnail_template,
Some(format!("{PROD_CDN}/cdn-cgi/image/width={{WIDTH}},quality=95/foo/bar.mp4-thumb.gif")));
}
}
#[test]
fn development_webp_is_served_directly() {
let links = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ServerEnvironment::Development, VIDEO, info(Some(2), cutover()));
let previews = links.maybe_video_previews.unwrap();
assert_eq!(previews.animated.as_str(), format!("{DEV_CDN}/foo/bar.mp4-thumb.webp"));
assert_eq!(previews.animated_thumbnail_template, format!("{DEV_CDN}/foo/bar.mp4-thumb.webp"));
assert_eq!(previews.maybe_catalina_fallback_animated_gif_thumbnail_template, Some(format!("{DEV_CDN}/foo/bar.mp4-thumb.gif")));
}
#[test]
fn non_videos_ignore_the_version() {
for path in ["/foo/bar.png", "/foo/bar.gif", "/foo/bar.wav"] {
let version_1 = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, path, info(Some(1), cutover()));
let version_2 = MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, path, info(Some(2), cutover()));
assert_eq!(version_1, version_2);
assert_eq!(version_2.maybe_video_previews, None);
}
}
fn assert_animated_suffix(thumbnail_info: VideoThumbnailInfo, suffix: &str) {
let previews = previews(thumbnail_info);
assert_eq!(previews.animated.as_str(), format!("{PROD_CDN}/foo/bar.mp4{suffix}"));
assert_eq!(previews.animated_thumbnail_template, format!("{PROD_CDN}/cdn-cgi/image/width={{WIDTH}},quality=95/foo/bar.mp4{suffix}"));
}
fn previews(thumbnail_info: VideoThumbnailInfo) -> artcraft_api_defs::common::responses::media_links::VideoPreviews {
MediaLinksBuilder::from_rooted_path_and_env(DOMAIN, ENV, VIDEO, thumbnail_info)
.maybe_video_previews
.expect("mp4 should have previews")
}
fn cutover() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 10, 10, 0, 0, 0).unwrap()
}
fn before_cutover() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 10, 9, 23, 59, 59).unwrap()
}
fn after_cutover() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2027, 1, 1, 0, 0, 0).unwrap()
}
}
/// Version 1 (gif) thumbnail info, for the path and CDN tests above.
fn version_1() -> VideoThumbnailInfo {
info(Some(1), Utc.with_ymd_and_hms(2026, 4, 1, 0, 0, 0).unwrap())
}
fn info(maybe_thumbnail_version: Option<u8>, created_at: DateTime<Utc>) -> VideoThumbnailInfo {
VideoThumbnailInfo { maybe_thumbnail_version, created_at }
}
}
@@ -16,6 +16,7 @@ use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
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::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::state::server_state::ServerState;
@@ -99,5 +100,5 @@ fn resolve_media_links(
record.maybe_public_bucket_prefix.as_deref(),
record.maybe_public_bucket_extension.as_deref(),
);
Some(MediaLinksBuilder::from_media_path_and_env(media_domain, server_environment, &path))
Some(MediaLinksBuilder::from_media_path_and_env(media_domain, server_environment, &path, VideoThumbnailInfo::new(record.maybe_thumbnail_version, record.created_at)))
}
@@ -15,6 +15,7 @@ use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
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::common_responses::media::media_links_builder::VideoThumbnailInfo;
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;
@@ -121,5 +122,5 @@ fn resolve_media_links(
record.maybe_public_bucket_prefix.as_deref(),
record.maybe_public_bucket_extension.as_deref(),
);
Some(MediaLinksBuilder::from_media_path_and_env(media_domain, server_environment, &path))
Some(MediaLinksBuilder::from_media_path_and_env(media_domain, server_environment, &path, VideoThumbnailInfo::new(record.maybe_thumbnail_version, record.created_at)))
}
@@ -15,6 +15,7 @@ use tokens::tokens::media_files::MediaFileToken;
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::common_responses::media::media_links_builder::VideoThumbnailInfo;
/// Gather every media-file token referenced by the given folder rows
/// (the four `last_media_*` slots + the custom cover) and batch-fetch
@@ -129,6 +130,7 @@ fn build_folder_thumbnail(
media_domain,
server_environment,
&bucket_path,
VideoThumbnailInfo::new(row.maybe_thumbnail_version, row.created_at),
);
// Only video media files have previews; `MediaLinksBuilder` returns
@@ -139,6 +141,8 @@ fn build_folder_thumbnail(
animated: previews.animated,
still_thumbnail_template: previews.still_thumbnail_template,
animated_thumbnail_template: previews.animated_thumbnail_template,
maybe_catalina_fallback_animated_gif: previews.maybe_catalina_fallback_animated_gif,
maybe_catalina_fallback_animated_gif_thumbnail_template: previews.maybe_catalina_fallback_animated_gif_thumbnail_template,
});
FolderThumbnail {
@@ -155,6 +159,7 @@ fn build_folder_thumbnail(
mod tests {
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_type::MediaFileType;
use chrono::{TimeZone, Utc};
use super::*;
@@ -163,7 +168,7 @@ mod tests {
#[test]
fn mp4_video_gets_video_previews() {
let thumbnail = build_thumbnail_for_extension(MediaFileClass::Video, MediaFileType::Mp4, ".mp4");
let thumbnail = build_thumbnail_for_extension(MediaFileClass::Video, MediaFileType::Mp4, ".mp4", Some(1));
assert_eq!(thumbnail.maybe_thumbnail_template, None);
@@ -182,9 +187,25 @@ mod tests {
format!("{PROD_CDN}/cdn-cgi/image/width={{WIDTH}},quality=95/media/t/6/c/n/y/{OBJECT_HASH}/storyteller_{OBJECT_HASH}.mp4-thumb.gif"));
}
#[test]
fn version_2_mp4_gets_webp_preview_and_catalina_gif_fallback() {
let thumbnail = build_thumbnail_for_extension(MediaFileClass::Video, MediaFileType::Mp4, ".mp4", Some(2));
let previews = thumbnail.maybe_video_previews.expect("video should have previews");
assert_eq!(
previews.animated.as_str(),
format!("{PROD_CDN}/media/t/6/c/n/y/{OBJECT_HASH}/storyteller_{OBJECT_HASH}.mp4-thumb.webp"));
assert_eq!(
previews.maybe_catalina_fallback_animated_gif.as_ref().map(|url| url.as_str()),
Some(format!("{PROD_CDN}/media/t/6/c/n/y/{OBJECT_HASH}/storyteller_{OBJECT_HASH}.mp4-thumb.gif").as_str()));
assert_eq!(
previews.maybe_catalina_fallback_animated_gif_thumbnail_template,
Some(format!("{PROD_CDN}/cdn-cgi/image/width={{WIDTH}},quality=95/media/t/6/c/n/y/{OBJECT_HASH}/storyteller_{OBJECT_HASH}.mp4-thumb.gif")));
}
#[test]
fn png_image_gets_thumbnail_template_but_no_video_previews() {
let thumbnail = build_thumbnail_for_extension(MediaFileClass::Image, MediaFileType::Png, ".png");
let thumbnail = build_thumbnail_for_extension(MediaFileClass::Image, MediaFileType::Png, ".png", None);
assert_eq!(
thumbnail.maybe_thumbnail_template,
@@ -196,6 +217,7 @@ mod tests {
media_class: MediaFileClass,
media_type: MediaFileType,
extension: &str,
maybe_thumbnail_version: Option<u8>,
) -> FolderThumbnail {
let row = MediaFileThumbnailRow {
token: MediaFileToken::new_from_str("m_test"),
@@ -204,6 +226,8 @@ mod tests {
public_bucket_directory_hash: OBJECT_HASH.to_string(),
maybe_public_bucket_prefix: Some("storyteller_".to_string()),
maybe_public_bucket_extension: Some(extension.to_string()),
maybe_thumbnail_version,
created_at: Utc.with_ymd_and_hms(2026, 4, 1, 0, 0, 0).unwrap(),
};
build_folder_thumbnail(row, MediaDomain::FakeYou, ServerEnvironment::Production)
@@ -31,6 +31,7 @@ use crate::http_server::common_responses::common_web_error::CommonWebError;
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;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::user_lookup::user_session::require_user_session::require_user_session;
use crate::state::server_state::ServerState;
@@ -203,6 +204,7 @@ fn folder_media_file_row_to_list_item(
media_domain,
server_environment,
&bucket_path,
VideoThumbnailInfo::new(row.maybe_thumbnail_version, row.created_at),
);
let cover_image = MediaFileCoverImageDetails::from_optional_db_fields(
@@ -50,6 +50,7 @@ use tokens::tokens::media_files::MediaFileToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::util::http_download_url_to_tempfile::http_download_url_to_tempfile;
use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::apply_media_cdn_override;
@@ -143,7 +144,8 @@ pub async fn fetch_reference_video_sources(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_environment,
&bucket_path);
&bucket_path,
VideoThumbnailInfo::new(file.maybe_thumbnail_version, file.created_at));
let cdn_url = apply_media_cdn_override(
&media_links.cdn_url,
@@ -2,6 +2,7 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
@@ -120,7 +121,8 @@ pub async fn remove_image_background_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
&bucket_path,
VideoThumbnailInfo::new(media_file.maybe_thumbnail_version, media_file.created_at));
info!("Fal webhook URL: {}", server_state.inference_providers.fal.webhook_url);
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::billing::wallets::temporary_test_wallet_deduction::temporary_test_wallet_deduction;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -131,7 +132,8 @@ pub async fn flux_pro_kontext_max_edit_image_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path);
&public_bucket_path,
VideoThumbnailInfo::new(image_media_file.maybe_thumbnail_version, image_media_file.created_at));
media_links.cdn_url.to_string()
};
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -127,7 +128,8 @@ pub async fn gemini_25_flash_edit_image_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path);
&public_bucket_path,
VideoThumbnailInfo::new(file.maybe_thumbnail_version, file.created_at));
media_links.cdn_url.to_string()
})
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -129,7 +130,8 @@ pub async fn gpt_image_1_edit_image_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path);
&public_bucket_path,
VideoThumbnailInfo::new(file.maybe_thumbnail_version, file.created_at));
media_links.cdn_url.to_string()
})
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -124,7 +125,8 @@ pub async fn qwen_edit_image_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path);
&public_bucket_path,
VideoThumbnailInfo::new(image_media_file.maybe_thumbnail_version, image_media_file.created_at));
media_links.cdn_url.to_string()
};
@@ -2,6 +2,7 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -123,7 +124,8 @@ pub async fn seededit_3_edit_image_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path);
&public_bucket_path,
VideoThumbnailInfo::new(image_media_file.maybe_thumbnail_version, image_media_file.created_at));
media_links.cdn_url.to_string()
};
@@ -2,6 +2,7 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -135,7 +136,8 @@ pub async fn flux_dev_juggernaut_inpaint_image_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path);
&public_bucket_path,
VideoThumbnailInfo::new(image_media_file.maybe_thumbnail_version, image_media_file.created_at));
media_links.cdn_url.to_string()
};
@@ -149,7 +151,8 @@ pub async fn flux_dev_juggernaut_inpaint_image_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path);
&public_bucket_path,
VideoThumbnailInfo::new(mask_media_file.maybe_thumbnail_version, mask_media_file.created_at));
media_links.cdn_url.to_string()
};
@@ -2,6 +2,7 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -135,7 +136,8 @@ pub async fn flux_pro_1_inpaint_image_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path);
&public_bucket_path,
VideoThumbnailInfo::new(image_media_file.maybe_thumbnail_version, image_media_file.created_at));
media_links.cdn_url.to_string()
};
@@ -149,7 +151,8 @@ pub async fn flux_pro_1_inpaint_image_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path);
&public_bucket_path,
VideoThumbnailInfo::new(mask_media_file.maybe_thumbnail_version, mask_media_file.created_at));
media_links.cdn_url.to_string()
};
@@ -2,6 +2,7 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
@@ -128,7 +129,8 @@ pub async fn generate_hunyuan_2_0_image_to_3d_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
&bucket_path,
VideoThumbnailInfo::new(media_file.maybe_thumbnail_version, media_file.created_at));
info!("Fal webhook URL: {}", server_state.inference_providers.fal.webhook_url);
@@ -2,6 +2,7 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
use crate::http_server::web_utils::get_request_platform_type::get_request_platform_type;
@@ -127,7 +128,8 @@ pub async fn generate_hunyuan_2_1_image_to_3d_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
&bucket_path,
VideoThumbnailInfo::new(media_file.maybe_thumbnail_version, media_file.created_at));
info!("Fal webhook URL: {}", server_state.inference_providers.fal.webhook_url);
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format;
@@ -130,7 +131,8 @@ pub async fn generate_kling_2_1_master_video_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
&bucket_path,
VideoThumbnailInfo::new(media_file.maybe_thumbnail_version, media_file.created_at));
info!("Fal webhook URL: {}", server_state.inference_providers.fal.webhook_url);
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::analytics::log_browser_session_handler::LogBrowserSessionError;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
@@ -137,7 +138,8 @@ pub async fn generate_veo_2_image_to_video_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
&bucket_path,
VideoThumbnailInfo::new(media_file.maybe_thumbnail_version, media_file.created_at));
info!("Fal webhook URL: {}", server_state.inference_providers.fal.webhook_url);
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::analytics::log_browser_session_handler::LogBrowserSessionError;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
@@ -129,7 +130,8 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
&bucket_path,
VideoThumbnailInfo::new(media_file.maybe_thumbnail_version, media_file.created_at));
info!("Fal webhook URL: {}", server_state.inference_providers.fal.webhook_url);
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::analytics::log_browser_session_handler::LogBrowserSessionError;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
@@ -132,7 +133,8 @@ pub async fn generate_veo_3_image_to_video_handler(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
&bucket_path,
VideoThumbnailInfo::new(media_file.maybe_thumbnail_version, media_file.created_at));
info!("Fal webhook URL: {}", server_state.inference_providers.fal.webhook_url);
@@ -4,6 +4,7 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
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::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
use crate::http_server::endpoints::inference_job::utils::extractors::extract_lipsync_details::extract_lipsync_details;
use crate::http_server::endpoints::inference_job::utils::extractors::extract_live_portrait_details::extract_live_portrait_details;
@@ -219,6 +220,8 @@ fn db_record_to_response_payload(
server_environment: ServerEnvironment,
media_domain: MediaDomain,
) -> BatchInferenceJobStatusResponsePayload {
// NB: Copied out so the result closure below doesn't borrow `record` (edition 2018).
let job_created_at = record.created_at;
let inference_category = record.request_details.inference_category;
// NB: Fail open. We don't want to fail the request if we can't extract the args.
@@ -326,7 +329,12 @@ fn db_record_to_response_payload(
media_links: MediaLinksBuilder::from_rooted_path_and_env(
media_domain,
server_environment,
&public_bucket_media_path
&public_bucket_media_path,
VideoThumbnailInfo::new(
result_details.maybe_media_file_thumbnail_version,
// NB: Non-media-file results (legacy tts/vc) have no media file row; they're
// never mp4s, so the job's creation time is only a placeholder for them.
result_details.maybe_media_file_created_at.unwrap_or(job_created_at))
),
maybe_public_bucket_media_path: Some(public_bucket_media_path),
maybe_successfully_completed_at: result_details.maybe_successfully_completed_at,
@@ -3,6 +3,7 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
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::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
use crate::http_server::endpoints::tts::enqueue_infer_tts_handler::enqueue_infer_tts_handler::SYNTHETIC_INFERENCE_JOB_TOKEN;
use crate::http_server::endpoints::inference_job::utils::extractors::extract_lipsync_details::extract_lipsync_details;
@@ -242,6 +243,8 @@ fn record_to_payload(
server_environment: ServerEnvironment,
media_domain: MediaDomain,
) -> InferenceJobStatusResponsePayload {
// NB: Copied out so the result closure below doesn't borrow `record` (edition 2018).
let job_created_at = record.created_at;
let inference_category = record.request_details.inference_category;
// NB: Fail open. We don't want to fail the request if we can't extract the args.
@@ -349,7 +352,12 @@ fn record_to_payload(
media_links: MediaLinksBuilder::from_rooted_path_and_env(
media_domain,
server_environment,
&public_bucket_media_path
&public_bucket_media_path,
VideoThumbnailInfo::new(
result_details.maybe_media_file_thumbnail_version,
// NB: Non-media-file results (legacy tts/vc) have no media file row; they're
// never mp4s, so the job's creation time is only a placeholder for them.
result_details.maybe_media_file_created_at.unwrap_or(job_created_at))
),
maybe_public_bucket_media_path: Some(public_bucket_media_path),
maybe_successfully_completed_at: result_details.maybe_successfully_completed_at,
@@ -4,6 +4,7 @@ use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
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::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::inference_job::utils::estimates::estimate_job_progress::estimate_job_progress;
use crate::http_server::endpoints::inference_job::utils::extractors::extract_lipsync_details::extract_lipsync_details;
use crate::http_server::endpoints::inference_job::utils::extractors::extract_live_portrait_details::extract_live_portrait_details;
@@ -165,6 +166,8 @@ fn db_record_to_response_payload(
server_environment: ServerEnvironment,
media_domain: MediaDomain,
) -> ListSessionJobsItem {
// NB: Copied out so the result closure below doesn't borrow `record` (edition 2018).
let job_created_at = record.created_at;
let inference_category = record.request_details.inference_category;
// NB: Fail open. We don't want to fail the request if we can't extract the args.
@@ -292,7 +295,12 @@ fn db_record_to_response_payload(
media_links: MediaLinksBuilder::from_rooted_path_and_env(
media_domain,
server_environment,
&public_bucket_media_path
&public_bucket_media_path,
VideoThumbnailInfo::new(
result_details.maybe_media_file_thumbnail_version,
// NB: Non-media-file results (legacy tts/vc) have no media file row; they're
// never mp4s, so the job's creation time is only a placeholder for them.
result_details.maybe_media_file_created_at.unwrap_or(job_created_at))
),
maybe_public_bucket_media_path: Some(public_bucket_media_path),
maybe_successfully_completed_at: result_details.maybe_successfully_completed_at,
@@ -25,6 +25,7 @@ use mysql_queries::queries::prompts::get_prompt::get_prompt_from_connection;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::common_responses::media::media_links_builder::MediaLinksBuilder;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::web_utils::require_internal_api_key::require_internal_api_key;
use crate::state::server_state::ServerState;
@@ -170,7 +171,8 @@ async fn build_prompt_details(
let media_links = MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&bucket_path);
&bucket_path,
VideoThumbnailInfo::new(item.maybe_thumbnail_version, item.created_at));
MinimaxJobMediaReference {
media_file_token: item.media_token,
@@ -26,6 +26,7 @@ use utoipa::{IntoParams, ToSchema};
use crate::http_server::common_responses::common_web_error::CommonWebError;
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::media_links_builder::VideoThumbnailInfo;
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
use crate::http_server::endpoints::media_files::common_responses::live_portrait::MediaFileLivePortraitDetails;
@@ -284,7 +285,8 @@ pub async fn batch_get_media_files_handler(
media_links: MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path
&public_bucket_path,
VideoThumbnailInfo::new(result.maybe_thumbnail_version, result.created_at)
),
public_bucket_path: public_bucket_path
.get_full_object_path_str()
@@ -4,6 +4,7 @@ use crate::http_server::common_responses::common_web_error::CommonWebError;
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;
use crate::http_server::common_responses::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::common_responses::simple_entity_stats::SimpleEntityStats;
use crate::http_server::common_responses::user_details_lite::UserDetailsLight;
use crate::http_server::endpoints::media_files::common_responses::live_portrait::MediaFileLivePortraitDetails;
@@ -314,7 +315,11 @@ async fn modern_media_file_lookup(
maybe_engine_extension,
maybe_batch_token: result.maybe_batch_token,
maybe_scene_source_media_file_token: result.maybe_scene_source_media_file_token,
media_links: MediaLinksBuilder::from_media_path_and_env(media_domain, server_state.server_environment, &public_bucket_path),
media_links: MediaLinksBuilder::from_media_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path,
VideoThumbnailInfo::new(result.maybe_thumbnail_version, result.created_at)),
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);
@@ -451,7 +456,13 @@ async fn emulate_media_file_with_legacy_tts_result_lookup(
maybe_engine_extension: None,
maybe_batch_token: None,
maybe_scene_source_media_file_token: None,
media_links: MediaLinksBuilder::from_rooted_path_and_env(media_domain, server_state.server_environment, &public_bucket_path),
// NB: Legacy TTS results are wav files (never videos) in a table without a
// thumbnail version, so there are no video previews to version.
media_links: MediaLinksBuilder::from_rooted_path_and_env(
media_domain,
server_state.server_environment,
&public_bucket_path,
VideoThumbnailInfo::new(None, result.created_at)),
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);
@@ -18,6 +18,7 @@ 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::common_responses::media::media_links_builder::VideoThumbnailInfo;
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;
@@ -93,6 +94,7 @@ pub async fn list_media_files_by_job_handler(
media_domain,
server_state.server_environment,
&public_bucket_path,
VideoThumbnailInfo::new(record.maybe_thumbnail_version, record.created_at),
),
cover_image: MediaFileCoverImageDetailsBuilder::from_optional_db_fields(
&record.token,
@@ -16,6 +16,7 @@ use mysql_queries::queries::media_files::list::list_session_media_files_by_class
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::media::media_links_builder::VideoThumbnailInfo;
use crate::http_server::common_responses::user_details_lite_builder::UserDetailsLightBuilder;
use crate::http_server::endpoints::media_files::helpers::get_media_domain::get_media_domain;
use crate::http_server::user_lookup::user_session::require_user_session::require_user_session;
@@ -102,6 +103,7 @@ pub async fn list_session_media_files_of_class(
media_domain,
server_state.server_environment,
&public_bucket_path,
VideoThumbnailInfo::new(record.maybe_thumbnail_version, record.created_at),
),
cover_image: MediaFileCoverImageDetailsBuilder::from_optional_db_fields(
&record.token,
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff. Show more