Compare commits
49 Commits
normalize-
...
localizati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a1884eb32 | ||
|
|
c874bd560e | ||
|
|
c7e1c3da69 | ||
|
|
3b8f6bd94c | ||
|
|
d8448ba37e | ||
|
|
ead0c7fe07 | ||
|
|
660feabced | ||
|
|
e52ac14d56 | ||
|
|
b77abd64e2 | ||
|
|
ed58d18334 | ||
|
|
c522e54805 | ||
|
|
5734ee43ff | ||
|
|
c9f0dba62f | ||
|
|
01d351c85d | ||
|
|
cf04e854d6 | ||
|
|
0103fe4751 | ||
|
|
14105a2d54 | ||
|
|
410dd27d93 | ||
|
|
86abe7b24c | ||
|
|
aff6db1500 | ||
|
|
9a1b1fb718 | ||
|
|
ca5178f05e | ||
|
|
47dcdd439c | ||
|
|
076032ff8b | ||
|
|
843806247d | ||
|
|
c15e6a5b63 | ||
|
|
3dc86239d2 | ||
|
|
8bc4107e54 | ||
|
|
b653e91fae | ||
|
|
0164d7ad31 | ||
|
|
e289199911 | ||
|
|
adaadee368 | ||
|
|
bede849fa6 | ||
|
|
fc31823fd2 | ||
|
|
b1f3bbe5b0 | ||
|
|
c8a8154e83 | ||
|
|
3ae3ea6102 | ||
|
|
6ef599e894 | ||
|
|
d1479ca4e5 | ||
|
|
26db935fad | ||
|
|
7aa7276fa3 | ||
|
|
5628fbc5d3 | ||
|
|
5cf41c8c8e | ||
|
|
07b483038a | ||
|
|
8dec195c2d | ||
|
|
d64b3b711c | ||
|
|
2b38361a26 | ||
|
|
b278525647 | ||
|
|
f629191b28 |
@@ -27,11 +27,11 @@ func printPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, quiet
|
||||
|
||||
// Common image extensions
|
||||
imageExts := map[string]bool{
|
||||
"jpg": true, "jpeg": true, "png": true, "gif": true, "webp": true, "bmp": true,
|
||||
"jpg": true, "jpeg": true, "png": true, "gif": true, "webp": true, "bmp": true, "avif": true,
|
||||
}
|
||||
|
||||
if imageExts[ext] {
|
||||
return printImagePhash(inputfile, quiet)
|
||||
return printImagePhash(ff, inputfile, quiet)
|
||||
}
|
||||
|
||||
return printVideoPhash(ff, ffp, inputfile, quiet)
|
||||
@@ -65,12 +65,12 @@ func printVideoPhash(ff *ffmpeg.FFMpeg, ffp *ffmpeg.FFProbe, inputfile string, q
|
||||
return nil
|
||||
}
|
||||
|
||||
func printImagePhash(inputfile string, quiet *bool) error {
|
||||
func printImagePhash(ff *ffmpeg.FFMpeg, inputfile string, quiet *bool) error {
|
||||
imgFile := &models.ImageFile{
|
||||
BaseFile: &models.BaseFile{Path: inputfile},
|
||||
}
|
||||
|
||||
phash, err := imagephash.Generate(imgFile)
|
||||
phash, err := imagephash.Generate(ff, imgFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -140,4 +140,8 @@ models:
|
||||
fields:
|
||||
plugins:
|
||||
resolver: true
|
||||
Performer:
|
||||
fields:
|
||||
career_length:
|
||||
resolver: true
|
||||
|
||||
|
||||
@@ -426,6 +426,10 @@ type Mutation {
|
||||
destroyFiles(ids: [ID!]!): Boolean!
|
||||
|
||||
fileSetFingerprints(input: FileSetFingerprintsInput!): Boolean!
|
||||
"Reveal the file in the system file manager"
|
||||
revealFileInFileManager(id: ID!): Boolean!
|
||||
"Reveal the folder in the system file manager"
|
||||
revealFolderInFileManager(id: ID!): Boolean!
|
||||
|
||||
# Saved filters
|
||||
saveFilter(input: SaveFilterInput!): SavedFilter!
|
||||
@@ -579,6 +583,8 @@ type Mutation {
|
||||
stashBoxBatchPerformerTag(input: StashBoxBatchTagInput!): String!
|
||||
"Run batch studio tag task. Returns the job ID."
|
||||
stashBoxBatchStudioTag(input: StashBoxBatchTagInput!): String!
|
||||
"Run batch tag tag task. Returns the job ID."
|
||||
stashBoxBatchTagTag(input: StashBoxBatchTagInput!): String!
|
||||
|
||||
"Enables DLNA for an optional duration. Has no effect if DLNA is enabled by default"
|
||||
enableDLNA(input: EnableDLNAInput!): Boolean!
|
||||
|
||||
@@ -184,6 +184,18 @@ input ConfigGeneralInput {
|
||||
scraperPackageSources: [PackageSourceInput!]
|
||||
"Source of plugin packages"
|
||||
pluginPackageSources: [PackageSourceInput!]
|
||||
|
||||
"Size of the longest dimension for each sprite in pixels"
|
||||
spriteScreenshotSize: Int
|
||||
|
||||
"True if sprite generation should use the sprite interval and min/max sprites settings instead of the default"
|
||||
useCustomSpriteInterval: Boolean
|
||||
"Time between two different scrubber sprites in seconds - only used if useCustomSpriteInterval is true"
|
||||
spriteInterval: Float
|
||||
"Minimum number of sprites to be generated - only used if useCustomSpriteInterval is true"
|
||||
minimumSprites: Int
|
||||
"Minimum number of sprites to be generated - only used if useCustomSpriteInterval is true"
|
||||
maximumSprites: Int
|
||||
}
|
||||
|
||||
type ConfigGeneralResult {
|
||||
@@ -287,6 +299,16 @@ type ConfigGeneralResult {
|
||||
logAccess: Boolean!
|
||||
"Maximum log size"
|
||||
logFileMaxSize: Int!
|
||||
"True if sprite generation should use the sprite interval and min/max sprites settings instead of the default"
|
||||
useCustomSpriteInterval: Boolean!
|
||||
"Time between two different scrubber sprites in seconds - only used if useCustomSpriteInterval is true"
|
||||
spriteInterval: Float!
|
||||
"Minimum number of sprites to be generated - only used if useCustomSpriteInterval is true"
|
||||
minimumSprites: Int!
|
||||
"Maximum number of sprites to be generated - only used if useCustomSpriteInterval is true"
|
||||
maximumSprites: Int!
|
||||
"Size of the longest dimension for each sprite in pixels"
|
||||
spriteScreenshotSize: Int!
|
||||
"Array of video file extensions"
|
||||
videoExtensions: [String!]!
|
||||
"Array of image file extensions"
|
||||
|
||||
@@ -6,11 +6,14 @@ type Fingerprint {
|
||||
type Folder {
|
||||
id: ID!
|
||||
path: String!
|
||||
basename: String!
|
||||
|
||||
parent_folder_id: ID @deprecated(reason: "Use parent_folder instead")
|
||||
zip_file_id: ID @deprecated(reason: "Use zip_file instead")
|
||||
|
||||
parent_folder: Folder
|
||||
"Returns all parent folders in order from immediate parent to top-level"
|
||||
parent_folders: [Folder!]!
|
||||
zip_file: BasicFile
|
||||
|
||||
mod_time: Time!
|
||||
|
||||
@@ -75,10 +75,26 @@ input OrientationCriterionInput {
|
||||
value: [OrientationEnum!]!
|
||||
}
|
||||
|
||||
input PHashDuplicationCriterionInput {
|
||||
duplicated: Boolean
|
||||
"Currently unimplemented"
|
||||
input DuplicationCriterionInput {
|
||||
duplicated: Boolean @deprecated(reason: "Use phash field instead")
|
||||
"Currently unimplemented. Intended for phash distance matching."
|
||||
distance: Int
|
||||
"Filter by phash duplication"
|
||||
phash: Boolean
|
||||
"Filter by URL duplication"
|
||||
url: Boolean
|
||||
"Filter by Stash ID duplication"
|
||||
stash_id: Boolean
|
||||
"Filter by title duplication"
|
||||
title: Boolean
|
||||
}
|
||||
|
||||
input FileDuplicationCriterionInput {
|
||||
duplicated: Boolean @deprecated(reason: "Use phash field instead")
|
||||
"Currently unimplemented. Intended for phash distance matching."
|
||||
distance: Int
|
||||
"Filter by phash duplication"
|
||||
phash: Boolean
|
||||
}
|
||||
|
||||
input StashIDCriterionInput {
|
||||
@@ -138,8 +154,13 @@ input PerformerFilterType {
|
||||
penis_length: FloatCriterionInput
|
||||
"Filter by ciricumcision"
|
||||
circumcised: CircumcisionCriterionInput
|
||||
"Filter by career length"
|
||||
"Deprecated: use career_start and career_end. This filter is non-functional."
|
||||
career_length: StringCriterionInput
|
||||
@deprecated(reason: "Use career_start and career_end")
|
||||
"Filter by career start year"
|
||||
career_start: IntCriterionInput
|
||||
"Filter by career end year"
|
||||
career_end: IntCriterionInput
|
||||
"Filter by tattoos"
|
||||
tattoos: StringCriterionInput
|
||||
"Filter by piercings"
|
||||
@@ -156,6 +177,8 @@ input PerformerFilterType {
|
||||
tag_count: IntCriterionInput
|
||||
"Filter by scene count"
|
||||
scene_count: IntCriterionInput
|
||||
"Filter by marker count (via scene)"
|
||||
marker_count: IntCriterionInput
|
||||
"Filter by image count"
|
||||
image_count: IntCriterionInput
|
||||
"Filter by gallery count"
|
||||
@@ -199,6 +222,8 @@ input PerformerFilterType {
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by related tags that meet this criteria"
|
||||
tags_filter: TagFilterType
|
||||
"Filter by related scene markers (via scene) that meet this criteria"
|
||||
markers_filter: SceneMarkerFilterType
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
@@ -261,8 +286,8 @@ input SceneFilterType {
|
||||
organized: Boolean
|
||||
"Filter by o-counter"
|
||||
o_counter: IntCriterionInput
|
||||
"Filter Scenes that have an exact phash match available"
|
||||
duplicated: PHashDuplicationCriterionInput
|
||||
"Filter Scenes by duplication criteria"
|
||||
duplicated: DuplicationCriterionInput
|
||||
"Filter by resolution"
|
||||
resolution: ResolutionCriterionInput
|
||||
"Filter by orientation"
|
||||
@@ -350,6 +375,8 @@ input SceneFilterType {
|
||||
markers_filter: SceneMarkerFilterType
|
||||
"Filter by related files that meet this criteria"
|
||||
files_filter: FileFilterType
|
||||
|
||||
custom_fields: [CustomFieldCriterionInput!]
|
||||
}
|
||||
|
||||
input MovieFilterType {
|
||||
@@ -432,11 +459,16 @@ input GroupFilterType {
|
||||
containing_group_count: IntCriterionInput
|
||||
"Filter by number of sub-groups the group has"
|
||||
sub_group_count: IntCriterionInput
|
||||
"Filter by number of scenes the group has"
|
||||
scene_count: IntCriterionInput
|
||||
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
|
||||
"Filter by custom fields"
|
||||
custom_fields: [CustomFieldCriterionInput!]
|
||||
}
|
||||
|
||||
input StudioFilterType {
|
||||
@@ -467,6 +499,8 @@ input StudioFilterType {
|
||||
image_count: IntCriterionInput
|
||||
"Filter by gallery count"
|
||||
gallery_count: IntCriterionInput
|
||||
"Filter by group count"
|
||||
group_count: IntCriterionInput
|
||||
"Filter by tag count"
|
||||
tag_count: IntCriterionInput
|
||||
"Filter by url"
|
||||
@@ -477,12 +511,16 @@ input StudioFilterType {
|
||||
child_count: IntCriterionInput
|
||||
"Filter by autotag ignore value"
|
||||
ignore_auto_tag: Boolean
|
||||
"Filter by organized"
|
||||
organized: Boolean
|
||||
"Filter by related scenes that meet this criteria"
|
||||
scenes_filter: SceneFilterType
|
||||
"Filter by related images that meet this criteria"
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by related groups that meet this criteria"
|
||||
groups_filter: GroupFilterType
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
@@ -565,6 +603,8 @@ input GalleryFilterType {
|
||||
files_filter: FileFilterType
|
||||
"Filter by related folders that meet this criteria"
|
||||
folders_filter: FolderFilterType
|
||||
|
||||
custom_fields: [CustomFieldCriterionInput!]
|
||||
}
|
||||
|
||||
input TagFilterType {
|
||||
@@ -642,12 +682,22 @@ input TagFilterType {
|
||||
images_filter: ImageFilterType
|
||||
"Filter by related galleries that meet this criteria"
|
||||
galleries_filter: GalleryFilterType
|
||||
"Filter by related groups that meet this criteria"
|
||||
groups_filter: GroupFilterType
|
||||
"Filter by related performers that meet this criteria"
|
||||
performers_filter: PerformerFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
"Filter by related scene markers that meet this criteria"
|
||||
markers_filter: SceneMarkerFilterType
|
||||
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
|
||||
custom_fields: [CustomFieldCriterionInput!]
|
||||
}
|
||||
|
||||
input ImageFilterType {
|
||||
@@ -721,6 +771,8 @@ input ImageFilterType {
|
||||
tags_filter: TagFilterType
|
||||
"Filter by related files that meet this criteria"
|
||||
files_filter: FileFilterType
|
||||
"Filter by custom fields"
|
||||
custom_fields: [CustomFieldCriterionInput!]
|
||||
}
|
||||
|
||||
input FileFilterType {
|
||||
@@ -738,8 +790,8 @@ input FileFilterType {
|
||||
"Filter by modification time"
|
||||
mod_time: TimestampCriterionInput
|
||||
|
||||
"Filter files that have an exact match available"
|
||||
duplicated: PHashDuplicationCriterionInput
|
||||
"Filter files by duplication criteria (only phash applies to files)"
|
||||
duplicated: FileDuplicationCriterionInput
|
||||
|
||||
"find files based on hash"
|
||||
hashes: [FingerprintFilterInput!]
|
||||
@@ -770,6 +822,7 @@ input FolderFilterType {
|
||||
NOT: FolderFilterType
|
||||
|
||||
path: StringCriterionInput
|
||||
basename: StringCriterionInput
|
||||
|
||||
parent_folder: HierarchicalMultiCriterionInput
|
||||
zip_file: MultiCriterionInput
|
||||
|
||||
@@ -32,6 +32,7 @@ type Gallery {
|
||||
cover: Image
|
||||
|
||||
paths: GalleryPathsType! # Resolver
|
||||
custom_fields: Map!
|
||||
image(index: Int!): Image!
|
||||
}
|
||||
|
||||
@@ -50,6 +51,8 @@ input GalleryCreateInput {
|
||||
studio_id: ID
|
||||
tag_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
|
||||
input GalleryUpdateInput {
|
||||
@@ -71,6 +74,8 @@ input GalleryUpdateInput {
|
||||
performer_ids: [ID!]
|
||||
|
||||
primary_file_id: ID
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input BulkGalleryUpdateInput {
|
||||
@@ -89,6 +94,8 @@ input BulkGalleryUpdateInput {
|
||||
studio_id: ID
|
||||
tag_ids: BulkUpdateIds
|
||||
performer_ids: BulkUpdateIds
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input GalleryDestroyInput {
|
||||
|
||||
@@ -31,6 +31,7 @@ type Group {
|
||||
sub_group_count(depth: Int): Int! # Resolver
|
||||
scenes: [Scene!]!
|
||||
o_counter: Int # Resolver
|
||||
custom_fields: Map!
|
||||
}
|
||||
|
||||
input GroupDescriptionInput {
|
||||
@@ -59,6 +60,8 @@ input GroupCreateInput {
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
back_image: String
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
|
||||
input GroupUpdateInput {
|
||||
@@ -82,6 +85,8 @@ input GroupUpdateInput {
|
||||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
back_image: String
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input BulkUpdateGroupDescriptionsInput {
|
||||
@@ -101,6 +106,8 @@ input BulkGroupUpdateInput {
|
||||
|
||||
containing_groups: BulkUpdateGroupDescriptionsInput
|
||||
sub_groups: BulkUpdateGroupDescriptionsInput
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input GroupDestroyInput {
|
||||
|
||||
@@ -21,6 +21,7 @@ type Image {
|
||||
studio: Studio
|
||||
tags: [Tag!]!
|
||||
performers: [Performer!]!
|
||||
custom_fields: Map!
|
||||
}
|
||||
|
||||
type ImageFileType {
|
||||
@@ -56,6 +57,7 @@ input ImageUpdateInput {
|
||||
gallery_ids: [ID!]
|
||||
|
||||
primary_file_id: ID
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input BulkImageUpdateInput {
|
||||
@@ -76,6 +78,7 @@ input BulkImageUpdateInput {
|
||||
performer_ids: BulkUpdateIds
|
||||
tag_ids: BulkUpdateIds
|
||||
gallery_ids: BulkUpdateIds
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input ImageDestroyInput {
|
||||
|
||||
@@ -215,7 +215,9 @@ input IdentifyMetadataOptionsInput {
|
||||
setCoverImage: Boolean
|
||||
setOrganized: Boolean
|
||||
"defaults to true if not provided"
|
||||
includeMalePerformers: Boolean
|
||||
includeMalePerformers: Boolean @deprecated(reason: "Use performerGenders")
|
||||
"Filter to only include performers with these genders. If not provided, all genders are included."
|
||||
performerGenders: [GenderEnum!]
|
||||
"defaults to true if not provided"
|
||||
skipMultipleMatches: Boolean
|
||||
"tag to tag skipped multiple matches with"
|
||||
@@ -260,7 +262,9 @@ type IdentifyMetadataOptions {
|
||||
setCoverImage: Boolean
|
||||
setOrganized: Boolean
|
||||
"defaults to true if not provided"
|
||||
includeMalePerformers: Boolean
|
||||
includeMalePerformers: Boolean @deprecated(reason: "Use performerGenders")
|
||||
"Filter to only include performers with these genders. If not provided, all genders are included."
|
||||
performerGenders: [GenderEnum!]
|
||||
"defaults to true if not provided"
|
||||
skipMultipleMatches: Boolean
|
||||
"tag to tag skipped multiple matches with"
|
||||
@@ -321,6 +325,8 @@ input ImportObjectsInput {
|
||||
|
||||
input BackupDatabaseInput {
|
||||
download: Boolean
|
||||
"If true, blob files will be included in the backup. This can significantly increase the size of the backup and the time it takes to create it, but allows for a complete backup of the system that can be restored without needing access to the original media files."
|
||||
includeBlobs: Boolean
|
||||
}
|
||||
|
||||
input AnonymiseDatabaseInput {
|
||||
|
||||
@@ -30,7 +30,9 @@ type Performer {
|
||||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
alias_list: [String!]!
|
||||
@@ -77,7 +79,9 @@ input PerformerCreateInput {
|
||||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
"Duplicate aliases and those equal to name will be ignored (case-insensitive)"
|
||||
@@ -116,7 +120,9 @@ input PerformerUpdateInput {
|
||||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
"Duplicate aliases and those equal to name will be ignored (case-insensitive)"
|
||||
@@ -160,7 +166,9 @@ input BulkPerformerUpdateInput {
|
||||
fake_tits: String
|
||||
penis_length: Float
|
||||
circumcised: CircumisedEnum
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
"Duplicate aliases and those equal to name will result in an error (case-insensitive)"
|
||||
|
||||
@@ -79,6 +79,8 @@ type Scene {
|
||||
performers: [Performer!]!
|
||||
stash_ids: [StashID!]!
|
||||
|
||||
custom_fields: Map!
|
||||
|
||||
"Return valid stream paths"
|
||||
sceneStreams: [SceneStreamEndpoint!]!
|
||||
}
|
||||
@@ -120,6 +122,8 @@ input SceneCreateInput {
|
||||
Files must not already be primary for another scene.
|
||||
"""
|
||||
file_ids: [ID!]
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
|
||||
input SceneUpdateInput {
|
||||
@@ -158,6 +162,8 @@ input SceneUpdateInput {
|
||||
)
|
||||
|
||||
primary_file_id: ID
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
enum BulkUpdateIdMode {
|
||||
@@ -190,6 +196,8 @@ input BulkSceneUpdateInput {
|
||||
tag_ids: BulkUpdateIds
|
||||
group_ids: BulkUpdateIds
|
||||
movie_ids: BulkUpdateIds @deprecated(reason: "Use group_ids")
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input SceneDestroyInput {
|
||||
|
||||
@@ -18,7 +18,9 @@ type ScrapedPerformer {
|
||||
fake_tits: String
|
||||
penis_length: String
|
||||
circumcised: String
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
# aliases must be comma-delimited to be parsed correctly
|
||||
@@ -54,7 +56,9 @@ input ScrapedPerformerInput {
|
||||
fake_tits: String
|
||||
penis_length: String
|
||||
circumcised: String
|
||||
career_length: String
|
||||
career_length: String @deprecated(reason: "Use career_start and career_end")
|
||||
career_start: Int
|
||||
career_end: Int
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
|
||||
@@ -71,6 +71,8 @@ type ScrapedTag {
|
||||
"Set if tag matched"
|
||||
stored_id: ID
|
||||
name: String!
|
||||
description: String
|
||||
alias_list: [String!]
|
||||
"Remote site ID, if applicable"
|
||||
remote_site_id: String
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type Studio {
|
||||
aliases: [String!]!
|
||||
tags: [Tag!]!
|
||||
ignore_auto_tag: Boolean!
|
||||
organized: Boolean!
|
||||
|
||||
image_path: String # Resolver
|
||||
scene_count(depth: Int): Int! # Resolver
|
||||
@@ -46,6 +47,7 @@ input StudioCreateInput {
|
||||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
organized: Boolean
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
@@ -67,6 +69,7 @@ input StudioUpdateInput {
|
||||
aliases: [String!]
|
||||
tag_ids: [ID!]
|
||||
ignore_auto_tag: Boolean
|
||||
organized: Boolean
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
@@ -82,6 +85,7 @@ input BulkStudioUpdateInput {
|
||||
details: String
|
||||
tag_ids: BulkUpdateIds
|
||||
ignore_auto_tag: Boolean
|
||||
organized: Boolean
|
||||
}
|
||||
|
||||
input StudioDestroyInput {
|
||||
|
||||
@@ -24,6 +24,7 @@ type Tag {
|
||||
|
||||
parent_count: Int! # Resolver
|
||||
child_count: Int! # Resolver
|
||||
custom_fields: Map!
|
||||
}
|
||||
|
||||
input TagCreateInput {
|
||||
@@ -41,6 +42,8 @@ input TagCreateInput {
|
||||
|
||||
parent_ids: [ID!]
|
||||
child_ids: [ID!]
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
|
||||
input TagUpdateInput {
|
||||
@@ -59,6 +62,8 @@ input TagUpdateInput {
|
||||
|
||||
parent_ids: [ID!]
|
||||
child_ids: [ID!]
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input TagDestroyInput {
|
||||
@@ -73,6 +78,8 @@ type FindTagsResultType {
|
||||
input TagsMergeInput {
|
||||
source: [ID!]!
|
||||
destination: ID!
|
||||
# values defined here will override values in the destination
|
||||
values: TagUpdateInput
|
||||
}
|
||||
|
||||
input BulkTagUpdateInput {
|
||||
|
||||
@@ -29,6 +29,8 @@ fragment StudioFragment on Studio {
|
||||
fragment TagFragment on Tag {
|
||||
name
|
||||
id
|
||||
description
|
||||
aliases
|
||||
}
|
||||
|
||||
fragment MeasurementsFragment on Measurements {
|
||||
@@ -120,18 +122,6 @@ fragment SceneFragment on Scene {
|
||||
}
|
||||
}
|
||||
|
||||
query FindSceneByFingerprint($fingerprint: FingerprintQueryInput!) {
|
||||
findSceneByFingerprint(fingerprint: $fingerprint) {
|
||||
...SceneFragment
|
||||
}
|
||||
}
|
||||
|
||||
query FindScenesByFullFingerprints($fingerprints: [FingerprintQueryInput!]!) {
|
||||
findScenesByFullFingerprints(fingerprints: $fingerprints) {
|
||||
...SceneFragment
|
||||
}
|
||||
}
|
||||
|
||||
query FindScenesBySceneFingerprints(
|
||||
$fingerprints: [[FingerprintQueryInput!]!]!
|
||||
) {
|
||||
|
||||
@@ -40,6 +40,8 @@ func authenticateHandler() func(http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
r = session.SetLocalRequest(r)
|
||||
|
||||
userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
|
||||
if err != nil {
|
||||
if !errors.Is(err, session.ErrUnauthorized) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//go:generate go run github.com/vektah/dataloaden GroupLoader int *github.com/stashapp/stash/pkg/models.Group
|
||||
//go:generate go run github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/models.FileID github.com/stashapp/stash/pkg/models.File
|
||||
//go:generate go run github.com/vektah/dataloaden FolderLoader github.com/stashapp/stash/pkg/models.FolderID *github.com/stashapp/stash/pkg/models.Folder
|
||||
//go:generate go run github.com/vektah/dataloaden FolderParentFolderIDsLoader github.com/stashapp/stash/pkg/models.FolderID []github.com/stashapp/stash/pkg/models.FolderID
|
||||
//go:generate go run github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
@@ -42,19 +43,22 @@ const (
|
||||
)
|
||||
|
||||
type Loaders struct {
|
||||
SceneByID *SceneLoader
|
||||
SceneFiles *SceneFileIDsLoader
|
||||
ScenePlayCount *ScenePlayCountLoader
|
||||
SceneOCount *SceneOCountLoader
|
||||
ScenePlayHistory *ScenePlayHistoryLoader
|
||||
SceneOHistory *SceneOHistoryLoader
|
||||
SceneLastPlayed *SceneLastPlayedLoader
|
||||
SceneByID *SceneLoader
|
||||
SceneFiles *SceneFileIDsLoader
|
||||
ScenePlayCount *ScenePlayCountLoader
|
||||
SceneOCount *SceneOCountLoader
|
||||
ScenePlayHistory *ScenePlayHistoryLoader
|
||||
SceneOHistory *SceneOHistoryLoader
|
||||
SceneLastPlayed *SceneLastPlayedLoader
|
||||
SceneCustomFields *CustomFieldsLoader
|
||||
|
||||
ImageFiles *ImageFileIDsLoader
|
||||
GalleryFiles *GalleryFileIDsLoader
|
||||
|
||||
GalleryByID *GalleryLoader
|
||||
ImageByID *ImageLoader
|
||||
GalleryByID *GalleryLoader
|
||||
GalleryCustomFields *CustomFieldsLoader
|
||||
ImageByID *ImageLoader
|
||||
ImageCustomFields *CustomFieldsLoader
|
||||
|
||||
PerformerByID *PerformerLoader
|
||||
PerformerCustomFields *CustomFieldsLoader
|
||||
@@ -62,10 +66,16 @@ type Loaders struct {
|
||||
StudioByID *StudioLoader
|
||||
StudioCustomFields *CustomFieldsLoader
|
||||
|
||||
TagByID *TagLoader
|
||||
GroupByID *GroupLoader
|
||||
FileByID *FileLoader
|
||||
FolderByID *FolderLoader
|
||||
TagByID *TagLoader
|
||||
TagCustomFields *CustomFieldsLoader
|
||||
|
||||
GroupByID *GroupLoader
|
||||
GroupCustomFields *CustomFieldsLoader
|
||||
|
||||
FileByID *FileLoader
|
||||
|
||||
FolderByID *FolderLoader
|
||||
FolderParentFolderIDs *FolderParentFolderIDsLoader
|
||||
}
|
||||
|
||||
type Middleware struct {
|
||||
@@ -86,11 +96,21 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGalleries(ctx),
|
||||
},
|
||||
GalleryCustomFields: &CustomFieldsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGalleryCustomFields(ctx),
|
||||
},
|
||||
ImageByID: &ImageLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchImages(ctx),
|
||||
},
|
||||
ImageCustomFields: &CustomFieldsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchImageCustomFields(ctx),
|
||||
},
|
||||
PerformerByID: &PerformerLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
@@ -106,6 +126,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchStudioCustomFields(ctx),
|
||||
},
|
||||
SceneCustomFields: &CustomFieldsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchSceneCustomFields(ctx),
|
||||
},
|
||||
StudioByID: &StudioLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
@@ -116,11 +141,21 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchTags(ctx),
|
||||
},
|
||||
TagCustomFields: &CustomFieldsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchTagCustomFields(ctx),
|
||||
},
|
||||
GroupByID: &GroupLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGroups(ctx),
|
||||
},
|
||||
GroupCustomFields: &CustomFieldsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGroupCustomFields(ctx),
|
||||
},
|
||||
FileByID: &FileLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
@@ -131,6 +166,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchFolders(ctx),
|
||||
},
|
||||
FolderParentFolderIDs: &FolderParentFolderIDsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchFoldersParentFolderIDs(ctx),
|
||||
},
|
||||
SceneFiles: &SceneFileIDsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
@@ -201,6 +241,18 @@ func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchSceneCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Scene.GetCustomFieldsBulk(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models.Image, []error) {
|
||||
return func(keys []int) (ret []*models.Image, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
@@ -213,6 +265,18 @@ func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchImageCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Image.GetCustomFieldsBulk(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*models.Gallery, []error) {
|
||||
return func(keys []int) (ret []*models.Gallery, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
@@ -283,6 +347,42 @@ func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.T
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchTagCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Tag.GetCustomFieldsBulk(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGroupCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Group.GetCustomFieldsBulk(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGalleryCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Gallery.GetCustomFieldsBulk(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGroups(ctx context.Context) func(keys []int) ([]*models.Group, []error) {
|
||||
return func(keys []int) (ret []*models.Group, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
@@ -316,6 +416,17 @@ func (m Middleware) fetchFolders(ctx context.Context) func(keys []models.FolderI
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchFoldersParentFolderIDs(ctx context.Context) func(keys []models.FolderID) ([][]models.FolderID, []error) {
|
||||
return func(keys []models.FolderID) (ret [][]models.FolderID, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Folder.GetManyParentFolderIDs(ctx, keys)
|
||||
return err
|
||||
})
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
|
||||
225
internal/api/loaders/folderparentfolderidsloader_gen.go
Normal file
@@ -0,0 +1,225 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// FolderParentFolderIDsLoaderConfig captures the config to create a new FolderParentFolderIDsLoader
|
||||
type FolderParentFolderIDsLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []models.FolderID) ([][]models.FolderID, []error)
|
||||
|
||||
// Wait is how long wait before sending a batch
|
||||
Wait time.Duration
|
||||
|
||||
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// NewFolderParentFolderIDsLoader creates a new FolderParentFolderIDsLoader given a fetch, wait, and maxBatch
|
||||
func NewFolderParentFolderIDsLoader(config FolderParentFolderIDsLoaderConfig) *FolderParentFolderIDsLoader {
|
||||
return &FolderParentFolderIDsLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// FolderParentFolderIDsLoader batches and caches requests
|
||||
type FolderParentFolderIDsLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []models.FolderID) ([][]models.FolderID, []error)
|
||||
|
||||
// how long to done before sending a batch
|
||||
wait time.Duration
|
||||
|
||||
// this will limit the maximum number of keys to send in one batch, 0 = no limit
|
||||
maxBatch int
|
||||
|
||||
// INTERNAL
|
||||
|
||||
// lazily created cache
|
||||
cache map[models.FolderID][]models.FolderID
|
||||
|
||||
// the current batch. keys will continue to be collected until timeout is hit,
|
||||
// then everything will be sent to the fetch method and out to the listeners
|
||||
batch *folderParentFolderIDsLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type folderParentFolderIDsLoaderBatch struct {
|
||||
keys []models.FolderID
|
||||
data [][]models.FolderID
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a FolderID by key, batching and caching will be applied automatically
|
||||
func (l *FolderParentFolderIDsLoader) Load(key models.FolderID) ([]models.FolderID, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a FolderID.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *FolderParentFolderIDsLoader) LoadThunk(key models.FolderID) func() ([]models.FolderID, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() ([]models.FolderID, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &folderParentFolderIDsLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() ([]models.FolderID, error) {
|
||||
<-batch.done
|
||||
|
||||
var data []models.FolderID
|
||||
if pos < len(batch.data) {
|
||||
data = batch.data[pos]
|
||||
}
|
||||
|
||||
var err error
|
||||
// its convenient to be able to return a single error for everything
|
||||
if len(batch.error) == 1 {
|
||||
err = batch.error[0]
|
||||
} else if batch.error != nil {
|
||||
err = batch.error[pos]
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
l.mu.Lock()
|
||||
l.unsafeSet(key, data)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||
// sub batches depending on how the loader is configured
|
||||
func (l *FolderParentFolderIDsLoader) LoadAll(keys []models.FolderID) ([][]models.FolderID, []error) {
|
||||
results := make([]func() ([]models.FolderID, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
folderIDs := make([][]models.FolderID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
folderIDs[i], errors[i] = thunk()
|
||||
}
|
||||
return folderIDs, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a FolderIDs.
|
||||
// This method should be used if you want one goroutine to make requests to many
|
||||
// different data loaders without blocking until the thunk is called.
|
||||
func (l *FolderParentFolderIDsLoader) LoadAllThunk(keys []models.FolderID) func() ([][]models.FolderID, []error) {
|
||||
results := make([]func() ([]models.FolderID, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([][]models.FolderID, []error) {
|
||||
folderIDs := make([][]models.FolderID, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
folderIDs[i], errors[i] = thunk()
|
||||
}
|
||||
return folderIDs, errors
|
||||
}
|
||||
}
|
||||
|
||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||
// and false is returned.
|
||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||
func (l *FolderParentFolderIDsLoader) Prime(key models.FolderID, value []models.FolderID) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||
// and end up with the whole cache pointing to the same value.
|
||||
cpy := make([]models.FolderID, len(value))
|
||||
copy(cpy, value)
|
||||
l.unsafeSet(key, cpy)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *FolderParentFolderIDsLoader) Clear(key models.FolderID) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *FolderParentFolderIDsLoader) unsafeSet(key models.FolderID, value []models.FolderID) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[models.FolderID][]models.FolderID{}
|
||||
}
|
||||
l.cache[key] = value
|
||||
}
|
||||
|
||||
// keyIndex will return the location of the key in the batch, if its not found
|
||||
// it will add the key to the batch
|
||||
func (b *folderParentFolderIDsLoaderBatch) keyIndex(l *FolderParentFolderIDsLoader, key models.FolderID) int {
|
||||
for i, existingKey := range b.keys {
|
||||
if key == existingKey {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
pos := len(b.keys)
|
||||
b.keys = append(b.keys, key)
|
||||
if pos == 0 {
|
||||
go b.startTimer(l)
|
||||
}
|
||||
|
||||
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
|
||||
if !b.closing {
|
||||
b.closing = true
|
||||
l.batch = nil
|
||||
go b.end(l)
|
||||
}
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
func (b *folderParentFolderIDsLoaderBatch) startTimer(l *FolderParentFolderIDsLoader) {
|
||||
time.Sleep(l.wait)
|
||||
l.mu.Lock()
|
||||
|
||||
// we must have hit a batch limit and are already finalizing this batch
|
||||
if b.closing {
|
||||
l.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.batch = nil
|
||||
l.mu.Unlock()
|
||||
|
||||
b.end(l)
|
||||
}
|
||||
|
||||
func (b *folderParentFolderIDsLoaderBatch) end(l *FolderParentFolderIDsLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
@@ -2,11 +2,16 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *folderResolver) Basename(ctx context.Context, obj *models.Folder) (string, error) {
|
||||
return filepath.Base(obj.Path), nil
|
||||
}
|
||||
|
||||
func (r *folderResolver) ParentFolder(ctx context.Context, obj *models.Folder) (*models.Folder, error) {
|
||||
if obj.ParentFolderID == nil {
|
||||
return nil, nil
|
||||
@@ -15,6 +20,17 @@ func (r *folderResolver) ParentFolder(ctx context.Context, obj *models.Folder) (
|
||||
return loaders.From(ctx).FolderByID.Load(*obj.ParentFolderID)
|
||||
}
|
||||
|
||||
func (r *folderResolver) ParentFolders(ctx context.Context, obj *models.Folder) ([]*models.Folder, error) {
|
||||
ids, err := loaders.From(ctx).FolderParentFolderIDs.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
ret, errs := loaders.From(ctx).FolderByID.LoadAll(ids)
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *folderResolver) ZipFile(ctx context.Context, obj *models.Folder) (*BasicFile, error) {
|
||||
return zipFileResolver(ctx, obj.ZipFileID)
|
||||
}
|
||||
|
||||
@@ -216,3 +216,16 @@ func (r *galleryResolver) Image(ctx context.Context, obj *models.Gallery, index
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *galleryResolver) CustomFields(ctx context.Context, obj *models.Gallery) (map[string]interface{}, error) {
|
||||
m, err := loaders.From(ctx).GalleryCustomFields.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -161,3 +161,12 @@ func (r *imageResolver) Urls(ctx context.Context, obj *models.Image) ([]string,
|
||||
|
||||
return obj.URLs.List(), nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) CustomFields(ctx context.Context, obj *models.Image) (map[string]interface{}, error) {
|
||||
customFields, err := loaders.From(ctx).ImageCustomFields.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return customFields, nil
|
||||
}
|
||||
|
||||
@@ -215,3 +215,16 @@ func (r *groupResolver) OCounter(ctx context.Context, obj *models.Group) (ret *i
|
||||
}
|
||||
return &count, nil
|
||||
}
|
||||
|
||||
func (r *groupResolver) CustomFields(ctx context.Context, obj *models.Group) (map[string]interface{}, error) {
|
||||
m, err := loaders.From(ctx).GroupCustomFields.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer) ([]string, error) {
|
||||
@@ -109,6 +110,15 @@ func (r *performerResolver) HeightCm(ctx context.Context, obj *models.Performer)
|
||||
return obj.Height, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) CareerLength(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.CareerStart == nil && obj.CareerEnd == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ret := utils.FormatYearRange(obj.CareerStart, obj.CareerEnd)
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) Birthdate(ctx context.Context, obj *models.Performer) (*string, error) {
|
||||
if obj.Birthdate != nil {
|
||||
ret := obj.Birthdate.String()
|
||||
|
||||
@@ -410,3 +410,16 @@ func (r *sceneResolver) OHistory(ctx context.Context, obj *models.Scene) ([]*tim
|
||||
|
||||
return ptrRet, nil
|
||||
}
|
||||
|
||||
func (r *sceneResolver) CustomFields(ctx context.Context, obj *models.Scene) (map[string]interface{}, error) {
|
||||
m, err := loaders.From(ctx).SceneCustomFields.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -181,3 +181,16 @@ func (r *tagResolver) ChildCount(ctx context.Context, obj *models.Tag) (ret int,
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) CustomFields(ctx context.Context, obj *models.Tag) (map[string]interface{}, error) {
|
||||
m, err := loaders.From(ctx).TagCustomFields.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@@ -287,6 +287,11 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||
if input.PreviewPreset != nil {
|
||||
c.SetString(config.PreviewPreset, input.PreviewPreset.String())
|
||||
}
|
||||
r.setConfigBool(config.UseCustomSpriteInterval, input.UseCustomSpriteInterval)
|
||||
r.setConfigFloat(config.SpriteInterval, input.SpriteInterval)
|
||||
r.setConfigInt(config.MinimumSprites, input.MinimumSprites)
|
||||
r.setConfigInt(config.MaximumSprites, input.MaximumSprites)
|
||||
r.setConfigInt(config.SpriteScreenshotSize, input.SpriteScreenshotSize)
|
||||
|
||||
r.setConfigBool(config.TranscodeHardwareAcceleration, input.TranscodeHardwareAcceleration)
|
||||
if input.MaxTranscodeSize != nil {
|
||||
|
||||
@@ -5,10 +5,14 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/internal/desktop"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/session"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
@@ -16,7 +20,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
fileStore := r.repository.File
|
||||
folderStore := r.repository.Folder
|
||||
mover := file.NewMover(fileStore, folderStore)
|
||||
mover := file.NewMover(fileStore, folderStore, manager.GetInstance().Config.GetStashPaths().Paths())
|
||||
mover.RegisterHooks(ctx)
|
||||
|
||||
var (
|
||||
@@ -54,13 +58,14 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||
folderPath := *input.DestinationFolder
|
||||
|
||||
// ensure folder path is within the library
|
||||
if err := r.validateFolderPath(folderPath); err != nil {
|
||||
stashPaths := manager.GetInstance().Config.GetStashPaths()
|
||||
if err := r.validateFolderPath(stashPaths, folderPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get or create folder hierarchy
|
||||
var err error
|
||||
folder, err = file.GetOrCreateFolderHierarchy(ctx, folderStore, folderPath)
|
||||
folder, err = file.GetOrCreateFolderHierarchy(ctx, folderStore, folderPath, stashPaths.Paths())
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting or creating folder hierarchy: %w", err)
|
||||
}
|
||||
@@ -109,8 +114,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) validateFolderPath(folderPath string) error {
|
||||
paths := manager.GetInstance().Config.GetStashPaths()
|
||||
func (r *mutationResolver) validateFolderPath(paths config.StashConfigs, folderPath string) error {
|
||||
if l := paths.GetStashFromDirPath(folderPath); l == nil {
|
||||
return fmt.Errorf("folder path %s must be within a stash library path", folderPath)
|
||||
}
|
||||
@@ -326,3 +330,71 @@ func (r *mutationResolver) FileSetFingerprints(ctx context.Context, input FileSe
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RevealFileInFileManager(ctx context.Context, id string) (bool, error) {
|
||||
// disallow if request did not come from localhost
|
||||
if !session.IsLocalRequest(ctx) {
|
||||
logger.Warnf("Attempt to reveal file in file manager from non-local request")
|
||||
return false, fmt.Errorf("access denied")
|
||||
}
|
||||
|
||||
fileIDInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
var filePath string
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
files, err := r.repository.File.Find(ctx, models.FileID(fileIDInt))
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding file: %w", err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("file with id %d not found", fileIDInt)
|
||||
}
|
||||
filePath = files[0].Base().Path
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := desktop.RevealInFileManager(filePath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RevealFolderInFileManager(ctx context.Context, id string) (bool, error) {
|
||||
// disallow if request did not come from localhost
|
||||
if !session.IsLocalRequest(ctx) {
|
||||
logger.Warnf("Attempt to reveal folder in file manager from non-local request")
|
||||
return false, fmt.Errorf("access denied")
|
||||
}
|
||||
|
||||
folderIDInt, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
var folderPath string
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
folder, err := r.repository.Folder.Find(ctx, models.FolderID(folderIDInt))
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding folder: %w", err)
|
||||
}
|
||||
if folder == nil {
|
||||
return fmt.Errorf("folder with id %d not found", folderIDInt)
|
||||
}
|
||||
folderPath = folder.Path
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := desktop.RevealInFileManager(folderPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||
}
|
||||
|
||||
// Populate a new gallery from the input
|
||||
newGallery := models.NewGallery()
|
||||
newGallery := models.CreateGalleryInput{
|
||||
Gallery: &models.Gallery{},
|
||||
}
|
||||
*newGallery.Gallery = models.NewGallery()
|
||||
|
||||
newGallery.Title = strings.TrimSpace(input.Title)
|
||||
newGallery.Code = translator.string(input.Code)
|
||||
@@ -81,10 +84,12 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
||||
newGallery.URLs = models.NewRelatedStrings([]string{strings.TrimSpace(*input.URL)})
|
||||
}
|
||||
|
||||
newGallery.CustomFields = convertMapJSONNumbers(input.CustomFields)
|
||||
|
||||
// Start the transaction and save the gallery
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Gallery
|
||||
if err := qb.Create(ctx, &newGallery, nil); err != nil {
|
||||
if err := qb.Create(ctx, &newGallery); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -241,6 +246,10 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
|
||||
if input.CustomFields != nil {
|
||||
updatedGallery.CustomFields = handleUpdateCustomFields(*input.CustomFields)
|
||||
}
|
||||
|
||||
// gallery scene is set from the scene only
|
||||
|
||||
gallery, err := qb.UpdatePartial(ctx, galleryID, updatedGallery)
|
||||
@@ -293,6 +302,10 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
||||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
|
||||
if input.CustomFields != nil {
|
||||
updatedGallery.CustomFields = handleUpdateCustomFields(*input.CustomFields)
|
||||
}
|
||||
|
||||
ret := []*models.Gallery{}
|
||||
|
||||
// Start the transaction and save the galleries
|
||||
|
||||
@@ -14,13 +14,17 @@ import (
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*models.CreateGroupInput, error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate a new group from the input
|
||||
newGroup := models.NewGroup()
|
||||
newGroupInput := &models.CreateGroupInput{
|
||||
Group: &models.Group{},
|
||||
}
|
||||
*newGroupInput.Group = models.NewGroup()
|
||||
newGroup := newGroupInput.Group
|
||||
|
||||
newGroup.Name = strings.TrimSpace(input.Name)
|
||||
newGroup.Aliases = translator.string(input.Aliases)
|
||||
@@ -59,28 +63,19 @@ func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*mo
|
||||
newGroup.URLs = models.NewRelatedStrings(stringslice.TrimSpace(input.Urls))
|
||||
}
|
||||
|
||||
return &newGroup, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
newGroup, err := groupFromGroupCreateInput(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newGroupInput.CustomFields = convertMapJSONNumbers(input.CustomFields)
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var frontimageData []byte
|
||||
if input.FrontImage != nil {
|
||||
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
newGroupInput.FrontImageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing front image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var backimageData []byte
|
||||
if input.BackImage != nil {
|
||||
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
newGroupInput.BackImageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing back image: %w", err)
|
||||
}
|
||||
@@ -88,13 +83,22 @@ func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInp
|
||||
|
||||
// HACK: if back image is being set, set the front image to the default.
|
||||
// This is because we can't have a null front image with a non-null back image.
|
||||
if len(frontimageData) == 0 && len(backimageData) != 0 {
|
||||
frontimageData = static.ReadAll(static.DefaultGroupImage)
|
||||
if len(newGroupInput.FrontImageData) == 0 && len(newGroupInput.BackImageData) != 0 {
|
||||
newGroupInput.FrontImageData = static.ReadAll(static.DefaultGroupImage)
|
||||
}
|
||||
|
||||
return newGroupInput, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
createGroupInput, err := groupFromGroupCreateInput(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start the transaction and save the group
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err = r.groupService.Create(ctx, newGroup, frontimageData, backimageData); err != nil {
|
||||
if err = r.groupService.Create(ctx, createGroupInput); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -104,9 +108,9 @@ func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInp
|
||||
}
|
||||
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.GroupCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.MovieCreatePost, input, nil)
|
||||
return r.getGroup(ctx, newGroup.ID)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, createGroupInput.Group.ID, hook.GroupCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, createGroupInput.Group.ID, hook.MovieCreatePost, input, nil)
|
||||
return r.getGroup(ctx, createGroupInput.Group.ID)
|
||||
}
|
||||
|
||||
func groupPartialFromGroupUpdateInput(translator changesetTranslator, input GroupUpdateInput) (ret models.GroupPartial, err error) {
|
||||
@@ -150,6 +154,12 @@ func groupPartialFromGroupUpdateInput(translator changesetTranslator, input Grou
|
||||
}
|
||||
|
||||
updatedGroup.URLs = translator.updateStrings(input.Urls, "urls")
|
||||
if input.CustomFields != nil {
|
||||
updatedGroup.CustomFields = *input.CustomFields
|
||||
// convert json.Numbers to int/float
|
||||
updatedGroup.CustomFields.Full = convertMapJSONNumbers(updatedGroup.CustomFields.Full)
|
||||
updatedGroup.CustomFields.Partial = convertMapJSONNumbers(updatedGroup.CustomFields.Partial)
|
||||
}
|
||||
|
||||
return updatedGroup, nil
|
||||
}
|
||||
@@ -246,6 +256,13 @@ func groupPartialFromBulkGroupUpdateInput(translator changesetTranslator, input
|
||||
|
||||
updatedGroup.URLs = translator.optionalURLsBulk(input.Urls, nil)
|
||||
|
||||
if input.CustomFields != nil {
|
||||
updatedGroup.CustomFields = *input.CustomFields
|
||||
// convert json.Numbers to int/float
|
||||
updatedGroup.CustomFields.Full = convertMapJSONNumbers(updatedGroup.CustomFields.Full)
|
||||
updatedGroup.CustomFields.Partial = convertMapJSONNumbers(updatedGroup.CustomFields.Partial)
|
||||
}
|
||||
|
||||
return updatedGroup, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,13 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input models.ImageUp
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
if input.CustomFields != nil {
|
||||
updatedImage.CustomFields = *input.CustomFields
|
||||
// convert json.Numbers to int/float
|
||||
updatedImage.CustomFields.Full = convertMapJSONNumbers(updatedImage.CustomFields.Full)
|
||||
updatedImage.CustomFields.Partial = convertMapJSONNumbers(updatedImage.CustomFields.Partial)
|
||||
}
|
||||
|
||||
qb := r.repository.Image
|
||||
image, err := qb.UpdatePartial(ctx, imageID, updatedImage)
|
||||
if err != nil {
|
||||
@@ -237,6 +244,13 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
if input.CustomFields != nil {
|
||||
updatedImage.CustomFields = *input.CustomFields
|
||||
// convert json.Numbers to int/float
|
||||
updatedImage.CustomFields.Full = convertMapJSONNumbers(updatedImage.CustomFields.Full)
|
||||
updatedImage.CustomFields.Partial = convertMapJSONNumbers(updatedImage.CustomFields.Partial)
|
||||
}
|
||||
|
||||
// Start the transaction and save the images
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
var updatedGalleryIDs []int
|
||||
|
||||
@@ -122,9 +122,10 @@ func (r *mutationResolver) MigrateHashNaming(ctx context.Context) (string, error
|
||||
func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatabaseInput) (*string, error) {
|
||||
// if download is true, then backup to temporary file and return a link
|
||||
download := input.Download != nil && *input.Download
|
||||
includeBlobs := input.IncludeBlobs != nil && *input.IncludeBlobs
|
||||
mgr := manager.GetInstance()
|
||||
|
||||
backupPath, backupName, err := mgr.BackupDatabase(download)
|
||||
backupPath, backupName, err := mgr.BackupDatabase(download, includeBlobs)
|
||||
if err != nil {
|
||||
logger.Errorf("Error backing up database: %v", err)
|
||||
return nil, err
|
||||
|
||||
@@ -52,7 +52,17 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
newPerformer.FakeTits = translator.string(input.FakeTits)
|
||||
newPerformer.PenisLength = input.PenisLength
|
||||
newPerformer.Circumcised = input.Circumcised
|
||||
newPerformer.CareerLength = translator.string(input.CareerLength)
|
||||
newPerformer.CareerStart = input.CareerStart
|
||||
newPerformer.CareerEnd = input.CareerEnd
|
||||
// if career_start/career_end not provided, parse deprecated career_length
|
||||
if newPerformer.CareerStart == nil && newPerformer.CareerEnd == nil && input.CareerLength != nil {
|
||||
start, end, err := utils.ParseYearRangeString(*input.CareerLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse career_length %q: %w", *input.CareerLength, err)
|
||||
}
|
||||
newPerformer.CareerStart = start
|
||||
newPerformer.CareerEnd = end
|
||||
}
|
||||
newPerformer.Tattoos = translator.string(input.Tattoos)
|
||||
newPerformer.Piercings = translator.string(input.Piercings)
|
||||
newPerformer.Favorite = translator.bool(input.Favorite)
|
||||
@@ -261,7 +271,22 @@ func performerPartialFromInput(input models.PerformerUpdateInput, translator cha
|
||||
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.PenisLength = translator.optionalFloat64(input.PenisLength, "penis_length")
|
||||
updatedPerformer.Circumcised = translator.optionalString((*string)(input.Circumcised), "circumcised")
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
// prefer career_start/career_end over deprecated career_length
|
||||
if translator.hasField("career_start") || translator.hasField("career_end") {
|
||||
updatedPerformer.CareerStart = translator.optionalInt(input.CareerStart, "career_start")
|
||||
updatedPerformer.CareerEnd = translator.optionalInt(input.CareerEnd, "career_end")
|
||||
} else if translator.hasField("career_length") && input.CareerLength != nil {
|
||||
start, end, err := utils.ParseYearRangeString(*input.CareerLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse career_length %q: %w", *input.CareerLength, err)
|
||||
}
|
||||
if start != nil {
|
||||
updatedPerformer.CareerStart = models.NewOptionalInt(*start)
|
||||
}
|
||||
if end != nil {
|
||||
updatedPerformer.CareerEnd = models.NewOptionalInt(*end)
|
||||
}
|
||||
}
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
@@ -417,7 +442,22 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
|
||||
updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits")
|
||||
updatedPerformer.PenisLength = translator.optionalFloat64(input.PenisLength, "penis_length")
|
||||
updatedPerformer.Circumcised = translator.optionalString((*string)(input.Circumcised), "circumcised")
|
||||
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
|
||||
// prefer career_start/career_end over deprecated career_length
|
||||
if translator.hasField("career_start") || translator.hasField("career_end") {
|
||||
updatedPerformer.CareerStart = translator.optionalInt(input.CareerStart, "career_start")
|
||||
updatedPerformer.CareerEnd = translator.optionalInt(input.CareerEnd, "career_end")
|
||||
} else if translator.hasField("career_length") && input.CareerLength != nil {
|
||||
start, end, err := utils.ParseYearRangeString(*input.CareerLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse career_length %q: %w", *input.CareerLength, err)
|
||||
}
|
||||
if start != nil {
|
||||
updatedPerformer.CareerStart = models.NewOptionalInt(*start)
|
||||
}
|
||||
if end != nil {
|
||||
updatedPerformer.CareerEnd = models.NewOptionalInt(*end)
|
||||
}
|
||||
}
|
||||
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
|
||||
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
|
||||
|
||||
|
||||
@@ -103,8 +103,15 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
|
||||
}
|
||||
}
|
||||
|
||||
customFields := convertMapJSONNumbers(input.CustomFields)
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.Resolver.sceneService.Create(ctx, &newScene, fileIDs, coverImageData)
|
||||
ret, err = r.Resolver.sceneService.Create(ctx, models.CreateSceneInput{
|
||||
Scene: &newScene,
|
||||
FileIDs: fileIDs,
|
||||
CoverImage: coverImageData,
|
||||
CustomFields: customFields,
|
||||
})
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -306,6 +313,15 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp
|
||||
}
|
||||
}
|
||||
|
||||
var customFields *models.CustomFieldsInput
|
||||
if input.CustomFields != nil {
|
||||
cfCopy := *input.CustomFields
|
||||
customFields = &cfCopy
|
||||
// convert json.Numbers to int/float
|
||||
customFields.Full = convertMapJSONNumbers(customFields.Full)
|
||||
customFields.Partial = convertMapJSONNumbers(customFields.Partial)
|
||||
}
|
||||
|
||||
scene, err := qb.UpdatePartial(ctx, sceneID, *updatedScene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -317,6 +333,12 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp
|
||||
}
|
||||
}
|
||||
|
||||
if customFields != nil {
|
||||
if err := qb.SetCustomFields(ctx, scene.ID, *customFields); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return scene, nil
|
||||
}
|
||||
|
||||
@@ -387,6 +409,12 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
|
||||
}
|
||||
}
|
||||
|
||||
var customFields *models.CustomFieldsInput
|
||||
if input.CustomFields != nil {
|
||||
cf := handleUpdateCustomFields(*input.CustomFields)
|
||||
customFields = &cf
|
||||
}
|
||||
|
||||
ret := []*models.Scene{}
|
||||
|
||||
// Start the transaction and save the scenes
|
||||
@@ -399,6 +427,12 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
|
||||
return err
|
||||
}
|
||||
|
||||
if customFields != nil {
|
||||
if err := qb.SetCustomFields(ctx, scene.ID, *customFields); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ret = append(ret, scene)
|
||||
}
|
||||
|
||||
@@ -575,6 +609,7 @@ func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput
|
||||
|
||||
var values *models.ScenePartial
|
||||
var coverImageData []byte
|
||||
var customFields *models.CustomFieldsInput
|
||||
|
||||
if input.Values != nil {
|
||||
translator := changesetTranslator{
|
||||
@@ -593,6 +628,11 @@ func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput
|
||||
return nil, fmt.Errorf("processing cover image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if input.Values.CustomFields != nil {
|
||||
cf := handleUpdateCustomFields(*input.Values.CustomFields)
|
||||
customFields = &cf
|
||||
}
|
||||
} else {
|
||||
v := models.NewScenePartial()
|
||||
values = &v
|
||||
@@ -626,7 +666,15 @@ func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput
|
||||
|
||||
// only update cover image if one was provided
|
||||
if len(coverImageData) > 0 {
|
||||
return r.sceneUpdateCoverImage(ctx, ret, coverImageData)
|
||||
if err := r.sceneUpdateCoverImage(ctx, ret, coverImageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if customFields != nil {
|
||||
if err := r.Resolver.repository.Scene.SetCustomFields(ctx, ret.ID, *customFields); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -58,6 +58,16 @@ func (r *mutationResolver) StashBoxBatchStudioTag(ctx context.Context, input man
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StashBoxBatchTagTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
|
||||
b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) //nolint:staticcheck
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jobID := manager.GetInstance().StashBoxBatchTagTag(ctx, b, input)
|
||||
return strconv.Itoa(jobID), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input StashBoxDraftSubmissionInput) (*string, error) {
|
||||
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
|
||||
if err != nil {
|
||||
|
||||
@@ -38,6 +38,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
newStudio.Favorite = translator.bool(input.Favorite)
|
||||
newStudio.Details = translator.string(input.Details)
|
||||
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
newStudio.Organized = translator.bool(input.Organized)
|
||||
newStudio.Aliases = models.NewRelatedStrings(stringslice.UniqueExcludeFold(stringslice.TrimSpace(input.Aliases), newStudio.Name))
|
||||
newStudio.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
|
||||
|
||||
@@ -120,6 +121,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
updatedStudio.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||
updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
updatedStudio.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases")
|
||||
updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
|
||||
|
||||
@@ -261,6 +263,7 @@ func (r *mutationResolver) BulkStudioUpdate(ctx context.Context, input BulkStudi
|
||||
partial.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||
partial.Details = translator.optionalString(input.Details, "details")
|
||||
partial.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||
partial.Organized = translator.optionalBool(input.Organized, "organized")
|
||||
|
||||
partial.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
||||
if err != nil {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
@@ -31,7 +30,10 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
}
|
||||
|
||||
// Populate a new tag from the input
|
||||
newTag := models.NewTag()
|
||||
newTag := models.CreateTagInput{
|
||||
Tag: &models.Tag{},
|
||||
}
|
||||
*newTag.Tag = models.NewTag()
|
||||
|
||||
newTag.Name = strings.TrimSpace(input.Name)
|
||||
newTag.SortName = translator.string(input.SortName)
|
||||
@@ -60,6 +62,8 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
return nil, fmt.Errorf("converting child tag ids: %w", err)
|
||||
}
|
||||
|
||||
newTag.CustomFields = convertMapJSONNumbers(input.CustomFields)
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var imageData []byte
|
||||
if input.Image != nil {
|
||||
@@ -73,7 +77,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Tag
|
||||
|
||||
if err := tag.ValidateCreate(ctx, newTag, qb); err != nil {
|
||||
if err := tag.ValidateCreate(ctx, *newTag.Tag, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -98,17 +102,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
||||
return r.getTag(ctx, newTag.ID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) (*models.Tag, error) {
|
||||
tagID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate tag from the input
|
||||
func tagPartialFromInput(input TagUpdateInput, translator changesetTranslator) (*models.TagPartial, error) {
|
||||
updatedTag := models.NewTagPartial()
|
||||
|
||||
updatedTag.Name = translator.optionalString(input.Name, "name")
|
||||
@@ -127,6 +121,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
}
|
||||
updatedTag.StashIDs = translator.updateStashIDs(updateStashIDInputs, "stash_ids")
|
||||
|
||||
var err error
|
||||
updatedTag.ParentIDs, err = translator.updateIds(input.ParentIds, "parent_ids")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting parent tag ids: %w", err)
|
||||
@@ -137,6 +132,32 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
return nil, fmt.Errorf("converting child tag ids: %w", err)
|
||||
}
|
||||
|
||||
if input.CustomFields != nil {
|
||||
updatedTag.CustomFields = *input.CustomFields
|
||||
// convert json.Numbers to int/float
|
||||
updatedTag.CustomFields.Full = convertMapJSONNumbers(updatedTag.CustomFields.Full)
|
||||
updatedTag.CustomFields.Partial = convertMapJSONNumbers(updatedTag.CustomFields.Partial)
|
||||
}
|
||||
|
||||
return &updatedTag, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) (*models.Tag, error) {
|
||||
tagID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate tag from the input
|
||||
updatedTag, err := tagPartialFromInput(input, translator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var imageData []byte
|
||||
imageIncluded := translator.hasField("image")
|
||||
if input.Image != nil {
|
||||
@@ -173,11 +194,11 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tag.ValidateUpdate(ctx, tagID, updatedTag, qb); err != nil {
|
||||
if err := tag.ValidateUpdate(ctx, tagID, *updatedTag, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, err = qb.UpdatePartial(ctx, tagID, updatedTag)
|
||||
t, err = qb.UpdatePartial(ctx, tagID, *updatedTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -325,6 +346,31 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var values *models.TagPartial
|
||||
var imageData []byte
|
||||
|
||||
if input.Values != nil {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getNamedUpdateInputMap(ctx, "input.values"),
|
||||
}
|
||||
|
||||
values, err = tagPartialFromInput(*input.Values, translator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if input.Values.Image != nil {
|
||||
var err error
|
||||
imageData, err = utils.ProcessImageInput(ctx, *input.Values.Image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing cover image: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
v := models.NewTagPartial()
|
||||
values = &v
|
||||
}
|
||||
|
||||
var t *models.Tag
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Tag
|
||||
@@ -339,28 +385,22 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput)
|
||||
return fmt.Errorf("tag with id %d not found", destination)
|
||||
}
|
||||
|
||||
parents, children, err := tag.MergeHierarchy(ctx, destination, source, qb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = qb.Merge(ctx, source, destination); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = qb.UpdateParentTags(ctx, destination, parents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = qb.UpdateChildTags(ctx, destination, children)
|
||||
if err != nil {
|
||||
if err := tag.ValidateUpdate(ctx, destination, *values, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tag.ValidateHierarchyExisting(ctx, t, parents, children, qb)
|
||||
if err != nil {
|
||||
logger.Errorf("Error merging tag: %s", err)
|
||||
return err
|
||||
if _, err := qb.UpdatePartial(ctx, destination, *values); err != nil {
|
||||
return fmt.Errorf("updating tag: %w", err)
|
||||
}
|
||||
|
||||
if len(imageData) > 0 {
|
||||
if err := qb.UpdateImage(ctx, destination, imageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -96,6 +96,11 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
ParallelTasks: config.GetParallelTasks(),
|
||||
UseCustomSpriteInterval: config.GetUseCustomSpriteInterval(),
|
||||
SpriteInterval: config.GetSpriteInterval(),
|
||||
SpriteScreenshotSize: config.GetSpriteScreenshotSize(),
|
||||
MinimumSprites: config.GetMinimumSprites(),
|
||||
MaximumSprites: config.GetMaximumSprites(),
|
||||
PreviewAudio: config.GetPreviewAudio(),
|
||||
PreviewSegments: config.GetPreviewSegments(),
|
||||
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||
|
||||
@@ -118,7 +118,7 @@ func createTag(ctx context.Context, qb models.TagWriter) error {
|
||||
Name: testName,
|
||||
}
|
||||
|
||||
err := qb.Create(ctx, &tag)
|
||||
err := qb.Create(ctx, &models.CreateTagInput{Tag: &tag})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -365,7 +365,10 @@ func makeImage(expectedResult bool) *models.Image {
|
||||
}
|
||||
|
||||
func createImage(ctx context.Context, w models.ImageWriter, o *models.Image, f *models.ImageFile) error {
|
||||
err := w.Create(ctx, o, []models.FileID{f.ID})
|
||||
err := w.Create(ctx, &models.CreateImageInput{
|
||||
Image: o,
|
||||
FileIDs: []models.FileID{f.ID},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create image with path '%s': %s", f.Path, err.Error())
|
||||
@@ -468,7 +471,10 @@ func makeGallery(expectedResult bool) *models.Gallery {
|
||||
}
|
||||
|
||||
func createGallery(ctx context.Context, w models.GalleryWriter, o *models.Gallery, f *models.BaseFile) error {
|
||||
err := w.Create(ctx, o, []models.FileID{f.ID})
|
||||
err := w.Create(ctx, &models.CreateGalleryInput{
|
||||
Gallery: o,
|
||||
FileIDs: []models.FileID{f.ID},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create gallery with path '%s': %s", f.Path, err.Error())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -155,15 +156,17 @@ func getIconPath() string {
|
||||
return path.Join(config.GetInstance().GetConfigPath(), "icon.png")
|
||||
}
|
||||
|
||||
func RevealInFileManager(path string) {
|
||||
exists, err := fsutil.FileExists(path)
|
||||
func RevealInFileManager(path string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
logger.Errorf("Error checking file: %s", err)
|
||||
return
|
||||
return fmt.Errorf("error checking path: %w", err)
|
||||
}
|
||||
if exists && IsDesktop() {
|
||||
revealInFileManager(path)
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting absolute path: %w", err)
|
||||
}
|
||||
return revealInFileManager(absPath, info)
|
||||
}
|
||||
|
||||
func getServerURL(path string) string {
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/kermieisinthehouse/gosx-notifier"
|
||||
gosxnotifier "github.com/kermieisinthehouse/gosx-notifier"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -32,8 +34,11 @@ func sendNotification(notificationTitle string, notificationText string) {
|
||||
}
|
||||
}
|
||||
|
||||
func revealInFileManager(path string) {
|
||||
exec.Command(`open`, `-R`, path)
|
||||
func revealInFileManager(path string, _ os.FileInfo) error {
|
||||
if err := exec.Command(`open`, `-R`, path).Run(); err != nil {
|
||||
return fmt.Errorf("error revealing path in Finder: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDoubleClickLaunched() bool {
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
@@ -33,8 +35,15 @@ func sendNotification(notificationTitle string, notificationText string) {
|
||||
}
|
||||
}
|
||||
|
||||
func revealInFileManager(path string) {
|
||||
|
||||
func revealInFileManager(path string, info os.FileInfo) error {
|
||||
dir := path
|
||||
if !info.IsDir() {
|
||||
dir = filepath.Dir(path)
|
||||
}
|
||||
if err := exec.Command("xdg-open", dir).Run(); err != nil {
|
||||
return fmt.Errorf("error opening directory in file manager: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDoubleClickLaunched() bool {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package desktop
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
@@ -83,6 +84,10 @@ func sendNotification(notificationTitle string, notificationText string) {
|
||||
}
|
||||
}
|
||||
|
||||
func revealInFileManager(path string) {
|
||||
exec.Command(`explorer`, `\select`, path)
|
||||
func revealInFileManager(path string, _ os.FileInfo) error {
|
||||
c := exec.Command(`explorer`, `/select,`, path)
|
||||
logger.Debugf("Running: %s", c.String())
|
||||
// explorer seems to return an error code even when it works, so ignore the error
|
||||
_ = c.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -147,6 +147,9 @@ func (t *SceneIdentifier) getOptions(source ScraperSource) MetadataOptions {
|
||||
if source.Options.IncludeMalePerformers != nil {
|
||||
options.IncludeMalePerformers = source.Options.IncludeMalePerformers
|
||||
}
|
||||
if source.Options.PerformerGenders != nil {
|
||||
options.PerformerGenders = source.Options.PerformerGenders
|
||||
}
|
||||
if source.Options.SkipMultipleMatches != nil {
|
||||
options.SkipMultipleMatches = source.Options.SkipMultipleMatches
|
||||
}
|
||||
@@ -204,13 +207,23 @@ func (t *SceneIdentifier) getSceneUpdater(ctx context.Context, s *models.Scene,
|
||||
ret.Partial.StudioID = models.NewOptionalInt(*studioID)
|
||||
}
|
||||
|
||||
includeMalePerformers := true
|
||||
if options.IncludeMalePerformers != nil {
|
||||
includeMalePerformers = *options.IncludeMalePerformers
|
||||
// Determine allowed genders for performer filtering
|
||||
var allowedGenders []models.GenderEnum
|
||||
if options.PerformerGenders != nil {
|
||||
// New field takes precedence
|
||||
allowedGenders = options.PerformerGenders
|
||||
} else if options.IncludeMalePerformers != nil && !*options.IncludeMalePerformers {
|
||||
// Legacy: if includeMalePerformers is false, include all genders except male
|
||||
for _, g := range models.AllGenderEnum {
|
||||
if g != models.GenderEnumMale {
|
||||
allowedGenders = append(allowedGenders, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
// nil allowedGenders means include all performers
|
||||
|
||||
addSkipSingleNamePerformerTag := false
|
||||
performerIDs, err := rel.performers(ctx, !includeMalePerformers)
|
||||
performerIDs, err := rel.performers(ctx, allowedGenders)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrSkipSingleNamePerformer) {
|
||||
addSkipSingleNamePerformerTag = true
|
||||
|
||||
@@ -60,9 +60,15 @@ func TestSceneIdentifier_Identify(t *testing.T) {
|
||||
)
|
||||
|
||||
defaultOptions := &MetadataOptions{
|
||||
SetOrganized: &boolFalse,
|
||||
SetCoverImage: &boolFalse,
|
||||
IncludeMalePerformers: &boolFalse,
|
||||
SetOrganized: &boolFalse,
|
||||
SetCoverImage: &boolFalse,
|
||||
PerformerGenders: []models.GenderEnum{
|
||||
models.GenderEnumFemale,
|
||||
models.GenderEnumTransgenderFemale,
|
||||
models.GenderEnumTransgenderMale,
|
||||
models.GenderEnumIntersex,
|
||||
models.GenderEnumNonBinary,
|
||||
},
|
||||
SkipSingleNamePerformers: &boolFalse,
|
||||
}
|
||||
sources := []ScraperSource{
|
||||
@@ -216,9 +222,15 @@ func TestSceneIdentifier_modifyScene(t *testing.T) {
|
||||
|
||||
boolFalse := false
|
||||
defaultOptions := &MetadataOptions{
|
||||
SetOrganized: &boolFalse,
|
||||
SetCoverImage: &boolFalse,
|
||||
IncludeMalePerformers: &boolFalse,
|
||||
SetOrganized: &boolFalse,
|
||||
SetCoverImage: &boolFalse,
|
||||
PerformerGenders: []models.GenderEnum{
|
||||
models.GenderEnumFemale,
|
||||
models.GenderEnumTransgenderFemale,
|
||||
models.GenderEnumTransgenderMale,
|
||||
models.GenderEnumIntersex,
|
||||
models.GenderEnumNonBinary,
|
||||
},
|
||||
SkipSingleNamePerformers: &boolFalse,
|
||||
}
|
||||
tr := &SceneIdentifier{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
)
|
||||
|
||||
@@ -32,7 +33,10 @@ type MetadataOptions struct {
|
||||
SetCoverImage *bool `json:"setCoverImage"`
|
||||
SetOrganized *bool `json:"setOrganized"`
|
||||
// defaults to true if not provided
|
||||
// Deprecated: use PerformerGenders instead
|
||||
IncludeMalePerformers *bool `json:"includeMalePerformers"`
|
||||
// Filter to only include performers with these genders. If not provided, all genders are included.
|
||||
PerformerGenders []models.GenderEnum `json:"performerGenders"`
|
||||
// defaults to true if not provided
|
||||
SkipMultipleMatches *bool `json:"skipMultipleMatches"`
|
||||
// ID of tag to tag skipped multiple matches with
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -69,7 +70,7 @@ func (g sceneRelationships) studio(ctx context.Context) (*int, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g sceneRelationships) performers(ctx context.Context, ignoreMale bool) ([]int, error) {
|
||||
func (g sceneRelationships) performers(ctx context.Context, allowedGenders []models.GenderEnum) ([]int, error) {
|
||||
fieldStrategy := g.fieldOptions["performers"]
|
||||
scraped := g.result.result.Performers
|
||||
|
||||
@@ -97,8 +98,11 @@ func (g sceneRelationships) performers(ctx context.Context, ignoreMale bool) ([]
|
||||
singleNamePerformerSkipped := false
|
||||
|
||||
for _, p := range scraped {
|
||||
if ignoreMale && p.Gender != nil && strings.EqualFold(*p.Gender, models.GenderEnumMale.String()) {
|
||||
continue
|
||||
if allowedGenders != nil && p.Gender != nil {
|
||||
gender := models.GenderEnum(strings.ToUpper(*p.Gender))
|
||||
if !slices.Contains(allowedGenders, gender) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
performerID, err := getPerformerID(ctx, endpoint, g.performerCreator, p, createMissing, g.skipSingleNamePerformers)
|
||||
@@ -167,7 +171,9 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) {
|
||||
} else if createMissing {
|
||||
newTag := t.ToTag(endpoint, nil)
|
||||
|
||||
err := g.tagCreator.Create(ctx, newTag)
|
||||
err := g.tagCreator.Create(ctx, &models.CreateTagInput{
|
||||
Tag: newTag,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating tag: %w", err)
|
||||
}
|
||||
|
||||
@@ -183,13 +183,13 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scene *models.Scene
|
||||
fieldOptions *FieldOptions
|
||||
scraped []*models.ScrapedPerformer
|
||||
ignoreMale bool
|
||||
want []int
|
||||
wantErr bool
|
||||
name string
|
||||
scene *models.Scene
|
||||
fieldOptions *FieldOptions
|
||||
scraped []*models.ScrapedPerformer
|
||||
allowedGenders []models.GenderEnum
|
||||
want []int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"ignore",
|
||||
@@ -202,7 +202,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
StoredID: &validStoredID,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -211,7 +211,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
emptyScene,
|
||||
defaultOptions,
|
||||
[]*models.ScrapedPerformer{},
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -225,7 +225,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
StoredID: &existingPerformerStr,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -239,7 +239,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
StoredID: &validStoredID,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
[]int{existingPerformerID, validStoredIDInt},
|
||||
false,
|
||||
},
|
||||
@@ -254,7 +254,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
Gender: &male,
|
||||
},
|
||||
},
|
||||
true,
|
||||
[]models.GenderEnum{models.GenderEnumFemale, models.GenderEnumTransgenderMale, models.GenderEnumTransgenderFemale, models.GenderEnumIntersex, models.GenderEnumNonBinary},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -270,7 +270,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
StoredID: &validStoredID,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
[]int{validStoredIDInt},
|
||||
false,
|
||||
},
|
||||
@@ -287,7 +287,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
Gender: &female,
|
||||
},
|
||||
},
|
||||
true,
|
||||
[]models.GenderEnum{models.GenderEnumFemale, models.GenderEnumTransgenderMale, models.GenderEnumTransgenderFemale, models.GenderEnumIntersex, models.GenderEnumNonBinary},
|
||||
[]int{validStoredIDInt},
|
||||
false,
|
||||
},
|
||||
@@ -304,7 +304,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
StoredID: &invalidStoredID,
|
||||
},
|
||||
},
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
@@ -319,7 +319,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
got, err := tr.performers(testCtx, tt.ignoreMale)
|
||||
got, err := tr.performers(testCtx, tt.allowedGenders)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("sceneRelationships.performers() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@@ -368,14 +368,14 @@ func Test_sceneRelationships_tags(t *testing.T) {
|
||||
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
db.Tag.On("Create", testCtx, mock.MatchedBy(func(p *models.Tag) bool {
|
||||
return p.Name == validName
|
||||
db.Tag.On("Create", testCtx, mock.MatchedBy(func(p *models.CreateTagInput) bool {
|
||||
return p.Tag.Name == validName
|
||||
})).Run(func(args mock.Arguments) {
|
||||
t := args.Get(1).(*models.Tag)
|
||||
t.ID = validStoredIDInt
|
||||
t := args.Get(1).(*models.CreateTagInput)
|
||||
t.Tag.ID = validStoredIDInt
|
||||
}).Return(nil)
|
||||
db.Tag.On("Create", testCtx, mock.MatchedBy(func(p *models.Tag) bool {
|
||||
return p.Name == invalidName
|
||||
db.Tag.On("Create", testCtx, mock.MatchedBy(func(p *models.CreateTagInput) bool {
|
||||
return p.Tag.Name == invalidName
|
||||
})).Return(errors.New("error creating tag"))
|
||||
|
||||
tr := sceneRelationships{
|
||||
|
||||
185
internal/manager/backup.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
type databaseBackupZip struct {
|
||||
*zip.Writer
|
||||
}
|
||||
|
||||
func (z *databaseBackupZip) zipFileRename(fn, outDir, outFn string) error {
|
||||
p := filepath.Join(outDir, outFn)
|
||||
p = filepath.ToSlash(p)
|
||||
|
||||
f, err := z.Create(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating zip entry for %s: %v", fn, err)
|
||||
}
|
||||
|
||||
i, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening %s: %v", fn, err)
|
||||
}
|
||||
|
||||
defer i.Close()
|
||||
|
||||
if _, err := io.Copy(f, i); err != nil {
|
||||
return fmt.Errorf("error writing %s to zip: %v", fn, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *databaseBackupZip) zipFile(fn, outDir string) error {
|
||||
return z.zipFileRename(fn, outDir, filepath.Base(fn))
|
||||
}
|
||||
|
||||
func (s *Manager) BackupDatabase(download bool, includeBlobs bool) (string, string, error) {
|
||||
var backupPath string
|
||||
var backupName string
|
||||
|
||||
// if we include blobs, then the output is a zip file
|
||||
// if not, using the same backup logic as before, which creates a sqlite file
|
||||
if !includeBlobs || s.Config.GetBlobsStorage() != config.BlobStorageTypeFilesystem {
|
||||
return s.backupDatabaseOnly(download)
|
||||
}
|
||||
|
||||
// use tmp directory for the backup
|
||||
backupDir := s.Paths.Generated.Tmp
|
||||
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
|
||||
}
|
||||
f, err := os.CreateTemp(backupDir, "backup*.sqlite")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
backupPath = f.Name()
|
||||
backupName = s.Database.DatabaseBackupPath("")
|
||||
f.Close()
|
||||
|
||||
// delete the temp file so that the backup operation can create it
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
return "", "", fmt.Errorf("could not remove temporary backup file %v: %w", backupPath, err)
|
||||
}
|
||||
|
||||
if err := s.Database.Backup(backupPath); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// create a zip file
|
||||
zipFileDir := s.Paths.Generated.Downloads
|
||||
if !download {
|
||||
zipFileDir = s.Config.GetBackupDirectoryPathOrDefault()
|
||||
if zipFileDir != "" {
|
||||
if err := fsutil.EnsureDir(zipFileDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create backup directory %v: %w", zipFileDir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zipFileName := backupName + ".zip"
|
||||
zipFilePath := filepath.Join(zipFileDir, zipFileName)
|
||||
|
||||
logger.Debugf("Preparing zip file for database backup at %v", zipFilePath)
|
||||
|
||||
zf, err := os.Create(zipFilePath)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("could not create zip file %v: %w", zipFilePath, err)
|
||||
}
|
||||
defer zf.Close()
|
||||
|
||||
z := databaseBackupZip{
|
||||
Writer: zip.NewWriter(zf),
|
||||
}
|
||||
|
||||
defer z.Close()
|
||||
|
||||
// move the database file into the zip
|
||||
dbFn := filepath.Base(s.Config.GetDatabasePath())
|
||||
if err := z.zipFileRename(backupPath, "", dbFn); err != nil {
|
||||
return "", "", fmt.Errorf("could not add database backup to zip file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
return "", "", fmt.Errorf("could not remove temporary backup file %v: %w", backupPath, err)
|
||||
}
|
||||
|
||||
// walk the blobs directory and add files to the zip
|
||||
blobsDir := s.Config.GetBlobsPath()
|
||||
err = filepath.WalkDir(blobsDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculate out dir by removing the blobsDir prefix from the path
|
||||
outDir := filepath.Join("blobs", strings.TrimPrefix(filepath.Dir(path), blobsDir))
|
||||
if err := z.zipFile(path, outDir); err != nil {
|
||||
return fmt.Errorf("could not add blob %v to zip file: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error walking blobs directory: %w", err)
|
||||
}
|
||||
|
||||
return zipFilePath, zipFileName, nil
|
||||
}
|
||||
|
||||
func (s *Manager) backupDatabaseOnly(download bool) (string, string, error) {
|
||||
var backupPath string
|
||||
var backupName string
|
||||
|
||||
if download {
|
||||
backupDir := s.Paths.Generated.Downloads
|
||||
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
|
||||
}
|
||||
f, err := os.CreateTemp(backupDir, "backup*.sqlite")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
backupPath = f.Name()
|
||||
backupName = s.Database.DatabaseBackupPath("")
|
||||
f.Close()
|
||||
|
||||
// delete the temp file so that the backup operation can create it
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
return "", "", fmt.Errorf("could not remove temporary backup file %v: %w", backupPath, err)
|
||||
}
|
||||
} else {
|
||||
backupDir := s.Config.GetBackupDirectoryPathOrDefault()
|
||||
if backupDir != "" {
|
||||
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
|
||||
}
|
||||
}
|
||||
backupPath = s.Database.DatabaseBackupPath(backupDir)
|
||||
backupName = filepath.Base(backupPath)
|
||||
}
|
||||
|
||||
err := s.Database.Backup(backupPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return backupPath, backupName, nil
|
||||
}
|
||||
@@ -83,6 +83,21 @@ const (
|
||||
ParallelTasks = "parallel_tasks"
|
||||
parallelTasksDefault = 1
|
||||
|
||||
UseCustomSpriteInterval = "use_custom_sprite_interval"
|
||||
UseCustomSpriteIntervalDefault = false
|
||||
|
||||
SpriteInterval = "sprite_interval"
|
||||
SpriteIntervalDefault = 30
|
||||
|
||||
MinimumSprites = "minimum_sprites"
|
||||
MinimumSpritesDefault = 10
|
||||
|
||||
MaximumSprites = "maximum_sprites"
|
||||
MaximumSpritesDefault = 500
|
||||
|
||||
SpriteScreenshotSize = "sprite_screenshot_width"
|
||||
spriteScreenshotSizeDefault = 160
|
||||
|
||||
PreviewPreset = "preview_preset"
|
||||
TranscodeHardwareAcceleration = "ffmpeg.hardware_acceleration"
|
||||
|
||||
@@ -975,6 +990,50 @@ func (i *Config) GetParallelTasksWithAutoDetection() int {
|
||||
return parallelTasks
|
||||
}
|
||||
|
||||
// GetUseCustomSpriteInterval returns true if the sprite minimum, maximum, and interval settings
|
||||
// should be used instead of the default
|
||||
func (i *Config) GetUseCustomSpriteInterval() bool {
|
||||
value := i.getBool(UseCustomSpriteInterval)
|
||||
return value
|
||||
}
|
||||
|
||||
// GetSpriteInterval returns the time (in seconds) to be between each scrubber sprite
|
||||
// A value of 0 indicates that the sprite interval should be automatically determined
|
||||
// based on the minimum sprite setting.
|
||||
func (i *Config) GetSpriteInterval() float64 {
|
||||
value := i.getFloat64(SpriteInterval)
|
||||
return value
|
||||
}
|
||||
|
||||
// GetMinimumSprites returns the minimum number of sprites that have to be generated
|
||||
// A value of 0 will be overridden with the default of 10.
|
||||
func (i *Config) GetMinimumSprites() int {
|
||||
value := i.getInt(MinimumSprites)
|
||||
if value <= 0 {
|
||||
return MinimumSpritesDefault
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// GetMaximumSprites returns the maximum number of sprites that can be generated
|
||||
// A value of 0 indicates no maximum.
|
||||
func (i *Config) GetMaximumSprites() int {
|
||||
value := i.getInt(MaximumSprites)
|
||||
return value
|
||||
}
|
||||
|
||||
// GetSpriteScreenshotSize returns the required size of the screenshots to be taken
|
||||
// during sprite generation in pixels. This will be the width for landscape scenes
|
||||
// and the height for portrait scenes, with the other dimension being scaled to maintain
|
||||
// the aspect ratio. If the value is less than or equal to 0, the default will be used.
|
||||
func (i *Config) GetSpriteScreenshotSize() int {
|
||||
value := i.getInt(SpriteScreenshotSize)
|
||||
if value <= 0 {
|
||||
return spriteScreenshotSizeDefault
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (i *Config) GetPreviewAudio() bool {
|
||||
return i.getBool(PreviewAudio)
|
||||
}
|
||||
@@ -1861,6 +1920,12 @@ func (i *Config) setDefaultValues() {
|
||||
i.setDefault(PreviewAudio, previewAudioDefault)
|
||||
i.setDefault(SoundOnPreview, false)
|
||||
|
||||
i.setDefault(UseCustomSpriteInterval, UseCustomSpriteIntervalDefault)
|
||||
i.setDefault(SpriteInterval, SpriteIntervalDefault)
|
||||
i.setDefault(MinimumSprites, MinimumSpritesDefault)
|
||||
i.setDefault(MaximumSprites, MaximumSpritesDefault)
|
||||
i.setDefault(SpriteScreenshotSize, spriteScreenshotSizeDefault)
|
||||
|
||||
i.setDefault(ThemeColor, DefaultThemeColor)
|
||||
|
||||
i.setDefault(WriteImageThumbnails, writeImageThumbnailsDefault)
|
||||
|
||||
@@ -38,3 +38,12 @@ func (s StashConfigs) GetStashFromDirPath(dirPath string) *StashConfig {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s StashConfigs) Paths() []string {
|
||||
paths := make([]string, len(s))
|
||||
for i, c := range s {
|
||||
// #6618 - clean the path to ensure comparison works correctly
|
||||
paths[i] = filepath.Clean(c.Path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -21,8 +21,7 @@ type SpriteGenerator struct {
|
||||
VideoChecksum string
|
||||
ImageOutputPath string
|
||||
VTTOutputPath string
|
||||
Rows int
|
||||
Columns int
|
||||
Config SpriteGeneratorConfig
|
||||
SlowSeek bool // use alternate seek function, very slow!
|
||||
|
||||
Overwrite bool
|
||||
@@ -30,13 +29,81 @@ type SpriteGenerator struct {
|
||||
g *generate.Generator
|
||||
}
|
||||
|
||||
func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageOutputPath string, vttOutputPath string, rows int, cols int) (*SpriteGenerator, error) {
|
||||
// SpriteGeneratorConfig holds configuration for the SpriteGenerator
|
||||
type SpriteGeneratorConfig struct {
|
||||
// MinimumSprites is the minimum number of sprites to generate, even if the video duration is short
|
||||
// SpriteInterval will be adjusted accordingly to ensure at least this many sprites are generated.
|
||||
// A value of 0 means no minimum, and the generator will use the provided SpriteInterval or
|
||||
// calculate it based on the video duration and MaximumSprites
|
||||
MinimumSprites int
|
||||
|
||||
// MaximumSprites is the maximum number of sprites to generate, even if the video duration is long
|
||||
// SpriteInterval will be adjusted accordingly to ensure no more than this many sprites are generated
|
||||
// A value of 0 means no maximum, and the generator will use the provided SpriteInterval or
|
||||
// calculate it based on the video duration and MinimumSprites
|
||||
MaximumSprites int
|
||||
|
||||
// SpriteInterval is the default interval in seconds between each sprite.
|
||||
// If MinimumSprites or MaximumSprites are set, this value will be adjusted accordingly
|
||||
// to ensure the desired number of sprites are generated
|
||||
// A value of 0 means the generator will calculate the interval based on the video duration and
|
||||
// the provided MinimumSprites and MaximumSprites
|
||||
SpriteInterval float64
|
||||
|
||||
// SpriteSize is the size in pixels of the longest dimension of each sprite image.
|
||||
// The other dimension will be automatically calculated to maintain the aspect ratio of the video
|
||||
SpriteSize int
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultSpriteAmount is the default number of sprites to generate if no configuration is provided
|
||||
// This corresponds to the legacy behavior of the generator, which generates 81 sprites at equal
|
||||
// intervals across the video duration
|
||||
DefaultSpriteAmount = 81
|
||||
|
||||
// DefaultSpriteSize is the default size in pixels of the longest dimension of each sprite image
|
||||
// if no configuration is provided. This corresponds to the legacy behavior of the generator.
|
||||
DefaultSpriteSize = 160
|
||||
)
|
||||
|
||||
var DefaultSpriteGeneratorConfig = SpriteGeneratorConfig{
|
||||
MinimumSprites: DefaultSpriteAmount,
|
||||
MaximumSprites: DefaultSpriteAmount,
|
||||
SpriteInterval: 0,
|
||||
SpriteSize: DefaultSpriteSize,
|
||||
}
|
||||
|
||||
// NewSpriteGenerator creates a new SpriteGenerator for the given video file and configuration
|
||||
// It calculates the appropriate sprite interval and count based on the video duration and the provided configuration
|
||||
func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageOutputPath string, vttOutputPath string, config SpriteGeneratorConfig) (*SpriteGenerator, error) {
|
||||
exists, err := fsutil.FileExists(videoFile.Path)
|
||||
if !exists {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if videoFile.VideoStreamDuration <= 0 {
|
||||
s := fmt.Sprintf("video %s: duration(%.3f)/frame count(%d) invalid, skipping sprite creation", videoFile.Path, videoFile.VideoStreamDuration, videoFile.FrameCount)
|
||||
return nil, errors.New(s)
|
||||
}
|
||||
|
||||
config.SpriteInterval = calculateSpriteInterval(videoFile, config)
|
||||
chunkCount := int(math.Ceil(videoFile.VideoStreamDuration / config.SpriteInterval))
|
||||
|
||||
// adjust the chunk count to the next highest perfect square, to ensure the sprite image
|
||||
// is completely filled (no empty space in the grid) and the grid is as square as possible (minimizing the number of rows/columns)
|
||||
gridSize := generate.GetSpriteGridSize(chunkCount)
|
||||
newChunkCount := gridSize * gridSize
|
||||
|
||||
if newChunkCount != chunkCount {
|
||||
logger.Debugf("[generator] adjusting chunk count from %d to %d to fit a %dx%d grid", chunkCount, newChunkCount, gridSize, gridSize)
|
||||
chunkCount = newChunkCount
|
||||
}
|
||||
|
||||
if config.SpriteSize <= 0 {
|
||||
config.SpriteSize = DefaultSpriteSize
|
||||
}
|
||||
|
||||
slowSeek := false
|
||||
chunkCount := rows * cols
|
||||
|
||||
// For files with small duration / low frame count try to seek using frame number intead of seconds
|
||||
if videoFile.VideoStreamDuration < 5 || (0 < videoFile.FrameCount && videoFile.FrameCount <= int64(chunkCount)) { // some files can have FrameCount == 0, only use SlowSeek if duration < 5
|
||||
@@ -71,9 +138,8 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO
|
||||
VideoChecksum: videoChecksum,
|
||||
ImageOutputPath: imageOutputPath,
|
||||
VTTOutputPath: vttOutputPath,
|
||||
Rows: rows,
|
||||
Config: config,
|
||||
SlowSeek: slowSeek,
|
||||
Columns: cols,
|
||||
g: &generate.Generator{
|
||||
Encoder: instance.FFMpeg,
|
||||
FFMpegConfig: instance.Config,
|
||||
@@ -83,6 +149,40 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO
|
||||
}, nil
|
||||
}
|
||||
|
||||
func calculateSpriteInterval(videoFile ffmpeg.VideoFile, config SpriteGeneratorConfig) float64 {
|
||||
// If a custom sprite interval is provided, start with that
|
||||
spriteInterval := config.SpriteInterval
|
||||
|
||||
// If no custom interval is provided, calculate the interval based on the
|
||||
// video duration and minimum sprite count
|
||||
if spriteInterval <= 0 {
|
||||
minSprites := config.MinimumSprites
|
||||
if minSprites <= 0 {
|
||||
panic("invalid configuration: MinimumSprites must be greater than 0 if SpriteInterval is not set")
|
||||
}
|
||||
|
||||
logger.Debugf("[generator] calculating sprite interval for video duration %.3fs with minimum sprites %d", videoFile.VideoStreamDuration, minSprites)
|
||||
return videoFile.VideoStreamDuration / float64(minSprites)
|
||||
}
|
||||
|
||||
// Calculate the number of sprites that would be generated with the provided interval
|
||||
spriteCount := int(math.Ceil(videoFile.VideoStreamDuration / spriteInterval))
|
||||
|
||||
// If the calculated sprite count is greater than the maximum, adjust the interval to meet the maximum
|
||||
if config.MaximumSprites > 0 && spriteCount > int(config.MaximumSprites) {
|
||||
spriteInterval = videoFile.VideoStreamDuration / float64(config.MaximumSprites)
|
||||
logger.Debugf("[generator] provided sprite interval %.1fs results in %d sprites, which exceeds the maximum of %d, adjusting interval to %.1fs", config.SpriteInterval, spriteCount, config.MaximumSprites, spriteInterval)
|
||||
}
|
||||
|
||||
// If the calculated sprite count is less than the minimum, adjust the interval to meet the minimum
|
||||
if config.MinimumSprites > 0 && spriteCount < int(config.MinimumSprites) {
|
||||
spriteInterval = videoFile.VideoStreamDuration / float64(config.MinimumSprites)
|
||||
logger.Debugf("[generator] provided sprite interval %.1fs results in %d sprites, which is less than the minimum of %d, adjusting interval to %.1fs", config.SpriteInterval, spriteCount, config.MinimumSprites, spriteInterval)
|
||||
}
|
||||
|
||||
return spriteInterval
|
||||
}
|
||||
|
||||
func (g *SpriteGenerator) Generate() error {
|
||||
if err := g.generateSpriteImage(); err != nil {
|
||||
return err
|
||||
@@ -100,6 +200,8 @@ func (g *SpriteGenerator) generateSpriteImage() error {
|
||||
|
||||
var images []image.Image
|
||||
|
||||
isPortrait := g.Info.VideoFile.Height > g.Info.VideoFile.Width
|
||||
|
||||
if !g.SlowSeek {
|
||||
logger.Infof("[generator] generating sprite image for %s", g.Info.VideoFile.Path)
|
||||
// generate `ChunkCount` thumbnails
|
||||
@@ -107,8 +209,7 @@ func (g *SpriteGenerator) generateSpriteImage() error {
|
||||
|
||||
for i := 0; i < g.Info.ChunkCount; i++ {
|
||||
time := float64(i) * stepSize
|
||||
|
||||
img, err := g.g.SpriteScreenshot(context.TODO(), g.Info.VideoFile.Path, time)
|
||||
img, err := g.g.SpriteScreenshot(context.TODO(), g.Info.VideoFile.Path, time, g.Config.SpriteSize, isPortrait)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -126,7 +227,7 @@ func (g *SpriteGenerator) generateSpriteImage() error {
|
||||
return errors.New("invalid frame number conversion")
|
||||
}
|
||||
|
||||
img, err := g.g.SpriteScreenshotSlow(context.TODO(), g.Info.VideoFile.Path, int(frame))
|
||||
img, err := g.g.SpriteScreenshotSlow(context.TODO(), g.Info.VideoFile.Path, int(frame), g.Config.SpriteSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,7 +259,7 @@ func (g *SpriteGenerator) generateSpriteVTT() error {
|
||||
stepSize /= g.Info.FrameRate
|
||||
}
|
||||
|
||||
return g.g.SpriteVTT(context.TODO(), g.VTTOutputPath, g.ImageOutputPath, stepSize)
|
||||
return g.g.SpriteVTT(context.TODO(), g.VTTOutputPath, g.ImageOutputPath, stepSize, g.Info.ChunkCount)
|
||||
}
|
||||
|
||||
func (g *SpriteGenerator) imageExists() bool {
|
||||
|
||||
@@ -313,46 +313,6 @@ func (s *Manager) validateFFmpeg() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Manager) BackupDatabase(download bool) (string, string, error) {
|
||||
var backupPath string
|
||||
var backupName string
|
||||
if download {
|
||||
backupDir := s.Paths.Generated.Downloads
|
||||
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
|
||||
}
|
||||
f, err := os.CreateTemp(backupDir, "backup*.sqlite")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
backupPath = f.Name()
|
||||
backupName = s.Database.DatabaseBackupPath("")
|
||||
f.Close()
|
||||
|
||||
// delete the temp file so that the backup operation can create it
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
return "", "", fmt.Errorf("could not remove temporary backup file %v: %w", backupPath, err)
|
||||
}
|
||||
} else {
|
||||
backupDir := s.Config.GetBackupDirectoryPathOrDefault()
|
||||
if backupDir != "" {
|
||||
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
|
||||
}
|
||||
}
|
||||
backupPath = s.Database.DatabaseBackupPath(backupDir)
|
||||
backupName = filepath.Base(backupPath)
|
||||
}
|
||||
|
||||
err := s.Database.Backup(backupPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return backupPath, backupName, nil
|
||||
}
|
||||
|
||||
func (s *Manager) AnonymiseDatabase(download bool) (string, string, error) {
|
||||
var outPath string
|
||||
var outName string
|
||||
|
||||
@@ -123,7 +123,8 @@ func (s *Manager) Scan(ctx context.Context, input ScanMetadataInput) (int, error
|
||||
ZipFileExtensions: cfg.GetGalleryExtensions(),
|
||||
// ScanFilters is set in ScanJob.Execute
|
||||
// HandlerRequiredFilters is set in ScanJob.Execute
|
||||
Rescan: input.Rescan,
|
||||
RootPaths: cfg.GetStashPaths().Paths(),
|
||||
Rescan: input.Rescan,
|
||||
}
|
||||
|
||||
scanJob := ScanJob{
|
||||
@@ -704,3 +705,133 @@ func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, box *models.StashB
|
||||
|
||||
return s.JobManager.Add(ctx, "Batch stash-box studio tag...", j)
|
||||
}
|
||||
|
||||
func (s *Manager) batchTagTagsByIds(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) {
|
||||
var tasks []Task
|
||||
|
||||
err := s.Repository.WithTxn(ctx, func(ctx context.Context) error {
|
||||
tagQuery := s.Repository.Tag
|
||||
|
||||
for _, tagID := range input.Ids {
|
||||
if id, err := strconv.Atoi(tagID); err == nil {
|
||||
t, err := tagQuery.Find(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := t.LoadStashIDs(ctx, tagQuery); err != nil {
|
||||
return fmt.Errorf("loading tag stash ids: %w", err)
|
||||
}
|
||||
|
||||
hasStashID := t.StashIDs.ForEndpoint(box.Endpoint) != nil
|
||||
if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) {
|
||||
tasks = append(tasks, &stashBoxBatchTagTagTask{
|
||||
tag: t,
|
||||
box: box,
|
||||
excludedFields: input.ExcludeFields,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
func (s *Manager) batchTagTagsByNamesOrStashIds(input StashBoxBatchTagInput, box *models.StashBox) []Task {
|
||||
var tasks []Task
|
||||
|
||||
for i := range input.StashIDs {
|
||||
stashID := input.StashIDs[i]
|
||||
if len(stashID) > 0 {
|
||||
tasks = append(tasks, &stashBoxBatchTagTagTask{
|
||||
stashID: &stashID,
|
||||
box: box,
|
||||
excludedFields: input.ExcludeFields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for i := range input.Names {
|
||||
name := input.Names[i]
|
||||
if len(name) > 0 {
|
||||
tasks = append(tasks, &stashBoxBatchTagTagTask{
|
||||
name: &name,
|
||||
box: box,
|
||||
excludedFields: input.ExcludeFields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
func (s *Manager) batchTagAllTags(ctx context.Context, input StashBoxBatchTagInput, box *models.StashBox) ([]Task, error) {
|
||||
var tasks []Task
|
||||
|
||||
err := s.Repository.WithTxn(ctx, func(ctx context.Context) error {
|
||||
tagQuery := s.Repository.Tag
|
||||
var tags []*models.Tag
|
||||
var err error
|
||||
|
||||
tags, err = tagQuery.FindByStashIDStatus(ctx, input.Refresh, box.Endpoint)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error querying tags: %v", err)
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
tasks = append(tasks, &stashBoxBatchTagTagTask{
|
||||
tag: t,
|
||||
box: box,
|
||||
excludedFields: input.ExcludeFields,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return tasks, err
|
||||
}
|
||||
|
||||
func (s *Manager) StashBoxBatchTagTag(ctx context.Context, box *models.StashBox, input StashBoxBatchTagInput) int {
|
||||
j := job.MakeJobExec(func(ctx context.Context, progress *job.Progress) error {
|
||||
logger.Infof("Initiating stash-box batch tag tag")
|
||||
|
||||
var tasks []Task
|
||||
var err error
|
||||
|
||||
switch input.getBatchTagType(false) {
|
||||
case batchTagByIds:
|
||||
tasks, err = s.batchTagTagsByIds(ctx, input, box)
|
||||
case batchTagByNamesOrStashIds:
|
||||
tasks = s.batchTagTagsByNamesOrStashIds(input, box)
|
||||
case batchTagAll:
|
||||
tasks, err = s.batchTagAllTags(ctx, input, box)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
progress.SetTotal(len(tasks))
|
||||
|
||||
logger.Infof("Starting stash-box batch operation for %d tags", len(tasks))
|
||||
|
||||
for _, task := range tasks {
|
||||
progress.ExecuteTask(task.GetDescription(), func() {
|
||||
task.Start(ctx)
|
||||
})
|
||||
|
||||
progress.Increment()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return s.JobManager.Add(ctx, "Batch stash-box tag tag...", j)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type SceneService interface {
|
||||
Create(ctx context.Context, input *models.Scene, fileIDs []models.FileID, coverImage []byte) (*models.Scene, error)
|
||||
Create(ctx context.Context, input models.CreateSceneInput) (*models.Scene, error)
|
||||
AssignFile(ctx context.Context, sceneID int, fileID models.FileID) error
|
||||
Merge(ctx context.Context, sourceIDs []int, destinationID int, fileDeleter *scene.FileDeleter, options scene.MergeOptions) error
|
||||
Destroy(ctx context.Context, scene *models.Scene, fileDeleter *scene.FileDeleter, deleteGenerated, deleteFile, destroyFileEntry bool) error
|
||||
@@ -39,7 +39,7 @@ type GalleryService interface {
|
||||
}
|
||||
|
||||
type GroupService interface {
|
||||
Create(ctx context.Context, group *models.Group, frontimageData []byte, backimageData []byte) error
|
||||
Create(ctx context.Context, input *models.CreateGroupInput) error
|
||||
UpdatePartial(ctx context.Context, id int, updatedGroup models.GroupPartial, frontImage group.ImageInput, backImage group.ImageInput) (*models.Group, error)
|
||||
|
||||
AddSubGroups(ctx context.Context, groupID int, subGroups []models.GroupIDDescription, insertIndex *int) error
|
||||
|
||||
@@ -565,6 +565,7 @@ func (j *CleanGeneratedJob) cleanMarkerFiles(ctx context.Context, progress *job.
|
||||
j.setProgressFromFilename(sceneHash[0:2], progress)
|
||||
|
||||
// check if the scene exists
|
||||
var walkErr error
|
||||
if err := j.Repository.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
scenes, err = j.getScenesWithHash(ctx, sceneHash)
|
||||
@@ -575,15 +576,18 @@ func (j *CleanGeneratedJob) cleanMarkerFiles(ctx context.Context, progress *job.
|
||||
if len(scenes) == 0 {
|
||||
j.logDelete("deleting unused marker directory: %s", sceneHash)
|
||||
j.deleteDir(path)
|
||||
} else {
|
||||
// get the markers now
|
||||
for _, scene := range scenes {
|
||||
thisMarkers, err := j.Repository.SceneMarker.FindBySceneID(ctx, scene.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting markers for scene: %v", err)
|
||||
}
|
||||
markers = append(markers, thisMarkers...)
|
||||
// #5911 - we've just deleted the directory, so skip it in the walk to avoid errors
|
||||
walkErr = fs.SkipDir
|
||||
return nil
|
||||
}
|
||||
|
||||
// get the markers now
|
||||
for _, scene := range scenes {
|
||||
thisMarkers, err := j.Repository.SceneMarker.FindBySceneID(ctx, scene.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting markers for scene: %v", err)
|
||||
}
|
||||
markers = append(markers, thisMarkers...)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -591,7 +595,7 @@ func (j *CleanGeneratedJob) cleanMarkerFiles(ctx context.Context, progress *job.
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
return walkErr
|
||||
}
|
||||
|
||||
filename := info.Name()
|
||||
|
||||
@@ -651,6 +651,7 @@ func (t *ExportTask) exportImage(ctx context.Context, wg *sync.WaitGroup, jobCha
|
||||
galleryReader := r.Gallery
|
||||
performerReader := r.Performer
|
||||
tagReader := r.Tag
|
||||
imageReader := r.Image
|
||||
|
||||
for s := range jobChan {
|
||||
imageHash := s.Checksum
|
||||
@@ -665,14 +666,17 @@ func (t *ExportTask) exportImage(ctx context.Context, wg *sync.WaitGroup, jobCha
|
||||
continue
|
||||
}
|
||||
|
||||
newImageJSON := image.ToBasicJSON(s)
|
||||
newImageJSON, err := image.ToBasicJSON(ctx, imageReader, s)
|
||||
if err != nil {
|
||||
logger.Errorf("[images] <%s> error converting image to JSON: %v", imageHash, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// export files
|
||||
for _, f := range s.Files.List() {
|
||||
t.exportFile(f)
|
||||
}
|
||||
|
||||
var err error
|
||||
newImageJSON.Studio, err = image.GetStudioName(ctx, studioReader, s)
|
||||
if err != nil {
|
||||
logger.Errorf("[images] <%s> error getting image studio name: %v", imageHash, err)
|
||||
@@ -779,6 +783,7 @@ func (t *ExportTask) exportGallery(ctx context.Context, wg *sync.WaitGroup, jobC
|
||||
studioReader := r.Studio
|
||||
performerReader := r.Performer
|
||||
tagReader := r.Tag
|
||||
galleryReader := r.Gallery
|
||||
galleryChapterReader := r.GalleryChapter
|
||||
|
||||
for g := range jobChan {
|
||||
@@ -847,6 +852,12 @@ func (t *ExportTask) exportGallery(ctx context.Context, wg *sync.WaitGroup, jobC
|
||||
|
||||
newGalleryJSON.Tags = tag.GetNames(tags)
|
||||
|
||||
newGalleryJSON.CustomFields, err = galleryReader.GetCustomFields(ctx, g.ID)
|
||||
if err != nil {
|
||||
logger.Errorf("[galleries] <%s> error getting gallery custom fields: %v", g.DisplayName(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if t.includeDependencies {
|
||||
if g.StudioID != nil {
|
||||
t.studios.IDs = sliceutil.AppendUnique(t.studios.IDs, *g.StudioID)
|
||||
|
||||
@@ -221,10 +221,10 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error
|
||||
logMsg += fmt.Sprintf(" %d heatmaps & speeds", totals.interactiveHeatmapSpeeds)
|
||||
}
|
||||
if j.input.ClipPreviews {
|
||||
logMsg += fmt.Sprintf(" %d Image Clip Previews", totals.clipPreviews)
|
||||
logMsg += fmt.Sprintf(" %d image clip previews", totals.clipPreviews)
|
||||
}
|
||||
if j.input.ImageThumbnails {
|
||||
logMsg += fmt.Sprintf(" %d Image Thumbnails", totals.imageThumbnails)
|
||||
logMsg += fmt.Sprintf(" %d image thumbnails", totals.imageThumbnails)
|
||||
}
|
||||
if logMsg == "Generating" {
|
||||
logMsg = "Nothing selected to generate"
|
||||
|
||||
@@ -41,7 +41,7 @@ func (t *GenerateImagePhashTask) Start(ctx context.Context) {
|
||||
}
|
||||
|
||||
if !set {
|
||||
generated, err := imagephash.Generate(t.File)
|
||||
generated, err := imagephash.Generate(instance.FFMpeg, t.File)
|
||||
if err != nil {
|
||||
logger.Errorf("Error generating phash for %q: %v", t.File.Path, err)
|
||||
logErrorOutput(err)
|
||||
|
||||
@@ -34,7 +34,17 @@ func (t *GenerateSpriteTask) Start(ctx context.Context) {
|
||||
sceneHash := t.Scene.GetHash(t.fileNamingAlgorithm)
|
||||
imagePath := instance.Paths.Scene.GetSpriteImageFilePath(sceneHash)
|
||||
vttPath := instance.Paths.Scene.GetSpriteVttFilePath(sceneHash)
|
||||
generator, err := NewSpriteGenerator(*videoFile, sceneHash, imagePath, vttPath, 9, 9)
|
||||
|
||||
cfg := DefaultSpriteGeneratorConfig
|
||||
cfg.SpriteSize = instance.Config.GetSpriteScreenshotSize()
|
||||
|
||||
if instance.Config.GetUseCustomSpriteInterval() {
|
||||
cfg.MinimumSprites = instance.Config.GetMinimumSprites()
|
||||
cfg.MaximumSprites = instance.Config.GetMaximumSprites()
|
||||
cfg.SpriteInterval = instance.Config.GetSpriteInterval()
|
||||
}
|
||||
|
||||
generator, err := NewSpriteGenerator(*videoFile, sceneHash, imagePath, vttPath, cfg)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error creating sprite generator: %s", err.Error())
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/stashbox"
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
"github.com/stashapp/stash/pkg/tag"
|
||||
)
|
||||
|
||||
// stashBoxBatchPerformerTagTask is used to tag or create performers from stash-box.
|
||||
@@ -275,6 +276,12 @@ func (t *stashBoxBatchStudioTagTask) getName() string {
|
||||
}
|
||||
|
||||
func (t *stashBoxBatchStudioTagTask) Start(ctx context.Context) {
|
||||
// Skip organized studios
|
||||
if t.studio != nil && t.studio.Organized {
|
||||
logger.Infof("Skipping organized studio %s", t.studio.Name)
|
||||
return
|
||||
}
|
||||
|
||||
studio, err := t.findStashBoxStudio(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf("Error fetching studio data from stash-box: %v", err)
|
||||
@@ -523,3 +530,175 @@ func (t *stashBoxBatchStudioTagTask) processParentStudio(ctx context.Context, pa
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// stashBoxBatchTagTagTask is used to tag or create tags from stash-box.
|
||||
//
|
||||
// Two modes of operation:
|
||||
// - Update existing tag: set tag to update from stash-box data
|
||||
// - Create new tag: set name or stashID to search stash-box and create locally
|
||||
type stashBoxBatchTagTagTask struct {
|
||||
box *models.StashBox
|
||||
name *string
|
||||
stashID *string
|
||||
tag *models.Tag
|
||||
excludedFields []string
|
||||
}
|
||||
|
||||
func (t *stashBoxBatchTagTagTask) getName() string {
|
||||
switch {
|
||||
case t.name != nil:
|
||||
return *t.name
|
||||
case t.stashID != nil:
|
||||
return *t.stashID
|
||||
case t.tag != nil:
|
||||
return t.tag.Name
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (t *stashBoxBatchTagTagTask) Start(ctx context.Context) {
|
||||
scrapedTag, err := t.findStashBoxTag(ctx)
|
||||
if err != nil {
|
||||
logger.Errorf("Error fetching tag data from stash-box: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
excluded := map[string]bool{}
|
||||
for _, field := range t.excludedFields {
|
||||
excluded[field] = true
|
||||
}
|
||||
|
||||
if scrapedTag != nil {
|
||||
t.processMatchedTag(ctx, scrapedTag, excluded)
|
||||
} else {
|
||||
logger.Infof("No match found for %s", t.getName())
|
||||
}
|
||||
}
|
||||
|
||||
func (t *stashBoxBatchTagTagTask) GetDescription() string {
|
||||
return fmt.Sprintf("Tagging tag %s from stash-box", t.getName())
|
||||
}
|
||||
|
||||
func (t *stashBoxBatchTagTagTask) findStashBoxTag(ctx context.Context) (*models.ScrapedTag, error) {
|
||||
var results []*models.ScrapedTag
|
||||
var err error
|
||||
|
||||
r := instance.Repository
|
||||
|
||||
client := stashbox.NewClient(*t.box, stashbox.ExcludeTagPatterns(instance.Config.GetScraperExcludeTagPatterns()))
|
||||
|
||||
switch {
|
||||
case t.name != nil:
|
||||
results, err = client.QueryTag(ctx, *t.name)
|
||||
case t.stashID != nil:
|
||||
results, err = client.QueryTag(ctx, *t.stashID)
|
||||
case t.tag != nil:
|
||||
var remoteID string
|
||||
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
if !t.tag.StashIDs.Loaded() {
|
||||
err = t.tag.LoadStashIDs(ctx, r.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, id := range t.tag.StashIDs.List() {
|
||||
if id.Endpoint == t.box.Endpoint {
|
||||
remoteID = id.StashID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if remoteID != "" {
|
||||
results, err = client.QueryTag(ctx, remoteID)
|
||||
} else {
|
||||
results, err = client.QueryTag(ctx, t.tag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := results[0]
|
||||
|
||||
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
|
||||
return match.ScrapedTag(ctx, r.Tag, result, t.box.Endpoint)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (t *stashBoxBatchTagTagTask) processMatchedTag(ctx context.Context, s *models.ScrapedTag, excluded map[string]bool) {
|
||||
// Determine the tag ID to update — either from the task's tag or from the
|
||||
// StoredID set by match.ScrapedTag (when batch adding by name and the tag
|
||||
// already exists locally).
|
||||
tagID := 0
|
||||
if t.tag != nil {
|
||||
tagID = t.tag.ID
|
||||
} else if s.StoredID != nil {
|
||||
tagID, _ = strconv.Atoi(*s.StoredID)
|
||||
}
|
||||
|
||||
if tagID > 0 {
|
||||
r := instance.Repository
|
||||
err := r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.Tag
|
||||
|
||||
existingStashIDs, err := qb.GetStashIDs(ctx, tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
storedID := strconv.Itoa(tagID)
|
||||
partial := s.ToPartial(storedID, t.box.Endpoint, excluded, existingStashIDs)
|
||||
|
||||
if err := tag.ValidateUpdate(ctx, tagID, partial, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := qb.UpdatePartial(ctx, tagID, partial); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to update tag %s: %v", s.Name, err)
|
||||
} else {
|
||||
logger.Infof("Updated tag %s", s.Name)
|
||||
}
|
||||
} else if s.Name != "" {
|
||||
// no existing tag, create a new one
|
||||
newTag := s.ToTag(t.box.Endpoint, excluded)
|
||||
|
||||
r := instance.Repository
|
||||
err := r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.Tag
|
||||
|
||||
if err := tag.ValidateCreate(ctx, *newTag, qb); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.Create(ctx, &models.CreateTagInput{Tag: newTag}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to create tag %s: %v", s.Name, err)
|
||||
} else {
|
||||
logger.Infof("Created tag %s", s.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
1
internal/static/performer/NoName02.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" fill-rule="evenodd" d="M480.72 36.58c-10.56.01-21.69 1.97-34.35 9.32-28.85 23.98-32.38 59.58-34.6 98.83 2.47 24.57 6.03 47.44-4.17 73.49-11.7 1.42-24.56 5.66-37.6 3.92-13.16-1.77-28.8.54-35.24 7.21-20.4 27.97-29.09 64.17-40.61 100.3-11.53 36.13-18.01 65.15-18.22 70.6-2.83 15.39-2.51 33.25 2.95 50.89 11.33 29.32 21.89 56.36 33.98 87.95.79 1.81-3.34 3.11-2.83 5.1s1.53 4.44 1.83 6.54c.31 2.09 1.54 4.43 1.7 6.54.16 2.12 3.58 4.21 3.68 6.28.2 4.13.72 8.09 1.09 11.57.35 3.46.94 6.44 2.03 8.64l10.12 14.39c1.57 2.4 7.54-.99 8.58.93 2.17 4.07 8.13 2.75 10.29.62 1.53-1.52 3.99 2.03 6.07 1.8 2.09-.21 4.15-.6 6.13-1.25-7.69 22.24-20 44.47-23.07 66.71-8.78 29.38-21.69 44.27-26.36 88.13-1.3 17.3-.07 34.6 0 51.89l-9.88 32.94L280 901.69c-19.57 58.62-20.55 113.74-24.72 174.61l-6.59 12.36v18.93c-3.64 30.03-16.39 58.76-20.53 88.73.83 2.15 1.18 4.75 3.06 6.7 3.02 3.11 8.17 5.46 11.77 8.47 4.47 3.09 9.69 2.36 14.92 1.66 54.03-16.43 20.05-34.21 27.03-52.83.98-17.36 1.37-34.58 13.18-51.07 2.31-9.33-1.25-18.67-3.3-28.01 5.88-43.68 48.11-124.8 50.24-134.25l15.66-65.07c16.14-20.59 23.3-52.18 32.94-80.72l43.65-86.48 21.31-51.73c9.84-.07 19-10.61 23.16-16.63l9.88.82c-.86 10.43.07 20.87 10.71 31.29l27.19 132.61 16.47 37.88 11.54 52.71 15.65 104.61 1.64 42-4.11 14.83.82 30.48-4.94 38.72-11.1 31.68c-1.36 4.76-.24 11.37 2.86 14.2 20.97 20.41 52.34-.35 55.72-11.59.63-18.29 1.25-32.22 4.12-50.52l-.54-35.66-2.47-4.94v-12.36l-3.3-18.12c4.08-41.75 10.1-82.84 9.89-126.02-.54-35.83-6.63-70.28-16.48-103.77-2.39-22.61-4.97-45.14-6.92-67.98-1.4-16.4-2.48-32.94-2.95-49.8 1.15-11.25 2.29-21.55 3.17-31.35 2.86-32.14 2.9-58.75-8.16-94.67l-2.43-6.59 4.95-3.29c3.65-.82 3.05 4.44 11.53-3.3 1.52 1.27 4.12.91 8.72-2.47 3.65-9.09 11.8-16.92 9.84-28.1-6.39-4.95-2.79-3.45-21.03-13.08-2.21-9.68.25-18.2.82-27.17.17-15.34-1.29-29.93-5.11-44.61-5.09-19.73-8.47-39.44-13-59.18-5.76-20.28-4.09-56.63-11.97-89.87-9.42-15.61-19.5-15.96-21.42-17.38l-11.08-31.12c3.56 6.11 6.41 12.23 10.71 18.33l-5.77-19.15c5.23 9.53 16.02 18.6 12.92 28.83 3.22-8.21-1.64-14.95-5.61-21.85 3.91 6.17 10.05 11.8 14.92 17.73-7.23-10.16-14.41-20.32-14.83-30.48l-4.94-24.71c3.06 7.81 2.43 15.63 9.65 23.44-4.6-8.91-1.76-17.82-2.24-26.75 1.76 9.07 3.58 18.13 9.89 27.19-6.47-12.36-6.92-24.71-9.06-37.06-2.43-8.59-5.1-17.18-6.13-25.77-1.33-11.18-.67-22.37-1.29-33.54l.83-42.84c-2.94-12.9-5.05-25.81-16.48-38.72-7.84-10.63-17.89-16.1-28.01-21.41-8-.74-15.89-1.91-24.11-1.91Zm69.42 210.28c.48 1.07 1.02 2.13 1.6 3.19-.6-1.05-1.15-2.11-1.6-3.19Zm-208.4 107.77c1.56 22.71.19 45.21 3.62 67.92 5.74 22.85 11.94 41.69 16.71 66.52.16 12.4 1.33 22.33-6.98 55.89l-4.83-13.46c-1.32-2.05-2.21-4.2-6.7-5.73-4.36-16.84-11.3-31.44-13.26-48.73-2.25-19.97-6.79-34.65-11.92-51.22 1.11-16.12 7.62-28.97 12.69-46.1l10.66-25.09Zm205.09 80.48 7.85 29.71c2.19 15.92 5.13 31.36 15.73 41.92l-6.7 17.77c.05-5.06.11-10.09-.59-15.14-3.01-7.45-6-12.83-9-17.16l-19.23-27.39c-1.85-2.04-4.76-4.67-8.16-7l-.87-7.26-1.76-7.29c10.68-.94 17.45-4.12 22.73-8.16Zm38.42 99.9 1.17 8.13-4.07 11.07-6.13-12.51 7-3.5 2.03-3.19Z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB |
1
internal/static/performer/NoName05.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="M376.15 24.84c-3.58 1.02-10.08 2.26-14.39 2.85-9.64 1.31-12.93 2.19-18.26 4.82-14.17 7.01-23.67 19.21-29.66 37.99-3.36 10.52-4.53 19.14-5.26 37.62-.37 9.64-1.1 18.99-1.75 21.99-3.43 16.14-10.3 24.47-23.3 28.12-5.33 1.53-7.52 1.75-20.09 1.75-7.67 0-14.03.07-14.03.15 0 .15 1.46.58 3.29 1.02s3.29 1.02 3.29 1.31-3.07.95-6.72 1.46c-12.2 1.75-22.72 5.41-27.98 9.86-1.68 1.46-1.97 1.97-1.17 2.56.73.44.37 1.1-1.68 2.7-3.36 2.7-6.72 7.38-8.18 11.4-1.31 3.8-1.46 11.25-.22 13.95s2.12 2.48 2.12-.58c0-7.38 8.04-12.2 16.44-9.86 1.83.51 3.29 1.02 3.29 1.17 0 .22-1.39 2.05-3.14 4.16-1.75 2.19-3.87 5.33-4.75 7.16-1.83 3.94-2.92 9.13-1.68 8.4 1.17-.73 1.1-.44-1.39 4.82-2.63 5.62-4.38 13.22-4.38 19.21 0 5.11 1.02 10.08 2.12 10.45.44.15.8 1.53.8 3.07 0 3.87 2.48 11.4 5.04 15.41 2.19 3.58 5.84 6.72 8.69 7.6l1.68.51-1.53-2.48c-.8-1.31-1.46-3.29-1.46-4.38 0-2.48 2.78-10.08 3.73-10.08.37 0 .66 1.75.66 3.8.07 2.12.51 4.68 1.1 5.7 1.02 1.75 1.1 1.75.66-1.17-.51-3.29.88-11.25 1.97-11.25.58 0 .22 5.55-.51 7.89-.58 2.05 1.31 10.67 3.36 15.19 2.19 4.82 3.36 6.06 2.78 2.85-.58-3.07.66-2.05 1.53 1.31.95 3.14 5.04 10.45 5.62 9.86.15-.22 0-2.7-.37-5.55-.73-5.84 0-13.88 1.83-19.87 1.24-4.09 5.84-13.81 7.82-16.44s2.12-.58.22 3.14c-3.36 6.72-5.62 15.92-6.06 24.69-.44 8.62.15 14.9 1.31 14.9.37 0 .66-.44.58-.95-.22-2.05.15-4.89.66-4.89.29 0 .44 2.12.29 4.68-.73 11.47 4.46 26.44 10.23 29.51 3.8 2.05 3.94 2.7 3.43 25.86-.51 24.4-1.24 31.63-8.33 81.3-5.92 41.57-6.57 46.9-8.4 66.26-2.19 24.11-2.12 23.74-12.35 43.25-2.19 4.16-3 6.57-3 8.91q0 3.21 4.24 5.26l4.16 1.97.22 6.65c.22 7.52.22 7.6 6.57 7.6h3.14l-.44 5.26c-1.1 12.71-2.78 24.18-4.53 32-3 13.22-2.85 14.76 2.41 25.93 4.09 8.84 13.37 25.71 18.85 34.33 2.05 3.07 3 3.87 5.99 4.82 3.43 1.02 3.58 1.24 4.09 4.31.29 1.83 1.1 19.72 1.68 39.81.58 20.09 1.75 45.07 2.56 55.52 3.43 43.83 3.36 54.64-.73 100.08-1.17 13.22-3.29 43.03-4.75 66.11-1.39 23.08-4.02 65.16-5.84 93.5-3.65 58.07-6.65 108.33-7.38 124.33l-.51 10.74 1.75.37c.95.22 4.53 1.1 8.04 1.83 8.99 2.12 14.83 4.46 21.33 8.55 5.33 3.36 5.62 3.73 5.26 5.77-1.9 11.18-.58 19.87 3.87 24.62 7.52 7.96 25.06 8.55 34.84 1.17 5.92-4.46 8.4-11.4 8.4-23.52v-6.79l7.74-3.43c4.31-1.97 9.42-4.09 11.54-4.82l3.73-1.24-.22-3.21c-.07-1.75-.95-19.65-1.97-39.74-3.8-75.83-4.68-90.58-9.13-148.29-4.46-57.71-5.19-71.44-5.62-104.83-.58-41.71.44-58.37 7.38-121.7 4.82-43.83 8.33-79.26 9.2-93.8.95-14.61 2.26-23.96 4.38-30.68 1.97-6.5 2.63-7.38 3.51-5.11.44 1.02 4.38 11.03 8.77 22.28 4.38 11.25 14.46 36.96 22.35 57.2 7.96 20.16 20.75 52.23 28.49 71.22 32.14 78.75 37.55 96.06 49.02 159.03 2.26 12.27 5.41 29.37 7.01 37.99 6.72 36.6 12.56 72.32 27.03 165.38 6.28 40.1 6.94 43.76 8.33 44.19.8.29 10.88 3.8 22.35 7.74l20.89 7.16.51 6.28c1.46 19.07 7.89 26.3 24.03 26.96 6.5.29 7.38.15 11.1-1.61 4.46-2.26 9.72-7.6 12.05-12.49 1.97-4.09 2.19-12.49.44-19.07-.66-2.48-1.17-4.68-1.17-4.89 0-.15 2.19-1.61 4.82-3.14 5.7-3.29 9.57-7.52 10.23-11.03.73-3.94-.88-11.91-7.38-36.31-7.67-28.78-33.24-118.05-39.45-137.77-2.63-8.25-10.52-32.07-17.6-52.96-17.82-52.81-19.21-57.93-34.63-124.91-8.99-39.3-18.7-80.79-30.68-130.76-5.55-23.3-11.18-47.77-12.42-54.42-2.63-14.03-5.84-24.11-13.15-41.27-2.92-6.87-6.65-16.07-8.25-20.45l-2.92-8.04-.22-18.63c-.07-10.23-.51-22.06-.95-26.22s-.58-7.89-.37-8.25c.29-.44 1.46-1.17 2.78-1.68 2.19-.95 2.34-.88 5.99 2.56 7.16 6.79 15.49 7.38 27.03 1.9 3.07-1.46 6.43-3.29 7.6-4.09l2.05-1.46 4.09 5.55 4.16 5.48 4.53-3.58c6.36-5.04 11.25-7.96 15.41-9.06 3-.8 4.02-1.61 6.79-5.11 4.53-5.99 8.99-14.54 13.73-26.88 2.26-5.84 6.28-15.19 8.91-20.82 18.34-39.23 23.08-55.96 19.58-69.32-.66-2.41-4.31-10.74-8.11-18.63-17.17-34.99-29.37-64.87-43.03-105.26-8.18-24.25-11.4-31.78-15.71-37.04-1.97-2.34-4.97-6.43-6.57-9.2-3.87-6.43-4.97-7.09-11.69-7.01-3.87.07-8.77-.73-16.8-2.56-7.6-1.83-14.83-2.92-21.99-3.51l-10.74-.88 1.31-3.8c11.18-32.43 14.03-69.18 7.38-96.79-6.43-27.25-27.9-59.46-47.92-71.95-3.07-1.9-8.4-4.46-11.91-5.62-5.7-2.05-7.38-2.26-16.58-2.48-9.06-.22-10.96 0-16.8 1.61Zm125.64 306.44c7.3 11.25 14.76 27.76 25.2 55.74 9.72 26.15 10.96 32.51 8.33 41.42-1.31 4.31-13.37 28.85-15.85 32.29-.95 1.31-3.14 2.63-5.84 3.65-5.55 1.97-8.55 4.82-8.55 7.89 0 1.83.73 3.07 3.29 5.55 1.83 1.75 3.29 3.58 3.29 4.09 0 .58-.95 1.53-2.05 2.19-1.17.73-3.51 2.78-5.19 4.6-2.92 3.21-3.07 3.51-1.9 4.75 1.1 1.24 1.1 1.39-.15 1.68-5.62 1.61-5.26 1.75-13.44-6.21-4.24-4.16-8.91-8.18-10.37-8.91-1.39-.8-2.78-2.12-3-2.92-.22-.88-.8-6.57-1.31-12.78-1.61-20.97 1.97-48.36 10.81-82.25 1.75-6.87 4.82-18.7 6.79-26.3 1.97-7.67 4.16-17.39 4.89-21.77.66-4.31 1.39-7.82 1.53-7.82s1.75 2.26 3.51 5.11ZM283.01 578.92c0 9.72-.66 11.61-2.19 6.79-.95-2.7-2.19-14.76-1.68-15.78.15-.29 1.1-.51 2.12-.51h1.75v9.5Zm-.73 60.92c.58.07 1.31.88 1.53 1.9.73 2.78.73 10.23.07 10.23-1.53 0-7.45-10.3-7.45-12.93 0-.22 1.1-.07 2.41.22 1.24.29 2.85.51 3.43.58Z" vector-effect="non-scaling-stroke"/></svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
1
internal/static/performer/NoName07.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="M472.98 38.67c-13.15.96-26.67 7.37-39.17 14.9-15.4 4.08-26.21 21.1-32.37 33.99-5.93 21.36-10.44 43.16-12.62 65.39-8.56 18.78 2.69 49.48-20.08 58.28-26.46 3.29-45.91 24.89-57.3 47.58-7.59 25.25-21.44 47.64-34.64 70.25-8.14 19.94-32.81 48.39-10.36 66.7 26.26 16.49 58.12 9.34 86.76 5.18 19.26 6.03 13.11 40.3 13.27 57.62.75 24.57-7.78 47.72-11.98 71.55-5.73 30.16 7.91 58.68 9.71 88.38 5.03 13.95-6.07 56.76 17.49 46.62 13.87 33.68 36.67 62.85 51.48 96.15 8.34 20.57 26.8 43.12 16.18 66.04-13.64 21.93-14.66 48.16-13.27 73.17-.62 26.72 8.46 52.33 10.04 78.64.81 38.12-21.73 71.17-25.9 108.47-1.34 21.76-26.4 39.99-12.31 61.54.39 2.59 6.27 16.37 9.72 29.42 2.95 11.19 7.33 18.65 11.32 25.9 7.35 14.5 20.87 3.11 23.96-3.83 8.76-19.79 14.33-29.42 13.27-5.18-1.06 24.24 47.37-4.77 52.13-24.35.58-2.38-.94-3.52 1.62-11.29 2.44-19.89-13.36-38.12-11.98-60.81-1.27-32.43-9.15-64.23-10.69-96.55-2.57-23.93 7.7-45.17 15.87-66.99 19.16-49.34 5.91-103.12 15.54-154.1 10.93-42.36 20.98-85.33 38.52-125.61-.89-18.39 20.38-20.42 25.58-30.76-17.12-45.49-12.18-96.36-31.73-141.15-11.79-19.53-23.53-41.51-34.95-59.57 10.41 8.04 27.29 13.76 37.22-2.59 2.46-11.07 14.4-21 9.72-32.7-8.61-21.13-4.68-43.27-8.42-65.72-10.33-20.34 26.7-1.53 19.42-11.33 21.04-14.01-21.31 8.01-11.98-12.3-4.57-23.82-10.07-47.05-19.75-69.29-7.05-48.59-12.56-98.26-29.46-144.71-7.04-10.12-4.91-16.98-13.6-28.49-10.4-14.73-23.1-19.4-36.26-18.45ZM343.16 329.72c5.59.8 1.72 20.91-.96 29.46-.23 6.92-4.89 5.55-9.4 4.21-23.94.47-6.9-16.97 1.29-26.87 4.43-5.2 7.2-7.07 9.06-6.8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
1
internal/static/performer/NoName09.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="M390.27 1215.71c-26.85-.4-39.98-26.54-37.66-50.52-5.05-22.51-3.56-45.85-4.53-68.85 2.06-23.14 21.76-41.17 15.57-66.3-3.11-37.23-4.87-74.88-.58-112.07 4.2-24.94 13.46-48.45 25.58-70.54.29-20.04-3.9-39.59-12.25-57.87-14.58-37.71-27.84-75.94-45.89-112.18-8.06-20.45-4.48-48.36-34.09-48.93-17.01-1.88-35.19-2.94-30.5-26.06-1.18-31.57 3.95-62.99 2.66-94.64 4.55-28.15-22.55-45.52-37.4-65.53-15.7-21.87-32.9-42.98-45.36-67-4.35-29.81 14.01-56.22 24.4-82.85 12.15-30.72 29.69-58.93 42.97-89.06 10.76-17.8 24.8-40.58 48.45-39.99 22.64-.45 46.86.54 65.3-14.85 17.26-15.49 22.96-38.33 32.33-58.42 12.24-28.4 35.57-54.65 67.74-58.99 24.33-3.89 50.81 5.21 68.9 21.61 3.9 22.95 19.71 38.48 25.56 60.65 2.49 21.53-2.29 45.5 24.99 52.18 24.74 16.83 50.28 32.79 73.42 51.79 14.52 18.65-5.63 46.05-27.33 47.31-28.5 13.29-59.15 21.79-90.55 24.61-27.88-4.29-18.63 31.66-1.72 39.88 23.56 21.16-22.88-12.64-23.18-12.36-.26 12.99-3.78 24.58-8.09 5.3-11.15-22.15 3.96 31.49-5.43 19.37-1.78-11.09-13.78-34.88-13.28-9.82 1.3 8.03 14.21 33.96-.67 15.64-6.99-15.67-3.58-33.86-5.09-50.74-26.39-2.77-11.88 22.66-7.91 36.5 5.97 25-25.13 39.55-22.68 61.96 12.94 16.43 31.57 29.5 42.84 49.13 24.41 33.32 29.99 74.92 40.63 113.67-1.49 14.89 32.93 29.94 3.81 30.29-16.72 11.07-10.87 39.91-21.52 57.74-7.71 38.93-19.29 77-31.3 114.78-8.06 27.2-18.27 54.63-17.23 83.57-.36 40.67-6.09 81.1-16.62 120.35-5.81 25.47-15.31 50.35-14.89 77.06-2.19 24.53 2.41 48.98 11.71 71.6 7.37 20.12-4.17 48.71-29.66 40.63-24.51-5.69-10.97 27.47 2.91 33.26 14.53 16.75 1.15 34.28-19.32 29.02-5.02 0-10.06.1-15.06-.33Zm-121.8-810.49c.16-22.09 10.41-42.25 5.24-64.23-2.08-9.98 4.9-41.86-5.44-38.66-6.72 22.85-29.25 40.55-28.37 65.34 8.51 16.52 21.61 30.41 27.22 48.67 1.8-3.45.92-7.45 1.35-11.12Zm252.68-105.3c.93-13.95-20.6-7.37-6.22 2.41 3.97 6.03 8.46 5.04 6.22-2.41Zm59.8-74.98c-11.22-15.73-26.65-27.69-40.46-40.91-11.99-1.74-12.72 24.78-21.55 33.89 1.23 17.12 35.65 4.3 49.7 7.82 4.1-.25 8.26-.03 12.31-.8Zm-20.67-81.28c4.51-20.53-17.82 12.62-1.15 4.06l1.15-4.06Zm-5.14-8.5c8.84-11.18-12.11-43.92-7.45-15.82 1.26 3.6-.62 29.43 7.45 15.82Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 40 KiB |
1
internal/static/performer/NoName12.svg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
1
internal/static/performer/NoName13.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="M515.92 50.69c-6.99 3-13.47 11.05-20.4 10.88-4.77-.12-9.35-6.12-13.66-8.18-13.67-6.52-28.43-11.2-43.59-9.53-12.73 1.41-23.74 8.9-32.71 17.7-6.07 5.96-12.45 15.75-16.36 23.18-3.9 7.43-9.53 25.88-9.53 25.88s-12.4-.29-17.7 0c-15.19.82-35.43 2.84-50.42 5.48-20.31 3.58-49.51 5.07-66.77 16.36-1.71 1.12-3.48 3.46-4.13 5.4-1.83 5.41-1.55 13.55 0 19.05 2.34 8.3 10.81 17 15.01 24.53 15.71 28.21 37.76 49.62 61.29 70.9 7.51 6.79 24.53 23.18 24.53 23.18 6.98 5.76 16.45 11.52 16.36 19.05-15.6 19.28-35.29 36.21-38.65 62.64-1.61 12.64 5.13 24.18 12.76 34.06 6.81 7.35 16.02 14.71 19.05 23.18 1.41 10.87-1.74 21.78-1.35 32.71.35 9.62 5.5 19.48 6.83 31.36 2.99 26.68 8.29 56.95-2.7 80.34-7.19 15.31-9.76 30.37-13.66 46.37-5.57 22.84-7.24 44.89-5.48 68.12 2.63 34.67-1.67 27.3 2.78 55.9 2.44 15.68-5.66 18.62-.59 51.65s7.77 115.9-8.94 159.54c-5.29 13.81-5.21 26.61-5.48 40.89-.47 25.23 1.66 51.32-2.7 76.3-2.69 15.41-13.49 34-17.7 49.07-2.24 8.03-4.51 18.95-5.48 27.23-1.34 11.36-1.15 26.67-1.35 38.11-.14 8.19-7.42 28.07 0 27.32 56.79-5.79 182.54 27.49 216.67 6.83 13.32-8.06 54.71-1.21 77.67-4.12 5.62-.71-7.64-26.93-2.72-35.42 14.6-25.2 11.84-56.1 13.57-84.47 2.94-48.13 1.52-94.96-9.53-141.72-9.64-40.79-28.29-77.44-25.88-119.89.84-14.79 2.8-34.5 5.48-49.07 2.36-12.87 8.08-29.45 10.88-42.24 5-22.83 10.66-46.09 9.53-69.55-2.61-53.82-23.61-105.61-51.77-151.25-32.73-38.42-13.69-70.93 2.7-117.19 18.54-51.11 46.6-98.49 64.42-149.75 3.43-11.62 2.55-23.64 2.35-35.55 1.61-45.84 4.24-91.75-1.35-137.59-.2-3.71-.23-8.77-1.35-12.31-1.53-4.87-3.98-12.03-8.18-14.92-26.26-13.42-56.44-1.36-81.78 9.53Zm39.54 51.77c-5.44 34.07-2.93 22.83-4.13 32.71-1.93 15.86-3.17 37.14-3.78 53.11-.33 8.59 0 28.66 0 28.66l-9.8 8.18-9.53 5.4c-1.92-21.24 3.39-42.82 5.4-63.99 1.51-15.9.47-32.33-4.05-47.72-2.47-8.42-13.66-25.88-13.66-25.88 13.63 0 9.12.6 13.66 0s22.31-.46 27.23-4.28c7.16-5.55-.48 8.39-1.35 13.8ZM368.8 148.74c4.68 1.82 8.99 4.45 12.22 8.18 6.06 6.98.33 21.93 1.35 31.36s5.48 31.36 5.48 31.36l.88 12.33-34.94-28.69-43.59-32.71s41.88-28.34 58.59-21.84Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
1
internal/static/performer/NoName14.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="M439.54 22.98h3.95c7.49.23 14.96 2.18 21.45 5.99 5.2 2.97 9.66 7.03 13.87 11.26 7.76 7.83 14.32 16.91 18.92 26.95 3.62 7.83 6.05 16.19 7.35 24.71.64 4.04.93 8.12 1.34 12.18 2.96 31.64 5.13 63.34 7.56 95.02.41 4.77.46 9.65 2 14.22 1.13 3.55 3.45 6.83 6.81 8.55 4.54 2.4 9.83 2.42 14.85 2.41 7.19.11 14.55.9 21.16 3.9 4.39 1.99 8.25 5.15 11.03 9.1 3.81 5.37 5.86 11.75 7.22 18.12 2.27 10.6 2.52 21.49 2.6 32.29.09 37.2-.42 74.39-.22 111.59.16 37.19 1.46 74.36 2.69 111.53 1.73 50.12 3.64 100.24 4.31 150.39v3.69c-.23 7.17-1.45 14.39-4.28 21.03-2.47 5.85-6.28 11.23-11.37 15.09-4.6 3.54-10.17 5.72-15.89 6.59-5.38.85-10.87.56-16.23-.25 2.52-6.96 5.5-13.77 7.54-20.9 2.25-7.83 4.71-15.6 6.48-23.55 1.4-6.12 2.14-12.37 2.34-18.64.32-8.17.53-16.57 3.53-24.29.74-2.03 1.77-3.94 2.77-5.85 1.71-3.69 1.72-7.9 1.35-11.88-.67-6.85-2.69-13.45-4.51-20.06-7.07-25.07-15.44-49.76-21.91-75-1.9-7.64-3.9-15.29-4.94-23.11-2.29-16.82-1.58-33.83-2.48-50.74-.38-6.42-.03-12.85-.05-19.27-2.16.06-4.31.04-6.47.13.03 18.82-.9 37.65.13 56.45 2.09 35.31 5.96 70.49 8.05 105.79 1.13 18.76 1.56 37.55 1.88 56.34.42 28.92.38 57.85.88 86.78.19 8.37.29 16.75.67 25.11 1 12 6.07 23.07 9.4 34.52 3.83 12.54 8.16 24.92 12.08 37.43 5.85 18.4 10.09 37.26 14.15 56.12 2.33 10.98 4.49 22 6.62 33.02.81 4.49-1.04 8.82-1.58 13.22-.36 2.27.01 4.56.22 6.84 1 10 2.45 19.96 3.84 29.91 1.16 8.03 2.31 16.1 2.21 24.23.05 20.62-5.81 40.93-15.27 59.15-3.27 6.43-7.24 12.46-10.92 18.65-12.65-5.55-25.15-11.48-38.19-16.07-13.93-4.95-28.7-7.95-43.54-7.37-5.28 33.1-2.88 67.28 6.18 99.51 1.81 6.51 3.88 12.96 6.18 19.32 2.15 6.05 4.85 11.94 6.03 18.29.93 4.73.95 9.83-1.09 14.29-2.31 5.14-6.75 8.91-11.31 12.03-4.47 2.94-9.13 5.91-14.48 6.91-5.99 1.26-12.58-.05-17.32-4.01-5.05-4.11-7.67-10.6-8.1-16.98-2.3-39.13-3.74-78.3-5.68-117.45-.34-4.9-1.05-9.77-1.6-14.64-1.67-.04-3.34-.08-5-.14-.48 5.59-1.41 11.13-1.66 16.74-.24 3.6.15 7.21.27 10.81 2.2 42.55 4.71 85.08 7.07 127.62.23 4.85.09 9.71.01 14.57-7.17-.22-14.35.1-21.53.17h-10.86c-12.07-.01-23.97-2.48-35.62-5.4-.84-.24-2.35-.62-1.97-1.79.93-1.73 2.79-2.65 4.38-3.65 2.58-1.58 5.51-2.49 7.96-4.27 3.26-2.14 6.56-4.24 9.99-6.1 4.07-2.03 7.89-5.08 9.62-9.41 1.32-3.12 1.7-6.54 1.81-9.89.75-16.74-1.12-33.49-4.28-49.91-3.13-16.3-7.48-32.34-11.69-48.38-10.8-40.85-19.33-82.24-28.74-123.42-.54-2.32-1.32-4.8-.45-7.14 3.38-10.97 2.52-22.83-.68-33.73-3.01-10.59-7.67-20.6-11.31-30.97-3.45-10.08-6.03-20.43-8.57-30.77-3.41-14.23-6.26-28.58-8.98-42.95-2.41-13.15-5.03-26.27-7.17-39.46-3.06-19.13-5.29-38.37-8.49-57.48-.41-4.68-4.21-8.38-8.29-10.24-4.78-1.52-9.44-3.57-13.5-6.55-5.08-3.65-9.18-8.55-12.22-14-3.94-7.03-6.3-14.8-8.22-22.58v-1.71c.59-4.84 1.45-9.64 2.13-14.46 5.15-33.97 10.32-67.93 15.48-101.9 2.31-15.05 4.52-30.12 7.02-45.14 2.98-18.14 6.09-36.27 8.28-54.53 2.44-20.62 3.82-41.35 4.47-62.09.42-16.02.63-32.06-.21-48.07-.96-20.03-2.92-39.99-3.64-60.03-.38-7.54.6-15.25 3.64-22.2 4.29-10.05 13.01-17.82 23.05-21.87 6.66-2.58 13.85-3.18 20.74-4.9 9.11-47.97 17.53-96.09 27.73-143.86.94-4.35 1.76-8.74 3.25-12.94 3.86-11.25 10.21-21.42 16.69-31.32 1.55-2.5 4.02-4.24 6.57-5.62 3.99-2.14 8.31-3.57 12.41-5.48 2.74-1.27 5.84-.27 8.68-1.02 6.44-1.61 13.01-2.81 19.65-3.15m-92.5 355.4c.01 19.2.01 38.39 0 57.59 2.11-3.68 3.52-7.73 4.62-11.82 2.49-9.87 3.06-20.25 1.46-30.32-.95-5.47-2.49-11.09-6.08-15.46M281.2 669.67c-.71 3.96.85 7.98 3.28 11.07 1.35 1.64 2.77 3.41 4.86 4.11 4.01 1.41 8.35.99 12.46 1.84.88-5.13 1.59-10.29 2.46-15.42-3.28 3-6.49 6.06-9.83 8.98-2.78-6.46-5.75-12.83-8.55-19.27-2.2 2.47-4.17 5.34-4.68 8.68Z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
1
internal/static/performer/NoName17.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="M582.86 677.29c-5.58-11.04-5.58-37.61-5.58-37.61-13.88-171.93-20.53-177.51-20.53-177.51-3.92-25.99 3.68-109.52 3.68-109.52 7.95-83.53-17.68-95.28-17.68-95.28l-9.37-6.05c9.37-2.37 9.73-7.83 9.73-7.83-17.68.83-8.19-20.41-8.19-20.41 7.83-13.29 2.37-28.24 2.37-28.24 14.83-24.32-9.37-57.19-9.37-57.19 3.09-8.66-24.32-43.07-24.32-43.07-6.29-7.83-23.61-45.56-23.61-45.56-20.53-39.16-68.58-26.82-68.58-26.82s-58.26 6.41-58.97 66.69c0 0-2.37 42.36-9.49 51.02 0 0-10.32 21.83-8.66 43.07 0 0-18.87 63.36.71 79.74 0 0-27.29-4.51-27.29 40.11v45.09s-15.9 27.88 4.51 51.38l4.63 60.04s-14.12 45.33-16.97 85.2c0 0-9.73 47.58-15.19 66.33-5.58 18.75-8.19 21.6-7.12 29.9 0 0-13.29 28.24-13.29 37.61 0 0-4.39 7.24-2.73 11.63 0 0-6.64 19.93-4.39 24.32 0 0 14.36 19.93 18.27 23.26 0 0 3.32-6.05-2.73-15.43l-3.32-12.7s11.04 6.64 13.29 7.71c0 0 15.43 1.07 23.73-3.92 0 0 5.58-8.31.59-9.37 0 0 1.07-4.98-2.73-6.64 0 0-3.32-11.04 1.07-22.07 0 0 7.24-13.76 3.32-34.29 0 0 6.05-34.29 13.29-44.73l2.25 6.05s-.24 22.54-3.68 48.65c0 0-8.9 111.66 3.8 178.11 0 0 4.98 41.53 3.32 64.19 0 0-3.32 32.63 5.58 40.94 0 0 0 13.88 5.58 18.75 0 0 13.88 54.46 34.89 114.5 10.8 30.73 7.71 54.7 7.71 54.7-3.92 11.63-11.04 41.53-11.04 41.53-17.09 9.37-19.34 25.99-19.34 25.99-6.05 2.73-6.64 17.09-6.64 17.09-5.58 18.75 13.29 16.02 13.29 16.02 51.38-6.64 46.99-33.7 46.99-33.7 1.66-16.02 19.22-43.19 19.22-43.19l.12 25.39c3.92 4.39 6.53-.47 6.53-.47.12-18.39 5.7-36.07 5.7-36.07 18.27-16.02 7.24-23.73 7.24-23.73 0-4.39-6.64-9.97-6.64-9.97 2.25-5.58-6.05-10.56-6.05-10.56-5.58-4.39-11.63-16.61-11.63-16.61-7.71-12.7-4.98-55.89-4.98-55.89 12.7-37.02-13.41-85.2-13.41-85.2 3.2-15.43 1.31-49.72 1.31-49.72-1.07-30.38 13.88-88.52 13.88-88.52 12.22-24.8 14.48-74.75 14.83-94.69 1.66 11.27 8.42 25.16 8.42 25.16 9.49 20.41 15.43 50.31 15.43 50.31 3.8 28.48 11.04 50.9 11.04 50.9.59 13.29 14.36 55.29 14.36 55.29-2.25 36.55 13.88 47.58 13.88 47.58 2.25 12.22 3.32 78.55 3.32 78.55-1.07 61.35-17.68 97.89-17.68 97.89-9.37 9.97-29.31 71.31-29.31 71.31-10.56 6.64-18.75 19.93-18.75 19.93-4.39 0-11.04 12.22-11.04 12.22-21 12.7 13.29 19.93 13.29 19.93 53.63 5.58 58.85-18.27 58.85-18.27 3.92-11.63 18.39-42.6 18.39-42.6v24.32c2.49 4.98 8.42 0 8.42 0-1.66-16.02 6.29-36.55 6.29-36.55 17.68-27.65 5.81-28.24 5.81-28.24 1.66-9.37-10.44-21.6-10.44-21.6-2.25-5.58 3.32-18.75 3.32-18.75 53.04-117.83 29.31-176.44 29.31-176.44-1.07-12.7-6.64-45.33-6.64-45.33 5.58-48.65 1.66-108.33 1.66-108.33 2.73 4.98 19.93-3.32 19.93-3.32l12.1-8.31c5.58-2.73 11.75-27.53 11.75-27.53 4.39-6.76-9.61-40.58-9.61-40.58Zm-60.16-87.92-2.73-6.05c-.59-15.43-38.68-95.64-38.68-95.64-4.98-15.43 7.24-66.92 7.24-66.92 0-4.98 16.02-37.61 16.02-37.61l7.12 84.6c-7.59 33.58 30.02 152 31.56 156.75v.12-.12c-3.09-9.73-20.53-35.12-20.53-35.12Zm26.46 138.95 1.19-33.34c3.2 6.29 4.15 16.61 4.15 16.61-.83 15.43-5.34 16.73-5.34 16.73Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
1
internal/static/performer/NoName19.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="m434.69 27.94 1.67-.07c1.39 2.28-4.86 6.26-3.95 9.02 11.38 3.47 22.47 7.89 33.36 12.65 1.55 2.77-2.74.28-3.78 1.94-3.26 3.39-8.62 1.06-12.76 2.02 1.92 6.03 10.2 1.14 14.42 4.13 6.59 3.05 14.27.61 20.81 3.8 10.62 2.71 21.79.16 32.49 2.56 38.39 8.38 78.55 6.98 116.34 18.22 12.55 5.44 30.09 9.58 33.14 25.25l.24 5.59c-2.35 14.64-10.42 27.45-17.79 40.02-11.3 20.29-23.93 39.78-36.34 59.39-9.89 18.01-22.55 34.24-34.25 51.06-13.31 18.09-19.06 40.24-24.76 61.58-3.57 20.11-11.88 39.14-14.61 59.39-1.43 4.61-3.81 8.9-4.95 13.6-1.24 9.62-5.54 18.48-8.59 27.6-7.2 12-14.5 24.8-14.35 39.27-3.54 21.74 14.85 37.5 22.63 55.94 11.03 28.11 23.51 55.69 32.64 84.52 8.37 18.83 13.29 38.91 17.52 59 7.94 37.57 7.19 76.17 11.65 114.18-.45 28.59 4.53 56.97 12.02 84.49 2.75 17.66 12.14 33.21 17.16 50.19 4.04 11.92 7.37 24.23 7.76 36.89.7 34.27 6.39 68.12 9.82 102.17 2.23 25.23 6.57 50.56 16.3 74.08 2.35 9.56 7.5 17.99 12.55 26.33 2.84 5.79 10.05 6.8 14.02 11.57 5.38 4.7 6.77 13.22 3.66 19.44-12.76 3.47-28.24 1.11-38.23 11.4-2.12 8.58 2.83 16.62 4.09 24.94-.51 1.22-1.52 2.13-2.47 3.01l-1.1.05c-4.12-4.16-4.25-10.82-7.22-15.77-3.44-9.49-11.85-15.78-20.22-20.7-2.77-1.44-2.8-5.93.24-7.01-4.42-6.03-2.64-14.1-1.52-20.92-.28-8.02-1.05-16.11-3.65-23.74-3.32-11.81-3.38-24.42-7.98-35.91-2.13-5.21-3.09-10.76-4.41-16.18-3.21-11.88-9.61-22.57-13.03-34.38-5.89-16.99-10.95-34.27-17.68-50.96-6.8-18.03-12.61-36.54-15.13-55.7-3.08-16.68-2.43-34.1-8.08-50.25-8.49-19.67-18.15-39.23-21.5-60.56-6.63-27.16-15.83-54.01-30.02-78.2-7.36-16.22-14.85-32.41-21.25-49.04-4.45-17.75-8.76-35.54-13.84-53.11-.63-1.71-1.3-4.26-3.6-4.05-7.25-1.2-14.48-.78-21.76-1.4-4.55 3.57-7.12 8.81-8.56 14.29-5.8 15.25-9.96 31.14-16.86 45.96-11.4 19.44-21 39.98-34.3 58.25-10.59 18.23-20.3 37.2-26.79 57.31-5.24 18.24-13.5 35.46-22.72 52-4.24 7.24-8.25 14.82-9.47 23.23-5.17 26.42-10.26 53.3-22.4 77.59-12.04 25.15-23.93 50.34-35.68 75.64-10.48 20.29-19.2 41.57-25.1 63.68-2.1 6.22-6.58 12.94-2.56 19.53 3.21 6.92 9.2 15.12 4.2 22.67 2.59-.33 3.96 2.22 3.56 4.52-3.93 4.88-8.68 9.1-12.08 14.42-7.84 11.3-9.39 25.29-13.63 38.07-1.7 1-3.68-.26-5.08-1.25 1.43-9.42 3.95-18.73 4.22-28.27-.04-5.48-7.07-8.39-11.27-5.13-12.78 4.61-26.14 13.15-40.1 9.73-2.24-2.91-3.24-6.59-4.68-9.9l-.07-1.7c-.23-7.84 4.77-14.02 9.7-19.48 1.16-4.41 5.29-5.56 9.14-6.57 6.88-17.21 13.42-34.62 21.79-51.19 6.73-15.66 11.39-32.12 16.11-48.46 6.36-33.01 15.66-65.54 21.52-98.79.98-13.65 4.05-27.19 8.03-40.26 6.15-16.38 14.88-31.63 22.72-47.21 7.09-20.5 14.78-41.02 17.7-62.62 4.13-25.36 8.76-50.63 13.04-75.96 4.29-30.12 10.04-60.2 20.71-88.77 6.67-22.68 18.32-43.39 30.73-63.35 5.99-15.39 14.98-29.53 19.18-45.62 4.32-16.26 13.84-30.49 18.97-46.42 3.33-16.59-.01-34.55-6.81-49.57-.53.26-.36 2.23-1.05 1.38-3.49-2.51-3.37-7.33-6.32-10.23-2.33-2.47-3.99-5.44-5.69-8.35-1.54 3.51.27 8.82-.7 12.96-3.4 2.05-3.28-5.64-6.24-2.22-2.56-4.25-7.1-6.8-9.1-11.48-2.01-1.77-3.2-6.37-6.37-4.17-3.6-3.03-5.61-8.32-9.56-11.31-.64 1.57.73 4.84-.96 5.42-.27-3.94.07-8.73-3.71-11.15-1.07 2.34-1.14 5.22-2.84 7.26.39-3.19 1.38-6.25 1.87-9.41-3.44-4.73-6.55-9.84-7.82-15.62-.43-5.23 2.56-9.8 4.57-14.38 1.64-6.65 2.47-13.63 5.91-19.69 2.6-4.58 3.99-10.86.66-15.42-10.32-15.16-15.01-33.93-28.74-46.73-24.72-24.64-46.13-52.22-68.42-79-12.37-17.13-27.93-32.24-36.88-51.63-4.09-8.33-3.1-19.07 3.12-26.1 9.18-9.65 20.47-16.86 31.64-23.96 20.09-9.27 39.62-19.78 60.32-27.73 7.73-4.35 15.5-8.68 23.81-11.89 13.3-7.14 27.05-13.79 41.92-16.91 16.9-5.78 35.25-10.3 53.04-6.4 6.55-.97 12.64-4.22 18.66-6.98m-78.8 47.61c-10.25 1.69-17.44 9.9-26.2 14.7-15.09 7.58-26.7 20.61-42.24 27.47-11.59 8.63-24.87 14.37-37.05 22.02-4 4.3 2.9 8.97 5.81 11.64 18.22 13.74 34.82 29.39 51.23 45.19 10.09 10.42 22.69 17.73 34.25 26.31 1.23-4.24 2.53-8.51 3.15-12.88.04-13.72-9.1-25.34-10.32-38.89.79-8.24-.35-16.62 1.14-24.79 3.11-13.04 12.91-23.42 24.07-30.3 3.65-1.9 6.15-5.19 9.22-7.81 5.51-3.53 6.39-10.85 11.79-14.5 3.64-3.08 8.66-3.42 12.76-5.69-1.3-2.5-4.36 1.26-6.15-.06 1.38-3.02 3.76-5.33 6.11-7.59-9-.66-18.6-.78-27.24-4.27-3.27-1.46-6.9-1.19-10.34-.55m142.15 19.7c-1.85.65-5.24.48-5.39 3.01 3.91.32 6.94 5.48 10.78 2.8 2.78 10.29 13.92 15.48 15.9 26.13.98 8.08-2.63 15.43-6.1 22.4 2.8 5.3 5.81 10.51 7.97 16.13 3.34 5.04 7.63 9.8 8.29 16.12-.7 10.97 11.3 18.21 9.91 29.34 25.71-21.59 42.67-51.25 66.87-74.3 3.49-3.9 8.02-6.91 10.55-11.61 1.02-1.96-.53-3.48-2.2-3.65-31.86-5.13-62.58-15.12-93.84-23-7.44-1.88-14.92-5.44-22.75-3.35m10.25 17.62c1.13 3 6.09 5.03 3.73 8.37 3.22 4.18-.54 9.55.96 14.2.77 3.09-1.26 5.96-3.74 7.61-.21 2.08 1.81 4.05 3.23 5.4 7.76-10.67 8.36-28.21-4.18-35.58Z"/></svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
1
internal/static/performer/NoName21.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="M423.52 18.72c-23.04 1.06-33.36 15.17-35.68 20.59-2.48 5.78-36.33 56.19-36.33 56.19l-20.59 27.23v1.23c-3.67 5.71-8.71 14.06-10.76 20.22-3.3 9.91-3.32 10.69-3.32 10.69l-2.46 10.76 4.98-4.12 4.12-14.01 7.58-19.5c.19 5.9.49 17.21-.14 26.08-.83 11.56-9.1 21.45-9.1 21.45v24.77s-2.47 4.13-9.9 5.78c-7.43 1.65-9.9.02-4.12 3.32s8.23 2.46 8.23 2.46l-7.44 15.67s-23.95 24.75-30.55 33.01-60.24 65.23-60.24 65.23-32.21 35.49-32.21 50.35 15.67 28.1 15.67 28.1 50.41 62.73 54.53 67.68c4.13 4.95 5.76 19.02 8.23 33.88 2.48 14.86 34.67-13.22 34.67-13.22s8.27 7.42 13.22 9.9c4.95 2.48 12.35-3.32 12.35-3.32s1.69 36.34.87 43.77c-.83 7.43-3.35 70.14-2.53 122.14.83 52.01-33.8 227.89-33.8 227.89l33.01 8.23s-4.17 26.41-.87 56.12c3.3 29.72 23.11 123.01 23.11 123.01l2.53 42.9-3.32 1.66-3.32 10.76.87 9.9s-4.15 11.56-3.32 23.11c.83 11.56 4.98 19.79 4.98 19.79s1.59 14.84 7.37 25.57 33.88 17.34 33.88 17.34l16.54-.79-7.44-36.33-3.32-14.01s-1.66-19.85-1.66-41.32-10.7-47.05-12.35-69.34c-1.65-22.29 4.96-44.54 7.44-68.48 2.48-23.94 5.78-92.45 5.78-92.45s38.76 2.42 74.25 3.25c35.5.83 66.89-4.12 66.89-4.12s1.61 23.15 4.91 41.32c3.3 18.16 11.61 35.45 14.08 45.36l19 75.98-2.53 42.11s-14.74 23.05-12.35 33.8c1.65 7.43 9.9 21.45 9.9 21.45l.79 47.09 5.78 3.25s-.83-39.61 0-44.57c.83-4.95 4.16-1.6 10.76 2.53s8.28 38.8 9.1 46.23c.83 7.43 20.59 9.03 20.59 9.03l54.53-3.25-33.88-26.44s-5.78-18.14-5.78-25.57-3.32-32.25-3.32-37.2-2.42-14.02-3.25-21.45-7.44-47.89-7.44-47.89-1.63-146.06-2.46-153.49c-.83-7.43-3.32-22.32-3.32-22.32l14.01-4.98s-9.91-36.31-11.56-44.57c-1.65-8.26-8.21-51.18-16.47-104.01-8.25-52.83-47.89-188.16-47.89-188.16V523.1l4.91.79s-1.66-20.63-1.66-33.01-12.35-59.44-12.35-59.44-1.66-13.21-1.66-17.34 4.98-18.13 4.98-18.13 20.63 36.31 33.01 33.01c25.72-6.86 42.1-20.68 34.67-99.1-8.89-93.86-21.45-126.26-21.45-126.26l-29.76-7.44c-9.91-2.48-37.92-13.22-37.92-13.22l3.25-9.1 2.53-17.34 9.03 11.56-6.57-18.13 2.46-9.1-3.32-11.56s-2.45-16.51-6.57-28.89c-4.13-12.38-15.72-33.83-28.1-50.35-12.38-16.51-33.82-30.52-60.24-31.35-1.65-.05-3.23-.07-4.77 0Zm93.9 104.01 4.19 8.23 1.59 10.76-4.12 7.37-1.66-19.79v-6.57Zm-149.37 10.69-7.44 15.67-11.56 7.44 9.1-16.47 9.9-6.65Zm146.12 20.66v21.45l-1.66 4.12-6.65-3.25 4.19-9.97 4.12-12.35Zm-171.69 18.13-4.19 13.22-7.37 4.98 4.12-9.9 7.44-8.31Zm1.59 15.75 1.66 9.03-10.69 2.53 9.03-11.56Zm-22.54 125.25c1.45.06 2.68.64 3.61 1.88 4.95 6.6 18.13 42.91 18.13 42.91l13.22 33.8 7.44 17.34v22.32l-7.44 1.66-28.89 30.55s-18.19-27.27-28.1-33.88-23.93-19-27.23-24.77c-3.3-5.78-18.17-21.43-13.22-29.69 4.95-8.26 21.46-37.97 34.67-46.23 10.73-6.71 21.51-16.13 27.81-15.89Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
1
internal/static/performer/NoName22.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="M492.92 13.57h1.22c8.75 2.8 18.36.25 26.68 4.72-4.42-8.75 6.46 5.52 9.19 4.26-.65-2.46 3.96 2.05 2.53-.41-.49-2.14-5.71-6.12-1.24-6.01 4.85 1.06 4.44 5.97 8.46 11.25 3.64.53 11.6 4.08 8.93 9.56 1.76 1.66 10 1.16 4.63 2.06 8.55 5.05 10.1 15.04 15.16 23-3.08-.73-1.47 3.49-1.71 5.31 2.09 10.05 3.13 23.41-2.45 30.72-1.69 10.53-5.39 20.6-9.76 30.27-6.24 3.63-4.6 16.5-11.72 17.12-3.03 5.57-5.73 11.37-8.59 17.03 3.18 9.37 4.47 19.3 6.56 28.96-3.53-1.28-3.96-1.06-5.85 1.93 3.06-1.74 5.15 7.26 1.47 5.04-5.55-2.09 2.94 10.49-3.19 8.26-5.99-3.58 4.06 10.49-3.79 8.07 1.23-9.35-2.68-18.67-3.61-28.08-.42-4.88 4.57-11.58 1.83-15.24-3.71 10.18-12.5 17.22-20.05 24.07 5.02 2.68 7.17 7.73 7.7 13.13 3.47 11.54 12.79 19.67 19.67 29.15 2.15 4.44 7.39 4.05 11.4 5.37 14.06 3.12 24.97 15.12 28.83 28.72 5.81 13.74 7.66 28.53 9.37 43.2v29.23c-4.21 19.23-1.5 39.1-4.97 58.43-5.84 23.1 3.14 46.42.3 69.8 4.91 45.45-13.06 88.7-16.36 133.52-8.02 16.62-5.15 35.58-7.43 53.37-5.71 11.46-9.44 24.48-19.83 32.66-5 .83-9.43 3.37-14.37 4.46.6 28.64 2.89 57.24 4.47 85.87 2.91 50.58 1.16 100.73 5.12 151.21-.02 9.3-2.74 18.89.16 27.99.6 1.59 4.63 4.13 1.3 4.85-10.61.84-21.36 2.31-31.99.98-7.98 1.53 2.02-9.72-7.62-7.24-2.3 22.07-7.9 43.77-14.83 64.82 2.95 20.97 8.13 42.79 20.19 60.69 7.25 6.58 10.59 16.06 16.02 24.15 5.27 16.37-9.3 29.55-13.25 44.51-2.86 13.26-1.97 26.95-2.07 40.42-2.39.41-4.98.82-7.24-.24-3.96-15.74 8.37-41.07-10.08-50.71-10.61 7.13-9.95 22.7-18.65 31.52-3.12 9.15-15.82 6.94-21 13.75-2.03 6.52-.15 25.04-11.79 16.3-.25-6.6 5.52-20.88-6.02-18.73-8.18 10.47-2.47 28.02-13.25 37.5-14.82 6.59-31.69 4.9-47.37 7.88h-4.96s-.02.03-4.6 0h-5.71c-4.63-.75-9.53-1.17-13.7-3.48-4.29-5.87-.68-12.54-3.31-18.62-.9-12.38 14.42-14.04 19.53-23.21 4.09-7 14.42-8.14 15.95-16.81 18.08-50.42 48.95-99.3 43.87-154.96-14.12-23.04-29.8-45.32-41.42-69.81-6.72-11.69-14.68-22.68-22.9-33.31-9.9 2.56-13.97-9.35-23.21-9.72-9.29.46-18-2.92-27.09-3.65 5.61-45.64 7.53-91.59 13.51-137.17 5.56-34.55 5.71-68.64 9.09-103.63 3.61-31.39 6.36-63.04 10.57-94.26-.44-7.88 7.17-19.22 2.9-28.52-5.18 8.56-11.91 16.39-15.02 26.06-1.45 9.74.45 20-3.3 29.38-4.07 10.99-11.82 22.96-5.99 34.73 1.69 5.3 9.13 7.61 7.96 13.89-6.92 2.6-13.55-4.98-17.65-9.79.59 9.15 13.45 10.95 15.72 19.37-.47 3.54-4.34 3.39-7.03 3.06-1.15 8.85-13.62-3.65-14.87 5.33-8.42-.39-15.28-8.46-20.32-14.84-8.39-8.15-2.45-20.95-6.6-30.67v-4.7c10.65-19.41 22.28-38.76 25.4-61.17 9.99-29.64 22.11-58.6 28.45-89.35 9.09-20.13 6.54-42.25 8.77-63.69 2.61-20.5 1.85-39.91 4.63-58.71 6.42-11.48 13.18-22.89 19.06-34.63 7.68-8.14 6.32-23.55 14.14-33.01 2.35-10.34 3.93-21.24 7.69-31.31 5.45-12.06 19.7-15.78 31.76-16.95 10.99 3.35 15.09-9.98 23.91-13.67 8.95-4.07 2.75-12.06 5.25-19.44-9.98 2.46 2.62-11.96-.29-15.74-4.5-.5.56 8.28-4.08 8.4-2.8-.31-.47-3.77-.18-5.39 3.21-4.35-6-2.03-2.97-5.64 1.96-9.66 7.09-18.28 10.3-27.57 4.26-2.6-4.76-2.76-4.75-6.7-2.88-11.29-4.94-23.64-3.45-35.09 11.8-13.66 4.09-33.9 14.3-48.39 2.96-7.43 9.56-12.05 14.57-17.97.58-2.08-1.08-1.71-2.45-1.16 3.86-3.41 9.47-3.91 13.5-6.96 4.19-9.19 18.7-4.1 23.95-8.97m26.87 376.08c-3.57 7.7-4.63 16.46-10.53 23.14-2.56 16.66-1.25 33.96-4.34 50.5 1.1 5.23 1.31 12.93-4.69 15.27-3.96-1.22-.65-6.18-.56-8.93 1-15.57 2.44-31.02 4.55-46.39-13.66 12.06-7.35 34.11-7.47 50.6 2.48 4.24-5.39 8.9-6.41 3.75.37-11.54-.13-23.05 1.85-34.5-2.38 3.66-3.32 8.64-3.56 12.47-9.39 2.22 4.76 10.24-2.38 14.37-8.4 4.94 1.62 14.71-4.54 21.13 2.41 5.86 8.08 11.65 5.27 19.06-3.14 5.12 5.21 7.9 3.65 13.38 8.02 17.32 14.1 35.59 25.04 51.36 5.11 9.37 7.14 20.04 9.75 30.18 6.66-8.25 7.47-19.49 8.05-29.65.27-36.01-5.69-71.8-6.2-107.78-3.5-25.77-1.7-53.15-7.51-77.96Zm-17.37 36.26q.33.33 0 0Zm-5.35 39.48q.33.33 0 0ZM307.75 638.7q.33.33 0 0Zm5.35 88.34q.33.33 0 0Zm-16.05 158.58q.33.33 0 0Zm43.49 309.13q.33.32 0 0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
1
internal/static/performer/NoName23.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 850 1250"><path fill="#fff" d="m280.5 271.75 1.28-3.04c.69-5.89-1.28 3.04-1.28 3.04Zm-17.82 37.38-1.98.23s-.6 2.67 1.98-.23Zm334.89 88.5c2.08-31.5-58.57-175.18-58.57-175.18-2.76-17.08-36.92-29.42-36.92-29.42l4.14-3.37c-2.7-27.39-31.34-39.72-31.34-39.72-13.67-2.06-6.15-8.9-6.15-8.9.66-108.74-93.4-119.08-93.4-119.08-71.66-13.6-86.65 104.73-86.65 104.73 4.77 17.06-1.33 36.23-1.33 36.23-5.46 13.71-11.63 10.97-11.63 10.97 7.51 4.08 8.91 0 8.91 0s9.52-4.1 2.04.7c-7.51 4.74-3.44 14.34-3.44 14.34l8.2-12.27-5.48 36.26c-6.79 23.25-31.36 19.13-31.36 19.13l6.13 3.45c14.01 13.91 24.13 9.05 26.18 7.73-.09.64-.16 1.17-.22 1.79-2.7 17.78-7.91 21.55-9.98 22.36 4.33-.65 7.94-5.26 7.94-5.26l-2.83 6.57c-.13 1-.32 2.4-.6 4.4-2.04 13.64-26.61 17.78-26.61 17.78 9.57 8.24 18.43 0 18.43 0l-3.45 8.24c-3.42 5.49-5.5 8.46-6.87 9.99l7.55-1.1-6.09 11.58c-32.71 26.65-10.24 34.96-10.24 34.96l2.05-4.17c2.04-15.75 6.83-7.52 6.83-7.52l-4.09 14.36C263.5 381.2 243 398.95 243 398.95s-4.04 0 2.06.71c6.15.7 8.87-2.7 8.87-2.7 5.72-1.35 7.72-3.21 8.45-4.38l-2.98 7.79c-13.66 6.15-12.28 35.58-12.28 35.58 12.96 11.6 6.13 29.44 6.13 29.44v8.16c-2.04 2.7.68 11.62.68 11.62 0 6.16-2.72 8.25-2.72 8.25-5.47 4.73-2.05 31.46-2.05 31.46l2.05 1.33c-7.5 18.45-2.73 18.45-2.73 18.45 1.32 2.74 18.42 30.84 18.42 30.84l-14.33 64.3c8.16 10.24 19.11 24.64 19.11 24.64-4.79 4.79 11.56 104.68 11.56 104.68l.66 7.55c0 34.9-7.46 97.81-7.46 97.81-19.11 39.01-7.51 123.16-7.51 123.16s-4.2-48.22 1.33 8.21c5.48 55.39 2.1 93.74 2.1 93.74-4.13 12.28-7.53 36.28-7.53 36.28-.65 13.01-6.11 25.99-6.11 25.99-8.93 55.43-5.47 59.47-5.47 59.47 3.42 15.13 40.95 3.58 41.61 3.43 4.42-1.23 6.79-19.15 6.79-19.15s2.72-47.16 8.2-59.56c.25-.5.43-1.01.65-1.58 4.13-11.85-3.38-24.43-3.38-24.43-5.47-13.68 3.4-62.87 3.4-62.87 12.96-36.94 23.22-121.16 23.22-121.16-2.78-19.07 4.05-59.47 4.05-59.47 7.51-21.21 14.31-64.36 14.31-64.36 2.75-4.73 20.48-136.09 20.48-136.09l40.89.63c3.43 15.75 35.52 166.34 35.52 166.34 10.2 20.47 19.04 60.43 19.11 62.88.65 24.64 13.61 78.03 13.61 78.03 16.31 36.93 31.34 111.53 31.34 111.53-5.43 19.17-1.32 34.22-1.32 34.22-2.07 47.19 1.32 79.99 1.32 79.99 0 19.16 52.5 8.24 52.5 8.24 13.64-19.86-10.21-94.4-10.21-94.4l-2.01-8.88c-2.78-18.56-10.94-37.02-10.94-37.02-1.38-6.77-2.03-80.67-2.03-80.67 6.8-66.39-5.47-102.65-5.47-102.65-2.04-14.67-14.32-49.24-14.32-49.24-1.33-17.09-8.19-56.1-8.19-56.1 0-12.99 4.09-114.29 4.09-114.29l3.43.68c4.1-7.52 6.12-75.25 6.12-75.25 9.5-.72 13.65-7.53 13.65-7.53 8.16-5.48 7.5-17.1 7.5-17.1 11.55 0 10.93-5.52 10.93-5.52l47.72-131.31c8.85-19.14-8.22-41.06-8.22-41.06Zm-263.15 25.28c-8.19 8.9-6.91 10.99-6.91 10.99s-28.6 67.73-35.43 68.37c-1.17.14-1.84.14-2.35.14l-3.07 1.9s13.59-62.22 28.61-84.12c0 0 5.43-32.15 5.43-38.97s-5.43-23.27-5.43-23.27 6.18 17.74 15.02 21.2c0 0 12.27 34.9 4.13 43.76Zm208.59-2.72s-2.67 4.82-2.67 10.94c0 0-4.29 62.94-8.23 78.53-.18.9-.43 1.67-.64 2.23-4.09 10.27-7.5 19.12-6.15 22.56 0 0-15.69 19.88-8.19 23.3l3.4 3.37-10.91-1.32s-8.16-8.24-7.56-24.68c0 0-5.41-6.16-6.78-5.43l-4.06-22.6 8.83-3.39s-9.49-57.5-16.37-69.79c0 0 3.47 6.84 10.28 17.74l5.45 7.56s8.18-4.75 6.81-17.11c0 0-6.16 1.36-8.87-8.19-2.69-9.59.65-2.09.65-2.09s6.89-.68 8.22-12.26c0 0 7.51 10.22 19.78 0l6.12-8.21 3.38-6.72c-1.6-.87-4.16-2.7-8.75-6.31 0 0-10.3-4.08-11.67-19.86 0 0-4.06-12.98 8.9-18.44 0 0 8.14 41.72 15.67 38.96 0 0 6.78 4.79 2.03 11 0 0 4.11 3.42 1.33 10.22Z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
1
internal/static/performer/NoName24.svg
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 11 KiB |