Compare commits
50
Commits
bt-query-perf
...
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 | ||
|
|
7a88118349 | ||
|
|
e07bf35cd0 | ||
|
|
a1147a98d2 | ||
|
|
f67fb7db31 | ||
|
|
7d4e89ced9 | ||
|
|
b9ee1bd225 | ||
|
|
68886b62fe | ||
|
|
35a859529f | ||
|
|
3a93e358cd | ||
|
|
1cb0b32898 | ||
|
|
c50bb78c32 |
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,16 +34,16 @@ 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
|
||||
```
|
||||
|
||||
## 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):**
|
||||
@@ -51,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`.
|
||||
@@ -64,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
|
||||
@@ -88,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
|
||||
|
||||
Generated
+9
@@ -313,6 +313,7 @@ dependencies = [
|
||||
"actix-http",
|
||||
"actix-rt",
|
||||
"actix-web",
|
||||
"domains",
|
||||
"log",
|
||||
"server_environment",
|
||||
"speculoos",
|
||||
@@ -2688,6 +2689,13 @@ dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "domains"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
@@ -8715,6 +8723,7 @@ dependencies = [
|
||||
"datetimes",
|
||||
"derive_more 0.99.17",
|
||||
"diesel",
|
||||
"domains",
|
||||
"easyenv",
|
||||
"elasticsearch",
|
||||
"elasticsearch_schema",
|
||||
|
||||
@@ -67,6 +67,7 @@ members = [
|
||||
"crates/lib/files/zip_archives",
|
||||
"crates/lib/http_headers",
|
||||
"crates/lib/jobs_common",
|
||||
"crates/lib/domains",
|
||||
"crates/lib/jwt_light",
|
||||
"crates/lib/opaque_cursors",
|
||||
"crates/lib/jwt_signer",
|
||||
@@ -252,6 +253,7 @@ grok_api_client = { path = "crates/api_clients/grok_api_client" }
|
||||
rootly_client = { path = "crates/api_clients/rootly_client" }
|
||||
rootly_config = { path = "crates/schema/config/rootly_config" }
|
||||
shared_env_var_config = { path = "crates/schema/config/shared_env_var_config" }
|
||||
domains = { path = "crates/lib/domains" }
|
||||
http_headers = { path = "crates/lib/http_headers" }
|
||||
images = { path = "crates/lib/files/images" }
|
||||
jobs_common = { path = "crates/lib/jobs_common" }
|
||||
|
||||
@@ -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:
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::common::responses::cover_image_links::CoverImageLinks;
|
||||
use crate::common::responses::media_file_cover_image_details::MediaFileDefaultCover;
|
||||
|
||||
// User-media list endpoints retain the deprecated bucket fields for compatibility.
|
||||
/// Everything we need to create a cover image.
|
||||
/// Cover images are small descriptive images that can be set for any media file.
|
||||
/// If a cover image is set, this is the path to the asset.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct LegacyMediaFileCoverImageDetails {
|
||||
/// (DEPRECATED) URL path to the media file
|
||||
#[deprecated(note="This field doesn't point to the full URL. Use media_links instead to leverage the CDN.")]
|
||||
pub maybe_cover_image_public_bucket_path: Option<String>,
|
||||
|
||||
/// (DEPRECATED) Full URL to the media file
|
||||
#[deprecated(note="This points to the bucket. Use media_links instead to leverage the CDN.")]
|
||||
pub maybe_cover_image_public_bucket_url: Option<Url>,
|
||||
|
||||
/// Links to the cover image (CDN direct link, thumbnail template)
|
||||
/// If a cover image is set, this is the path to the asset.
|
||||
/// If a cover image is not set, use the information in `default_cover` instead.
|
||||
/// Rich CDN links to the media, including thumbnails, previews, and more.
|
||||
pub maybe_links: Option<CoverImageLinks>,
|
||||
|
||||
/// For items without a cover image, we can use one of our own.
|
||||
pub default_cover: MediaFileDefaultCover,
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use tokens::tokens::model_weights::ModelWeightToken;
|
||||
|
||||
/// Fields useful for enriching media file listings
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MediaFileOriginDetails {
|
||||
/// Where the file came from (broadly)
|
||||
pub origin_category: MediaFileOriginCategory,
|
||||
|
||||
/// Where the file came from (specifically, a product area, eg. "face_animator")
|
||||
pub product_category: MediaFileOriginProductCategory,
|
||||
|
||||
/// If the file was produced by a model, the details.
|
||||
pub maybe_model: Option<MediaFileModelDetails>,
|
||||
}
|
||||
|
||||
/// Information about the model weights
|
||||
/// https://serde.rs/enum-representations.html#untagged
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum MediaFileModelDetails {
|
||||
// Try the specific shape first so deserialization retains token and title.
|
||||
/// A model weight is typically a user-submitted model.
|
||||
/// We store lots of these.
|
||||
ModelWeight {
|
||||
/// The type of model weight
|
||||
model_type: MediaFileOriginModelType,
|
||||
/// The model token
|
||||
token: ModelWeightToken,
|
||||
/// The model title (typically only populated for `model_weights` models, not legacy tables such as `tts_models`.)
|
||||
title: String,
|
||||
},
|
||||
/// A system model is one that we maintain and update,
|
||||
/// eg. SadTalker, Wav2Lip, MocapNet, etc.
|
||||
SystemModel {
|
||||
/// The type of model weight
|
||||
model_type: MediaFileOriginModelType,
|
||||
},
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
pub mod cover_image_links;
|
||||
pub mod job_details;
|
||||
pub mod legacy_media_file_cover_image_details;
|
||||
pub mod media_file_cover_image_details;
|
||||
pub mod media_file_origin_details;
|
||||
pub mod media_links;
|
||||
pub mod pagination_cursors;
|
||||
pub mod simple_entity_stats;
|
||||
pub mod simple_generic_json_success;
|
||||
pub mod user_details_light;
|
||||
pub mod user_details_light;
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
// TODO(bt,2025-08-20): Replace the storyteller-web version of this.
|
||||
|
||||
/// Simple stats that can be attached to any entity
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct SimpleEntityStats {
|
||||
/// Number of positive ratings (or "likes") for this item
|
||||
pub positive_rating_count: u32,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use crate::media_file::list::user_media_file_list_item::MediaFileForUserListItem;
|
||||
|
||||
pub const LIST_MEDIA_FILES_FOR_USER_V2_URL_PATH: &str = "/v1/media_files/list_v2/user/{username}";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListMediaFilesForUserV2PathInfo {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, IntoParams)]
|
||||
pub struct ListMediaFilesForUserV2QueryParams {
|
||||
pub sort_ascending: Option<bool>,
|
||||
pub page_size: Option<usize>,
|
||||
pub page_index: Option<usize>,
|
||||
|
||||
/// NB: This can be one (or more comma-separated values) from `MediaFileClass`,
|
||||
/// which are the broad category of media files: image, video, etc.
|
||||
///
|
||||
/// Usage:
|
||||
/// - `?filter_media_classes=audio`
|
||||
/// - `?filter_media_classes=image,video`
|
||||
/// - `?filter_media_classes=dimensional`
|
||||
/// - etc.
|
||||
pub filter_media_classes: Option<String>,
|
||||
|
||||
/// NB: This can be one (or more comma-separated values) from `MediaFileType`,
|
||||
/// which are mimetype-like / format-like categories of media files: glb, gltf,
|
||||
/// scene_json, jpg, png, mp4, wav, etc.
|
||||
///
|
||||
/// Usage:
|
||||
/// - `?filter_media_type=scene_json`
|
||||
/// - `?filter_media_type=glb,gltf,fbx`
|
||||
/// - `?filter_media_type=pmd,vmd,pmx`
|
||||
/// - `?filter_media_type=jpg,png,gif`
|
||||
/// - `?filter_media_type=wav,mp3`
|
||||
/// - `?filter_media_type=mp4`
|
||||
/// - etc.
|
||||
pub filter_media_type: Option<String>,
|
||||
|
||||
/// NB: This can be one (or more comma-separated values) from `MediaFileEngineCategory`.
|
||||
///
|
||||
/// Usage:
|
||||
/// - `?filter_engine_categories=scene`
|
||||
/// - `?filter_engine_categories=animation,character,object`
|
||||
/// - etc.
|
||||
pub filter_engine_categories: Option<String>,
|
||||
|
||||
/// Include user uploaded files in the results.
|
||||
/// By default, we do not return them unless this flag is set to true.
|
||||
pub include_user_uploads: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListMediaFilesForUserV2SuccessResponse {
|
||||
pub success: bool,
|
||||
pub results: Vec<MediaFileForUserListItem>,
|
||||
pub pagination: UserMediaFileV2Pagination,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct UserMediaFileV2Pagination {
|
||||
pub current: usize,
|
||||
pub has_more: bool,
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
pub mod by_type;
|
||||
pub mod list_media_files_for_user_v2;
|
||||
pub mod user_media_file_list_item;
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_model_type::MediaFileOriginModelType;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::visibility::Visibility;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::common::responses::legacy_media_file_cover_image_details::LegacyMediaFileCoverImageDetails;
|
||||
use crate::common::responses::media_file_origin_details::MediaFileOriginDetails;
|
||||
use crate::common::responses::media_links::MediaLinks;
|
||||
use crate::common::responses::simple_entity_stats::SimpleEntityStats;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct MediaFileForUserListItem {
|
||||
pub token: MediaFileToken,
|
||||
|
||||
/// The coarse-grained class of media file: image, video, etc.
|
||||
pub media_class: MediaFileClass,
|
||||
|
||||
/// Type of media will dictate which fields are populated and what
|
||||
/// the frontend should display (eg. video player vs audio player).
|
||||
/// This is closer in meaning to a "mime type".
|
||||
pub media_type: MediaFileType,
|
||||
|
||||
/// If this is an engine/3D asset, this is the broad category (scene,
|
||||
/// animation, etc.) of that object.
|
||||
/// This can also be used for filtering in list/batch endpoints.
|
||||
pub maybe_engine_category: Option<MediaFileEngineCategory>,
|
||||
|
||||
/// If this is an engine/3D asset for an animation or a rig that can
|
||||
/// be animated with either (or both) skeletal or blend shape animations,
|
||||
/// this describes the animation regime used or supported.
|
||||
pub maybe_animation_type: Option<MediaFileAnimationType>,
|
||||
|
||||
/// Details where the media file came from.
|
||||
pub origin: MediaFileOriginDetails,
|
||||
|
||||
#[deprecated(note="Use MediaFileOriginDetails instead")]
|
||||
pub origin_category: MediaFileOriginCategory,
|
||||
|
||||
#[deprecated(note="Use MediaFileOriginDetails instead")]
|
||||
pub origin_product_category: MediaFileOriginProductCategory,
|
||||
|
||||
#[deprecated(note="Use MediaFileOriginDetails instead")]
|
||||
pub maybe_origin_model_type: Option<MediaFileOriginModelType>,
|
||||
|
||||
#[deprecated(note="Use MediaFileOriginDetails instead")]
|
||||
pub maybe_origin_model_token: Option<String>,
|
||||
|
||||
/// (DEPRECATED) URL path to the media file
|
||||
#[deprecated(note="This field doesn't point to the full URL. Use media_links instead to leverage the CDN.")]
|
||||
pub public_bucket_path: String,
|
||||
|
||||
/// (DEPRECATED) Full URL to the media file
|
||||
#[deprecated(note="This points to the bucket. Use media_links instead to leverage the CDN.")]
|
||||
pub public_bucket_url: String,
|
||||
|
||||
/// Rich CDN links to the media, including thumbnails, previews, and more.
|
||||
pub media_links: MediaLinks,
|
||||
|
||||
/// The foreign key to the prompt used to generate the media, if applicable.
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
/// Information about the cover image. Many media files do not require a cover image,
|
||||
/// e.g. image files, video files with thumbnails, audio files, etc.
|
||||
/// 3D files require them.
|
||||
pub cover_image: LegacyMediaFileCoverImageDetails,
|
||||
|
||||
pub creator_set_visibility: Visibility,
|
||||
|
||||
/// The file was uploaded by the user.
|
||||
/// This does not include files generated on the client side, like studio renders.
|
||||
pub is_user_upload: bool,
|
||||
|
||||
/// The file was created by the system.
|
||||
/// This includes files generated on the client side, like studio renders.
|
||||
pub is_intermediate_system_file: bool,
|
||||
|
||||
/// The name or title of the media file (optional)
|
||||
pub maybe_title: Option<String>,
|
||||
|
||||
/// Text transcripts for TTS, etc.
|
||||
pub maybe_text_transcript: Option<String>,
|
||||
|
||||
/// For Comfy / Video Style Transfer jobs, this might include
|
||||
/// the name of the selected style.
|
||||
pub maybe_style_name: Option<StyleTransferName>,
|
||||
|
||||
/// Duration for audio and video files, if available.
|
||||
/// Measured in milliseconds.
|
||||
pub maybe_duration_millis: Option<u64>,
|
||||
|
||||
/// Statistics about the media file
|
||||
pub stats: SimpleEntityStats,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -20,6 +20,7 @@ normal = ["workspace-build-acceleration"]
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
domains.workspace = true
|
||||
server_environment.workspace = true
|
||||
|
||||
# External
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
use actix_cors::Cors;
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::artcraft::ARTCRAFT_2D;
|
||||
use domains::storyteller::STORYTELLER_2D;
|
||||
|
||||
pub fn add_artcraft_2d(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://2d.storyteller.ai")
|
||||
.allowed_origin("https://2d.getartcraft.com")
|
||||
.allowed_origin(STORYTELLER_2D.https_link_without_path())
|
||||
.allowed_origin(ARTCRAFT_2D.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-2d.netlify.app")
|
||||
@@ -15,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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
use actix_cors::Cors;
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::artcraft::ARTCRAFT_3D;
|
||||
use domains::storyteller::STORYTELLER_3D;
|
||||
|
||||
pub fn add_artcraft_3d(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://3d.storyteller.ai")
|
||||
.allowed_origin("https://3d.getartcraft.com")
|
||||
.allowed_origin(STORYTELLER_3D.https_link_without_path())
|
||||
.allowed_origin(ARTCRAFT_3D.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-3d.netlify.app")
|
||||
@@ -18,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use actix_cors::Cors;
|
||||
use domains::artcraft::ARTCRAFT_DESKTOP;
|
||||
|
||||
pub fn add_artcraft_desktop(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Stable identity sent by the Artcraft native HTTP bridge (dev and packaged apps).
|
||||
.allowed_origin(ARTCRAFT_DESKTOP.https_link_without_path())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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;
|
||||
use crate::testing::{assert_origin_invalid, assert_origin_ok};
|
||||
|
||||
const DESKTOP_ORIGIN: &str = "https://desktop.getartcraft.com";
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn desktop_requests_and_preflights_allow_credentials() {
|
||||
for environment in [ServerEnvironment::Production, ServerEnvironment::Development] {
|
||||
let service = build_cors_config(environment)
|
||||
.new_transform(test::ok_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for method in [Method::GET, Method::POST, Method::OPTIONS] {
|
||||
let mut request = TestRequest::default()
|
||||
.uri("/v1/session")
|
||||
.insert_header((header::ORIGIN, DESKTOP_ORIGIN));
|
||||
if method == Method::OPTIONS {
|
||||
request = request
|
||||
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
|
||||
.insert_header((header::ACCESS_CONTROL_REQUEST_HEADERS, "content-type,session,x-artcraft-version"));
|
||||
}
|
||||
let response = test::call_service(&service, request.method(method).to_srv_request()).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(), DESKTOP_ORIGIN);
|
||||
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_CREDENTIALS).unwrap(), "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn desktop_origin_is_an_exact_https_allowlist_entry() {
|
||||
let cors = build_cors_config(ServerEnvironment::Production);
|
||||
for origin in [
|
||||
"http://desktop.getartcraft.com",
|
||||
"https://desktop.getartcraft.com:8443",
|
||||
"https://desktop.getartcraft.com.example.com",
|
||||
"https://sub.desktop.getartcraft.com",
|
||||
] {
|
||||
assert_origin_invalid(&cors, origin).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn existing_desktop_clients_remain_allowed() {
|
||||
let cors = build_cors_config(ServerEnvironment::Production);
|
||||
for origin in ["https://studio.storyteller.ai", "http://tauri.localhost", "tauri://localhost"] {
|
||||
assert_origin_ok(&cors, origin).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,34 @@
|
||||
use actix_cors::Cors;
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::artcraft::ARTCRAFT_WEBAPP;
|
||||
|
||||
pub fn add_artcraft_webapp(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Actual domains
|
||||
.allowed_origin("https://app.getartcraft.com")
|
||||
.allowed_origin(ARTCRAFT_WEBAPP.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
use actix_cors::Cors;
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::artcraft::ARTCRAFT_AI;
|
||||
use domains::artcraft::ARTCRAFT_AI_WWW;
|
||||
use domains::artcraft::ARTCRAFT_WEBSITE;
|
||||
use domains::artcraft::ARTCRAFT_WEBSITE_WWW;
|
||||
|
||||
pub fn add_artcraft_website(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Actual domains
|
||||
.allowed_origin("https://getartcraft.com")
|
||||
.allowed_origin("https://www.getartcraft.com")
|
||||
.allowed_origin(ARTCRAFT_WEBSITE.https_link_without_path())
|
||||
.allowed_origin(ARTCRAFT_WEBSITE_WWW.https_link_without_path())
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://artcraft.ai")
|
||||
.allowed_origin("https://www.artcraft.ai")
|
||||
.allowed_origin(ARTCRAFT_AI.https_link_without_path())
|
||||
.allowed_origin(ARTCRAFT_AI_WWW.https_link_without_path())
|
||||
// Development against production
|
||||
.allowed_origin("http://localhost:4200")
|
||||
.allowed_origin("http://localhost:4201")
|
||||
@@ -18,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)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
use actix_cors::Cors;
|
||||
use log::warn;
|
||||
use url::{Host, Url};
|
||||
use domains::fakeyou::FAKEYOU;
|
||||
use domains::fakeyou::FAKEYOU_API;
|
||||
use domains::fakeyou::FAKEYOU_ENGINE;
|
||||
use domains::fakeyou::FAKEYOU_STAGING;
|
||||
|
||||
pub fn add_fakeyou(cors: Cors, is_production: bool) -> Cors {
|
||||
if is_production {
|
||||
cors
|
||||
// Storyteller Engine (Production)
|
||||
.allowed_origin("https://engine.fakeyou.com")
|
||||
.allowed_origin(FAKEYOU_ENGINE.https_link_without_path())
|
||||
// FakeYou (Production)
|
||||
.allowed_origin("https://api.fakeyou.com")
|
||||
.allowed_origin("https://fakeyou.com")
|
||||
.allowed_origin(FAKEYOU_API.https_link_without_path())
|
||||
.allowed_origin(FAKEYOU.https_link_without_path())
|
||||
// FakeYou (Staging)
|
||||
.allowed_origin("https://staging.fakeyou.com")
|
||||
.allowed_origin(FAKEYOU_STAGING.https_link_without_path())
|
||||
// Allow Netlify domains within "fakeyou" project.
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
let maybe_url = origin.to_str()
|
||||
@@ -50,7 +54,7 @@ pub fn add_fakeyou(cors: Cors, is_production: bool) -> Cors {
|
||||
.allowed_origin("https://dev.fakeyou.com:7000") // Yarn default port
|
||||
.allowed_origin("https://dev.fakeyou.com:7001") // NB: Mac frontend
|
||||
// Storyteller Engine (Development)
|
||||
.allowed_origin("https://engine.fakeyou.com") // NB: We use prod for integration testing
|
||||
.allowed_origin(FAKEYOU_ENGINE.https_link_without_path()) // NB: We use prod for integration testing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub (crate) mod artcraft_admin_dashboard;
|
||||
pub (crate) mod artcraft_2d;
|
||||
pub (crate) mod artcraft_3d;
|
||||
pub (crate) mod artcraft_desktop;
|
||||
pub (crate) mod artcraft_webapp;
|
||||
pub (crate) mod artcraft_website;
|
||||
pub (crate) mod development_only;
|
||||
@@ -11,4 +12,4 @@ pub (crate) mod storyteller;
|
||||
pub (crate) mod storyteller_board;
|
||||
pub (crate) mod storyteller_render;
|
||||
pub (crate) mod storyteller_studio;
|
||||
pub (crate) mod tauri;
|
||||
pub (crate) mod tauri;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,26 @@
|
||||
use actix_cors::Cors;
|
||||
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::storyteller::STORYTELLER;
|
||||
use domains::storyteller::STORYTELLER_API;
|
||||
use domains::storyteller::STORYTELLER_ENGINE;
|
||||
use domains::storyteller::STORYTELLER_STAGING;
|
||||
use domains::storyteller::STORYTELLER_STUDIO;
|
||||
|
||||
pub fn add_storyteller(cors: Cors, is_production: bool) -> Cors {
|
||||
if is_production {
|
||||
cors
|
||||
// Storyteller Engine (Production)
|
||||
.allowed_origin("https://engine.storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_ENGINE.https_link_without_path())
|
||||
// Storyteller Studio (Production)
|
||||
.allowed_origin("https://studio.storyteller.ai")
|
||||
// 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("https://api.storyteller.ai")
|
||||
.allowed_origin("https://storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_API.https_link_without_path())
|
||||
.allowed_origin(STORYTELLER.https_link_without_path())
|
||||
// Storyteller.ai (Staging)
|
||||
.allowed_origin("https://staging.storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_STAGING.https_link_without_path())
|
||||
// Allow Netlify domains within "storyteller-ai" project.
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-ai.netlify.app")
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use actix_cors::Cors;
|
||||
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::storyteller::STORYTELLER_BOARD;
|
||||
|
||||
pub fn add_storyteller_board(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://memeboard.ai")
|
||||
.allowed_origin("https://dingboard.ai")
|
||||
.allowed_origin("https://board.storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_BOARD.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-board.netlify.app")
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use actix_cors::Cors;
|
||||
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::storyteller::STORYTELLER_RENDER;
|
||||
|
||||
pub fn add_storyteller_render(cors: Cors, _is_production: bool) -> Cors {
|
||||
cors
|
||||
// Hypothetical domains
|
||||
.allowed_origin("https://render.storyteller.ai")
|
||||
.allowed_origin(STORYTELLER_RENDER.https_link_without_path())
|
||||
// Netlify project
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-render.netlify.app")
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
use actix_cors::Cors;
|
||||
|
||||
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
|
||||
use domains::storyteller::STORYTELLER_ANIMATE;
|
||||
use domains::storyteller::STORYTELLER_STUDIO;
|
||||
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("https://studio.storyteller.ai")
|
||||
.allowed_origin("https://studio-staging.studio.storyteller.ai")
|
||||
.allowed_origin("https://studio-testing.studio.storyteller.ai")
|
||||
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| {
|
||||
netlify_branch_domain_matches(origin, "storytellerstudio.netlify.app")
|
||||
})
|
||||
@@ -13,7 +20,7 @@ pub fn add_storyteller_studio(cors: Cors, _is_production: bool) -> Cors {
|
||||
netlify_branch_domain_matches(origin, "pipeline-gottagofast.netlify.app")
|
||||
})
|
||||
.allowed_origin("http://localhost:5173")
|
||||
.allowed_origin("https://animate.storyteller.ai") // NB: Gen2 Studio
|
||||
.allowed_origin(STORYTELLER_ANIMATE.https_link_without_path()) // NB: Gen2 Studio
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,5 +5,90 @@ 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")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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;
|
||||
use crate::testing::assert_origin_invalid;
|
||||
|
||||
const WINDOWS_ORIGIN: &str = "http://tauri.localhost";
|
||||
const MAC_ORIGIN: &str = "tauri://localhost";
|
||||
|
||||
mod windows {
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn requests_and_preflights_allow_credentials() {
|
||||
assert_requests_and_preflights_allow_credentials(WINDOWS_ORIGIN).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn production_rejects_origin_variants() {
|
||||
let cors = build_cors_config(ServerEnvironment::Production);
|
||||
for origin in [
|
||||
"https://tauri.localhost",
|
||||
"http://tauri.localhost:8080",
|
||||
"http://tauri.localhost.example.com",
|
||||
"http://sub.tauri.localhost",
|
||||
] {
|
||||
assert_origin_invalid(&cors, origin).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod mac {
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn requests_and_preflights_allow_credentials() {
|
||||
assert_requests_and_preflights_allow_credentials(MAC_ORIGIN).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn production_rejects_origin_variants() {
|
||||
let cors = build_cors_config(ServerEnvironment::Production);
|
||||
for origin in [
|
||||
"http://localhost",
|
||||
"https://localhost",
|
||||
"tauri://localhost:8080",
|
||||
"tauri://localhost.example.com",
|
||||
"tauri://sub.localhost",
|
||||
] {
|
||||
assert_origin_invalid(&cors, origin).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn assert_requests_and_preflights_allow_credentials(origin: &str) {
|
||||
for environment in [ServerEnvironment::Production, ServerEnvironment::Development] {
|
||||
let service = build_cors_config(environment)
|
||||
.new_transform(test::ok_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for method in [Method::GET, Method::POST, Method::OPTIONS] {
|
||||
let mut request = TestRequest::default()
|
||||
.uri("/v1/session")
|
||||
.insert_header((header::ORIGIN, origin));
|
||||
if method == Method::OPTIONS {
|
||||
request = request
|
||||
.insert_header((header::ACCESS_CONTROL_REQUEST_METHOD, "POST"))
|
||||
.insert_header((header::ACCESS_CONTROL_REQUEST_HEADERS, "content-type,session,x-artcraft-version"));
|
||||
}
|
||||
let response = test::call_service(&service, request.method(method).to_srv_request()).await;
|
||||
assert_eq!(response.status(), StatusCode::OK, "Origin: {}", origin);
|
||||
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(), origin);
|
||||
assert_eq!(response.headers().get(header::ACCESS_CONTROL_ALLOW_CREDENTIALS).unwrap(), "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use log::info;
|
||||
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::development_only::add_development_only;
|
||||
@@ -48,6 +49,7 @@ fn do_build_cors_config(is_production: bool) -> Cors {
|
||||
|
||||
// Artcraft
|
||||
cors = add_tauri(cors, is_production);
|
||||
cors = add_artcraft_desktop(cors, is_production);
|
||||
cors = add_artcraft_2d(cors, is_production);
|
||||
cors = add_artcraft_3d(cors, is_production);
|
||||
cors = add_artcraft_webapp(cors, is_production);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "domains"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "domains"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Adds `Domain::https_url()`, returning a cached `&'static url::Url`.
|
||||
url = ["dep:url"]
|
||||
|
||||
# No dependencies by default: the domain constants and their string forms are
|
||||
# built entirely at compile time.
|
||||
[dependencies]
|
||||
url = { workspace = true, optional = true }
|
||||
@@ -0,0 +1,69 @@
|
||||
//! ArtCraft domains.
|
||||
|
||||
use crate::{Domain, domain};
|
||||
|
||||
/// Public marketing website (Next.js).
|
||||
pub const ARTCRAFT_WEBSITE: Domain = domain!("getartcraft.com");
|
||||
pub const ARTCRAFT_WEBSITE_WWW: Domain = domain!("www.getartcraft.com");
|
||||
|
||||
/// Browser web application.
|
||||
pub const ARTCRAFT_WEBAPP: Domain = domain!("app.getartcraft.com");
|
||||
|
||||
/// Desktop app landing page.
|
||||
pub const ARTCRAFT_DESKTOP: Domain = domain!("desktop.getartcraft.com");
|
||||
|
||||
/// HTTP API.
|
||||
pub const ARTCRAFT_API: Domain = domain!("api.getartcraft.com");
|
||||
|
||||
pub const ARTCRAFT_2D: Domain = domain!("2d.getartcraft.com");
|
||||
pub const ARTCRAFT_3D: Domain = domain!("3d.getartcraft.com");
|
||||
|
||||
/// Alternate marketing domain.
|
||||
pub const ARTCRAFT_AI: Domain = domain!("artcraft.ai");
|
||||
pub const ARTCRAFT_AI_WWW: Domain = domain!("www.artcraft.ai");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn website() {
|
||||
assert_eq!(ARTCRAFT_WEBSITE.https_link_without_path(), "https://getartcraft.com");
|
||||
assert_eq!(ARTCRAFT_WEBSITE_WWW.https_link_without_path(), "https://www.getartcraft.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webapp() {
|
||||
assert_eq!(ARTCRAFT_WEBAPP.https_link_without_path(), "https://app.getartcraft.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop() {
|
||||
assert_eq!(ARTCRAFT_DESKTOP.bare_domain(), "desktop.getartcraft.com");
|
||||
assert_eq!(ARTCRAFT_DESKTOP.https_link_without_path(), "https://desktop.getartcraft.com");
|
||||
assert_eq!(ARTCRAFT_DESKTOP.https_link_with_root_path(), "https://desktop.getartcraft.com/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api() {
|
||||
assert_eq!(ARTCRAFT_API.https_link_without_path(), "https://api.getartcraft.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_d_and_three_d() {
|
||||
assert_eq!(ARTCRAFT_2D.https_link_without_path(), "https://2d.getartcraft.com");
|
||||
assert_eq!(ARTCRAFT_3D.https_link_without_path(), "https://3d.getartcraft.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artcraft_ai() {
|
||||
assert_eq!(ARTCRAFT_AI.https_link_without_path(), "https://artcraft.ai");
|
||||
assert_eq!(ARTCRAFT_AI_WWW.https_link_without_path(), "https://www.artcraft.ai");
|
||||
}
|
||||
|
||||
#[cfg(feature = "url")]
|
||||
#[test]
|
||||
fn desktop_url() {
|
||||
assert_eq!(ARTCRAFT_DESKTOP.https_url().as_str(), "https://desktop.getartcraft.com/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/// A validated domain name, with its `https://` link forms precomputed at
|
||||
/// compile time. Build one with the [`domain!`](crate::domain) macro:
|
||||
///
|
||||
/// ```
|
||||
/// use domains::{domain, Domain};
|
||||
///
|
||||
/// const EXAMPLE: Domain = domain!("desktop.example.com");
|
||||
///
|
||||
/// assert_eq!(EXAMPLE.bare_domain(), "desktop.example.com");
|
||||
/// assert_eq!(EXAMPLE.https_link_without_path(), "https://desktop.example.com");
|
||||
/// assert_eq!(EXAMPLE.https_link_with_root_path(), "https://desktop.example.com/");
|
||||
/// ```
|
||||
///
|
||||
/// An invalid literal fails to compile:
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// const BAD: domains::Domain = domains::domain!("https://example.com");
|
||||
/// ```
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Domain {
|
||||
bare: &'static str,
|
||||
https_without_path: &'static str,
|
||||
https_with_root_path: &'static str,
|
||||
}
|
||||
|
||||
/// Declares a [`Domain`] from a string literal. Validation runs at compile
|
||||
/// time: an invalid domain is a build error, never a runtime panic.
|
||||
#[macro_export]
|
||||
macro_rules! domain {
|
||||
($domain:literal) => {
|
||||
$crate::Domain::__from_macro(
|
||||
$domain,
|
||||
::core::concat!("https://", $domain),
|
||||
::core::concat!("https://", $domain, "/"),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
impl Domain {
|
||||
/// Only for the `domain!` macro, which guarantees the three strings agree.
|
||||
/// Panics (a compile error in const context) on an invalid domain.
|
||||
#[doc(hidden)]
|
||||
pub const fn __from_macro(
|
||||
bare: &'static str,
|
||||
https_without_path: &'static str,
|
||||
https_with_root_path: &'static str,
|
||||
) -> Self {
|
||||
crate::validate::assert_valid_domain(bare);
|
||||
Self { bare, https_without_path, https_with_root_path }
|
||||
}
|
||||
|
||||
/// eg. `desktop.getartcraft.com`
|
||||
pub const fn bare_domain(&self) -> &'static str {
|
||||
self.bare
|
||||
}
|
||||
|
||||
/// eg. `https://desktop.getartcraft.com`. This is also the browser `Origin`
|
||||
/// header value, so use it for CORS and origin allowlists.
|
||||
pub const fn https_link_without_path(&self) -> &'static str {
|
||||
self.https_without_path
|
||||
}
|
||||
|
||||
/// eg. `https://desktop.getartcraft.com/`
|
||||
pub const fn https_link_with_root_path(&self) -> &'static str {
|
||||
self.https_with_root_path
|
||||
}
|
||||
|
||||
/// eg. `https://desktop.getartcraft.com/` as a `Url`.
|
||||
///
|
||||
/// Parsed on first use per domain, then served from a process-wide cache,
|
||||
/// so every call returns the same `&'static Url`.
|
||||
#[cfg(feature = "url")]
|
||||
pub fn https_url(&self) -> &'static url::Url {
|
||||
crate::url_cache::https_url(self.bare, self.https_with_root_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Domain {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Domain({})", self.bare)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Domain {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.bare)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Domain;
|
||||
|
||||
const TEST_DOMAIN: Domain = domain!("desktop.example.com");
|
||||
|
||||
#[test]
|
||||
fn bare_domain() {
|
||||
assert_eq!(TEST_DOMAIN.bare_domain(), "desktop.example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_links() {
|
||||
assert_eq!(TEST_DOMAIN.https_link_without_path(), "https://desktop.example.com");
|
||||
assert_eq!(TEST_DOMAIN.https_link_with_root_path(), "https://desktop.example.com/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usable_in_const_context() {
|
||||
const ORIGINS: &[&str] = &[TEST_DOMAIN.https_link_without_path()];
|
||||
assert_eq!(ORIGINS, &["https://desktop.example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_and_debug() {
|
||||
assert_eq!(TEST_DOMAIN.to_string(), "desktop.example.com");
|
||||
assert_eq!(format!("{TEST_DOMAIN:?}"), "Domain(desktop.example.com)");
|
||||
}
|
||||
|
||||
#[cfg(feature = "url")]
|
||||
mod url_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn https_url_has_root_path() {
|
||||
let url = TEST_DOMAIN.https_url();
|
||||
assert_eq!(url.as_str(), "https://desktop.example.com/");
|
||||
assert_eq!(url.scheme(), "https");
|
||||
assert_eq!(url.host_str(), Some("desktop.example.com"));
|
||||
assert_eq!(url.path(), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_url_is_parsed_once_and_shared() {
|
||||
let first = TEST_DOMAIN.https_url();
|
||||
let second = TEST_DOMAIN.https_url();
|
||||
assert!(std::ptr::eq(first, second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_url_is_shared_across_threads() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<Domain>();
|
||||
assert_send_sync::<&'static url::Url>();
|
||||
|
||||
// Borrowed into 'static threads with no Arc or cloning; every thread
|
||||
// sees the same parsed instance.
|
||||
let main_thread_url = TEST_DOMAIN.https_url();
|
||||
let handles: Vec<_> = (0..8)
|
||||
.map(|_| std::thread::spawn(|| TEST_DOMAIN.https_url()))
|
||||
.collect();
|
||||
for handle in handles {
|
||||
assert!(std::ptr::eq(handle.join().unwrap(), main_thread_url));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_url_matches_string_forms() {
|
||||
let url = TEST_DOMAIN.https_url();
|
||||
assert_eq!(url.as_str(), TEST_DOMAIN.https_link_with_root_path());
|
||||
assert_eq!(url.origin().ascii_serialization(), TEST_DOMAIN.https_link_without_path());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//! FakeYou domains.
|
||||
|
||||
use crate::{Domain, domain};
|
||||
|
||||
/// Main website.
|
||||
pub const FAKEYOU: Domain = domain!("fakeyou.com");
|
||||
|
||||
/// HTTP API.
|
||||
pub const FAKEYOU_API: Domain = domain!("api.fakeyou.com");
|
||||
|
||||
/// Engine frontend (also used against production for integration testing).
|
||||
pub const FAKEYOU_ENGINE: Domain = domain!("engine.fakeyou.com");
|
||||
|
||||
pub const FAKEYOU_STAGING: Domain = domain!("staging.fakeyou.com");
|
||||
|
||||
/// Local development hosts (resolve to a developer machine).
|
||||
pub const FAKEYOU_DEV: Domain = domain!("dev.fakeyou.com");
|
||||
pub const FAKEYOU_DEV_PROXY: Domain = domain!("devproxy.fakeyou.com");
|
||||
|
||||
/// Production media CDN. Serves both FakeYou and Storyteller media.
|
||||
pub const FAKEYOU_CDN: Domain = domain!("cdn-2.fakeyou.com");
|
||||
|
||||
/// Development media CDN (a Cloudflare R2 public bucket). Serves both FakeYou
|
||||
/// and Storyteller development media, like `FAKEYOU_CDN` does in production.
|
||||
pub const FAKEYOU_DEV_CDN: Domain = domain!("pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn website() {
|
||||
assert_eq!(FAKEYOU.bare_domain(), "fakeyou.com");
|
||||
assert_eq!(FAKEYOU.https_link_without_path(), "https://fakeyou.com");
|
||||
assert_eq!(FAKEYOU.https_link_with_root_path(), "https://fakeyou.com/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api() {
|
||||
assert_eq!(FAKEYOU_API.https_link_without_path(), "https://api.fakeyou.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_and_staging() {
|
||||
assert_eq!(FAKEYOU_ENGINE.https_link_without_path(), "https://engine.fakeyou.com");
|
||||
assert_eq!(FAKEYOU_STAGING.https_link_without_path(), "https://staging.fakeyou.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn development() {
|
||||
assert_eq!(FAKEYOU_DEV.bare_domain(), "dev.fakeyou.com");
|
||||
assert_eq!(FAKEYOU_DEV_PROXY.bare_domain(), "devproxy.fakeyou.com");
|
||||
}
|
||||
|
||||
mod cdn {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn production() {
|
||||
assert_eq!(FAKEYOU_CDN.bare_domain(), "cdn-2.fakeyou.com");
|
||||
assert_eq!(FAKEYOU_CDN.https_link_without_path(), "https://cdn-2.fakeyou.com");
|
||||
assert_eq!(FAKEYOU_CDN.https_link_with_root_path(), "https://cdn-2.fakeyou.com/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn development() {
|
||||
assert_eq!(FAKEYOU_DEV_CDN.https_link_without_path(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev");
|
||||
}
|
||||
|
||||
#[cfg(feature = "url")]
|
||||
#[test]
|
||||
fn urls() {
|
||||
assert_eq!(FAKEYOU_CDN.https_url().as_str(), "https://cdn-2.fakeyou.com/");
|
||||
assert_eq!(FAKEYOU_DEV_CDN.https_url().as_str(), "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! First-party domain names, validated at compile time.
|
||||
//!
|
||||
//! ```
|
||||
//! use domains::artcraft::ARTCRAFT_DESKTOP;
|
||||
//!
|
||||
//! assert_eq!(ARTCRAFT_DESKTOP.https_link_without_path(), "https://desktop.getartcraft.com");
|
||||
//! ```
|
||||
//!
|
||||
//! Enable the `url` feature for `Domain::https_url()`, a cached `&'static url::Url`.
|
||||
|
||||
pub mod artcraft;
|
||||
pub mod fakeyou;
|
||||
pub mod storyteller;
|
||||
|
||||
mod domain;
|
||||
mod validate;
|
||||
|
||||
#[cfg(feature = "url")]
|
||||
mod url_cache;
|
||||
|
||||
pub use domain::Domain;
|
||||
@@ -0,0 +1,80 @@
|
||||
//! Storyteller domains. Storyteller media is served from `fakeyou::FAKEYOU_CDN`.
|
||||
|
||||
use crate::{Domain, domain};
|
||||
|
||||
/// Main website.
|
||||
pub const STORYTELLER: Domain = domain!("storyteller.ai");
|
||||
|
||||
/// HTTP API.
|
||||
pub const STORYTELLER_API: Domain = domain!("api.storyteller.ai");
|
||||
|
||||
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");
|
||||
|
||||
/// Gen2 Studio.
|
||||
pub const STORYTELLER_ANIMATE: Domain = domain!("animate.storyteller.ai");
|
||||
|
||||
pub const STORYTELLER_BOARD: Domain = domain!("board.storyteller.ai");
|
||||
pub const STORYTELLER_RENDER: Domain = domain!("render.storyteller.ai");
|
||||
|
||||
/// Legacy hosts for the ArtCraft 2D and 3D apps.
|
||||
pub const STORYTELLER_2D: Domain = domain!("2d.storyteller.ai");
|
||||
pub const STORYTELLER_3D: Domain = domain!("3d.storyteller.ai");
|
||||
|
||||
/// Local development hosts (resolve to a developer machine).
|
||||
pub const STORYTELLER_DEV: Domain = domain!("dev.storyteller.ai");
|
||||
pub const STORYTELLER_DEV_PROXY: Domain = domain!("devproxy.storyteller.ai");
|
||||
|
||||
/// Storyteller Studio static assets.
|
||||
pub const STORYTELLER_CDN: Domain = domain!("cdn.storyteller.ai");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn website() {
|
||||
assert_eq!(STORYTELLER.bare_domain(), "storyteller.ai");
|
||||
assert_eq!(STORYTELLER.https_link_without_path(), "https://storyteller.ai");
|
||||
assert_eq!(STORYTELLER.https_link_with_root_path(), "https://storyteller.ai/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api() {
|
||||
assert_eq!(STORYTELLER_API.https_link_without_path(), "https://api.storyteller.ai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn studio() {
|
||||
assert_eq!(STORYTELLER_STUDIO.https_link_without_path(), "https://studio.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_STUDIO_STAGING.https_link_without_path(), "https://studio-staging.studio.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_STUDIO_TESTING.https_link_without_path(), "https://studio-testing.studio.storyteller.ai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apps() {
|
||||
assert_eq!(STORYTELLER_BOARD.https_link_without_path(), "https://board.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_RENDER.https_link_without_path(), "https://render.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_2D.https_link_without_path(), "https://2d.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_3D.https_link_without_path(), "https://3d.storyteller.ai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn development() {
|
||||
assert_eq!(STORYTELLER_DEV.bare_domain(), "dev.storyteller.ai");
|
||||
assert_eq!(STORYTELLER_DEV_PROXY.bare_domain(), "devproxy.storyteller.ai");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cdn() {
|
||||
assert_eq!(STORYTELLER_CDN.https_link_without_path(), "https://cdn.storyteller.ai");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//! Process-wide cache backing `Domain::https_url()`.
|
||||
//!
|
||||
//! `Url::parse` can't run at compile time, and `Domain` stays free of
|
||||
//! interior mutability so its constants remain usable in `const` contexts.
|
||||
//! So each domain's `Url` is parsed on first use and leaked into this map.
|
||||
//! Domains only come from `domain!` literals, so the set (and the leak) is
|
||||
//! bounded by the constants in the source code.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use url::Url;
|
||||
|
||||
static URLS: Mutex<BTreeMap<&'static str, &'static Url>> = Mutex::new(BTreeMap::new());
|
||||
|
||||
pub(crate) fn https_url(bare: &'static str, https_with_root_path: &'static str) -> &'static Url {
|
||||
// The map is only ever inserted into, so a poisoned lock still holds
|
||||
// valid entries.
|
||||
let mut urls = URLS.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
urls.entry(bare).or_insert_with(|| {
|
||||
// The domain was validated at compile time, so this parse can't fail.
|
||||
let url = Url::parse(https_with_root_path).expect("compile-time validated domain should parse");
|
||||
Box::leak(Box::new(url))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//! Compile-time domain name validation.
|
||||
//!
|
||||
//! `assert_valid_domain` is a `const fn` that panics on an invalid name. The
|
||||
//! `domain!` macro calls it while evaluating a `const`/`static`, where a panic
|
||||
//! is a compile error, so a bad domain literal never builds.
|
||||
|
||||
const MAX_DOMAIN_LEN: usize = 253;
|
||||
const MAX_LABEL_LEN: usize = 63;
|
||||
|
||||
/// Panics (a compile error in const context) unless `domain` is a canonical,
|
||||
/// fully-qualified DNS name: lowercase `a-z`, `0-9` and `-` labels separated
|
||||
/// by single dots, at least two labels, no scheme, port, path or trailing dot,
|
||||
/// and a non-numeric top-level label (so IP addresses are rejected).
|
||||
pub const fn assert_valid_domain(domain: &str) {
|
||||
match check_domain(domain) {
|
||||
Ok(()) => {}
|
||||
Err(DomainError::Empty) => panic!("invalid domain: empty"),
|
||||
Err(DomainError::TooLong) => panic!("invalid domain: longer than 253 bytes"),
|
||||
Err(DomainError::EmptyLabel) => panic!("invalid domain: empty label (leading, trailing or doubled dot)"),
|
||||
Err(DomainError::LabelTooLong) => panic!("invalid domain: label longer than 63 bytes"),
|
||||
Err(DomainError::InvalidCharacter) => panic!("invalid domain: only lowercase a-z, 0-9, '-' and '.' are allowed"),
|
||||
Err(DomainError::HyphenAtLabelEdge) => panic!("invalid domain: label starts or ends with '-'"),
|
||||
Err(DomainError::SingleLabel) => panic!("invalid domain: needs at least two labels (eg. example.com)"),
|
||||
Err(DomainError::NumericTopLevelLabel) => panic!("invalid domain: top-level label is numeric (IP addresses are not domains)"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum DomainError {
|
||||
Empty,
|
||||
TooLong,
|
||||
EmptyLabel,
|
||||
LabelTooLong,
|
||||
InvalidCharacter,
|
||||
HyphenAtLabelEdge,
|
||||
SingleLabel,
|
||||
NumericTopLevelLabel,
|
||||
}
|
||||
|
||||
pub(crate) const fn check_domain(domain: &str) -> Result<(), DomainError> {
|
||||
let bytes = domain.as_bytes();
|
||||
if bytes.is_empty() {
|
||||
return Err(DomainError::Empty);
|
||||
}
|
||||
if bytes.len() > MAX_DOMAIN_LEN {
|
||||
return Err(DomainError::TooLong);
|
||||
}
|
||||
|
||||
let mut labels = 0;
|
||||
let mut label_start = 0;
|
||||
let mut last_label_start = 0;
|
||||
let mut i = 0;
|
||||
// Walk one past the end so the final label is checked like the others.
|
||||
while i <= bytes.len() {
|
||||
if i == bytes.len() || bytes[i] == b'.' {
|
||||
match check_label(bytes, label_start, i) {
|
||||
Ok(()) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
labels += 1;
|
||||
last_label_start = label_start;
|
||||
label_start = i + 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if labels < 2 {
|
||||
return Err(DomainError::SingleLabel);
|
||||
}
|
||||
if is_all_digits(bytes, last_label_start, bytes.len()) {
|
||||
return Err(DomainError::NumericTopLevelLabel);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks `bytes[start..end]` as a single label.
|
||||
const fn check_label(bytes: &[u8], start: usize, end: usize) -> Result<(), DomainError> {
|
||||
let len = end - start;
|
||||
if len == 0 {
|
||||
return Err(DomainError::EmptyLabel);
|
||||
}
|
||||
if len > MAX_LABEL_LEN {
|
||||
return Err(DomainError::LabelTooLong);
|
||||
}
|
||||
let mut i = start;
|
||||
while i < end {
|
||||
match bytes[i] {
|
||||
b'a'..=b'z' | b'0'..=b'9' | b'-' => {}
|
||||
_ => return Err(DomainError::InvalidCharacter),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if bytes[start] == b'-' || bytes[end - 1] == b'-' {
|
||||
return Err(DomainError::HyphenAtLabelEdge);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn is_all_digits(bytes: &[u8], start: usize, end: usize) -> bool {
|
||||
let mut i = start;
|
||||
while i < end {
|
||||
if !bytes[i].is_ascii_digit() {
|
||||
return false;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DomainError::*;
|
||||
use super::check_domain;
|
||||
|
||||
mod valid {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apex_domain() {
|
||||
assert_eq!(check_domain("getartcraft.com"), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subdomains() {
|
||||
assert_eq!(check_domain("desktop.getartcraft.com"), Ok(()));
|
||||
assert_eq!(check_domain("studio-staging.studio.storyteller.ai"), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn digits_and_hyphens_inside_labels() {
|
||||
assert_eq!(check_domain("cdn-2.fakeyou.com"), Ok(()));
|
||||
assert_eq!(check_domain("3d.getartcraft.com"), Ok(()));
|
||||
assert_eq!(check_domain("pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev"), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_lengths() {
|
||||
let label_63 = "a".repeat(63);
|
||||
assert_eq!(check_domain(&format!("{label_63}.com")), Ok(()));
|
||||
|
||||
// 63 + 1 + 63 + 1 + 63 + 1 + 61 = 253
|
||||
let domain_253 = format!("{label_63}.{label_63}.{label_63}.{}", "a".repeat(61));
|
||||
assert_eq!(domain_253.len(), 253);
|
||||
assert_eq!(check_domain(&domain_253), Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
mod invalid {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(check_domain(""), Err(Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_long() {
|
||||
let label_63 = "a".repeat(63);
|
||||
let domain_254 = format!("{label_63}.{label_63}.{label_63}.{}", "a".repeat(62));
|
||||
assert_eq!(check_domain(&domain_254), Err(TooLong));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_labels() {
|
||||
assert_eq!(check_domain(".getartcraft.com"), Err(EmptyLabel));
|
||||
assert_eq!(check_domain("getartcraft.com."), Err(EmptyLabel));
|
||||
assert_eq!(check_domain("desktop..getartcraft.com"), Err(EmptyLabel));
|
||||
assert_eq!(check_domain("."), Err(EmptyLabel));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn label_too_long() {
|
||||
assert_eq!(check_domain(&format!("{}.com", "a".repeat(64))), Err(LabelTooLong));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uppercase() {
|
||||
assert_eq!(check_domain("GetArtCraft.com"), Err(InvalidCharacter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheme_port_path_and_whitespace() {
|
||||
assert_eq!(check_domain("https://getartcraft.com"), Err(InvalidCharacter));
|
||||
assert_eq!(check_domain("getartcraft.com:443"), Err(InvalidCharacter));
|
||||
assert_eq!(check_domain("getartcraft.com/"), Err(InvalidCharacter));
|
||||
assert_eq!(check_domain(" getartcraft.com"), Err(InvalidCharacter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ascii() {
|
||||
assert_eq!(check_domain("artcräft.com"), Err(InvalidCharacter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn underscore() {
|
||||
assert_eq!(check_domain("dev_proxy.fakeyou.com"), Err(InvalidCharacter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphen_at_label_edge() {
|
||||
assert_eq!(check_domain("-desktop.getartcraft.com"), Err(HyphenAtLabelEdge));
|
||||
assert_eq!(check_domain("desktop-.getartcraft.com"), Err(HyphenAtLabelEdge));
|
||||
assert_eq!(check_domain("getartcraft.com-"), Err(HyphenAtLabelEdge));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_label() {
|
||||
assert_eq!(check_domain("localhost"), Err(SingleLabel));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_addresses() {
|
||||
assert_eq!(check_domain("127.0.0.1"), Err(NumericTopLevelLabel));
|
||||
assert_eq!(check_domain("example.123"), Err(NumericTopLevelLabel));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?`,
|
||||
|
||||
Loaded 100 of 617 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user