Compare commits
39
Commits
bt-domain-config
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3048db996d | ||
|
|
bcff742052 | ||
|
|
854869ba53 | ||
|
|
25f38b79b8 | ||
|
|
2893b6f713 | ||
|
|
ad1ca86b00 | ||
|
|
165a4b272c | ||
|
|
b45df084b8 | ||
|
|
28bd2e7ee3 | ||
|
|
02a3de2509 | ||
|
|
4ae6fe7966 | ||
|
|
f37f82b400 | ||
|
|
42b1872760 | ||
|
|
a93ebd6f84 | ||
|
|
b57f9155da | ||
|
|
c762161646 | ||
|
|
c489e8c650 | ||
|
|
17373314ae | ||
|
|
931a8b925c | ||
|
|
0e1991de6d | ||
|
|
d950e57352 | ||
|
|
652146ad96 | ||
|
|
dafe769cda | ||
|
|
ad9b10ba94 | ||
|
|
3880127f68 | ||
|
|
03dea1c660 | ||
|
|
b5272188f7 | ||
|
|
b02f710c76 | ||
|
|
708de238c5 | ||
|
|
fc78787f30 | ||
|
|
e92253c5b4 | ||
|
|
847edc81e5 | ||
|
|
2e596cc4b3 | ||
|
|
c3d72fb321 | ||
|
|
cd27599c0a | ||
|
|
46fd907182 | ||
|
|
5bdc19f573 | ||
|
|
6b34f23b43 | ||
|
|
ddf151794e |
No files matched your search
@@ -43,7 +43,7 @@ jobs:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
# Free up disk on the runner before BuildKit fills it up. The cargo
|
||||
# release build inside the Dockerfile produces multi-GB target dirs
|
||||
@@ -60,10 +60,10 @@ jobs:
|
||||
|
||||
# BuildKit is required for the `cache-from`/`cache-to` options on
|
||||
# docker/build-push-action.
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
name: Set up Docker Buildx
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
name: Login to GitHub Container Registry
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
|
||||
- id: meta
|
||||
name: Compute Docker tags
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
with:
|
||||
images: ghcr.io/storytold/artcraft-services
|
||||
tags: |
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
type=raw,value=${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
with:
|
||||
context: .
|
||||
file: ./build/service_cpu.Dockerfile
|
||||
|
||||
+24
-13
@@ -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"
|
||||
}
|
||||
+14
-3
@@ -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"
|
||||
}
|
||||
+25
-3
@@ -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"
|
||||
}
|
||||
+26
-15
@@ -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"
|
||||
}
|
||||
+31
-9
@@ -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"
|
||||
}
|
||||
+23
-12
@@ -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"
|
||||
}
|
||||
+23
-12
@@ -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"
|
||||
}
|
||||
+19
-8
@@ -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"
|
||||
}
|
||||
+18
-7
@@ -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"
|
||||
}
|
||||
+25
-14
@@ -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"
|
||||
}
|
||||
+26
-15
@@ -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"
|
||||
}
|
||||
+23
-12
@@ -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"
|
||||
}
|
||||
+25
-14
@@ -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"
|
||||
}
|
||||
+24
-13
@@ -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"
|
||||
}
|
||||
+23
-12
@@ -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"
|
||||
}
|
||||
+25
-14
@@ -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"
|
||||
}
|
||||
+18
-7
@@ -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"
|
||||
}
|
||||
+14
-3
@@ -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"
|
||||
}
|
||||
+25
-14
@@ -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"
|
||||
}
|
||||
+25
-14
@@ -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"
|
||||
}
|
||||
@@ -34,7 +34,6 @@ artcraft-services/
|
||||
├── frontend/
|
||||
│ ├── apps/artcraft-webapp/ # Browser application at app.getartcraft.com
|
||||
│ ├── apps/artcraft-website-next/ # Current public website at getartcraft.com (Next.js)
|
||||
│ ├── apps/artcraft-website/ # Legacy product website (Vite)
|
||||
│ ├── apps/artcraft-desktop-website/ # Static landing page at desktop.getartcraft.com (Vite)
|
||||
│ └── libs/ # Shared React components and TypeScript libraries
|
||||
└── Cargo.toml # Rust workspace
|
||||
@@ -42,9 +41,9 @@ artcraft-services/
|
||||
|
||||
## Updating Desktop Download Links
|
||||
|
||||
Desktop release links are configured independently in **three places**. When
|
||||
Desktop release links are configured independently in **two places**. When
|
||||
updating the advertised ArtCraft release, update both `WINDOWS_VERSION` and
|
||||
`MAC_VERSION` in all three unless the request explicitly targets one platform or
|
||||
`MAC_VERSION` in both unless the request explicitly targets one platform or
|
||||
app:
|
||||
|
||||
1. **Current public website (Next.js):**
|
||||
@@ -52,11 +51,7 @@ app:
|
||||
This controls the installer links and visible version at
|
||||
`https://getartcraft.com/download`. Add the release to `DOWNLOAD_HISTORY`
|
||||
(newest first), then update both version selectors.
|
||||
2. **Legacy website (Vite):**
|
||||
[frontend/apps/artcraft-website/src/config/github_download_links.ts](frontend/apps/artcraft-website/src/config/github_download_links.ts).
|
||||
Add the release to `DOWNLOAD_HISTORY` and update both version selectors.
|
||||
Updating only this file does **not** update the current public website.
|
||||
3. **Browser webapp:**
|
||||
2. **Browser webapp:**
|
||||
[frontend/apps/artcraft-webapp/src/config/github_download_links.ts](frontend/apps/artcraft-webapp/src/config/github_download_links.ts).
|
||||
Update both version constants; these links are used by the welcome and
|
||||
checkout-success pages at `app.getartcraft.com`.
|
||||
@@ -65,7 +60,7 @@ Keep previous releases for rollback. The Next.js download page reads its visible
|
||||
version from `DOWNLOAD_VERSIONS`, which uses the same selectors as the URLs; do
|
||||
not hardcode a separate version in the page component.
|
||||
|
||||
Check that all three configs produce the requested release tag and installer
|
||||
Check that both configs produce the requested release tag and installer
|
||||
filenames for Windows (`ArtCraft_<version>_x64-setup.exe`) and macOS
|
||||
(`ArtCraft_<version>_universal.dmg`). Run `npm run typecheck` from
|
||||
`frontend/apps/artcraft-website-next` after changing its config. Repository edits
|
||||
@@ -89,6 +84,11 @@ report a local update as a deployed update.
|
||||
|
||||
Organize for top-to-bottom reading. Important things first, details later.
|
||||
|
||||
- **`mod.rs` holds module declarations only** (`pub mod foo;`, plus re-exports if needed). Never
|
||||
put functions, structs, enums, constants, impls or tests in a `mod.rs`: give each its own file
|
||||
in that directory (for example `threads/poll_foo_thread/poll_foo.rs`, declared from
|
||||
`threads/poll_foo_thread/mod.rs`).
|
||||
|
||||
- **Constants** at the top (after imports)
|
||||
- **Structs/enums** next; outer structs above inner sub-structs
|
||||
- **API types** in order: Request, Response, Error
|
||||
|
||||
@@ -104,7 +104,7 @@ and signals for state, Three.js for 3D scenes, and shared UI and generation tool
|
||||
| Path | Purpose |
|
||||
|----------------------------------|-----------------------------------------------|
|
||||
| `frontend/apps/artcraft-webapp` | Browser application at `app.getartcraft.com` |
|
||||
| `frontend/apps/artcraft-website` | Product website at `getartcraft.com` |
|
||||
| `frontend/apps/artcraft-website-next` | Product website at `getartcraft.com` (Next.js) |
|
||||
| `frontend/libs/api` | HTTP clients, API host selection, and models |
|
||||
| `frontend/libs/omni-gen` | Shared generation logic |
|
||||
| `frontend/libs/components` | Reusable UI, editors, and generation controls |
|
||||
@@ -190,14 +190,11 @@ The browser app runs at `http://localhost:4201`. `VITE_USE_LOCAL_API=true` selec
|
||||
the hosted API. API host selection lives in
|
||||
[`StorytellerApiHostStore`](./frontend/libs/api/src/lib/config/StorytellerApiHostStore.ts).
|
||||
|
||||
From `frontend`, build the web app or run the product website with:
|
||||
From `frontend`, build the web app with `npx nx build artcraft-webapp`. The
|
||||
product website is a standalone Next.js app (not part of the Nx workspace); run it
|
||||
from the repository root with `./script/website/unix_frontend_website_dev.sh`.
|
||||
|
||||
```bash
|
||||
npx nx build artcraft-webapp
|
||||
npx nx dev artcraft-website
|
||||
```
|
||||
|
||||
The website runs at `http://localhost:4200`. Repository-root launchers are in
|
||||
The website runs at `http://localhost:3000`. Repository-root launchers are in
|
||||
[`script/website`](./script/website). For desktop development, use the
|
||||
[ArtCraft desktop repository](https://github.com/storytold/artcraft).
|
||||
|
||||
|
||||
@@ -255,7 +255,6 @@ From the services repository:
|
||||
SQLX_OFFLINE=true cargo test --offline -p storyteller-web --bin storyteller-web login_challenges
|
||||
cd frontend
|
||||
npm exec vitest -- run --config apps/artcraft-webapp/vite.config.ts src/pages/login/desktop-login.spec.tsx src/pages/login/login-bridge-continuation.spec.tsx
|
||||
npm exec vitest -- run --config apps/artcraft-website/vite.config.ts src/pages/login/desktop-login.spec.tsx
|
||||
```
|
||||
|
||||
`SQLX_OFFLINE` controls compile-time query metadata only. These tests still execute
|
||||
|
||||
+2
-2
@@ -35,6 +35,6 @@ VITE_USE_LOCAL_API=true npx nx dev artcraft-webapp
|
||||
The web app runs at `http://localhost:4201`, using the local backend. To develop
|
||||
against the hosted API, omit `VITE_USE_LOCAL_API=true`.
|
||||
|
||||
For the product website, run `npx nx dev artcraft-website` from `frontend`; it uses
|
||||
`http://localhost:4200`. Repository-root launchers live under [`script/website`](../script/website).
|
||||
For the product website, run `./script/website/unix_frontend_website_dev.sh` from the
|
||||
repository root; it is a standalone Next.js app on `http://localhost:3000`. Repository-root launchers live under [`script/website`](../script/website).
|
||||
See the [frontend README](../frontend/README.md) for builds and troubleshooting.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copy this file to `.env` and change the values.
|
||||
# `.env` is ignored by git; never commit real keys.
|
||||
|
||||
# ----- Local infrastructure (used by docker-compose.yml) -----
|
||||
|
||||
MYSQL_ROOT_PASSWORD=change-me-root
|
||||
MYSQL_PASSWORD=change-me
|
||||
|
||||
# Credentials for the local S3-compatible storage (SeaweedFS).
|
||||
S3_ACCESS_KEY=artcraft
|
||||
S3_SECRET_KEY=change-me-please
|
||||
|
||||
# ----- storyteller-web settings -----
|
||||
|
||||
# Cookies are served over plain HTTP locally.
|
||||
COOKIE_SECURE=false
|
||||
COOKIE_DOMAIN=localhost
|
||||
COOKIE_SECRET=change-me
|
||||
SORT_KEY_SECRET=change-me
|
||||
WEBSITE_HOMEPAGE_REDIRECT=http://localhost:4201
|
||||
|
||||
ENABLE_PAGING=false
|
||||
NUM_WORKERS=4
|
||||
|
||||
# ----- Third-party providers -----
|
||||
# storyteller-web requires these to be set at startup. Placeholders let the server
|
||||
# start; features backed by a provider only work once you add a real key.
|
||||
|
||||
FAL_API_KEY=unset
|
||||
FAL_WEBHOOK_URL=http://localhost:12345/v1/webhooks/fal
|
||||
GMICLOUD_API_KEY=unset
|
||||
GROK_API_KEY=unset
|
||||
OPENAI_API_KEY=unset
|
||||
WORLDLABS_API_KEY=unset
|
||||
BEEBLE_API_KEY=unset
|
||||
BEEBLE_WEBHOOK_URL=http://localhost:12345/v1/webhooks/beeble
|
||||
SEEDANCE2PRO_COOKIES=unset
|
||||
SEEDANCE2PRO_WHITELIST_COOKIES=unset
|
||||
SEEDANCE2PRO_BYTEPLUS_ULTRA_COOKIES=unset
|
||||
|
||||
# Email (Resend)
|
||||
RESEND_API_KEY=unset
|
||||
|
||||
# ----- Stripe (billing) -----
|
||||
|
||||
STRIPE_SECRET_KEY=unset
|
||||
STRIPE_SECRET_WEBHOOK_SIGNING_KEY=unset
|
||||
STRIPE_FAKEYOU_ACCOUNT_ID=unset
|
||||
STRIPE_PORTAL_DEFAULT_CONFIG_ID=unset
|
||||
STRIPE_CHECKOUT_SUCCESS_URL_PATH=/checkout_success
|
||||
STRIPE_CHECKOUT_CANCEL_URL_PATH=/checkout_cancel
|
||||
STRIPE_PORTAL_RETURN_URL_PATH=/portal_success
|
||||
|
||||
STRIPE_ARTCRAFT_ACCOUNT_ID=unset
|
||||
STRIPE_ARTCRAFT_SECRET_KEY=unset
|
||||
STRIPE_ARTCRAFT_SECRET_WEBHOOK_KEY=unset
|
||||
STRIPE_ARTCRAFT_CHECKOUT_SUCCESS_URL=http://localhost:4201/checkout_success
|
||||
STRIPE_ARTCRAFT_CHECKOUT_CANCEL_URL=http://localhost:4201/checkout_cancel
|
||||
STRIPE_ARTCRAFT_PORTAL_RETURN_URL=http://localhost:4201/portal_closed
|
||||
@@ -0,0 +1,77 @@
|
||||
# Self-hosting with Docker Compose
|
||||
|
||||
This directory runs the ArtCraft backend (`storyteller-web`) and the services it
|
||||
depends on with Docker Compose.
|
||||
|
||||
| Service | Image | Purpose |
|
||||
|-------------------|--------------------------|---------------------------------------------|
|
||||
| `storyteller-web` | built from this repo | HTTP API, on `http://localhost:12345` |
|
||||
| `mysql` | `mysql:8.4` | Accounts, media metadata, jobs, billing |
|
||||
| `migrate` | `mysql:8.4` | Applies `_database/sql/migrations`, exits |
|
||||
| `redis` | `redis:7` | Caching, rate limiting, job progress |
|
||||
| `elasticsearch` | `elasticsearch:8.15.3` | Search |
|
||||
| `seaweedfs` | `chrislusf/seaweedfs` | S3-compatible storage for media |
|
||||
| `storage-init` | `chrislusf/seaweedfs` | Creates the storage buckets, exits |
|
||||
|
||||
Only the API port (`12345`) is published to the host.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker with Docker Compose v2.23 or newer.
|
||||
- About 8 GB of memory for Docker and 20 GB of free disk. The first build
|
||||
compiles the Rust workspace in release mode and can take a long time.
|
||||
|
||||
## Usage
|
||||
|
||||
From this directory:
|
||||
|
||||
```bash
|
||||
cp .env.example .env # then edit the passwords and secrets
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
Check that the server is up:
|
||||
|
||||
```bash
|
||||
curl http://localhost:12345/_status
|
||||
```
|
||||
|
||||
Stop everything with `docker compose down`. Add `-v` to also delete the
|
||||
database, search index and stored media.
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings live in `.env`, which git ignores. `docker-compose.yml` sets the
|
||||
connection settings for MySQL, Redis, Elasticsearch and storage, so you only
|
||||
need to change passwords and secrets.
|
||||
|
||||
`storyteller-web` requires credentials for every third-party provider (FAL,
|
||||
OpenAI, Stripe, Resend and others) to be present at startup. `.env.example`
|
||||
sets them to the placeholder `unset`, which lets the server start. Features
|
||||
that call a provider only work after you add a real key for it.
|
||||
|
||||
Provider webhooks, such as `FAL_WEBHOOK_URL`, must be reachable from the
|
||||
internet for asynchronous generation results to arrive.
|
||||
|
||||
## Database migrations
|
||||
|
||||
The `migrate` service applies every migration in `_database/sql/migrations` in
|
||||
order and records each one in `__diesel_schema_migrations`, the table the diesel
|
||||
CLI uses. It runs on every `docker compose up` and only applies migrations that
|
||||
have not run yet.
|
||||
|
||||
## Connecting the web app
|
||||
|
||||
To point the browser app at this server, run it from `frontend` as described in
|
||||
the repository README:
|
||||
|
||||
```bash
|
||||
VITE_USE_LOCAL_API=true npx nx dev artcraft-webapp
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only the HTTP API is included. Background workers (provider polling, video
|
||||
thumbnails, email, analytics) are not started.
|
||||
- Elasticsearch starts with no indexes. Index definitions are in
|
||||
`_database/elasticsearch/index_definitions`.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Self-hosted ArtCraft backend (storyteller-web) with its dependencies.
|
||||
#
|
||||
# Usage (from this directory):
|
||||
# cp .env.example .env
|
||||
# docker compose up --build
|
||||
#
|
||||
# See README.md in this directory for details.
|
||||
|
||||
name: artcraft-selfhost
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: storyteller
|
||||
MYSQL_USER: storyteller
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
# NB: Use TCP (127.0.0.1), not the socket, so the check fails while the image's
|
||||
# temporary init server (which has networking disabled) is still running.
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD}", "--silent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
# Applies the SQL migrations in `_database/sql/migrations` in order.
|
||||
# Applied versions are recorded in `__diesel_schema_migrations` (the same table
|
||||
# the diesel CLI uses), so re-running is safe and only applies new migrations.
|
||||
migrate:
|
||||
image: mysql:8.4
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MYSQL_PWD: ${MYSQL_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- ../../_database/sql/migrations:/migrations:ro
|
||||
entrypoint: ["bash", "-c"]
|
||||
command:
|
||||
- |
|
||||
set -euo pipefail
|
||||
run_sql() { mysql -h mysql -u root storyteller "$$@"; }
|
||||
run_sql -e "CREATE TABLE IF NOT EXISTS __diesel_schema_migrations (
|
||||
version VARCHAR(50) PRIMARY KEY NOT NULL,
|
||||
run_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"
|
||||
for dir in /migrations/*/; do
|
||||
name=$$(basename "$$dir")
|
||||
version=$$(echo "$${name%%_*}" | tr -d '-')
|
||||
if [ -n "$$(run_sql -N -e "SELECT 1 FROM __diesel_schema_migrations WHERE version = '$$version'")" ]; then
|
||||
continue
|
||||
fi
|
||||
echo "Applying migration $$name"
|
||||
run_sql < "$$dir/up.sql"
|
||||
run_sql -e "INSERT INTO __diesel_schema_migrations (version) VALUES ('$$version')"
|
||||
done
|
||||
echo "Migrations complete."
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: "false"
|
||||
ES_JAVA_OPTS: -Xms512m -Xmx512m
|
||||
volumes:
|
||||
- elasticsearch-data:/usr/share/elasticsearch/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fs http://localhost:9200/_cluster/health || exit 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
# S3-compatible object storage (SeaweedFS) for uploaded and generated media.
|
||||
seaweedfs:
|
||||
image: chrislusf/seaweedfs:3.97
|
||||
restart: unless-stopped
|
||||
command: server -dir=/data -s3 -s3.port=8333 -s3.config=/etc/seaweedfs/s3.json
|
||||
configs:
|
||||
- source: seaweedfs-s3
|
||||
target: /etc/seaweedfs/s3.json
|
||||
volumes:
|
||||
- seaweedfs-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8333/healthz || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
|
||||
# Creates the buckets storyteller-web expects.
|
||||
storage-init:
|
||||
image: chrislusf/seaweedfs:3.97
|
||||
depends_on:
|
||||
seaweedfs:
|
||||
condition: service_healthy
|
||||
entrypoint: ["sh", "-c"]
|
||||
command:
|
||||
- |
|
||||
set -e
|
||||
for bucket in artcraft-private artcraft-public artcraft-public-gc; do
|
||||
echo "s3.bucket.create -name $$bucket" | weed shell -master=seaweedfs:9333
|
||||
done
|
||||
|
||||
storyteller-web:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: build/service_cpu.Dockerfile
|
||||
image: artcraft-services:local
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
storage-init:
|
||||
condition: service_completed_successfully
|
||||
redis:
|
||||
condition: service_started
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
env_file: .env
|
||||
environment:
|
||||
SERVER_ENVIRONMENT: development
|
||||
BIND_ADDRESS: 0.0.0.0:12345
|
||||
MYSQL_URL: mysql://storyteller:${MYSQL_PASSWORD}@mysql:3306/storyteller
|
||||
REDIS_0_URL: redis://redis:6379/0
|
||||
ELASTICSEARCH_URL: http://elasticsearch:9200
|
||||
S3_COMPATIBLE_ENDPOINT_URL: http://seaweedfs:8333
|
||||
ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||
SECRET_KEY: ${S3_SECRET_KEY}
|
||||
REGION_NAME: us-east-1
|
||||
W2L_PRIVATE_DOWNLOAD_BUCKET_NAME: artcraft-private
|
||||
W2L_PUBLIC_DOWNLOAD_BUCKET_NAME: artcraft-public
|
||||
GC_ENABLED_PUBLIC_BUCKET_NAME: artcraft-public-gc
|
||||
AUDIO_UPLOADS_BUCKET_ROOT: /user_uploaded_audio
|
||||
ports:
|
||||
- "12345:12345"
|
||||
|
||||
configs:
|
||||
seaweedfs-s3:
|
||||
content: |
|
||||
{
|
||||
"identities": [
|
||||
{
|
||||
"name": "artcraft",
|
||||
"credentials": [{ "accessKey": "${S3_ACCESS_KEY}", "secretKey": "${S3_SECRET_KEY}" }],
|
||||
"actions": ["Admin", "Read", "Write", "List", "Tagging"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
redis-data:
|
||||
elasticsearch-data:
|
||||
seaweedfs-data:
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod release_info;
|
||||
@@ -0,0 +1,203 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
// ── GET /v1/craft_apps/release_info ──
|
||||
//
|
||||
// The latest release of every Craft App (PhotoCraft, VectorCraft, …, and ArtCraft Launcher
|
||||
// itself), as storyteller-web last fetched it from GitHub, plus renames and notices for the
|
||||
// ArtCraft Launcher. The launcher polls this instead of GitHub; GitHub is its fallback.
|
||||
//
|
||||
// Compatibility: clients must ignore fields they don't know, and treat every field as optional.
|
||||
// New fields may be added; existing ones keep their meaning.
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct GetCraftAppsReleaseInfoResponse {
|
||||
pub success: bool,
|
||||
|
||||
pub release_info: CraftAppsReleaseInfo,
|
||||
|
||||
/// Apps that changed their name (for example PdfCraft → DocuCraft).
|
||||
pub app_renames: Vec<CraftAppRename>,
|
||||
|
||||
/// Apps whose GitHub repository moved (for example `storytold/photocraft` → `art/photocraft`).
|
||||
pub repo_renames: Vec<CraftAppRepoRename>,
|
||||
|
||||
pub important_notices: CraftAppsImportantNotices,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppsReleaseInfo {
|
||||
/// Keyed by app key: the app's lowercase machine name, as used in its binaries, asset file
|
||||
/// names and `ai.storyteller.<key>` bundle id (`photocraft`, `cadcraft`, `artcraft-launcher`).
|
||||
/// Not the display name and not the repo name. Clients should ignore keys they don't know.
|
||||
pub apps: BTreeMap<String, CraftAppReleaseInfo>,
|
||||
|
||||
/// When the least recently fetched app was fetched: every app's data is at least this fresh.
|
||||
/// Absent until every app has been fetched at least once.
|
||||
pub last_fetched: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppReleaseInfo {
|
||||
/// Display name (`PhotoCraft`).
|
||||
pub name: String,
|
||||
|
||||
/// GitHub repository, `organization/project` (`storytold/photocraft`).
|
||||
pub repo: String,
|
||||
|
||||
/// The latest published, non-draft, non-prerelease release. Absent when the repository has no
|
||||
/// release yet, or when it hasn't been fetched yet (see `app_last_fetched`).
|
||||
pub latest_release: Option<CraftAppRelease>,
|
||||
|
||||
/// When this app's release data was last fetched successfully. Absent if it never was; then
|
||||
/// `latest_release` says nothing and clients should ask GitHub themselves.
|
||||
pub app_last_fetched: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppRelease {
|
||||
/// Git tag (`v0.5.0`).
|
||||
pub tag: String,
|
||||
|
||||
/// The tag without its leading `v` (`0.5.0`, `0.6.0-rc.1`).
|
||||
pub version: String,
|
||||
|
||||
/// Release title (`PhotoCraft v0.5.0`).
|
||||
pub name: String,
|
||||
|
||||
pub published_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// The release's page on GitHub.
|
||||
pub html_url: String,
|
||||
|
||||
pub prerelease: bool,
|
||||
|
||||
/// Release notes (Markdown), truncated to 64 KiB.
|
||||
pub notes_markdown: String,
|
||||
|
||||
pub assets: Vec<CraftAppReleaseAsset>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppReleaseAsset {
|
||||
/// File name (`photocraft-0.5.0-windows-x64.msi`).
|
||||
pub name: String,
|
||||
|
||||
pub size_bytes: u64,
|
||||
|
||||
/// `https://github.com/<repo>/releases/download/<tag>/<name>`.
|
||||
pub download_url: String,
|
||||
|
||||
/// Lowercase hex SHA-256, when GitHub published one for the file.
|
||||
pub sha256: Option<String>,
|
||||
|
||||
/// What the file name says it is, when it follows the Craft App naming scheme
|
||||
/// (`<app>-<version>-<os>-<arch>[-portable].<ext>`).
|
||||
pub platform: Option<CraftAppAssetPlatform>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppAssetPlatform {
|
||||
pub os: CraftAppOs,
|
||||
|
||||
/// Absent for files that aren't for one CPU (the web build, checksums).
|
||||
pub arch: Option<CraftAppArch>,
|
||||
|
||||
pub package_type: CraftAppPackageType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CraftAppOs {
|
||||
Windows,
|
||||
Macos,
|
||||
Linux,
|
||||
Freebsd,
|
||||
/// The browser build.
|
||||
Web,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CraftAppArch {
|
||||
X64,
|
||||
X86,
|
||||
Arm64,
|
||||
/// macOS universal binary (Apple silicon and Intel).
|
||||
Universal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CraftAppPackageType {
|
||||
/// Windows Installer.
|
||||
Msi,
|
||||
/// Windows portable zip.
|
||||
PortableZip,
|
||||
/// macOS disk image.
|
||||
Dmg,
|
||||
/// macOS command-line tools zip.
|
||||
CliZip,
|
||||
AppImage,
|
||||
/// AppImage delta-update file.
|
||||
AppImageZsync,
|
||||
Deb,
|
||||
Rpm,
|
||||
Flatpak,
|
||||
/// Linux or FreeBSD tarball.
|
||||
TarGz,
|
||||
/// The web (WebAssembly) build.
|
||||
WebZip,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppRename {
|
||||
/// User-facing name before (`PdfCraft`).
|
||||
pub old_name: String,
|
||||
/// Lowercase name before (`pdfcraft`).
|
||||
pub old_name_lower: String,
|
||||
/// User-facing name after (`DocuCraft`).
|
||||
pub new_name: String,
|
||||
/// Lowercase name after (`docucraft`).
|
||||
pub new_name_lower: String,
|
||||
/// A sentence or two to show beside the rename.
|
||||
pub description: String,
|
||||
pub renamed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppRepoRename {
|
||||
/// `organization/project` before.
|
||||
pub old_name: String,
|
||||
/// `organization/project` after.
|
||||
pub new_name: String,
|
||||
pub renamed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppsImportantNotices {
|
||||
/// News, shown inline; the user can dismiss each.
|
||||
pub announcements: Vec<CraftAppsAnnouncement>,
|
||||
/// Shown as a popup the user acknowledges.
|
||||
pub alerts: Vec<CraftAppsAlert>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppsAnnouncement {
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CraftAppsAlert {
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
/// A page the popup can offer to open.
|
||||
pub maybe_web_link: Option<Url>,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub mod analytics;
|
||||
pub mod api_keys;
|
||||
pub mod characters;
|
||||
pub mod common;
|
||||
pub mod craft_apps;
|
||||
pub mod credits;
|
||||
pub mod folders;
|
||||
pub mod generate;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
+165
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -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,
|
||||
|
||||
+2
@@ -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?`,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+8
@@ -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")?,
|
||||
|
||||
+6
@@ -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.
|
||||
|
||||
+8
@@ -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")?,
|
||||
|
||||
+11
-1
@@ -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>,
|
||||
}
|
||||
+5
@@ -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")?,
|
||||
|
||||
+8
@@ -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,
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
+5
@@ -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")?,
|
||||
|
||||
+5
@@ -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")?,
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
|
||||
+3
@@ -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,
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+4
@@ -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),
|
||||
|
||||
+9
-1
@@ -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
|
||||
|
||||
+10
-1
@@ -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>>())
|
||||
}
|
||||
|
||||
+1
@@ -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?`,
|
||||
|
||||
+1
@@ -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?`,
|
||||
|
||||
+1
@@ -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?`,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+8
@@ -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")?,
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
//! The latest release of one Craft App, shared by every storyteller-web instance so the fleet
|
||||
//! polls GitHub once per app instead of once per pod.
|
||||
//!
|
||||
//! A Redis hash, written by `poll_craft_app_releases_thread` (storyteller-web), which documents
|
||||
//! the fields. Expires if nobody refreshes it for a week, which only happens if polling stops.
|
||||
|
||||
use chrono::Duration;
|
||||
|
||||
pub struct CraftAppReleaseRedisKey(pub String);
|
||||
|
||||
impl_string_key!(CraftAppReleaseRedisKey);
|
||||
|
||||
const REDIS_KEY_TTL_DURATION: Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 7);
|
||||
|
||||
impl CraftAppReleaseRedisKey {
|
||||
/// `app_key` is the app's lowercase machine name (`photocraft`, `artcraft-launcher`).
|
||||
pub fn new_for_app(app_key: &str) -> Self {
|
||||
let key = format!("craftApps:releases:app:{}", app_key);
|
||||
Self(key)
|
||||
}
|
||||
|
||||
pub fn get_redis_ttl() -> Duration {
|
||||
REDIS_KEY_TTL_DURATION
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_for_app() {
|
||||
let key = CraftAppReleaseRedisKey::new_for_app("photocraft");
|
||||
assert_eq!(key.as_str(), "craftApps:releases:app:photocraft");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration() {
|
||||
assert_eq!(CraftAppReleaseRedisKey::get_redis_ttl().num_days(), 7);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
//! The fleet-wide GitHub backoff for Craft App release polling: after GitHub rate-limits us (or
|
||||
//! every request of a round fails), no storyteller-web instance asks GitHub again until it ends.
|
||||
//!
|
||||
//! A Redis hash, written by `poll_craft_app_releases_thread` (storyteller-web), which documents
|
||||
//! the fields. It outlives the backoff itself so repeated trouble keeps growing the wait; it is
|
||||
//! deleted after a good round.
|
||||
|
||||
use chrono::Duration;
|
||||
|
||||
pub struct CraftAppsReleaseBackoffRedisKey(pub String);
|
||||
|
||||
impl_string_key!(CraftAppsReleaseBackoffRedisKey);
|
||||
|
||||
const REDIS_KEY_TTL_DURATION: Duration = Duration::milliseconds(1000 * 60 * 60 * 24);
|
||||
|
||||
impl CraftAppsReleaseBackoffRedisKey {
|
||||
pub fn new_for_fleet() -> Self {
|
||||
Self("craftApps:releases:backoff".to_string())
|
||||
}
|
||||
|
||||
pub fn get_redis_ttl() -> Duration {
|
||||
REDIS_KEY_TTL_DURATION
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_for_fleet() {
|
||||
assert_eq!(CraftAppsReleaseBackoffRedisKey::new_for_fleet().as_str(), "craftApps:releases:backoff");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration() {
|
||||
assert_eq!(CraftAppsReleaseBackoffRedisKey::get_redis_ttl().num_hours(), 24);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod craft_app_release_redis_key;
|
||||
pub mod craft_apps_release_backoff_redis_key;
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod craft_apps;
|
||||
pub mod premium;
|
||||
pub mod inference_job;
|
||||
pub mod inference_job;
|
||||
@@ -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)?;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::info;
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use sqlx::mysql::{MySqlConnectOptions, MySqlPoolOptions};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
pub async fn connect_to_database() -> AnyhowResult<MySqlPool> {
|
||||
@@ -20,20 +21,37 @@ pub async fn connect_to_database() -> AnyhowResult<MySqlPool> {
|
||||
// shorter timeout sheds load and surfaces the problem instead of hiding it behind long waits.
|
||||
let acquire_timeout_seconds = easyenv::get_env_num::<u64>("MYSQL_ACQUIRE_TIMEOUT_SECONDS", 10)?;
|
||||
|
||||
// NB: Every cached prepared statement stays open on the server until its connection evicts it
|
||||
// or closes, and the server caps the total across ALL clients (`max_prepared_stmt_count`,
|
||||
// 16382 on the managed DB). sqlx's default of 100 per connection let `replicas *
|
||||
// MYSQL_MAX_CONNECTIONS * 100` blow past that cap, failing new prepares with MySQL error 1461.
|
||||
// Keep `replicas * MYSQL_MAX_CONNECTIONS * MYSQL_STATEMENT_CACHE_CAPACITY` well under it.
|
||||
let statement_cache_capacity = easyenv::get_env_num::<usize>("MYSQL_STATEMENT_CACHE_CAPACITY", 25)?;
|
||||
|
||||
// NB: Recycling connections releases any server-side statements orphaned by cancelled queries
|
||||
// (eg. a client disconnect between prepare and close). sqlx's default lifetime is 30 minutes.
|
||||
let max_lifetime_seconds = easyenv::get_env_num::<u64>("MYSQL_MAX_LIFETIME_SECONDS", 600)?;
|
||||
|
||||
// NB: Log the settings before building/connecting so a connect failure has context. We do NOT
|
||||
// log the connection string (it contains credentials).
|
||||
info!(
|
||||
"Building MySQL pool: max_connections={}, min_connections={}, acquire_timeout_seconds={}",
|
||||
"Building MySQL pool: max_connections={}, min_connections={}, acquire_timeout_seconds={}, \
|
||||
statement_cache_capacity={}, max_lifetime_seconds={}",
|
||||
max_connections, min_connections, acquire_timeout_seconds,
|
||||
statement_cache_capacity, max_lifetime_seconds,
|
||||
);
|
||||
|
||||
let connect_options = MySqlConnectOptions::from_str(&db_connection_string)?
|
||||
.statement_cache_capacity(statement_cache_capacity);
|
||||
|
||||
let pool_options = MySqlPoolOptions::new()
|
||||
.max_connections(max_connections)
|
||||
.min_connections(min_connections)
|
||||
.acquire_timeout(Duration::from_secs(acquire_timeout_seconds));
|
||||
.acquire_timeout(Duration::from_secs(acquire_timeout_seconds))
|
||||
.max_lifetime(Some(Duration::from_secs(max_lifetime_seconds)));
|
||||
|
||||
let pool = pool_options
|
||||
.connect(&db_connection_string)
|
||||
.connect_with(connect_options)
|
||||
.await?;
|
||||
|
||||
Ok(pool)
|
||||
|
||||
@@ -162,6 +162,7 @@ use artcraft_api_defs::moderation::user_daily_spends::user_daily_spends_list::*;
|
||||
use artcraft_api_defs::moderation::top_spenders::list::*;
|
||||
use artcraft_api_defs::moderation::user_spend_events::list::*;
|
||||
use artcraft_api_defs::folders::subfolder::*;
|
||||
use artcraft_api_defs::craft_apps::release_info::*;
|
||||
use artcraft_api_defs::tags::add_media_file_tags::*;
|
||||
use artcraft_api_defs::tags::bulk_add_tags::*;
|
||||
use artcraft_api_defs::tags::bulk_list_media_file_tags::*;
|
||||
@@ -734,6 +735,9 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::folders::media_files::bulk_move_folder_media_files_handler::bulk_move_folder_media_files_handler,
|
||||
crate::http_server::endpoints::folders::media_files::bulk_remove_folder_media_files_handler::bulk_remove_folder_media_files_handler,
|
||||
|
||||
// Craft Apps
|
||||
crate::http_server::endpoints::craft_apps::get_craft_apps_release_info_handler::get_craft_apps_release_info_handler,
|
||||
|
||||
// Tags
|
||||
crate::http_server::endpoints::tags::list_tags_handler::list_tags_handler,
|
||||
crate::http_server::endpoints::tags::list_media_file_tags_handler::list_media_file_tags_handler,
|
||||
@@ -1606,6 +1610,20 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
ListTaggedMediaFilesSuccessResponse,
|
||||
ListTagsQueryParams,
|
||||
ListTagsSuccessResponse,
|
||||
GetCraftAppsReleaseInfoResponse,
|
||||
CraftAppsReleaseInfo,
|
||||
CraftAppReleaseInfo,
|
||||
CraftAppRelease,
|
||||
CraftAppReleaseAsset,
|
||||
CraftAppAssetPlatform,
|
||||
CraftAppOs,
|
||||
CraftAppArch,
|
||||
CraftAppPackageType,
|
||||
CraftAppRename,
|
||||
CraftAppRepoRename,
|
||||
CraftAppsImportantNotices,
|
||||
CraftAppsAnnouncement,
|
||||
CraftAppsAlert,
|
||||
ListUntaggedMediaFilesQueryParams,
|
||||
ListUntaggedMediaFilesSuccessResponse,
|
||||
MediaFileTagsEntry,
|
||||
|
||||
+2
@@ -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(
|
||||
|
||||
+189
-44
@@ -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 }
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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)))
|
||||
}
|
||||
+2
-1
@@ -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)))
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::http::header::{CacheControl, CacheDirective};
|
||||
use actix_web::{web, HttpResponse};
|
||||
|
||||
use artcraft_api_defs::craft_apps::release_info::{CraftAppsImportantNotices, CraftAppsReleaseInfo, GetCraftAppsReleaseInfoResponse};
|
||||
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Shared caches (Cloudflare) may serve the same answer for this long once every app has been
|
||||
/// fetched: the data changes at most once per polling interval.
|
||||
const CACHE_MAX_AGE_SECS: u32 = 60;
|
||||
|
||||
/// The latest release of every Craft App (and ArtCraft Launcher), as storyteller-web last fetched
|
||||
/// it from GitHub, plus app/repo renames and notices for the launcher. Never fails: before the
|
||||
/// first fetch, apps are listed without a release and without `app_last_fetched`.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Craft Apps",
|
||||
path = "/v1/craft_apps/release_info",
|
||||
responses(
|
||||
(status = 200, body = GetCraftAppsReleaseInfoResponse),
|
||||
),
|
||||
)]
|
||||
pub async fn get_craft_apps_release_info_handler(
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> HttpResponse {
|
||||
let release_info = server_state.craft_apps_release_cache.snapshot();
|
||||
let cache_control = cache_control_for(&release_info);
|
||||
let response = GetCraftAppsReleaseInfoResponse {
|
||||
success: true,
|
||||
release_info,
|
||||
// None yet. When there are, they're listed here (newest first).
|
||||
app_renames: Vec::new(),
|
||||
repo_renames: Vec::new(),
|
||||
important_notices: CraftAppsImportantNotices::default(),
|
||||
};
|
||||
HttpResponse::Ok()
|
||||
.insert_header(cache_control)
|
||||
.json(response)
|
||||
}
|
||||
|
||||
/// Cacheable only once every app has been fetched (`last_fetched` is set). Until then (an
|
||||
/// instance that just started, or one whose first fetch failed) the answer is `no-store`, so a
|
||||
/// shared cache never keeps handing out "not fetched yet" after this instance has warmed up.
|
||||
fn cache_control_for(release_info: &CraftAppsReleaseInfo) -> CacheControl {
|
||||
if release_info.last_fetched.is_some() {
|
||||
CacheControl(vec![CacheDirective::Public, CacheDirective::MaxAge(CACHE_MAX_AGE_SECS)])
|
||||
} else {
|
||||
CacheControl(vec![CacheDirective::NoStore])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_fully_fetched_answer_is_cacheable() {
|
||||
let release_info = CraftAppsReleaseInfo { apps: BTreeMap::new(), last_fetched: Some(Utc::now()) };
|
||||
assert_eq!(cache_control_for(&release_info).to_string(), "public, max-age=60");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_answer_with_unfetched_apps_is_never_stored() {
|
||||
let release_info = CraftAppsReleaseInfo { apps: BTreeMap::new(), last_fetched: None };
|
||||
assert_eq!(cache_control_for(&release_info).to_string(), "no-store");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod get_craft_apps_release_info_handler;
|
||||
+26
-2
@@ -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)
|
||||
|
||||
+2
@@ -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(
|
||||
|
||||
+3
-1
@@ -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,
|
||||
|
||||
+3
-1
@@ -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
-1
@@ -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
-1
@@ -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()
|
||||
})
|
||||
|
||||
Loaded 100 of 581 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user