mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Add group graphql interfaces (#5017)
* Deprecate movie and add group interfaces * UI changes
This commit is contained in:
@@ -51,6 +51,11 @@ models:
|
|||||||
fieldName: DurationFinite
|
fieldName: DurationFinite
|
||||||
frame_rate:
|
frame_rate:
|
||||||
fieldName: FrameRateFinite
|
fieldName: FrameRateFinite
|
||||||
|
# group is movie under the hood
|
||||||
|
Group:
|
||||||
|
model: github.com/stashapp/stash/pkg/models.Movie
|
||||||
|
GroupFilterType:
|
||||||
|
model: github.com/stashapp/stash/pkg/models.MovieFilterType
|
||||||
# autobind on config causes generation issues
|
# autobind on config causes generation issues
|
||||||
BlobsStorageType:
|
BlobsStorageType:
|
||||||
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
|
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
|
||||||
|
|||||||
@@ -77,13 +77,22 @@ type Query {
|
|||||||
): FindStudiosResultType!
|
): FindStudiosResultType!
|
||||||
|
|
||||||
"Find a movie by ID"
|
"Find a movie by ID"
|
||||||
findMovie(id: ID!): Movie
|
findMovie(id: ID!): Movie @deprecated(reason: "Use findGroup instead")
|
||||||
"A function which queries Movie objects"
|
"A function which queries Movie objects"
|
||||||
findMovies(
|
findMovies(
|
||||||
movie_filter: MovieFilterType
|
movie_filter: MovieFilterType
|
||||||
filter: FindFilterType
|
filter: FindFilterType
|
||||||
ids: [ID!]
|
ids: [ID!]
|
||||||
): FindMoviesResultType!
|
): FindMoviesResultType! @deprecated(reason: "Use findGroups instead")
|
||||||
|
|
||||||
|
"Find a group by ID"
|
||||||
|
findGroup(id: ID!): Group
|
||||||
|
"A function which queries Group objects"
|
||||||
|
findGroups(
|
||||||
|
group_filter: GroupFilterType
|
||||||
|
filter: FindFilterType
|
||||||
|
ids: [ID!]
|
||||||
|
): FindGroupsResultType!
|
||||||
|
|
||||||
findGallery(id: ID!): Gallery
|
findGallery(id: ID!): Gallery
|
||||||
findGalleries(
|
findGalleries(
|
||||||
@@ -156,7 +165,13 @@ type Query {
|
|||||||
scrapeSingleMovie(
|
scrapeSingleMovie(
|
||||||
source: ScraperSourceInput!
|
source: ScraperSourceInput!
|
||||||
input: ScrapeSingleMovieInput!
|
input: ScrapeSingleMovieInput!
|
||||||
): [ScrapedMovie!]!
|
): [ScrapedMovie!]! @deprecated(reason: "Use scrapeSingleGroup instead")
|
||||||
|
|
||||||
|
"Scrape for a single group"
|
||||||
|
scrapeSingleGroup(
|
||||||
|
source: ScraperSourceInput!
|
||||||
|
input: ScrapeSingleGroupInput!
|
||||||
|
): [ScrapedGroup!]!
|
||||||
|
|
||||||
"Scrapes content based on a URL"
|
"Scrapes content based on a URL"
|
||||||
scrapeURL(url: String!, ty: ScrapeContentType!): ScrapedContent
|
scrapeURL(url: String!, ty: ScrapeContentType!): ScrapedContent
|
||||||
@@ -169,6 +184,9 @@ type Query {
|
|||||||
scrapeGalleryURL(url: String!): ScrapedGallery
|
scrapeGalleryURL(url: String!): ScrapedGallery
|
||||||
"Scrapes a complete movie record based on a URL"
|
"Scrapes a complete movie record based on a URL"
|
||||||
scrapeMovieURL(url: String!): ScrapedMovie
|
scrapeMovieURL(url: String!): ScrapedMovie
|
||||||
|
@deprecated(reason: "Use scrapeGroupURL instead")
|
||||||
|
"Scrapes a complete group record based on a URL"
|
||||||
|
scrapeGroupURL(url: String!): ScrapedGroup
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
"List loaded plugins"
|
"List loaded plugins"
|
||||||
@@ -214,7 +232,7 @@ type Query {
|
|||||||
allPerformers: [Performer!]!
|
allPerformers: [Performer!]!
|
||||||
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
||||||
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
|
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
|
||||||
allMovies: [Movie!]! @deprecated(reason: "Use findMovies instead")
|
allMovies: [Movie!]! @deprecated(reason: "Use findGroups instead")
|
||||||
|
|
||||||
# Get everything with minimal metadata
|
# Get everything with minimal metadata
|
||||||
|
|
||||||
@@ -316,10 +334,21 @@ type Mutation {
|
|||||||
studiosDestroy(ids: [ID!]!): Boolean!
|
studiosDestroy(ids: [ID!]!): Boolean!
|
||||||
|
|
||||||
movieCreate(input: MovieCreateInput!): Movie
|
movieCreate(input: MovieCreateInput!): Movie
|
||||||
|
@deprecated(reason: "Use groupCreate instead")
|
||||||
movieUpdate(input: MovieUpdateInput!): Movie
|
movieUpdate(input: MovieUpdateInput!): Movie
|
||||||
|
@deprecated(reason: "Use groupUpdate instead")
|
||||||
movieDestroy(input: MovieDestroyInput!): Boolean!
|
movieDestroy(input: MovieDestroyInput!): Boolean!
|
||||||
|
@deprecated(reason: "Use groupDestroy instead")
|
||||||
moviesDestroy(ids: [ID!]!): Boolean!
|
moviesDestroy(ids: [ID!]!): Boolean!
|
||||||
|
@deprecated(reason: "Use groupsDestroy instead")
|
||||||
bulkMovieUpdate(input: BulkMovieUpdateInput!): [Movie!]
|
bulkMovieUpdate(input: BulkMovieUpdateInput!): [Movie!]
|
||||||
|
@deprecated(reason: "Use bulkGroupUpdate instead")
|
||||||
|
|
||||||
|
groupCreate(input: GroupCreateInput!): Group
|
||||||
|
groupUpdate(input: GroupUpdateInput!): Group
|
||||||
|
groupDestroy(input: GroupDestroyInput!): Boolean!
|
||||||
|
groupsDestroy(ids: [ID!]!): Boolean!
|
||||||
|
bulkGroupUpdate(input: BulkGroupUpdateInput!): [Group!]
|
||||||
|
|
||||||
tagCreate(input: TagCreateInput!): Tag
|
tagCreate(input: TagCreateInput!): Tag
|
||||||
tagUpdate(input: TagUpdateInput!): Tag
|
tagUpdate(input: TagUpdateInput!): Tag
|
||||||
|
|||||||
@@ -257,7 +257,9 @@ input SceneFilterType {
|
|||||||
"Filter to only include scenes with this studio"
|
"Filter to only include scenes with this studio"
|
||||||
studios: HierarchicalMultiCriterionInput
|
studios: HierarchicalMultiCriterionInput
|
||||||
"Filter to only include scenes with this movie"
|
"Filter to only include scenes with this movie"
|
||||||
movies: MultiCriterionInput
|
movies: MultiCriterionInput @deprecated(reason: "use groups instead")
|
||||||
|
"Filter to only include scenes with this group"
|
||||||
|
groups: MultiCriterionInput
|
||||||
"Filter to only include scenes with this gallery"
|
"Filter to only include scenes with this gallery"
|
||||||
galleries: MultiCriterionInput
|
galleries: MultiCriterionInput
|
||||||
"Filter to only include scenes with these tags"
|
"Filter to only include scenes with these tags"
|
||||||
@@ -309,6 +311,9 @@ input SceneFilterType {
|
|||||||
tags_filter: TagFilterType
|
tags_filter: TagFilterType
|
||||||
"Filter by related movies that meet this criteria"
|
"Filter by related movies that meet this criteria"
|
||||||
movies_filter: MovieFilterType
|
movies_filter: MovieFilterType
|
||||||
|
@deprecated(reason: "use groups_filter instead")
|
||||||
|
"Filter by related groups that meet this criteria"
|
||||||
|
groups_filter: GroupFilterType
|
||||||
"Filter by related markers that meet this criteria"
|
"Filter by related markers that meet this criteria"
|
||||||
markers_filter: SceneMarkerFilterType
|
markers_filter: SceneMarkerFilterType
|
||||||
}
|
}
|
||||||
@@ -351,6 +356,44 @@ input MovieFilterType {
|
|||||||
studios_filter: StudioFilterType
|
studios_filter: StudioFilterType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input GroupFilterType {
|
||||||
|
AND: GroupFilterType
|
||||||
|
OR: GroupFilterType
|
||||||
|
NOT: GroupFilterType
|
||||||
|
|
||||||
|
name: StringCriterionInput
|
||||||
|
director: StringCriterionInput
|
||||||
|
synopsis: StringCriterionInput
|
||||||
|
|
||||||
|
"Filter by duration (in seconds)"
|
||||||
|
duration: IntCriterionInput
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: IntCriterionInput
|
||||||
|
"Filter to only include groups with this studio"
|
||||||
|
studios: HierarchicalMultiCriterionInput
|
||||||
|
"Filter to only include groups missing this property"
|
||||||
|
is_missing: String
|
||||||
|
"Filter by url"
|
||||||
|
url: StringCriterionInput
|
||||||
|
"Filter to only include groups where performer appears in a scene"
|
||||||
|
performers: MultiCriterionInput
|
||||||
|
"Filter to only include groups with these tags"
|
||||||
|
tags: HierarchicalMultiCriterionInput
|
||||||
|
"Filter by tag count"
|
||||||
|
tag_count: IntCriterionInput
|
||||||
|
"Filter by date"
|
||||||
|
date: DateCriterionInput
|
||||||
|
"Filter by creation time"
|
||||||
|
created_at: TimestampCriterionInput
|
||||||
|
"Filter by last update time"
|
||||||
|
updated_at: TimestampCriterionInput
|
||||||
|
|
||||||
|
"Filter by related scenes that meet this criteria"
|
||||||
|
scenes_filter: SceneFilterType
|
||||||
|
"Filter by related studios that meet this criteria"
|
||||||
|
studios_filter: StudioFilterType
|
||||||
|
}
|
||||||
|
|
||||||
input StudioFilterType {
|
input StudioFilterType {
|
||||||
AND: StudioFilterType
|
AND: StudioFilterType
|
||||||
OR: StudioFilterType
|
OR: StudioFilterType
|
||||||
@@ -508,6 +551,9 @@ input TagFilterType {
|
|||||||
"Filter by number of movies with this tag"
|
"Filter by number of movies with this tag"
|
||||||
movie_count: IntCriterionInput
|
movie_count: IntCriterionInput
|
||||||
|
|
||||||
|
"Filter by number of group with this tag"
|
||||||
|
group_count: IntCriterionInput
|
||||||
|
|
||||||
"Filter by number of markers with this tag"
|
"Filter by number of markers with this tag"
|
||||||
marker_count: IntCriterionInput
|
marker_count: IntCriterionInput
|
||||||
|
|
||||||
@@ -702,6 +748,7 @@ enum FilterMode {
|
|||||||
GALLERIES
|
GALLERIES
|
||||||
SCENE_MARKERS
|
SCENE_MARKERS
|
||||||
MOVIES
|
MOVIES
|
||||||
|
GROUPS
|
||||||
TAGS
|
TAGS
|
||||||
IMAGES
|
IMAGES
|
||||||
}
|
}
|
||||||
|
|||||||
80
graphql/schema/types/group.graphql
Normal file
80
graphql/schema/types/group.graphql
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
type Group {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
aliases: String
|
||||||
|
"Duration in seconds"
|
||||||
|
duration: Int
|
||||||
|
date: String
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: Int
|
||||||
|
studio: Studio
|
||||||
|
director: String
|
||||||
|
synopsis: String
|
||||||
|
urls: [String!]!
|
||||||
|
tags: [Tag!]!
|
||||||
|
created_at: Time!
|
||||||
|
updated_at: Time!
|
||||||
|
|
||||||
|
front_image_path: String # Resolver
|
||||||
|
back_image_path: String # Resolver
|
||||||
|
scene_count: Int! # Resolver
|
||||||
|
scenes: [Scene!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
input GroupCreateInput {
|
||||||
|
name: String!
|
||||||
|
aliases: String
|
||||||
|
"Duration in seconds"
|
||||||
|
duration: Int
|
||||||
|
date: String
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: Int
|
||||||
|
studio_id: ID
|
||||||
|
director: String
|
||||||
|
synopsis: String
|
||||||
|
urls: [String!]
|
||||||
|
tag_ids: [ID!]
|
||||||
|
"This should be a URL or a base64 encoded data URL"
|
||||||
|
front_image: String
|
||||||
|
"This should be a URL or a base64 encoded data URL"
|
||||||
|
back_image: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input GroupUpdateInput {
|
||||||
|
id: ID!
|
||||||
|
name: String
|
||||||
|
aliases: String
|
||||||
|
duration: Int
|
||||||
|
date: String
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: Int
|
||||||
|
studio_id: ID
|
||||||
|
director: String
|
||||||
|
synopsis: String
|
||||||
|
urls: [String!]
|
||||||
|
tag_ids: [ID!]
|
||||||
|
"This should be a URL or a base64 encoded data URL"
|
||||||
|
front_image: String
|
||||||
|
"This should be a URL or a base64 encoded data URL"
|
||||||
|
back_image: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input BulkGroupUpdateInput {
|
||||||
|
clientMutationId: String
|
||||||
|
ids: [ID!]
|
||||||
|
# rating expressed as 1-100
|
||||||
|
rating100: Int
|
||||||
|
studio_id: ID
|
||||||
|
director: String
|
||||||
|
urls: BulkUpdateStrings
|
||||||
|
tag_ids: BulkUpdateIds
|
||||||
|
}
|
||||||
|
|
||||||
|
input GroupDestroyInput {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type FindGroupsResultType {
|
||||||
|
count: Int!
|
||||||
|
groups: [Group!]!
|
||||||
|
}
|
||||||
@@ -284,7 +284,8 @@ input ExportObjectsInput {
|
|||||||
studios: ExportObjectTypeInput
|
studios: ExportObjectTypeInput
|
||||||
performers: ExportObjectTypeInput
|
performers: ExportObjectTypeInput
|
||||||
tags: ExportObjectTypeInput
|
tags: ExportObjectTypeInput
|
||||||
movies: ExportObjectTypeInput
|
groups: ExportObjectTypeInput
|
||||||
|
movies: ExportObjectTypeInput @deprecated(reason: "Use groups instead")
|
||||||
galleries: ExportObjectTypeInput
|
galleries: ExportObjectTypeInput
|
||||||
includeDependencies: Boolean
|
includeDependencies: Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ type Performer {
|
|||||||
scene_count: Int! # Resolver
|
scene_count: Int! # Resolver
|
||||||
image_count: Int! # Resolver
|
image_count: Int! # Resolver
|
||||||
gallery_count: Int! # Resolver
|
gallery_count: Int! # Resolver
|
||||||
movie_count: Int! # Resolver
|
group_count: Int! # Resolver
|
||||||
|
movie_count: Int! @deprecated(reason: "use group_count instead") # Resolver
|
||||||
performer_count: Int! # Resolver
|
performer_count: Int! # Resolver
|
||||||
o_counter: Int # Resolver
|
o_counter: Int # Resolver
|
||||||
scenes: [Scene!]!
|
scenes: [Scene!]!
|
||||||
@@ -55,7 +56,8 @@ type Performer {
|
|||||||
weight: Int
|
weight: Int
|
||||||
created_at: Time!
|
created_at: Time!
|
||||||
updated_at: Time!
|
updated_at: Time!
|
||||||
movies: [Movie!]!
|
groups: [Group!]! @deprecated(reason: "use groups instead")
|
||||||
|
movies: [Movie!]! @deprecated(reason: "use groups instead")
|
||||||
}
|
}
|
||||||
|
|
||||||
input PerformerCreateInput {
|
input PerformerCreateInput {
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ type SceneMovie {
|
|||||||
scene_index: Int
|
scene_index: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SceneGroup {
|
||||||
|
group: Group!
|
||||||
|
scene_index: Int
|
||||||
|
}
|
||||||
|
|
||||||
type VideoCaption {
|
type VideoCaption {
|
||||||
language_code: String!
|
language_code: String!
|
||||||
caption_type: String!
|
caption_type: String!
|
||||||
@@ -68,7 +73,8 @@ type Scene {
|
|||||||
scene_markers: [SceneMarker!]!
|
scene_markers: [SceneMarker!]!
|
||||||
galleries: [Gallery!]!
|
galleries: [Gallery!]!
|
||||||
studio: Studio
|
studio: Studio
|
||||||
movies: [SceneMovie!]!
|
groups: [SceneGroup!]!
|
||||||
|
movies: [SceneMovie!]! @deprecated(reason: "Use groups")
|
||||||
tags: [Tag!]!
|
tags: [Tag!]!
|
||||||
performers: [Performer!]!
|
performers: [Performer!]!
|
||||||
stash_ids: [StashID!]!
|
stash_ids: [StashID!]!
|
||||||
@@ -82,6 +88,11 @@ input SceneMovieInput {
|
|||||||
scene_index: Int
|
scene_index: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input SceneGroupInput {
|
||||||
|
group_id: ID!
|
||||||
|
scene_index: Int
|
||||||
|
}
|
||||||
|
|
||||||
input SceneCreateInput {
|
input SceneCreateInput {
|
||||||
title: String
|
title: String
|
||||||
code: String
|
code: String
|
||||||
@@ -96,7 +107,8 @@ input SceneCreateInput {
|
|||||||
studio_id: ID
|
studio_id: ID
|
||||||
gallery_ids: [ID!]
|
gallery_ids: [ID!]
|
||||||
performer_ids: [ID!]
|
performer_ids: [ID!]
|
||||||
movies: [SceneMovieInput!]
|
groups: [SceneGroupInput!]
|
||||||
|
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
|
||||||
tag_ids: [ID!]
|
tag_ids: [ID!]
|
||||||
"This should be a URL or a base64 encoded data URL"
|
"This should be a URL or a base64 encoded data URL"
|
||||||
cover_image: String
|
cover_image: String
|
||||||
@@ -128,7 +140,8 @@ input SceneUpdateInput {
|
|||||||
studio_id: ID
|
studio_id: ID
|
||||||
gallery_ids: [ID!]
|
gallery_ids: [ID!]
|
||||||
performer_ids: [ID!]
|
performer_ids: [ID!]
|
||||||
movies: [SceneMovieInput!]
|
groups: [SceneGroupInput!]
|
||||||
|
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
|
||||||
tag_ids: [ID!]
|
tag_ids: [ID!]
|
||||||
"This should be a URL or a base64 encoded data URL"
|
"This should be a URL or a base64 encoded data URL"
|
||||||
cover_image: String
|
cover_image: String
|
||||||
@@ -175,7 +188,8 @@ input BulkSceneUpdateInput {
|
|||||||
gallery_ids: BulkUpdateIds
|
gallery_ids: BulkUpdateIds
|
||||||
performer_ids: BulkUpdateIds
|
performer_ids: BulkUpdateIds
|
||||||
tag_ids: BulkUpdateIds
|
tag_ids: BulkUpdateIds
|
||||||
movie_ids: BulkUpdateIds
|
group_ids: BulkUpdateIds
|
||||||
|
movie_ids: BulkUpdateIds @deprecated(reason: "Use group_ids")
|
||||||
}
|
}
|
||||||
|
|
||||||
input SceneDestroyInput {
|
input SceneDestroyInput {
|
||||||
|
|||||||
@@ -31,3 +31,35 @@ input ScrapedMovieInput {
|
|||||||
synopsis: String
|
synopsis: String
|
||||||
# not including tags for the input
|
# not including tags for the input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"A group from a scraping operation..."
|
||||||
|
type ScrapedGroup {
|
||||||
|
stored_id: ID
|
||||||
|
name: String
|
||||||
|
aliases: String
|
||||||
|
duration: String
|
||||||
|
date: String
|
||||||
|
rating: String
|
||||||
|
director: String
|
||||||
|
urls: [String!]
|
||||||
|
synopsis: String
|
||||||
|
studio: ScrapedStudio
|
||||||
|
tags: [ScrapedTag!]
|
||||||
|
|
||||||
|
"This should be a base64 encoded data URL"
|
||||||
|
front_image: String
|
||||||
|
"This should be a base64 encoded data URL"
|
||||||
|
back_image: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input ScrapedGroupInput {
|
||||||
|
name: String
|
||||||
|
aliases: String
|
||||||
|
duration: String
|
||||||
|
date: String
|
||||||
|
rating: String
|
||||||
|
director: String
|
||||||
|
urls: [String!]
|
||||||
|
synopsis: String
|
||||||
|
# not including tags for the input
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ enum ScrapeType {
|
|||||||
enum ScrapeContentType {
|
enum ScrapeContentType {
|
||||||
GALLERY
|
GALLERY
|
||||||
MOVIE
|
MOVIE
|
||||||
|
GROUP
|
||||||
PERFORMER
|
PERFORMER
|
||||||
SCENE
|
SCENE
|
||||||
}
|
}
|
||||||
@@ -22,6 +23,7 @@ union ScrapedContent =
|
|||||||
| ScrapedScene
|
| ScrapedScene
|
||||||
| ScrapedGallery
|
| ScrapedGallery
|
||||||
| ScrapedMovie
|
| ScrapedMovie
|
||||||
|
| ScrapedGroup
|
||||||
| ScrapedPerformer
|
| ScrapedPerformer
|
||||||
|
|
||||||
type ScraperSpec {
|
type ScraperSpec {
|
||||||
@@ -40,7 +42,9 @@ type Scraper {
|
|||||||
"Details for gallery scraper"
|
"Details for gallery scraper"
|
||||||
gallery: ScraperSpec
|
gallery: ScraperSpec
|
||||||
"Details for movie scraper"
|
"Details for movie scraper"
|
||||||
movie: ScraperSpec
|
movie: ScraperSpec @deprecated(reason: "use group")
|
||||||
|
"Details for group scraper"
|
||||||
|
group: ScraperSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrapedStudio {
|
type ScrapedStudio {
|
||||||
@@ -76,7 +80,8 @@ type ScrapedScene {
|
|||||||
studio: ScrapedStudio
|
studio: ScrapedStudio
|
||||||
tags: [ScrapedTag!]
|
tags: [ScrapedTag!]
|
||||||
performers: [ScrapedPerformer!]
|
performers: [ScrapedPerformer!]
|
||||||
movies: [ScrapedMovie!]
|
movies: [ScrapedMovie!] @deprecated(reason: "use groups")
|
||||||
|
groups: [ScrapedGroup!]
|
||||||
|
|
||||||
remote_site_id: String
|
remote_site_id: String
|
||||||
duration: Int
|
duration: Int
|
||||||
@@ -190,10 +195,19 @@ input ScrapeSingleMovieInput {
|
|||||||
query: String
|
query: String
|
||||||
"Instructs to query by movie id"
|
"Instructs to query by movie id"
|
||||||
movie_id: ID
|
movie_id: ID
|
||||||
"Instructs to query by gallery fragment"
|
"Instructs to query by movie fragment"
|
||||||
movie_input: ScrapedMovieInput
|
movie_input: ScrapedMovieInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input ScrapeSingleGroupInput {
|
||||||
|
"Instructs to query by string"
|
||||||
|
query: String
|
||||||
|
"Instructs to query by group id"
|
||||||
|
group_id: ID
|
||||||
|
"Instructs to query by group fragment"
|
||||||
|
group_input: ScrapedGroupInput
|
||||||
|
}
|
||||||
|
|
||||||
input StashBoxSceneQueryInput {
|
input StashBoxSceneQueryInput {
|
||||||
"Index of the configured stash-box instance to use"
|
"Index of the configured stash-box instance to use"
|
||||||
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ type StatsResultType {
|
|||||||
gallery_count: Int!
|
gallery_count: Int!
|
||||||
performer_count: Int!
|
performer_count: Int!
|
||||||
studio_count: Int!
|
studio_count: Int!
|
||||||
movie_count: Int!
|
group_count: Int!
|
||||||
|
movie_count: Int! @deprecated(reason: "use group_count instead")
|
||||||
tag_count: Int!
|
tag_count: Int!
|
||||||
total_o_count: Int!
|
total_o_count: Int!
|
||||||
total_play_duration: Float!
|
total_play_duration: Float!
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ type Studio {
|
|||||||
image_count(depth: Int): Int! # Resolver
|
image_count(depth: Int): Int! # Resolver
|
||||||
gallery_count(depth: Int): Int! # Resolver
|
gallery_count(depth: Int): Int! # Resolver
|
||||||
performer_count(depth: Int): Int! # Resolver
|
performer_count(depth: Int): Int! # Resolver
|
||||||
movie_count(depth: Int): Int! # Resolver
|
group_count(depth: Int): Int! # Resolver
|
||||||
|
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
|
||||||
stash_ids: [StashID!]!
|
stash_ids: [StashID!]!
|
||||||
# rating expressed as 1-100
|
# rating expressed as 1-100
|
||||||
rating100: Int
|
rating100: Int
|
||||||
@@ -21,7 +22,8 @@ type Studio {
|
|||||||
details: String
|
details: String
|
||||||
created_at: Time!
|
created_at: Time!
|
||||||
updated_at: Time!
|
updated_at: Time!
|
||||||
movies: [Movie!]!
|
groups: [Group!]!
|
||||||
|
movies: [Movie!]! @deprecated(reason: "use groups instead")
|
||||||
}
|
}
|
||||||
|
|
||||||
input StudioCreateInput {
|
input StudioCreateInput {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ type Tag {
|
|||||||
gallery_count(depth: Int): Int! # Resolver
|
gallery_count(depth: Int): Int! # Resolver
|
||||||
performer_count(depth: Int): Int! # Resolver
|
performer_count(depth: Int): Int! # Resolver
|
||||||
studio_count(depth: Int): Int! # Resolver
|
studio_count(depth: Int): Int! # Resolver
|
||||||
movie_count(depth: Int): Int! # Resolver
|
group_count(depth: Int): Int! # Resolver
|
||||||
|
movie_count(depth: Int): Int! @deprecated(reason: "use group_count instead") # Resolver
|
||||||
parents: [Tag!]!
|
parents: [Tag!]!
|
||||||
children: [Tag!]!
|
children: [Tag!]!
|
||||||
|
|
||||||
|
|||||||
@@ -355,6 +355,33 @@ func (t changesetTranslator) relatedMovies(value []models.SceneMovieInput) (mode
|
|||||||
return models.NewRelatedMovies(moviesScenes), nil
|
return models.NewRelatedMovies(moviesScenes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func moviesScenesFromGroupInput(input []models.SceneGroupInput) ([]models.MoviesScenes, error) {
|
||||||
|
ret := make([]models.MoviesScenes, len(input))
|
||||||
|
|
||||||
|
for i, v := range input {
|
||||||
|
mID, err := strconv.Atoi(v.GroupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid group ID: %s", v.GroupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret[i] = models.MoviesScenes{
|
||||||
|
MovieID: mID,
|
||||||
|
SceneIndex: v.SceneIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t changesetTranslator) relatedMoviesFromGroups(value []models.SceneGroupInput) (models.RelatedMovies, error) {
|
||||||
|
moviesScenes, err := moviesScenesFromGroupInput(value)
|
||||||
|
if err != nil {
|
||||||
|
return models.RelatedMovies{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.NewRelatedMovies(moviesScenes), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t changesetTranslator) updateMovieIDs(value []models.SceneMovieInput, field string) (*models.UpdateMovieIDs, error) {
|
func (t changesetTranslator) updateMovieIDs(value []models.SceneMovieInput, field string) (*models.UpdateMovieIDs, error) {
|
||||||
if !t.hasField(field) {
|
if !t.hasField(field) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -371,6 +398,22 @@ func (t changesetTranslator) updateMovieIDs(value []models.SceneMovieInput, fiel
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t changesetTranslator) updateMovieIDsFromGroups(value []models.SceneGroupInput, field string) (*models.UpdateMovieIDs, error) {
|
||||||
|
if !t.hasField(field) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
moviesScenes, err := moviesScenesFromGroupInput(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.UpdateMovieIDs{
|
||||||
|
Movies: moviesScenes,
|
||||||
|
Mode: models.RelationshipUpdateModeSet,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t changesetTranslator) updateMovieIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateMovieIDs, error) {
|
func (t changesetTranslator) updateMovieIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateMovieIDs, error) {
|
||||||
if !t.hasField(field) || value == nil {
|
if !t.hasField(field) || value == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@@ -72,9 +72,14 @@ func (r *Resolver) SceneMarker() SceneMarkerResolver {
|
|||||||
func (r *Resolver) Studio() StudioResolver {
|
func (r *Resolver) Studio() StudioResolver {
|
||||||
return &studioResolver{r}
|
return &studioResolver{r}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) Group() GroupResolver {
|
||||||
|
return &groupResolver{&movieResolver{r}}
|
||||||
|
}
|
||||||
func (r *Resolver) Movie() MovieResolver {
|
func (r *Resolver) Movie() MovieResolver {
|
||||||
return &movieResolver{r}
|
return &movieResolver{r}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) Subscription() SubscriptionResolver {
|
func (r *Resolver) Subscription() SubscriptionResolver {
|
||||||
return &subscriptionResolver{r}
|
return &subscriptionResolver{r}
|
||||||
}
|
}
|
||||||
@@ -111,7 +116,11 @@ type sceneResolver struct{ *Resolver }
|
|||||||
type sceneMarkerResolver struct{ *Resolver }
|
type sceneMarkerResolver struct{ *Resolver }
|
||||||
type imageResolver struct{ *Resolver }
|
type imageResolver struct{ *Resolver }
|
||||||
type studioResolver struct{ *Resolver }
|
type studioResolver struct{ *Resolver }
|
||||||
|
|
||||||
|
// group is movie under the hood
|
||||||
type movieResolver struct{ *Resolver }
|
type movieResolver struct{ *Resolver }
|
||||||
|
type groupResolver struct{ *movieResolver }
|
||||||
|
|
||||||
type tagResolver struct{ *Resolver }
|
type tagResolver struct{ *Resolver }
|
||||||
type galleryFileResolver struct{ *Resolver }
|
type galleryFileResolver struct{ *Resolver }
|
||||||
type videoFileResolver struct{ *Resolver }
|
type videoFileResolver struct{ *Resolver }
|
||||||
@@ -218,7 +227,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
moviesCount, err := movieQB.Count(ctx)
|
groupsCount, err := movieQB.Count(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -262,7 +271,8 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
|
|||||||
GalleryCount: galleryCount,
|
GalleryCount: galleryCount,
|
||||||
PerformerCount: performersCount,
|
PerformerCount: performersCount,
|
||||||
StudioCount: studiosCount,
|
StudioCount: studiosCount,
|
||||||
MovieCount: moviesCount,
|
GroupCount: groupsCount,
|
||||||
|
MovieCount: groupsCount,
|
||||||
TagCount: tagsCount,
|
TagCount: tagsCount,
|
||||||
TotalOCount: totalOCount,
|
TotalOCount: totalOCount,
|
||||||
TotalPlayDuration: totalPlayDuration,
|
TotalPlayDuration: totalPlayDuration,
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
func (r *performerResolver) GroupCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
|
ret, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
|
||||||
return err
|
return err
|
||||||
@@ -190,6 +190,11 @@ func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performe
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||||
|
return r.GroupCount(ctx, obj)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *performerResolver) PerformerCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
func (r *performerResolver) PerformerCount(ctx context.Context, obj *models.Performer) (ret int, err error) {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = performer.CountByAppearsWith(ctx, r.repository.Performer, obj.ID)
|
ret, err = performer.CountByAppearsWith(ctx, r.repository.Performer, obj.ID)
|
||||||
@@ -252,7 +257,7 @@ func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
|
func (r *performerResolver) Groups(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
|
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
|
||||||
return err
|
return err
|
||||||
@@ -262,3 +267,8 @@ func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (
|
|||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Movie, err error) {
|
||||||
|
return r.Groups(ctx, obj)
|
||||||
|
}
|
||||||
|
|||||||
@@ -214,6 +214,37 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *sceneResolver) Groups(ctx context.Context, obj *models.Scene) (ret []*SceneGroup, err error) {
|
||||||
|
if !obj.Movies.Loaded() {
|
||||||
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
qb := r.repository.Scene
|
||||||
|
|
||||||
|
return obj.LoadMovies(ctx, qb)
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loader := loaders.From(ctx).MovieByID
|
||||||
|
|
||||||
|
for _, sm := range obj.Movies.List() {
|
||||||
|
movie, err := loader.Load(sm.MovieID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneIdx := sm.SceneIndex
|
||||||
|
sceneGroup := &SceneGroup{
|
||||||
|
Group: movie,
|
||||||
|
SceneIndex: sceneIdx,
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, sceneGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
|
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
|
||||||
if !obj.TagIDs.Loaded() {
|
if !obj.TagIDs.Loaded() {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func (r *studioResolver) PerformerCount(ctx context.Context, obj *models.Studio,
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
func (r *studioResolver) GroupCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = movie.CountByStudioID(ctx, r.repository.Movie, obj.ID, depth)
|
ret, err = movie.CountByStudioID(ctx, r.repository.Movie, obj.ID, depth)
|
||||||
return err
|
return err
|
||||||
@@ -109,6 +109,11 @@ func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, dep
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, depth *int) (ret int, err error) {
|
||||||
|
return r.GroupCount(ctx, obj, depth)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
|
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
|
||||||
if obj.ParentID == nil {
|
if obj.ParentID == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -144,7 +149,7 @@ func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*in
|
|||||||
return obj.Rating, nil
|
return obj.Rating, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
|
func (r *studioResolver) Groups(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
|
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
|
||||||
return err
|
return err
|
||||||
@@ -154,3 +159,8 @@ func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []
|
|||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []*models.Movie, err error) {
|
||||||
|
return r.Groups(ctx, obj)
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func (r *tagResolver) StudioCount(ctx context.Context, obj *models.Tag, depth *i
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *tagResolver) MovieCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
func (r *tagResolver) GroupCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = movie.CountByTagID(ctx, r.repository.Movie, obj.ID, depth)
|
ret, err = movie.CountByTagID(ctx, r.repository.Movie, obj.ID, depth)
|
||||||
return err
|
return err
|
||||||
@@ -131,6 +131,10 @@ func (r *tagResolver) MovieCount(ctx context.Context, obj *models.Tag, depth *in
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *tagResolver) MovieCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
|
||||||
|
return r.GroupCount(ctx, obj, depth)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
|
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
|
||||||
var hasImage bool
|
var hasImage bool
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
|||||||
335
internal/api/resolver_mutation_group.go
Normal file
335
internal/api/resolver_mutation_group.go
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/internal/static"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func movieFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*models.Movie, error) {
|
||||||
|
translator := changesetTranslator{
|
||||||
|
inputMap: getUpdateInputMap(ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate a new movie from the input
|
||||||
|
newMovie := models.NewMovie()
|
||||||
|
|
||||||
|
newMovie.Name = input.Name
|
||||||
|
newMovie.Aliases = translator.string(input.Aliases)
|
||||||
|
newMovie.Duration = input.Duration
|
||||||
|
newMovie.Rating = input.Rating100
|
||||||
|
newMovie.Director = translator.string(input.Director)
|
||||||
|
newMovie.Synopsis = translator.string(input.Synopsis)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
newMovie.Date, err = translator.datePtr(input.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting date: %w", err)
|
||||||
|
}
|
||||||
|
newMovie.StudioID, err = translator.intPtrFromString(input.StudioID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newMovie.TagIDs, err = translator.relatedIds(input.TagIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Urls != nil {
|
||||||
|
newMovie.URLs = models.NewRelatedStrings(input.Urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &newMovie, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInput) (*models.Movie, error) {
|
||||||
|
newMovie, err := movieFromGroupCreateInput(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the base 64 encoded image string
|
||||||
|
var frontimageData []byte
|
||||||
|
if input.FrontImage != nil {
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("processing back image: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.DefaultMovieImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the transaction and save the movie
|
||||||
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
|
qb := r.repository.Movie
|
||||||
|
|
||||||
|
err = qb.Create(ctx, newMovie)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update image table
|
||||||
|
if len(frontimageData) > 0 {
|
||||||
|
if err := qb.UpdateFrontImage(ctx, newMovie.ID, frontimageData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(backimageData) > 0 {
|
||||||
|
if err := qb.UpdateBackImage(ctx, newMovie.ID, backimageData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.GroupCreatePost, input, nil)
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.MovieCreatePost, input, nil)
|
||||||
|
return r.getMovie(ctx, newMovie.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func moviePartialFromGroupUpdateInput(translator changesetTranslator, input GroupUpdateInput) (ret models.MoviePartial, err error) {
|
||||||
|
// Populate movie from the input
|
||||||
|
updatedMovie := models.NewMoviePartial()
|
||||||
|
|
||||||
|
updatedMovie.Name = translator.optionalString(input.Name, "name")
|
||||||
|
updatedMovie.Aliases = translator.optionalString(input.Aliases, "aliases")
|
||||||
|
updatedMovie.Duration = translator.optionalInt(input.Duration, "duration")
|
||||||
|
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||||
|
updatedMovie.Director = translator.optionalString(input.Director, "director")
|
||||||
|
updatedMovie.Synopsis = translator.optionalString(input.Synopsis, "synopsis")
|
||||||
|
|
||||||
|
updatedMovie.Date, err = translator.optionalDate(input.Date, "date")
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("converting date: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("converting studio id: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMovie.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("converting tag ids: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMovie.URLs = translator.updateStrings(input.Urls, "urls")
|
||||||
|
|
||||||
|
return updatedMovie, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) GroupUpdate(ctx context.Context, input GroupUpdateInput) (*models.Movie, error) {
|
||||||
|
movieID, err := strconv.Atoi(input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
translator := changesetTranslator{
|
||||||
|
inputMap: getUpdateInputMap(ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMovie, err := moviePartialFromGroupUpdateInput(translator, input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var frontimageData []byte
|
||||||
|
frontImageIncluded := translator.hasField("front_image")
|
||||||
|
if input.FrontImage != nil {
|
||||||
|
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("processing front image: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var backimageData []byte
|
||||||
|
backImageIncluded := translator.hasField("back_image")
|
||||||
|
if input.BackImage != nil {
|
||||||
|
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("processing back image: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the transaction and save the movie
|
||||||
|
var movie *models.Movie
|
||||||
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
|
qb := r.repository.Movie
|
||||||
|
movie, err = qb.UpdatePartial(ctx, movieID, updatedMovie)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update image table
|
||||||
|
if frontImageIncluded {
|
||||||
|
if err := qb.UpdateFrontImage(ctx, movie.ID, frontimageData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if backImageIncluded {
|
||||||
|
if err := qb.UpdateBackImage(ctx, movie.ID, backimageData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.GroupUpdatePost, input, translator.getFields())
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||||
|
return r.getMovie(ctx, movie.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func moviePartialFromBulkGroupUpdateInput(translator changesetTranslator, input BulkGroupUpdateInput) (ret models.MoviePartial, err error) {
|
||||||
|
updatedMovie := models.NewMoviePartial()
|
||||||
|
|
||||||
|
updatedMovie.Rating = translator.optionalInt(input.Rating100, "rating100")
|
||||||
|
updatedMovie.Director = translator.optionalString(input.Director, "director")
|
||||||
|
|
||||||
|
updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("converting studio id: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMovie.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("converting tag ids: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedMovie.URLs = translator.optionalURLsBulk(input.Urls, nil)
|
||||||
|
|
||||||
|
return updatedMovie, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) BulkGroupUpdate(ctx context.Context, input BulkGroupUpdateInput) ([]*models.Movie, error) {
|
||||||
|
movieIDs, err := stringslice.StringSliceToIntSlice(input.Ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting ids: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
translator := changesetTranslator{
|
||||||
|
inputMap: getUpdateInputMap(ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate movie from the input
|
||||||
|
updatedMovie, err := moviePartialFromBulkGroupUpdateInput(translator, input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := []*models.Movie{}
|
||||||
|
|
||||||
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
|
qb := r.repository.Movie
|
||||||
|
|
||||||
|
for _, movieID := range movieIDs {
|
||||||
|
movie, err := qb.UpdatePartial(ctx, movieID, updatedMovie)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, movie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var newRet []*models.Movie
|
||||||
|
for _, movie := range ret {
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.GroupUpdatePost, input, translator.getFields())
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||||
|
|
||||||
|
movie, err = r.getMovie(ctx, movie.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newRet = append(newRet, movie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) GroupDestroy(ctx context.Context, input GroupDestroyInput) (bool, error) {
|
||||||
|
id, err := strconv.Atoi(input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("converting id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
|
return r.repository.Movie.Destroy(ctx, id)
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, input, nil)
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) GroupsDestroy(ctx context.Context, groupIDs []string) (bool, error) {
|
||||||
|
ids, err := stringslice.StringSliceToIntSlice(groupIDs)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("converting ids: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
|
qb := r.repository.Movie
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := qb.Destroy(ctx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, groupIDs, nil)
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, groupIDs, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@@ -112,6 +112,8 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.GroupCreatePost, input, nil)
|
||||||
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.MovieCreatePost, input, nil)
|
r.hookExecutor.ExecutePostHooks(ctx, newMovie.ID, hook.MovieCreatePost, input, nil)
|
||||||
return r.getMovie(ctx, newMovie.ID)
|
return r.getMovie(ctx, newMovie.ID)
|
||||||
}
|
}
|
||||||
@@ -197,6 +199,8 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.GroupUpdatePost, input, translator.getFields())
|
||||||
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||||
return r.getMovie(ctx, movie.ID)
|
return r.getMovie(ctx, movie.ID)
|
||||||
}
|
}
|
||||||
@@ -250,6 +254,8 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
|
|||||||
|
|
||||||
var newRet []*models.Movie
|
var newRet []*models.Movie
|
||||||
for _, movie := range ret {
|
for _, movie := range ret {
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.GroupUpdatePost, input, translator.getFields())
|
||||||
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
r.hookExecutor.ExecutePostHooks(ctx, movie.ID, hook.MovieUpdatePost, input, translator.getFields())
|
||||||
|
|
||||||
movie, err = r.getMovie(ctx, movie.ID)
|
movie, err = r.getMovie(ctx, movie.ID)
|
||||||
@@ -275,6 +281,8 @@ func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyI
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, input, nil)
|
||||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
|
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, input, nil)
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -300,6 +308,8 @@ func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
|
// for backwards compatibility - run both movie and group hooks
|
||||||
|
r.hookExecutor.ExecutePostHooks(ctx, id, hook.GroupDestroyPost, movieIDs, nil)
|
||||||
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, movieIDs, nil)
|
r.hookExecutor.ExecutePostHooks(ctx, id, hook.MovieDestroyPost, movieIDs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,17 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
|
|||||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newScene.Movies, err = translator.relatedMovies(input.Movies)
|
// prefer groups over movies
|
||||||
if err != nil {
|
if len(input.Groups) > 0 {
|
||||||
return nil, fmt.Errorf("converting movies: %w", err)
|
newScene.Movies, err = translator.relatedMoviesFromGroups(input.Groups)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting groups: %w", err)
|
||||||
|
}
|
||||||
|
} else if len(input.Movies) > 0 {
|
||||||
|
newScene.Movies, err = translator.relatedMovies(input.Movies)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting movies: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var coverImageData []byte
|
var coverImageData []byte
|
||||||
@@ -216,9 +224,16 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
|
|||||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedScene.MovieIDs, err = translator.updateMovieIDs(input.Movies, "movies")
|
if translator.hasField("groups") {
|
||||||
if err != nil {
|
updatedScene.MovieIDs, err = translator.updateMovieIDsFromGroups(input.Groups, "groups")
|
||||||
return nil, fmt.Errorf("converting movies: %w", err)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting movies: %w", err)
|
||||||
|
}
|
||||||
|
} else if translator.hasField("movies") {
|
||||||
|
updatedScene.MovieIDs, err = translator.updateMovieIDs(input.Movies, "movies")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting movies: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &updatedScene, nil
|
return &updatedScene, nil
|
||||||
@@ -358,9 +373,16 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
|
|||||||
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
return nil, fmt.Errorf("converting gallery ids: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(input.MovieIds, "movie_ids")
|
if translator.hasField("groups") {
|
||||||
if err != nil {
|
updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(input.GroupIds, "group_ids")
|
||||||
return nil, fmt.Errorf("converting movie ids: %w", err)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting group ids: %w", err)
|
||||||
|
}
|
||||||
|
} else if translator.hasField("movies") {
|
||||||
|
updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(input.MovieIds, "movie_ids")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting movie ids: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := []*models.Scene{}
|
ret := []*models.Scene{}
|
||||||
|
|||||||
59
internal/api/resolver_query_find_group.go
Normal file
59
internal/api/resolver_query_find_group.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *queryResolver) FindGroup(ctx context.Context, id string) (ret *models.Movie, err error) {
|
||||||
|
idInt, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
ret, err = r.repository.Movie.Find(ctx, idInt)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) FindGroups(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType, ids []string) (ret *FindGroupsResultType, err error) {
|
||||||
|
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
var movies []*models.Movie
|
||||||
|
var err error
|
||||||
|
var total int
|
||||||
|
|
||||||
|
if len(idInts) > 0 {
|
||||||
|
movies, err = r.repository.Movie.FindMany(ctx, idInts)
|
||||||
|
total = len(movies)
|
||||||
|
} else {
|
||||||
|
movies, total, err = r.repository.Movie.Query(ctx, movieFilter, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = &FindGroupsResultType{
|
||||||
|
Count: total,
|
||||||
|
Groups: movies,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
@@ -213,6 +213,39 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) ScrapeGroupURL(ctx context.Context, url string) (*models.ScrapedGroup, error) {
|
||||||
|
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeMovie)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := marshalScrapedMovie(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filterMovieTags([]*models.ScrapedMovie{ret})
|
||||||
|
|
||||||
|
// convert to scraped group
|
||||||
|
group := &models.ScrapedGroup{
|
||||||
|
StoredID: ret.StoredID,
|
||||||
|
Name: ret.Name,
|
||||||
|
Aliases: ret.Aliases,
|
||||||
|
Duration: ret.Duration,
|
||||||
|
Date: ret.Date,
|
||||||
|
Rating: ret.Rating,
|
||||||
|
Director: ret.Director,
|
||||||
|
URLs: ret.URLs,
|
||||||
|
Synopsis: ret.Synopsis,
|
||||||
|
Studio: ret.Studio,
|
||||||
|
Tags: ret.Tags,
|
||||||
|
FrontImage: ret.FrontImage,
|
||||||
|
BackImage: ret.BackImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*scraper.ScrapedScene, error) {
|
func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*scraper.ScrapedScene, error) {
|
||||||
var ret []*scraper.ScrapedScene
|
var ret []*scraper.ScrapedScene
|
||||||
|
|
||||||
@@ -461,3 +494,7 @@ func (r *queryResolver) ScrapeSingleGallery(ctx context.Context, source scraper.
|
|||||||
func (r *queryResolver) ScrapeSingleMovie(ctx context.Context, source scraper.Source, input ScrapeSingleMovieInput) ([]*models.ScrapedMovie, error) {
|
func (r *queryResolver) ScrapeSingleMovie(ctx context.Context, source scraper.Source, input ScrapeSingleMovieInput) ([]*models.ScrapedMovie, error) {
|
||||||
return nil, ErrNotSupported
|
return nil, ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) ScrapeSingleGroup(ctx context.Context, source scraper.Source, input ScrapeSingleGroupInput) ([]*models.ScrapedGroup, error) {
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ type ExportTask struct {
|
|||||||
scenes *exportSpec
|
scenes *exportSpec
|
||||||
images *exportSpec
|
images *exportSpec
|
||||||
performers *exportSpec
|
performers *exportSpec
|
||||||
movies *exportSpec
|
groups *exportSpec
|
||||||
tags *exportSpec
|
tags *exportSpec
|
||||||
studios *exportSpec
|
studios *exportSpec
|
||||||
galleries *exportSpec
|
galleries *exportSpec
|
||||||
@@ -63,7 +63,8 @@ type ExportObjectsInput struct {
|
|||||||
Studios *ExportObjectTypeInput `json:"studios"`
|
Studios *ExportObjectTypeInput `json:"studios"`
|
||||||
Performers *ExportObjectTypeInput `json:"performers"`
|
Performers *ExportObjectTypeInput `json:"performers"`
|
||||||
Tags *ExportObjectTypeInput `json:"tags"`
|
Tags *ExportObjectTypeInput `json:"tags"`
|
||||||
Movies *ExportObjectTypeInput `json:"movies"`
|
Groups *ExportObjectTypeInput `json:"groups"`
|
||||||
|
Movies *ExportObjectTypeInput `json:"movies"` // deprecated
|
||||||
Galleries *ExportObjectTypeInput `json:"galleries"`
|
Galleries *ExportObjectTypeInput `json:"galleries"`
|
||||||
IncludeDependencies *bool `json:"includeDependencies"`
|
IncludeDependencies *bool `json:"includeDependencies"`
|
||||||
}
|
}
|
||||||
@@ -97,13 +98,19 @@ func CreateExportTask(a models.HashAlgorithm, input ExportObjectsInput) *ExportT
|
|||||||
includeDeps = *input.IncludeDependencies
|
includeDeps = *input.IncludeDependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle deprecated Movies field
|
||||||
|
groupSpec := input.Groups
|
||||||
|
if groupSpec == nil && input.Movies != nil {
|
||||||
|
groupSpec = input.Movies
|
||||||
|
}
|
||||||
|
|
||||||
return &ExportTask{
|
return &ExportTask{
|
||||||
repository: GetInstance().Repository,
|
repository: GetInstance().Repository,
|
||||||
fileNamingAlgorithm: a,
|
fileNamingAlgorithm: a,
|
||||||
scenes: newExportSpec(input.Scenes),
|
scenes: newExportSpec(input.Scenes),
|
||||||
images: newExportSpec(input.Images),
|
images: newExportSpec(input.Images),
|
||||||
performers: newExportSpec(input.Performers),
|
performers: newExportSpec(input.Performers),
|
||||||
movies: newExportSpec(input.Movies),
|
groups: newExportSpec(groupSpec),
|
||||||
tags: newExportSpec(input.Tags),
|
tags: newExportSpec(input.Tags),
|
||||||
studios: newExportSpec(input.Studios),
|
studios: newExportSpec(input.Studios),
|
||||||
galleries: newExportSpec(input.Galleries),
|
galleries: newExportSpec(input.Galleries),
|
||||||
@@ -282,11 +289,11 @@ func (t *ExportTask) populateMovieScenes(ctx context.Context) {
|
|||||||
|
|
||||||
var movies []*models.Movie
|
var movies []*models.Movie
|
||||||
var err error
|
var err error
|
||||||
all := t.full || (t.movies != nil && t.movies.all)
|
all := t.full || (t.groups != nil && t.groups.all)
|
||||||
if all {
|
if all {
|
||||||
movies, err = reader.All(ctx)
|
movies, err = reader.All(ctx)
|
||||||
} else if t.movies != nil && len(t.movies.IDs) > 0 {
|
} else if t.groups != nil && len(t.groups.IDs) > 0 {
|
||||||
movies, err = reader.FindMany(ctx, t.movies.IDs)
|
movies, err = reader.FindMany(ctx, t.groups.IDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -574,7 +581,7 @@ func (t *ExportTask) exportScene(ctx context.Context, wg *sync.WaitGroup, jobCha
|
|||||||
logger.Errorf("[scenes] <%s> error getting scene movies: %v", sceneHash, err)
|
logger.Errorf("[scenes] <%s> error getting scene movies: %v", sceneHash, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
t.movies.IDs = sliceutil.AppendUniques(t.movies.IDs, movieIDs)
|
t.groups.IDs = sliceutil.AppendUniques(t.groups.IDs, movieIDs)
|
||||||
|
|
||||||
t.performers.IDs = sliceutil.AppendUniques(t.performers.IDs, performer.GetIDs(performers))
|
t.performers.IDs = sliceutil.AppendUniques(t.performers.IDs, performer.GetIDs(performers))
|
||||||
}
|
}
|
||||||
@@ -1080,11 +1087,11 @@ func (t *ExportTask) ExportMovies(ctx context.Context, workers int) {
|
|||||||
reader := t.repository.Movie
|
reader := t.repository.Movie
|
||||||
var movies []*models.Movie
|
var movies []*models.Movie
|
||||||
var err error
|
var err error
|
||||||
all := t.full || (t.movies != nil && t.movies.all)
|
all := t.full || (t.groups != nil && t.groups.all)
|
||||||
if all {
|
if all {
|
||||||
movies, err = reader.All(ctx)
|
movies, err = reader.All(ctx)
|
||||||
} else if t.movies != nil && len(t.movies.IDs) > 0 {
|
} else if t.groups != nil && len(t.groups.IDs) > 0 {
|
||||||
movies, err = reader.FindMany(ctx, t.movies.IDs)
|
movies, err = reader.FindMany(ctx, t.groups.IDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const (
|
|||||||
FilterModeGalleries FilterMode = "GALLERIES"
|
FilterModeGalleries FilterMode = "GALLERIES"
|
||||||
FilterModeSceneMarkers FilterMode = "SCENE_MARKERS"
|
FilterModeSceneMarkers FilterMode = "SCENE_MARKERS"
|
||||||
FilterModeMovies FilterMode = "MOVIES"
|
FilterModeMovies FilterMode = "MOVIES"
|
||||||
|
FilterModeGroups FilterMode = "GROUPS"
|
||||||
FilterModeTags FilterMode = "TAGS"
|
FilterModeTags FilterMode = "TAGS"
|
||||||
FilterModeImages FilterMode = "IMAGES"
|
FilterModeImages FilterMode = "IMAGES"
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,7 @@ var AllFilterMode = []FilterMode{
|
|||||||
FilterModeStudios,
|
FilterModeStudios,
|
||||||
FilterModeGalleries,
|
FilterModeGalleries,
|
||||||
FilterModeSceneMarkers,
|
FilterModeSceneMarkers,
|
||||||
|
FilterModeGroups,
|
||||||
FilterModeMovies,
|
FilterModeMovies,
|
||||||
FilterModeTags,
|
FilterModeTags,
|
||||||
FilterModeImages,
|
FilterModeImages,
|
||||||
@@ -32,7 +34,7 @@ var AllFilterMode = []FilterMode{
|
|||||||
|
|
||||||
func (e FilterMode) IsValid() bool {
|
func (e FilterMode) IsValid() bool {
|
||||||
switch e {
|
switch e {
|
||||||
case FilterModeScenes, FilterModePerformers, FilterModeStudios, FilterModeGalleries, FilterModeSceneMarkers, FilterModeMovies, FilterModeTags, FilterModeImages:
|
case FilterModeScenes, FilterModePerformers, FilterModeStudios, FilterModeGalleries, FilterModeSceneMarkers, FilterModeMovies, FilterModeGroups, FilterModeTags, FilterModeImages:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -414,3 +414,24 @@ type ScrapedMovie struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ScrapedMovie) IsScrapedContent() {}
|
func (ScrapedMovie) IsScrapedContent() {}
|
||||||
|
|
||||||
|
// ScrapedGroup is a group from a scraping operation
|
||||||
|
type ScrapedGroup struct {
|
||||||
|
StoredID *string `json:"stored_id"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Aliases *string `json:"aliases"`
|
||||||
|
Duration *string `json:"duration"`
|
||||||
|
Date *string `json:"date"`
|
||||||
|
Rating *string `json:"rating"`
|
||||||
|
Director *string `json:"director"`
|
||||||
|
URLs []string `json:"urls"`
|
||||||
|
Synopsis *string `json:"synopsis"`
|
||||||
|
Studio *ScrapedStudio `json:"studio"`
|
||||||
|
Tags []*ScrapedTag `json:"tags"`
|
||||||
|
// This should be a base64 encoded data URL
|
||||||
|
FrontImage *string `json:"front_image"`
|
||||||
|
// This should be a base64 encoded data URL
|
||||||
|
BackImage *string `json:"back_image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ScrapedGroup) IsScrapedContent() {}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ type SceneFilterType struct {
|
|||||||
IsMissing *string `json:"is_missing"`
|
IsMissing *string `json:"is_missing"`
|
||||||
// Filter to only include scenes with this studio
|
// Filter to only include scenes with this studio
|
||||||
Studios *HierarchicalMultiCriterionInput `json:"studios"`
|
Studios *HierarchicalMultiCriterionInput `json:"studios"`
|
||||||
|
// Filter to only include scenes with this group
|
||||||
|
Groups *MultiCriterionInput `json:"groups"`
|
||||||
// Filter to only include scenes with this movie
|
// Filter to only include scenes with this movie
|
||||||
Movies *MultiCriterionInput `json:"movies"`
|
Movies *MultiCriterionInput `json:"movies"`
|
||||||
// Filter to only include scenes with this gallery
|
// Filter to only include scenes with this gallery
|
||||||
@@ -103,6 +105,8 @@ type SceneFilterType struct {
|
|||||||
StudiosFilter *StudioFilterType `json:"studios_filter"`
|
StudiosFilter *StudioFilterType `json:"studios_filter"`
|
||||||
// Filter by related tags that meet this criteria
|
// Filter by related tags that meet this criteria
|
||||||
TagsFilter *TagFilterType `json:"tags_filter"`
|
TagsFilter *TagFilterType `json:"tags_filter"`
|
||||||
|
// Filter by related groups that meet this criteria
|
||||||
|
GroupsFilter *MovieFilterType `json:"groups_filter"`
|
||||||
// Filter by related movies that meet this criteria
|
// Filter by related movies that meet this criteria
|
||||||
MoviesFilter *MovieFilterType `json:"movies_filter"`
|
MoviesFilter *MovieFilterType `json:"movies_filter"`
|
||||||
// Filter by related markers that meet this criteria
|
// Filter by related markers that meet this criteria
|
||||||
@@ -131,11 +135,17 @@ type SceneQueryResult struct {
|
|||||||
resolveErr error
|
resolveErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SceneMovieInput is used for groups and movies
|
||||||
type SceneMovieInput struct {
|
type SceneMovieInput struct {
|
||||||
MovieID string `json:"movie_id"`
|
MovieID string `json:"movie_id"`
|
||||||
SceneIndex *int `json:"scene_index"`
|
SceneIndex *int `json:"scene_index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SceneGroupInput struct {
|
||||||
|
GroupID string `json:"group_id"`
|
||||||
|
SceneIndex *int `json:"scene_index"`
|
||||||
|
}
|
||||||
|
|
||||||
type SceneCreateInput struct {
|
type SceneCreateInput struct {
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
Code *string `json:"code"`
|
Code *string `json:"code"`
|
||||||
@@ -150,6 +160,7 @@ type SceneCreateInput struct {
|
|||||||
GalleryIds []string `json:"gallery_ids"`
|
GalleryIds []string `json:"gallery_ids"`
|
||||||
PerformerIds []string `json:"performer_ids"`
|
PerformerIds []string `json:"performer_ids"`
|
||||||
Movies []SceneMovieInput `json:"movies"`
|
Movies []SceneMovieInput `json:"movies"`
|
||||||
|
Groups []SceneGroupInput `json:"groups"`
|
||||||
TagIds []string `json:"tag_ids"`
|
TagIds []string `json:"tag_ids"`
|
||||||
// This should be a URL or a base64 encoded data URL
|
// This should be a URL or a base64 encoded data URL
|
||||||
CoverImage *string `json:"cover_image"`
|
CoverImage *string `json:"cover_image"`
|
||||||
@@ -177,6 +188,7 @@ type SceneUpdateInput struct {
|
|||||||
GalleryIds []string `json:"gallery_ids"`
|
GalleryIds []string `json:"gallery_ids"`
|
||||||
PerformerIds []string `json:"performer_ids"`
|
PerformerIds []string `json:"performer_ids"`
|
||||||
Movies []SceneMovieInput `json:"movies"`
|
Movies []SceneMovieInput `json:"movies"`
|
||||||
|
Groups []SceneGroupInput `json:"groups"`
|
||||||
TagIds []string `json:"tag_ids"`
|
TagIds []string `json:"tag_ids"`
|
||||||
// This should be a URL or a base64 encoded data URL
|
// This should be a URL or a base64 encoded data URL
|
||||||
CoverImage *string `json:"cover_image"`
|
CoverImage *string `json:"cover_image"`
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ type TagFilterType struct {
|
|||||||
PerformerCount *IntCriterionInput `json:"performer_count"`
|
PerformerCount *IntCriterionInput `json:"performer_count"`
|
||||||
// Filter by number of studios with this tag
|
// Filter by number of studios with this tag
|
||||||
StudioCount *IntCriterionInput `json:"studio_count"`
|
StudioCount *IntCriterionInput `json:"studio_count"`
|
||||||
|
// Filter by number of groups with this tag
|
||||||
|
GroupCount *IntCriterionInput `json:"group_count"`
|
||||||
// Filter by number of movies with this tag
|
// Filter by number of movies with this tag
|
||||||
MovieCount *IntCriterionInput `json:"movie_count"`
|
MovieCount *IntCriterionInput `json:"movie_count"`
|
||||||
// Filter by number of markers with this tag
|
// Filter by number of markers with this tag
|
||||||
|
|||||||
@@ -26,10 +26,16 @@ const (
|
|||||||
GalleryChapterUpdatePost TriggerEnum = "GalleryChapter.Update.Post"
|
GalleryChapterUpdatePost TriggerEnum = "GalleryChapter.Update.Post"
|
||||||
GalleryChapterDestroyPost TriggerEnum = "GalleryChapter.Destroy.Post"
|
GalleryChapterDestroyPost TriggerEnum = "GalleryChapter.Destroy.Post"
|
||||||
|
|
||||||
|
// deprecated - use Group hooks instead
|
||||||
|
// for now, both movie and group hooks will be executed
|
||||||
MovieCreatePost TriggerEnum = "Movie.Create.Post"
|
MovieCreatePost TriggerEnum = "Movie.Create.Post"
|
||||||
MovieUpdatePost TriggerEnum = "Movie.Update.Post"
|
MovieUpdatePost TriggerEnum = "Movie.Update.Post"
|
||||||
MovieDestroyPost TriggerEnum = "Movie.Destroy.Post"
|
MovieDestroyPost TriggerEnum = "Movie.Destroy.Post"
|
||||||
|
|
||||||
|
GroupCreatePost TriggerEnum = "Group.Create.Post"
|
||||||
|
GroupUpdatePost TriggerEnum = "Group.Update.Post"
|
||||||
|
GroupDestroyPost TriggerEnum = "Group.Destroy.Post"
|
||||||
|
|
||||||
PerformerCreatePost TriggerEnum = "Performer.Create.Post"
|
PerformerCreatePost TriggerEnum = "Performer.Create.Post"
|
||||||
PerformerUpdatePost TriggerEnum = "Performer.Update.Post"
|
PerformerUpdatePost TriggerEnum = "Performer.Update.Post"
|
||||||
PerformerDestroyPost TriggerEnum = "Performer.Destroy.Post"
|
PerformerDestroyPost TriggerEnum = "Performer.Destroy.Post"
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ func (c config) spec() Scraper {
|
|||||||
|
|
||||||
if len(movie.SupportedScrapes) > 0 {
|
if len(movie.SupportedScrapes) > 0 {
|
||||||
ret.Movie = &movie
|
ret.Movie = &movie
|
||||||
|
ret.Group = &movie
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@@ -312,7 +313,7 @@ func (c config) supports(ty ScrapeContentType) bool {
|
|||||||
return (c.SceneByName != nil && c.SceneByQueryFragment != nil) || c.SceneByFragment != nil || len(c.SceneByURL) > 0
|
return (c.SceneByName != nil && c.SceneByQueryFragment != nil) || c.SceneByFragment != nil || len(c.SceneByURL) > 0
|
||||||
case ScrapeContentTypeGallery:
|
case ScrapeContentTypeGallery:
|
||||||
return c.GalleryByFragment != nil || len(c.GalleryByURL) > 0
|
return c.GalleryByFragment != nil || len(c.GalleryByURL) > 0
|
||||||
case ScrapeContentTypeMovie:
|
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||||
return len(c.MovieByURL) > 0
|
return len(c.MovieByURL) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +340,7 @@ func (c config) matchesURL(url string, ty ScrapeContentType) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ScrapeContentTypeMovie:
|
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||||
for _, scraper := range c.MovieByURL {
|
for _, scraper := range c.MovieByURL {
|
||||||
if scraper.matchesURL(url) {
|
if scraper.matchesURL(url) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func loadUrlCandidates(c config, ty ScrapeContentType) []*scrapeByURLConfig {
|
|||||||
return c.PerformerByURL
|
return c.PerformerByURL
|
||||||
case ScrapeContentTypeScene:
|
case ScrapeContentTypeScene:
|
||||||
return c.SceneByURL
|
return c.SceneByURL
|
||||||
case ScrapeContentTypeMovie:
|
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||||
return c.MovieByURL
|
return c.MovieByURL
|
||||||
case ScrapeContentTypeGallery:
|
case ScrapeContentTypeGallery:
|
||||||
return c.GalleryByURL
|
return c.GalleryByURL
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (s *jsonScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCont
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ret, nil
|
return ret, nil
|
||||||
case ScrapeContentTypeMovie:
|
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||||
ret, err := scraper.scrapeMovie(ctx, q)
|
ret, err := scraper.scrapeMovie(ctx, q)
|
||||||
if err != nil || ret == nil {
|
if err != nil || ret == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type ScrapedScene struct {
|
|||||||
Studio *models.ScrapedStudio `json:"studio"`
|
Studio *models.ScrapedStudio `json:"studio"`
|
||||||
Tags []*models.ScrapedTag `json:"tags"`
|
Tags []*models.ScrapedTag `json:"tags"`
|
||||||
Performers []*models.ScrapedPerformer `json:"performers"`
|
Performers []*models.ScrapedPerformer `json:"performers"`
|
||||||
|
Groups []*models.ScrapedGroup `json:"groups"`
|
||||||
Movies []*models.ScrapedMovie `json:"movies"`
|
Movies []*models.ScrapedMovie `json:"movies"`
|
||||||
RemoteSiteID *string `json:"remote_site_id"`
|
RemoteSiteID *string `json:"remote_site_id"`
|
||||||
Duration *int `json:"duration"`
|
Duration *int `json:"duration"`
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type ScrapeContentType string
|
|||||||
const (
|
const (
|
||||||
ScrapeContentTypeGallery ScrapeContentType = "GALLERY"
|
ScrapeContentTypeGallery ScrapeContentType = "GALLERY"
|
||||||
ScrapeContentTypeMovie ScrapeContentType = "MOVIE"
|
ScrapeContentTypeMovie ScrapeContentType = "MOVIE"
|
||||||
|
ScrapeContentTypeGroup ScrapeContentType = "GROUP"
|
||||||
ScrapeContentTypePerformer ScrapeContentType = "PERFORMER"
|
ScrapeContentTypePerformer ScrapeContentType = "PERFORMER"
|
||||||
ScrapeContentTypeScene ScrapeContentType = "SCENE"
|
ScrapeContentTypeScene ScrapeContentType = "SCENE"
|
||||||
)
|
)
|
||||||
@@ -38,13 +39,14 @@ const (
|
|||||||
var AllScrapeContentType = []ScrapeContentType{
|
var AllScrapeContentType = []ScrapeContentType{
|
||||||
ScrapeContentTypeGallery,
|
ScrapeContentTypeGallery,
|
||||||
ScrapeContentTypeMovie,
|
ScrapeContentTypeMovie,
|
||||||
|
ScrapeContentTypeGroup,
|
||||||
ScrapeContentTypePerformer,
|
ScrapeContentTypePerformer,
|
||||||
ScrapeContentTypeScene,
|
ScrapeContentTypeScene,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ScrapeContentType) IsValid() bool {
|
func (e ScrapeContentType) IsValid() bool {
|
||||||
switch e {
|
switch e {
|
||||||
case ScrapeContentTypeGallery, ScrapeContentTypeMovie, ScrapeContentTypePerformer, ScrapeContentTypeScene:
|
case ScrapeContentTypeGallery, ScrapeContentTypeMovie, ScrapeContentTypeGroup, ScrapeContentTypePerformer, ScrapeContentTypeScene:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -81,6 +83,8 @@ type Scraper struct {
|
|||||||
// Details for gallery scraper
|
// Details for gallery scraper
|
||||||
Gallery *ScraperSpec `json:"gallery"`
|
Gallery *ScraperSpec `json:"gallery"`
|
||||||
// Details for movie scraper
|
// Details for movie scraper
|
||||||
|
Group *ScraperSpec `json:"group"`
|
||||||
|
// Details for movie scraper
|
||||||
Movie *ScraperSpec `json:"movie"`
|
Movie *ScraperSpec `json:"movie"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ func (s *scriptScraper) scrape(ctx context.Context, input string, ty ScrapeConte
|
|||||||
var scene *ScrapedScene
|
var scene *ScrapedScene
|
||||||
err := s.runScraperScript(ctx, input, &scene)
|
err := s.runScraperScript(ctx, input, &scene)
|
||||||
return scene, err
|
return scene, err
|
||||||
case ScrapeContentTypeMovie:
|
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||||
var movie *models.ScrapedMovie
|
var movie *models.ScrapedMovie
|
||||||
err := s.runScraperScript(ctx, input, &movie)
|
err := s.runScraperScript(ctx, input, &movie)
|
||||||
return movie, err
|
return movie, err
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (s *xpathScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCon
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ret, nil
|
return ret, nil
|
||||||
case ScrapeContentTypeMovie:
|
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||||
ret, err := scraper.scrapeMovie(ctx, q)
|
ret, err := scraper.scrapeMovie(ctx, q)
|
||||||
if err != nil || ret == nil {
|
if err != nil || ret == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1881,7 +1881,7 @@ func TestGalleryQueryIsMissingPerformers(t *testing.T) {
|
|||||||
|
|
||||||
assert.True(t, len(galleries) > 0)
|
assert.True(t, len(galleries) > 0)
|
||||||
|
|
||||||
// ensure non of the ids equal the one with movies
|
// ensure non of the ids equal the one with galleries
|
||||||
for _, gallery := range galleries {
|
for _, gallery := range galleries {
|
||||||
assert.NotEqual(t, galleryIDs[galleryIdxWithPerformer], gallery.ID)
|
assert.NotEqual(t, galleryIDs[galleryIdxWithPerformer], gallery.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2053,7 +2053,7 @@ func TestImageQueryIsMissingPerformers(t *testing.T) {
|
|||||||
|
|
||||||
assert.True(t, len(images) > 0)
|
assert.True(t, len(images) > 0)
|
||||||
|
|
||||||
// ensure non of the ids equal the one with movies
|
// ensure non of the ids equal the one with performers
|
||||||
for _, image := range images {
|
for _, image := range images {
|
||||||
assert.NotEqual(t, imageIDs[imageIdxWithPerformer], image.ID)
|
assert.NotEqual(t, imageIDs[imageIdxWithPerformer], image.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1330,7 +1330,7 @@ func verifyPerformerQuery(t *testing.T, filter models.PerformerFilterType, verif
|
|||||||
|
|
||||||
for _, performer := range performers {
|
for _, performer := range performers {
|
||||||
if err := performer.LoadURLs(ctx, db.Performer); err != nil {
|
if err := performer.LoadURLs(ctx, db.Performer); err != nil {
|
||||||
t.Errorf("Error loading movie relationships: %v", err)
|
t.Errorf("Error loading url relationships: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,10 +228,21 @@ func (qb *SavedFilterStore) getMany(ctx context.Context, q *goqu.SelectDataset)
|
|||||||
func (qb *SavedFilterStore) FindByMode(ctx context.Context, mode models.FilterMode) ([]*models.SavedFilter, error) {
|
func (qb *SavedFilterStore) FindByMode(ctx context.Context, mode models.FilterMode) ([]*models.SavedFilter, error) {
|
||||||
// SELECT * FROM %s WHERE mode = ? AND name != ? ORDER BY name ASC
|
// SELECT * FROM %s WHERE mode = ? AND name != ? ORDER BY name ASC
|
||||||
table := qb.table()
|
table := qb.table()
|
||||||
sq := qb.selectDataset().Prepared(true).Where(
|
|
||||||
table.Col("mode").Eq(mode),
|
// TODO - querying on groups needs to include movies
|
||||||
table.Col("name").Neq(savedFilterDefaultName),
|
// remove this when we migrate to remove the movies filter mode in the database
|
||||||
).Order(table.Col("name").Asc())
|
var whereClause exp.Expression
|
||||||
|
|
||||||
|
if mode == models.FilterModeGroups || mode == models.FilterModeMovies {
|
||||||
|
whereClause = goqu.Or(
|
||||||
|
table.Col("mode").Eq(models.FilterModeGroups),
|
||||||
|
table.Col("mode").Eq(models.FilterModeMovies),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
whereClause = table.Col("mode").Eq(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
sq := qb.selectDataset().Prepared(true).Where(whereClause).Order(table.Col("name").Asc())
|
||||||
ret, err := qb.getMany(ctx, sq)
|
ret, err := qb.getMany(ctx, sq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1074,6 +1074,7 @@ var sceneSortOptions = sortOptions{
|
|||||||
"duration",
|
"duration",
|
||||||
"file_mod_time",
|
"file_mod_time",
|
||||||
"framerate",
|
"framerate",
|
||||||
|
"group_scene_number",
|
||||||
"id",
|
"id",
|
||||||
"interactive",
|
"interactive",
|
||||||
"interactive_speed",
|
"interactive_speed",
|
||||||
@@ -1140,7 +1141,7 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
|
|||||||
|
|
||||||
direction := findFilter.GetDirection()
|
direction := findFilter.GetDirection()
|
||||||
switch sort {
|
switch sort {
|
||||||
case "movie_scene_number":
|
case "movie_scene_number", "group_scene_number":
|
||||||
query.join(moviesScenesTable, "", "scenes.id = movies_scenes.scene_id")
|
query.join(moviesScenesTable, "", "scenes.id = movies_scenes.scene_id")
|
||||||
query.sortAndPagination += getSort("scene_index", direction, moviesScenesTable)
|
query.sortAndPagination += getSort("scene_index", direction, moviesScenesTable)
|
||||||
case "tag_count":
|
case "tag_count":
|
||||||
|
|||||||
@@ -147,7 +147,10 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
|||||||
qb.performersCriterionHandler(sceneFilter.Performers),
|
qb.performersCriterionHandler(sceneFilter.Performers),
|
||||||
qb.performerCountCriterionHandler(sceneFilter.PerformerCount),
|
qb.performerCountCriterionHandler(sceneFilter.PerformerCount),
|
||||||
studioCriterionHandler(sceneTable, sceneFilter.Studios),
|
studioCriterionHandler(sceneTable, sceneFilter.Studios),
|
||||||
qb.moviesCriterionHandler(sceneFilter.Movies),
|
|
||||||
|
qb.groupsCriterionHandler(sceneFilter.Groups),
|
||||||
|
qb.groupsCriterionHandler(sceneFilter.Movies),
|
||||||
|
|
||||||
qb.galleriesCriterionHandler(sceneFilter.Galleries),
|
qb.galleriesCriterionHandler(sceneFilter.Galleries),
|
||||||
qb.performerTagsCriterionHandler(sceneFilter.PerformerTags),
|
qb.performerTagsCriterionHandler(sceneFilter.PerformerTags),
|
||||||
qb.performerFavoriteCriterionHandler(sceneFilter.PerformerFavorite),
|
qb.performerFavoriteCriterionHandler(sceneFilter.PerformerFavorite),
|
||||||
@@ -480,7 +483,7 @@ func (qb *sceneFilterHandler) performerAgeCriterionHandler(performerAge *models.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneFilterHandler) moviesCriterionHandler(movies *models.MultiCriterionInput) criterionHandlerFunc {
|
func (qb *sceneFilterHandler) groupsCriterionHandler(movies *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
addJoinsFunc := func(f *filterBuilder) {
|
addJoinsFunc := func(f *filterBuilder) {
|
||||||
sceneRepository.movies.join(f, "", "scenes.id")
|
sceneRepository.movies.join(f, "", "scenes.id")
|
||||||
f.addLeftJoin("movies", "", "movies_scenes.movie_id = movies.id")
|
f.addLeftJoin("movies", "", "movies_scenes.movie_id = movies.id")
|
||||||
|
|||||||
@@ -278,9 +278,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
savedFilterIdxDefaultScene = iota
|
savedFilterIdxScene = iota
|
||||||
savedFilterIdxDefaultImage
|
|
||||||
savedFilterIdxScene
|
|
||||||
savedFilterIdxImage
|
savedFilterIdxImage
|
||||||
|
|
||||||
// new indexes above
|
// new indexes above
|
||||||
@@ -1777,9 +1775,9 @@ func createChapter(ctx context.Context, mqb models.GalleryChapterReaderWriter, c
|
|||||||
|
|
||||||
func getSavedFilterMode(index int) models.FilterMode {
|
func getSavedFilterMode(index int) models.FilterMode {
|
||||||
switch index {
|
switch index {
|
||||||
case savedFilterIdxScene, savedFilterIdxDefaultScene:
|
case savedFilterIdxScene:
|
||||||
return models.FilterModeScenes
|
return models.FilterModeScenes
|
||||||
case savedFilterIdxImage, savedFilterIdxDefaultImage:
|
case savedFilterIdxImage:
|
||||||
return models.FilterModeImages
|
return models.FilterModeImages
|
||||||
default:
|
default:
|
||||||
return models.FilterModeScenes
|
return models.FilterModeScenes
|
||||||
@@ -1787,11 +1785,6 @@ func getSavedFilterMode(index int) models.FilterMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getSavedFilterName(index int) string {
|
func getSavedFilterName(index int) string {
|
||||||
if index <= savedFilterIdxDefaultImage {
|
|
||||||
// empty string for default filters
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if index <= savedFilterIdxImage {
|
if index <= savedFilterIdxImage {
|
||||||
// use the same name for the first two - should be possible
|
// use the same name for the first two - should be possible
|
||||||
return firstSavedFilterName
|
return firstSavedFilterName
|
||||||
|
|||||||
@@ -683,7 +683,7 @@ func (qb *TagStore) getTagSort(query *queryBuilder, findFilter *models.FindFilte
|
|||||||
sortQuery += getCountSort(tagTable, performersTagsTable, tagIDColumn, direction)
|
sortQuery += getCountSort(tagTable, performersTagsTable, tagIDColumn, direction)
|
||||||
case "studios_count":
|
case "studios_count":
|
||||||
sortQuery += getCountSort(tagTable, studiosTagsTable, tagIDColumn, direction)
|
sortQuery += getCountSort(tagTable, studiosTagsTable, tagIDColumn, direction)
|
||||||
case "movies_count":
|
case "movies_count", "groups_count":
|
||||||
sortQuery += getCountSort(tagTable, moviesTagsTable, tagIDColumn, direction)
|
sortQuery += getCountSort(tagTable, moviesTagsTable, tagIDColumn, direction)
|
||||||
default:
|
default:
|
||||||
sortQuery += getSort(sort, direction, "tags")
|
sortQuery += getSort(sort, direction, "tags")
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ func (qb *tagFilterHandler) criterionHandler() criterionHandler {
|
|||||||
qb.galleryCountCriterionHandler(tagFilter.GalleryCount),
|
qb.galleryCountCriterionHandler(tagFilter.GalleryCount),
|
||||||
qb.performerCountCriterionHandler(tagFilter.PerformerCount),
|
qb.performerCountCriterionHandler(tagFilter.PerformerCount),
|
||||||
qb.studioCountCriterionHandler(tagFilter.StudioCount),
|
qb.studioCountCriterionHandler(tagFilter.StudioCount),
|
||||||
qb.movieCountCriterionHandler(tagFilter.MovieCount),
|
|
||||||
|
qb.groupCountCriterionHandler(tagFilter.GroupCount),
|
||||||
|
qb.groupCountCriterionHandler(tagFilter.MovieCount),
|
||||||
|
|
||||||
qb.markerCountCriterionHandler(tagFilter.MarkerCount),
|
qb.markerCountCriterionHandler(tagFilter.MarkerCount),
|
||||||
qb.parentsCriterionHandler(tagFilter.Parents),
|
qb.parentsCriterionHandler(tagFilter.Parents),
|
||||||
qb.childrenCriterionHandler(tagFilter.Children),
|
qb.childrenCriterionHandler(tagFilter.Children),
|
||||||
@@ -187,7 +190,7 @@ func (qb *tagFilterHandler) studioCountCriterionHandler(studioCount *models.IntC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *tagFilterHandler) movieCountCriterionHandler(movieCount *models.IntCriterionInput) criterionHandlerFunc {
|
func (qb *tagFilterHandler) groupCountCriterionHandler(movieCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if movieCount != nil {
|
if movieCount != nil {
|
||||||
f.addLeftJoin("movies_tags", "", "movies_tags.tag_id = tags.id")
|
f.addLeftJoin("movies_tags", "", "movies_tags.tag_id = tags.id")
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
fragment SlimMovieData on Movie {
|
fragment SlimGroupData on Group {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
front_image_path
|
front_image_path
|
||||||
rating100
|
rating100
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment SelectMovieData on Movie {
|
fragment SelectGroupData on Group {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
aliases
|
aliases
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
fragment MovieData on Movie {
|
fragment GroupData on Group {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
aliases
|
aliases
|
||||||
@@ -23,7 +23,7 @@ fragment PerformerData on Performer {
|
|||||||
scene_count
|
scene_count
|
||||||
image_count
|
image_count
|
||||||
gallery_count
|
gallery_count
|
||||||
movie_count
|
group_count
|
||||||
performer_count
|
performer_count
|
||||||
o_counter
|
o_counter
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ fragment SlimSceneData on Scene {
|
|||||||
image_path
|
image_path
|
||||||
}
|
}
|
||||||
|
|
||||||
movies {
|
groups {
|
||||||
movie {
|
group {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
front_image_path
|
front_image_path
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ fragment SceneData on Scene {
|
|||||||
...SlimStudioData
|
...SlimStudioData
|
||||||
}
|
}
|
||||||
|
|
||||||
movies {
|
groups {
|
||||||
movie {
|
group {
|
||||||
...MovieData
|
...GroupData
|
||||||
}
|
}
|
||||||
scene_index
|
scene_index
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ fragment ScrapedScenePerformerData on ScrapedPerformer {
|
|||||||
weight
|
weight
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment ScrapedMovieStudioData on ScrapedStudio {
|
fragment ScrapedGroupStudioData on ScrapedStudio {
|
||||||
stored_id
|
stored_id
|
||||||
name
|
name
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment ScrapedMovieData on ScrapedMovie {
|
fragment ScrapedGroupData on ScrapedGroup {
|
||||||
name
|
name
|
||||||
aliases
|
aliases
|
||||||
duration
|
duration
|
||||||
@@ -92,14 +92,14 @@ fragment ScrapedMovieData on ScrapedMovie {
|
|||||||
back_image
|
back_image
|
||||||
|
|
||||||
studio {
|
studio {
|
||||||
...ScrapedMovieStudioData
|
...ScrapedGroupStudioData
|
||||||
}
|
}
|
||||||
tags {
|
tags {
|
||||||
...ScrapedSceneTagData
|
...ScrapedSceneTagData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment ScrapedSceneMovieData on ScrapedMovie {
|
fragment ScrapedSceneGroupData on ScrapedGroup {
|
||||||
stored_id
|
stored_id
|
||||||
name
|
name
|
||||||
aliases
|
aliases
|
||||||
@@ -113,7 +113,7 @@ fragment ScrapedSceneMovieData on ScrapedMovie {
|
|||||||
back_image
|
back_image
|
||||||
|
|
||||||
studio {
|
studio {
|
||||||
...ScrapedMovieStudioData
|
...ScrapedGroupStudioData
|
||||||
}
|
}
|
||||||
tags {
|
tags {
|
||||||
...ScrapedSceneTagData
|
...ScrapedSceneTagData
|
||||||
@@ -173,8 +173,8 @@ fragment ScrapedSceneData on ScrapedScene {
|
|||||||
...ScrapedScenePerformerData
|
...ScrapedScenePerformerData
|
||||||
}
|
}
|
||||||
|
|
||||||
movies {
|
groups {
|
||||||
...ScrapedSceneMovieData
|
...ScrapedSceneGroupData
|
||||||
}
|
}
|
||||||
|
|
||||||
fingerprints {
|
fingerprints {
|
||||||
@@ -245,8 +245,8 @@ fragment ScrapedStashBoxSceneData on ScrapedScene {
|
|||||||
...ScrapedScenePerformerData
|
...ScrapedScenePerformerData
|
||||||
}
|
}
|
||||||
|
|
||||||
movies {
|
groups {
|
||||||
...ScrapedSceneMovieData
|
...ScrapedSceneGroupData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ fragment StudioData on Studio {
|
|||||||
gallery_count_all: gallery_count(depth: -1)
|
gallery_count_all: gallery_count(depth: -1)
|
||||||
performer_count
|
performer_count
|
||||||
performer_count_all: performer_count(depth: -1)
|
performer_count_all: performer_count(depth: -1)
|
||||||
movie_count
|
group_count
|
||||||
movie_count_all: movie_count(depth: -1)
|
group_count_all: group_count(depth: -1)
|
||||||
stash_ids {
|
stash_ids {
|
||||||
stash_id
|
stash_id
|
||||||
endpoint
|
endpoint
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ fragment TagData on Tag {
|
|||||||
performer_count_all: performer_count(depth: -1)
|
performer_count_all: performer_count(depth: -1)
|
||||||
studio_count
|
studio_count
|
||||||
studio_count_all: studio_count(depth: -1)
|
studio_count_all: studio_count(depth: -1)
|
||||||
movie_count
|
group_count
|
||||||
movie_count_all: movie_count(depth: -1)
|
group_count_all: group_count(depth: -1)
|
||||||
|
|
||||||
parents {
|
parents {
|
||||||
...SlimTagData
|
...SlimTagData
|
||||||
|
|||||||
25
ui/v2.5/graphql/mutations/group.graphql
Normal file
25
ui/v2.5/graphql/mutations/group.graphql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
mutation GroupCreate($input: GroupCreateInput!) {
|
||||||
|
groupCreate(input: $input) {
|
||||||
|
...GroupData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation GroupUpdate($input: GroupUpdateInput!) {
|
||||||
|
groupUpdate(input: $input) {
|
||||||
|
...GroupData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation BulkGroupUpdate($input: BulkGroupUpdateInput!) {
|
||||||
|
bulkGroupUpdate(input: $input) {
|
||||||
|
...GroupData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation GroupDestroy($id: ID!) {
|
||||||
|
groupDestroy(input: { id: $id })
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation GroupsDestroy($ids: [ID!]!) {
|
||||||
|
groupsDestroy(ids: $ids)
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
mutation MovieCreate($input: MovieCreateInput!) {
|
|
||||||
movieCreate(input: $input) {
|
|
||||||
...MovieData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mutation MovieUpdate($input: MovieUpdateInput!) {
|
|
||||||
movieUpdate(input: $input) {
|
|
||||||
...MovieData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mutation BulkMovieUpdate($input: BulkMovieUpdateInput!) {
|
|
||||||
bulkMovieUpdate(input: $input) {
|
|
||||||
...MovieData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mutation MovieDestroy($id: ID!) {
|
|
||||||
movieDestroy(input: { id: $id })
|
|
||||||
}
|
|
||||||
|
|
||||||
mutation MoviesDestroy($ids: [ID!]!) {
|
|
||||||
moviesDestroy(ids: $ids)
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ query Stats {
|
|||||||
gallery_count
|
gallery_count
|
||||||
performer_count
|
performer_count
|
||||||
studio_count
|
studio_count
|
||||||
movie_count
|
group_count
|
||||||
tag_count
|
tag_count
|
||||||
total_o_count
|
total_o_count
|
||||||
total_play_duration
|
total_play_duration
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
query FindMovies($filter: FindFilterType, $movie_filter: MovieFilterType) {
|
query FindGroups($filter: FindFilterType, $group_filter: GroupFilterType) {
|
||||||
findMovies(filter: $filter, movie_filter: $movie_filter) {
|
findGroups(filter: $filter, group_filter: $group_filter) {
|
||||||
count
|
count
|
||||||
movies {
|
groups {
|
||||||
...MovieData
|
...GroupData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query FindMovie($id: ID!) {
|
query FindGroup($id: ID!) {
|
||||||
findMovie(id: $id) {
|
findGroup(id: $id) {
|
||||||
...MovieData
|
...GroupData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query FindMoviesForSelect(
|
query FindGroupsForSelect(
|
||||||
$filter: FindFilterType
|
$filter: FindFilterType
|
||||||
$movie_filter: MovieFilterType
|
$group_filter: GroupFilterType
|
||||||
$ids: [ID!]
|
$ids: [ID!]
|
||||||
) {
|
) {
|
||||||
findMovies(filter: $filter, movie_filter: $movie_filter, ids: $ids) {
|
findGroups(filter: $filter, group_filter: $group_filter, ids: $ids) {
|
||||||
count
|
count
|
||||||
movies {
|
groups {
|
||||||
...SelectMovieData
|
...SelectGroupData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ query ListGalleryScrapers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query ListMovieScrapers {
|
query ListGroupScrapers {
|
||||||
listScrapers(types: [MOVIE]) {
|
listScrapers(types: [GROUP]) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
movie {
|
group {
|
||||||
urls
|
urls
|
||||||
supported_scrapes
|
supported_scrapes
|
||||||
}
|
}
|
||||||
@@ -114,9 +114,9 @@ query ScrapeGalleryURL($url: String!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query ScrapeMovieURL($url: String!) {
|
query ScrapeGroupURL($url: String!) {
|
||||||
scrapeMovieURL(url: $url) {
|
scrapeGroupURL(url: $url) {
|
||||||
...ScrapedMovieData
|
...ScrapedGroupData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const RecommendationRow: React.FC<IFilter> = ({ mode, filter, header }) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case GQL.FilterMode.Movies:
|
case GQL.FilterMode.Movies:
|
||||||
|
case GQL.FilterMode.Groups:
|
||||||
return (
|
return (
|
||||||
<GroupRecommendationRow
|
<GroupRecommendationRow
|
||||||
isTouch={isTouch}
|
isTouch={isTouch}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const FilterModeToMessageID = {
|
|||||||
[GQL.FilterMode.Galleries]: "galleries",
|
[GQL.FilterMode.Galleries]: "galleries",
|
||||||
[GQL.FilterMode.Images]: "images",
|
[GQL.FilterMode.Images]: "images",
|
||||||
[GQL.FilterMode.Movies]: "groups",
|
[GQL.FilterMode.Movies]: "groups",
|
||||||
|
[GQL.FilterMode.Groups]: "groups",
|
||||||
[GQL.FilterMode.Performers]: "performers",
|
[GQL.FilterMode.Performers]: "performers",
|
||||||
[GQL.FilterMode.SceneMarkers]: "markers",
|
[GQL.FilterMode.SceneMarkers]: "markers",
|
||||||
[GQL.FilterMode.Scenes]: "scenes",
|
[GQL.FilterMode.Scenes]: "scenes",
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ const FilterModeToConfigKey = {
|
|||||||
[FilterMode.Galleries]: "galleries",
|
[FilterMode.Galleries]: "galleries",
|
||||||
[FilterMode.Images]: "images",
|
[FilterMode.Images]: "images",
|
||||||
[FilterMode.Movies]: "groups",
|
[FilterMode.Movies]: "groups",
|
||||||
|
[FilterMode.Groups]: "groups",
|
||||||
[FilterMode.Performers]: "performers",
|
[FilterMode.Performers]: "performers",
|
||||||
[FilterMode.SceneMarkers]: "sceneMarkers",
|
[FilterMode.SceneMarkers]: "sceneMarkers",
|
||||||
[FilterMode.Scenes]: "scenes",
|
[FilterMode.Scenes]: "scenes",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Form, Col, Row } from "react-bootstrap";
|
import { Form, Col, Row } from "react-bootstrap";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useBulkMovieUpdate } from "src/core/StashService";
|
import { useBulkGroupUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { ModalComponent } from "../Shared/Modal";
|
import { ModalComponent } from "../Shared/Modal";
|
||||||
import { StudioSelect } from "../Shared/Select";
|
import { StudioSelect } from "../Shared/Select";
|
||||||
@@ -20,7 +20,7 @@ import { isEqual } from "lodash-es";
|
|||||||
import { MultiSet } from "../Shared/MultiSet";
|
import { MultiSet } from "../Shared/MultiSet";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.MovieDataFragment[];
|
selected: GQL.GroupDataFragment[];
|
||||||
onClose: (applied: boolean) => void;
|
onClose: (applied: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,32 +39,32 @@ export const EditGroupsDialog: React.FC<IListOperationProps> = (
|
|||||||
const [tagIds, setTagIds] = useState<string[]>();
|
const [tagIds, setTagIds] = useState<string[]>();
|
||||||
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
||||||
|
|
||||||
const [updateMovies] = useBulkMovieUpdate(getMovieInput());
|
const [updateGroups] = useBulkGroupUpdate(getGroupInput());
|
||||||
|
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
function getMovieInput(): GQL.BulkMovieUpdateInput {
|
function getGroupInput(): GQL.BulkGroupUpdateInput {
|
||||||
const aggregateRating = getAggregateRating(props.selected);
|
const aggregateRating = getAggregateRating(props.selected);
|
||||||
const aggregateStudioId = getAggregateStudioId(props.selected);
|
const aggregateStudioId = getAggregateStudioId(props.selected);
|
||||||
const aggregateTagIds = getAggregateTagIds(props.selected);
|
const aggregateTagIds = getAggregateTagIds(props.selected);
|
||||||
|
|
||||||
const movieInput: GQL.BulkMovieUpdateInput = {
|
const groupInput: GQL.BulkGroupUpdateInput = {
|
||||||
ids: props.selected.map((movie) => movie.id),
|
ids: props.selected.map((group) => group.id),
|
||||||
director,
|
director,
|
||||||
};
|
};
|
||||||
|
|
||||||
// if rating is undefined
|
// if rating is undefined
|
||||||
movieInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
groupInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
|
||||||
movieInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
groupInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
|
||||||
movieInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
groupInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
||||||
|
|
||||||
return movieInput;
|
return groupInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSave() {
|
async function onSave() {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
try {
|
try {
|
||||||
await updateMovies();
|
await updateGroups();
|
||||||
Toast.success(
|
Toast.success(
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{ id: "toast.updated_entity" },
|
{ id: "toast.updated_entity" },
|
||||||
@@ -88,26 +88,26 @@ export const EditGroupsDialog: React.FC<IListOperationProps> = (
|
|||||||
let updateDirector: string | undefined;
|
let updateDirector: string | undefined;
|
||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
state.forEach((movie: GQL.MovieDataFragment) => {
|
state.forEach((group: GQL.GroupDataFragment) => {
|
||||||
const movieTagIDs = (movie.tags ?? []).map((p) => p.id).sort();
|
const groupTagIDs = (group.tags ?? []).map((p) => p.id).sort();
|
||||||
|
|
||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false;
|
||||||
updateRating = movie.rating100 ?? undefined;
|
updateRating = group.rating100 ?? undefined;
|
||||||
updateStudioId = movie.studio?.id ?? undefined;
|
updateStudioId = group.studio?.id ?? undefined;
|
||||||
updateTagIds = movieTagIDs;
|
updateTagIds = groupTagIDs;
|
||||||
updateDirector = movie.director ?? undefined;
|
updateDirector = group.director ?? undefined;
|
||||||
} else {
|
} else {
|
||||||
if (movie.rating100 !== updateRating) {
|
if (group.rating100 !== updateRating) {
|
||||||
updateRating = undefined;
|
updateRating = undefined;
|
||||||
}
|
}
|
||||||
if (movie.studio?.id !== updateStudioId) {
|
if (group.studio?.id !== updateStudioId) {
|
||||||
updateStudioId = undefined;
|
updateStudioId = undefined;
|
||||||
}
|
}
|
||||||
if (movie.director !== updateDirector) {
|
if (group.director !== updateDirector) {
|
||||||
updateDirector = undefined;
|
updateDirector = undefined;
|
||||||
}
|
}
|
||||||
if (!isEqual(movieTagIDs, updateTagIds)) {
|
if (!isEqual(groupTagIDs, updateTagIds)) {
|
||||||
updateTagIds = [];
|
updateTagIds = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import ScreenUtils from "src/utils/screen";
|
import ScreenUtils from "src/utils/screen";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
group: GQL.MovieDataFragment;
|
group: GQL.GroupDataFragment;
|
||||||
containerWidth?: number;
|
containerWidth?: number;
|
||||||
sceneIndex?: number;
|
sceneIndex?: number;
|
||||||
selecting?: boolean;
|
selecting?: boolean;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { GroupCard } from "./MovieCard";
|
|||||||
import { useContainerDimensions } from "../Shared/GridCard/GridCard";
|
import { useContainerDimensions } from "../Shared/GridCard/GridCard";
|
||||||
|
|
||||||
interface IGroupCardGrid {
|
interface IGroupCardGrid {
|
||||||
groups: GQL.MovieDataFragment[];
|
groups: GQL.GroupDataFragment[];
|
||||||
selectedIds: Set<string>;
|
selectedIds: Set<string>;
|
||||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import cx from "classnames";
|
|||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
useFindMovie,
|
useFindGroup,
|
||||||
useMovieUpdate,
|
useGroupUpdate,
|
||||||
useMovieDestroy,
|
useGroupDestroy,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useHistory, RouteComponentProps } from "react-router-dom";
|
import { useHistory, RouteComponentProps } from "react-router-dom";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
@@ -19,7 +19,7 @@ import { ModalComponent } from "src/components/Shared/Modal";
|
|||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { GroupScenesPanel } from "./MovieScenesPanel";
|
import { GroupScenesPanel } from "./MovieScenesPanel";
|
||||||
import {
|
import {
|
||||||
CompressedMovieDetailsPanel,
|
CompressedGroupDetailsPanel,
|
||||||
GroupDetailsPanel,
|
GroupDetailsPanel,
|
||||||
} from "./MovieDetailsPanel";
|
} from "./MovieDetailsPanel";
|
||||||
import { GroupEditPanel } from "./MovieEditPanel";
|
import { GroupEditPanel } from "./MovieEditPanel";
|
||||||
@@ -38,7 +38,7 @@ import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
|||||||
import { ExternalLinksButton } from "src/components/Shared/ExternalLinksButton";
|
import { ExternalLinksButton } from "src/components/Shared/ExternalLinksButton";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
group: GQL.MovieDataFragment;
|
group: GQL.GroupDataFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGroupParams {
|
interface IGroupParams {
|
||||||
@@ -64,7 +64,7 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
|
|||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing movie state
|
// Editing group state
|
||||||
const [frontImage, setFrontImage] = useState<string | null>();
|
const [frontImage, setFrontImage] = useState<string | null>();
|
||||||
const [backImage, setBackImage] = useState<string | null>();
|
const [backImage, setBackImage] = useState<string | null>();
|
||||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
@@ -106,8 +106,8 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
|
|||||||
images: lightboxImages,
|
images: lightboxImages,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateMovie, { loading: updating }] = useMovieUpdate();
|
const [updateGroup, { loading: updating }] = useGroupUpdate();
|
||||||
const [deleteMovie, { loading: deleting }] = useMovieDestroy({
|
const [deleteGroup, { loading: deleting }] = useGroupDestroy({
|
||||||
id: group.id,
|
id: group.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,8 +131,8 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
|
|||||||
setRating
|
setRating
|
||||||
);
|
);
|
||||||
|
|
||||||
async function onSave(input: GQL.MovieCreateInput) {
|
async function onSave(input: GQL.GroupCreateInput) {
|
||||||
await updateMovie({
|
await updateGroup({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
@@ -151,12 +151,12 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
|
|||||||
|
|
||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
try {
|
try {
|
||||||
await deleteMovie();
|
await deleteGroup();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to movies page
|
// redirect to groups page
|
||||||
history.push(`/groups`);
|
history.push(`/groups`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +287,7 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
|
|||||||
|
|
||||||
function setRating(v: number | null) {
|
function setRating(v: number | null) {
|
||||||
if (group.id) {
|
if (group.id) {
|
||||||
updateMovie({
|
updateGroup({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
@@ -343,7 +343,7 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
|
|||||||
|
|
||||||
function maybeRenderCompressedDetails() {
|
function maybeRenderCompressedDetails() {
|
||||||
if (!isEditing && loadStickyHeader) {
|
if (!isEditing && loadStickyHeader) {
|
||||||
return <CompressedMovieDetailsPanel group={group} />;
|
return <CompressedGroupDetailsPanel group={group} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,16 +441,16 @@ const GroupLoader: React.FC<RouteComponentProps<IGroupParams>> = ({
|
|||||||
match,
|
match,
|
||||||
}) => {
|
}) => {
|
||||||
const { id } = match.params;
|
const { id } = match.params;
|
||||||
const { data, loading, error } = useFindMovie(id);
|
const { data, loading, error } = useFindGroup(id);
|
||||||
|
|
||||||
useScrollToTopOnMount();
|
useScrollToTopOnMount();
|
||||||
|
|
||||||
if (loading) return <LoadingIndicator />;
|
if (loading) return <LoadingIndicator />;
|
||||||
if (error) return <ErrorMessage error={error.message} />;
|
if (error) return <ErrorMessage error={error.message} />;
|
||||||
if (!data?.findMovie)
|
if (!data?.findGroup)
|
||||||
return <ErrorMessage error={`No movie found with id ${id}.`} />;
|
return <ErrorMessage error={`No group found with id ${id}.`} />;
|
||||||
|
|
||||||
return <GroupPage group={data.findMovie} />;
|
return <GroupPage group={data.findGroup} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GroupLoader;
|
export default GroupLoader;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useMovieCreate } from "src/core/StashService";
|
import { useGroupCreate } from "src/core/StashService";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
@@ -18,23 +18,23 @@ const GroupCreate: React.FC = () => {
|
|||||||
name: query.get("q") ?? undefined,
|
name: query.get("q") ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Editing movie state
|
// Editing group state
|
||||||
const [frontImage, setFrontImage] = useState<string | null>();
|
const [frontImage, setFrontImage] = useState<string | null>();
|
||||||
const [backImage, setBackImage] = useState<string | null>();
|
const [backImage, setBackImage] = useState<string | null>();
|
||||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
const [createMovie] = useMovieCreate();
|
const [createGroup] = useGroupCreate();
|
||||||
|
|
||||||
async function onSave(input: GQL.MovieCreateInput) {
|
async function onSave(input: GQL.GroupCreateInput) {
|
||||||
const result = await createMovie({
|
const result = await createGroup({
|
||||||
variables: { input },
|
variables: { input },
|
||||||
});
|
});
|
||||||
if (result.data?.movieCreate?.id) {
|
if (result.data?.groupCreate?.id) {
|
||||||
history.push(`/groups/${result.data.movieCreate.id}`);
|
history.push(`/groups/${result.data.groupCreate.id}`);
|
||||||
Toast.success(
|
Toast.success(
|
||||||
intl.formatMessage(
|
intl.formatMessage(
|
||||||
{ id: "toast.created_entity" },
|
{ id: "toast.created_entity" },
|
||||||
{ entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase() }
|
{ entity: intl.formatMessage({ id: "group" }).toLocaleLowerCase() }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { DirectorLink } from "src/components/Shared/Link";
|
|||||||
import { TagLink } from "src/components/Shared/TagLink";
|
import { TagLink } from "src/components/Shared/TagLink";
|
||||||
|
|
||||||
interface IGroupDetailsPanel {
|
interface IGroupDetailsPanel {
|
||||||
group: GQL.MovieDataFragment;
|
group: GQL.GroupDataFragment;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export const GroupDetailsPanel: React.FC<IGroupDetailsPanel> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CompressedMovieDetailsPanel: React.FC<IGroupDetailsPanel> = ({
|
export const CompressedGroupDetailsPanel: React.FC<IGroupDetailsPanel> = ({
|
||||||
group,
|
group,
|
||||||
}) => {
|
}) => {
|
||||||
function scrollToTop() {
|
function scrollToTop() {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import {
|
import {
|
||||||
queryScrapeMovieURL,
|
queryScrapeGroupURL,
|
||||||
useListMovieScrapers,
|
useListGroupScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
@@ -28,8 +28,8 @@ import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
|||||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||||
|
|
||||||
interface IGroupEditPanel {
|
interface IGroupEditPanel {
|
||||||
group: Partial<GQL.MovieDataFragment>;
|
group: Partial<GQL.GroupDataFragment>;
|
||||||
onSubmit: (movie: GQL.MovieCreateInput) => Promise<void>;
|
onSubmit: (group: GQL.GroupCreateInput) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
setFrontImage: (image?: string | null) => void;
|
setFrontImage: (image?: string | null) => void;
|
||||||
@@ -56,8 +56,8 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||||||
|
|
||||||
const [imageClipboard, setImageClipboard] = useState<string>();
|
const [imageClipboard, setImageClipboard] = useState<string>();
|
||||||
|
|
||||||
const Scrapers = useListMovieScrapers();
|
const Scrapers = useListGroupScrapers();
|
||||||
const [scrapedGroup, setScrapedGroup] = useState<GQL.ScrapedMovie>();
|
const [scrapedGroup, setScrapedGroup] = useState<GQL.ScrapedGroup>();
|
||||||
|
|
||||||
const [studio, setStudio] = useState<Studio | null>(null);
|
const [studio, setStudio] = useState<Studio | null>(null);
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function updateGroupEditStateFromScraper(
|
function updateGroupEditStateFromScraper(
|
||||||
state: Partial<GQL.ScrapedMovieDataFragment>
|
state: Partial<GQL.ScrapedGroupDataFragment>
|
||||||
) {
|
) {
|
||||||
if (state.name) {
|
if (state.name) {
|
||||||
formik.setFieldValue("name", state.name);
|
formik.setFieldValue("name", state.name);
|
||||||
@@ -190,21 +190,21 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScrapeMovieURL(url: string) {
|
async function onScrapeGroupURL(url: string) {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await queryScrapeMovieURL(url);
|
const result = await queryScrapeGroupURL(url);
|
||||||
if (!result.data || !result.data.scrapeMovieURL) {
|
if (!result.data || !result.data.scrapeGroupURL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this is a new group, just dump the data
|
// if this is a new group, just dump the data
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
updateGroupEditStateFromScraper(result.data.scrapeMovieURL);
|
updateGroupEditStateFromScraper(result.data.scrapeGroupURL);
|
||||||
} else {
|
} else {
|
||||||
setScrapedGroup(result.data.scrapeMovieURL);
|
setScrapedGroup(result.data.scrapeGroupURL);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -217,7 +217,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||||||
return (
|
return (
|
||||||
!!scrapedUrl &&
|
!!scrapedUrl &&
|
||||||
(Scrapers?.data?.listScrapers ?? []).some((s) =>
|
(Scrapers?.data?.listScrapers ?? []).some((s) =>
|
||||||
(s?.movie?.urls ?? []).some((u) => scrapedUrl.includes(u))
|
(s?.group?.urls ?? []).some((u) => scrapedUrl.includes(u))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScrapeDialogClosed(p?: GQL.ScrapedMovieDataFragment) {
|
function onScrapeDialogClosed(p?: GQL.ScrapedGroupDataFragment) {
|
||||||
if (p) {
|
if (p) {
|
||||||
updateGroupEditStateFromScraper(p);
|
updateGroupEditStateFromScraper(p);
|
||||||
}
|
}
|
||||||
@@ -381,7 +381,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||||||
<Prompt
|
<Prompt
|
||||||
when={formik.dirty}
|
when={formik.dirty}
|
||||||
message={(location, action) => {
|
message={(location, action) => {
|
||||||
// Check if it's a redirect after movie creation
|
// Check if it's a redirect after group creation
|
||||||
if (action === "PUSH" && location.pathname.startsWith("/groups/"))
|
if (action === "PUSH" && location.pathname.startsWith("/groups/"))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
|
|||||||
{renderDateField("date")}
|
{renderDateField("date")}
|
||||||
{renderStudioField()}
|
{renderStudioField()}
|
||||||
{renderInputField("director")}
|
{renderInputField("director")}
|
||||||
{renderURLListField("urls", onScrapeMovieURL, urlScrapable)}
|
{renderURLListField("urls", onScrapeGroupURL, urlScrapable)}
|
||||||
{renderInputField("synopsis", "textarea")}
|
{renderInputField("synopsis", "textarea")}
|
||||||
{renderTagsField()}
|
{renderTagsField()}
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { MoviesCriterion } from "src/models/list-filter/criteria/movies";
|
import { GroupsCriterion as GroupsCriterion } from "src/models/list-filter/criteria/movies";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { SceneList } from "src/components/Scenes/SceneList";
|
import { SceneList } from "src/components/Scenes/SceneList";
|
||||||
import { View } from "src/components/List/views";
|
import { View } from "src/components/List/views";
|
||||||
|
|
||||||
interface IGroupScenesPanel {
|
interface IGroupScenesPanel {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
group: GQL.MovieDataFragment;
|
group: GQL.GroupDataFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupScenesPanel: React.FC<IGroupScenesPanel> = ({
|
export const GroupScenesPanel: React.FC<IGroupScenesPanel> = ({
|
||||||
@@ -15,32 +15,32 @@ export const GroupScenesPanel: React.FC<IGroupScenesPanel> = ({
|
|||||||
group,
|
group,
|
||||||
}) => {
|
}) => {
|
||||||
function filterHook(filter: ListFilterModel) {
|
function filterHook(filter: ListFilterModel) {
|
||||||
const movieValue = { id: group.id, label: group.name };
|
const groupValue = { id: group.id, label: group.name };
|
||||||
// if movie is already present, then we modify it, otherwise add
|
// if group is already present, then we modify it, otherwise add
|
||||||
let movieCriterion = filter.criteria.find((c) => {
|
let groupCriterion = filter.criteria.find((c) => {
|
||||||
return c.criterionOption.type === "movies";
|
return c.criterionOption.type === "groups";
|
||||||
}) as MoviesCriterion | undefined;
|
}) as GroupsCriterion | undefined;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
movieCriterion &&
|
groupCriterion &&
|
||||||
(movieCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
(groupCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
||||||
movieCriterion.modifier === GQL.CriterionModifier.Includes)
|
groupCriterion.modifier === GQL.CriterionModifier.Includes)
|
||||||
) {
|
) {
|
||||||
// add the movie if not present
|
// add the group if not present
|
||||||
if (
|
if (
|
||||||
!movieCriterion.value.find((p) => {
|
!groupCriterion.value.find((p) => {
|
||||||
return p.id === group.id;
|
return p.id === group.id;
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
movieCriterion.value.push(movieValue);
|
groupCriterion.value.push(groupValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
movieCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
groupCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
||||||
} else {
|
} else {
|
||||||
// overwrite
|
// overwrite
|
||||||
movieCriterion = new MoviesCriterion();
|
groupCriterion = new GroupsCriterion();
|
||||||
movieCriterion.value = [movieValue];
|
groupCriterion.value = [groupValue];
|
||||||
filter.criteria.push(movieCriterion);
|
filter.criteria.push(groupCriterion);
|
||||||
}
|
}
|
||||||
|
|
||||||
return filter;
|
return filter;
|
||||||
@@ -50,7 +50,7 @@ export const GroupScenesPanel: React.FC<IGroupScenesPanel> = ({
|
|||||||
return (
|
return (
|
||||||
<SceneList
|
<SceneList
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
defaultSort="movie_scene_number"
|
defaultSort="group_scene_number"
|
||||||
alterQuery={active}
|
alterQuery={active}
|
||||||
view={View.GroupScenes}
|
view={View.GroupScenes}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ import { Tag } from "src/components/Tags/TagSelect";
|
|||||||
import { useScrapedTags } from "src/components/Shared/ScrapeDialog/scrapedTags";
|
import { useScrapedTags } from "src/components/Shared/ScrapeDialog/scrapedTags";
|
||||||
|
|
||||||
interface IGroupScrapeDialogProps {
|
interface IGroupScrapeDialogProps {
|
||||||
group: Partial<GQL.MovieUpdateInput>;
|
group: Partial<GQL.GroupUpdateInput>;
|
||||||
groupStudio: Studio | null;
|
groupStudio: Studio | null;
|
||||||
groupTags: Tag[];
|
groupTags: Tag[];
|
||||||
scraped: GQL.ScrapedMovie;
|
scraped: GQL.ScrapedGroup;
|
||||||
|
|
||||||
onClose: (scrapedMovie?: GQL.ScrapedMovie) => void;
|
onClose: (scrapedGroup?: GQL.ScrapedGroup) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
|
export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
|
||||||
@@ -126,7 +126,7 @@ export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeNewScrapedItem(): GQL.ScrapedMovie {
|
function makeNewScrapedItem(): GQL.ScrapedGroup {
|
||||||
const newStudioValue = studio.getNewValue();
|
const newStudioValue = studio.getNewValue();
|
||||||
const durationString = duration.getNewValue();
|
const durationString = duration.getNewValue();
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
|||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
queryFindMovies,
|
queryFindGroups,
|
||||||
useFindMovies,
|
useFindGroups,
|
||||||
useMoviesDestroy,
|
useGroupsDestroy,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { makeItemList, showWhenSelected } from "../List/ItemList";
|
import { makeItemList, showWhenSelected } from "../List/ItemList";
|
||||||
import { ExportDialog } from "../Shared/ExportDialog";
|
import { ExportDialog } from "../Shared/ExportDialog";
|
||||||
@@ -19,13 +19,13 @@ import { EditGroupsDialog } from "./EditMoviesDialog";
|
|||||||
import { View } from "../List/views";
|
import { View } from "../List/views";
|
||||||
|
|
||||||
const GroupItemList = makeItemList({
|
const GroupItemList = makeItemList({
|
||||||
filterMode: GQL.FilterMode.Movies,
|
filterMode: GQL.FilterMode.Groups,
|
||||||
useResult: useFindMovies,
|
useResult: useFindGroups,
|
||||||
getItems(result: GQL.FindMoviesQueryResult) {
|
getItems(result: GQL.FindGroupsQueryResult) {
|
||||||
return result?.data?.findMovies?.movies ?? [];
|
return result?.data?.findGroups?.groups ?? [];
|
||||||
},
|
},
|
||||||
getCount(result: GQL.FindMoviesQueryResult) {
|
getCount(result: GQL.FindGroupsQueryResult) {
|
||||||
return result?.data?.findMovies?.count ?? 0;
|
return result?.data?.findGroups?.count ?? 0;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export const GroupList: React.FC<IGroupList> = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
function addKeybinds(
|
function addKeybinds(
|
||||||
result: GQL.FindMoviesQueryResult,
|
result: GQL.FindGroupsQueryResult,
|
||||||
filter: ListFilterModel
|
filter: ListFilterModel
|
||||||
) {
|
) {
|
||||||
Mousetrap.bind("p r", () => {
|
Mousetrap.bind("p r", () => {
|
||||||
@@ -75,21 +75,21 @@ export const GroupList: React.FC<IGroupList> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function viewRandom(
|
async function viewRandom(
|
||||||
result: GQL.FindMoviesQueryResult,
|
result: GQL.FindGroupsQueryResult,
|
||||||
filter: ListFilterModel
|
filter: ListFilterModel
|
||||||
) {
|
) {
|
||||||
// query for a random image
|
// query for a random image
|
||||||
if (result.data?.findMovies) {
|
if (result.data?.findGroups) {
|
||||||
const { count } = result.data.findMovies;
|
const { count } = result.data.findGroups;
|
||||||
|
|
||||||
const index = Math.floor(Math.random() * count);
|
const index = Math.floor(Math.random() * count);
|
||||||
const filterCopy = cloneDeep(filter);
|
const filterCopy = cloneDeep(filter);
|
||||||
filterCopy.itemsPerPage = 1;
|
filterCopy.itemsPerPage = 1;
|
||||||
filterCopy.currentPage = index + 1;
|
filterCopy.currentPage = index + 1;
|
||||||
const singleResult = await queryFindMovies(filterCopy);
|
const singleResult = await queryFindGroups(filterCopy);
|
||||||
if (singleResult.data.findMovies.movies.length === 1) {
|
if (singleResult.data.findGroups.groups.length === 1) {
|
||||||
const { id } = singleResult.data.findMovies.movies[0];
|
const { id } = singleResult.data.findGroups.groups[0];
|
||||||
// navigate to the movie page
|
// navigate to the group page
|
||||||
history.push(`/groups/${id}`);
|
history.push(`/groups/${id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ export const GroupList: React.FC<IGroupList> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderContent(
|
function renderContent(
|
||||||
result: GQL.FindMoviesQueryResult,
|
result: GQL.FindGroupsQueryResult,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
selectedIds: Set<string>,
|
selectedIds: Set<string>,
|
||||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
|
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
|
||||||
@@ -116,7 +116,7 @@ export const GroupList: React.FC<IGroupList> = ({
|
|||||||
return (
|
return (
|
||||||
<ExportDialog
|
<ExportDialog
|
||||||
exportInput={{
|
exportInput={{
|
||||||
movies: {
|
groups: {
|
||||||
ids: Array.from(selectedIds.values()),
|
ids: Array.from(selectedIds.values()),
|
||||||
all: isExportAll,
|
all: isExportAll,
|
||||||
},
|
},
|
||||||
@@ -128,12 +128,12 @@ export const GroupList: React.FC<IGroupList> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderGroups() {
|
function renderGroups() {
|
||||||
if (!result.data?.findMovies) return;
|
if (!result.data?.findGroups) return;
|
||||||
|
|
||||||
if (filter.displayMode === DisplayMode.Grid) {
|
if (filter.displayMode === DisplayMode.Grid) {
|
||||||
return (
|
return (
|
||||||
<GroupCardGrid
|
<GroupCardGrid
|
||||||
groups={result.data.findMovies.movies}
|
groups={result.data.findGroups.groups}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
onSelectChange={onSelectChange}
|
onSelectChange={onSelectChange}
|
||||||
/>
|
/>
|
||||||
@@ -152,14 +152,14 @@ export const GroupList: React.FC<IGroupList> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderEditDialog(
|
function renderEditDialog(
|
||||||
selectedGroups: GQL.MovieDataFragment[],
|
selectedGroups: GQL.GroupDataFragment[],
|
||||||
onClose: (applied: boolean) => void
|
onClose: (applied: boolean) => void
|
||||||
) {
|
) {
|
||||||
return <EditGroupsDialog selected={selectedGroups} onClose={onClose} />;
|
return <EditGroupsDialog selected={selectedGroups} onClose={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDeleteDialog(
|
function renderDeleteDialog(
|
||||||
selectedGroups: GQL.SlimMovieDataFragment[],
|
selectedGroups: GQL.SlimGroupDataFragment[],
|
||||||
onClose: (confirmed: boolean) => void
|
onClose: (confirmed: boolean) => void
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -168,7 +168,7 @@ export const GroupList: React.FC<IGroupList> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
singularEntity={intl.formatMessage({ id: "group" })}
|
singularEntity={intl.formatMessage({ id: "group" })}
|
||||||
pluralEntity={intl.formatMessage({ id: "groups" })}
|
pluralEntity={intl.formatMessage({ id: "groups" })}
|
||||||
destroyMutation={useMoviesDestroy}
|
destroyMutation={useGroupsDestroy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useFindMovies } from "src/core/StashService";
|
import { useFindGroups } from "src/core/StashService";
|
||||||
import Slider from "@ant-design/react-slick";
|
import Slider from "@ant-design/react-slick";
|
||||||
import { GroupCard } from "./MovieCard";
|
import { GroupCard } from "./MovieCard";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
@@ -15,8 +15,8 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GroupRecommendationRow: React.FC<IProps> = (props: IProps) => {
|
export const GroupRecommendationRow: React.FC<IProps> = (props: IProps) => {
|
||||||
const result = useFindMovies(props.filter);
|
const result = useFindGroups(props.filter);
|
||||||
const cardCount = result.data?.findMovies.count;
|
const cardCount = result.data?.findGroups.count;
|
||||||
|
|
||||||
if (!result.loading && !cardCount) {
|
if (!result.loading && !cardCount) {
|
||||||
return null;
|
return null;
|
||||||
@@ -42,8 +42,8 @@ export const GroupRecommendationRow: React.FC<IProps> = (props: IProps) => {
|
|||||||
? [...Array(props.filter.itemsPerPage)].map((i) => (
|
? [...Array(props.filter.itemsPerPage)].map((i) => (
|
||||||
<div key={`_${i}`} className="group-skeleton skeleton-card"></div>
|
<div key={`_${i}`} className="group-skeleton skeleton-card"></div>
|
||||||
))
|
))
|
||||||
: result.data?.findMovies.movies.map((m) => (
|
: result.data?.findGroups.groups.map((g) => (
|
||||||
<GroupCard key={m.id} group={m} />
|
<GroupCard key={g.id} group={g} />
|
||||||
))}
|
))}
|
||||||
</Slider>
|
</Slider>
|
||||||
</RecommendationRow>
|
</RecommendationRow>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import cx from "classnames";
|
|||||||
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
queryFindMoviesForSelect,
|
queryFindGroupsForSelect,
|
||||||
queryFindMoviesByIDForSelect,
|
queryFindGroupsByIDForSelect,
|
||||||
useMovieCreate,
|
useGroupCreate,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
@@ -31,29 +31,29 @@ import { PatchComponent, PatchFunction } from "src/patch";
|
|||||||
import { TruncatedText } from "../Shared/TruncatedText";
|
import { TruncatedText } from "../Shared/TruncatedText";
|
||||||
|
|
||||||
export type Group = Pick<
|
export type Group = Pick<
|
||||||
GQL.Movie,
|
GQL.Group,
|
||||||
"id" | "name" | "date" | "front_image_path" | "aliases"
|
"id" | "name" | "date" | "front_image_path" | "aliases"
|
||||||
> & {
|
> & {
|
||||||
studio?: Pick<GQL.Studio, "name"> | null;
|
studio?: Pick<GQL.Studio, "name"> | null;
|
||||||
};
|
};
|
||||||
type Option = SelectOption<Group>;
|
type Option = SelectOption<Group>;
|
||||||
|
|
||||||
type FindMoviesResult = Awaited<
|
type FindGroupsResult = Awaited<
|
||||||
ReturnType<typeof queryFindMoviesForSelect>
|
ReturnType<typeof queryFindGroupsForSelect>
|
||||||
>["data"]["findMovies"]["movies"];
|
>["data"]["findGroups"]["groups"];
|
||||||
|
|
||||||
function sortMoviesByRelevance(input: string, movies: FindMoviesResult) {
|
function sortGroupsByRelevance(input: string, groups: FindGroupsResult) {
|
||||||
return sortByRelevance(
|
return sortByRelevance(
|
||||||
input,
|
input,
|
||||||
movies,
|
groups,
|
||||||
(m) => m.name,
|
(m) => m.name,
|
||||||
(m) => (m.aliases ? [m.aliases] : [])
|
(m) => (m.aliases ? [m.aliases] : [])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const movieSelectSort = PatchFunction(
|
const groupSelectSort = PatchFunction(
|
||||||
"MovieSelect.sort",
|
"GroupSelect.sort",
|
||||||
sortMoviesByRelevance
|
sortGroupsByRelevance
|
||||||
);
|
);
|
||||||
|
|
||||||
const _GroupSelect: React.FC<
|
const _GroupSelect: React.FC<
|
||||||
@@ -63,7 +63,7 @@ const _GroupSelect: React.FC<
|
|||||||
excludeIds?: string[];
|
excludeIds?: string[];
|
||||||
}
|
}
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const [createMovie] = useMovieCreate();
|
const [createGroup] = useGroupCreate();
|
||||||
|
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -74,23 +74,23 @@ const _GroupSelect: React.FC<
|
|||||||
|
|
||||||
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
|
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
|
||||||
|
|
||||||
async function loadMovies(input: string): Promise<Option[]> {
|
async function loadGroups(input: string): Promise<Option[]> {
|
||||||
const filter = new ListFilterModel(GQL.FilterMode.Movies);
|
const filter = new ListFilterModel(GQL.FilterMode.Groups);
|
||||||
filter.searchTerm = input;
|
filter.searchTerm = input;
|
||||||
filter.currentPage = 1;
|
filter.currentPage = 1;
|
||||||
filter.itemsPerPage = maxOptionsShown;
|
filter.itemsPerPage = maxOptionsShown;
|
||||||
filter.sortBy = "name";
|
filter.sortBy = "name";
|
||||||
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
||||||
const query = await queryFindMoviesForSelect(filter);
|
const query = await queryFindGroupsForSelect(filter);
|
||||||
let ret = query.data.findMovies.movies.filter((movie) => {
|
let ret = query.data.findGroups.groups.filter((group) => {
|
||||||
// HACK - we should probably exclude these in the backend query, but
|
// HACK - we should probably exclude these in the backend query, but
|
||||||
// this will do in the short-term
|
// this will do in the short-term
|
||||||
return !exclude.includes(movie.id.toString());
|
return !exclude.includes(group.id.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
return movieSelectSort(input, ret).map((movie) => ({
|
return groupSelectSort(input, ret).map((group) => ({
|
||||||
value: movie.id,
|
value: group.id,
|
||||||
object: movie,
|
object: group,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,12 +184,12 @@ const _GroupSelect: React.FC<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCreate = async (name: string) => {
|
const onCreate = async (name: string) => {
|
||||||
const result = await createMovie({
|
const result = await createGroup({
|
||||||
variables: { input: { name } },
|
variables: { input: { name } },
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
value: result.data!.movieCreate!.id,
|
value: result.data!.groupCreate!.id,
|
||||||
item: result.data!.movieCreate!,
|
item: result.data!.groupCreate!,
|
||||||
message: "Created group",
|
message: "Created group",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -230,7 +230,7 @@ const _GroupSelect: React.FC<
|
|||||||
},
|
},
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
loadOptions={loadMovies}
|
loadOptions={loadGroups}
|
||||||
getNamedObject={getNamedObject}
|
getNamedObject={getNamedObject}
|
||||||
isValidNewOption={isValidNewOption}
|
isValidNewOption={isValidNewOption}
|
||||||
components={{
|
components={{
|
||||||
@@ -273,10 +273,10 @@ const _GroupIDSelect: React.FC<IFilterProps & IFilterIDProps<Group>> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Group[]> {
|
async function loadObjectsByID(idsToLoad: string[]): Promise<Group[]> {
|
||||||
const query = await queryFindMoviesByIDForSelect(idsToLoad);
|
const query = await queryFindGroupsByIDForSelect(idsToLoad);
|
||||||
const { movies: loadedMovies } = query.data.findMovies;
|
const { groups: loadedGroups } = query.data.findGroups;
|
||||||
|
|
||||||
return loadedMovies;
|
return loadedGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface IPerformerCardExtraCriteria {
|
|||||||
scenes?: Criterion<CriterionValue>[];
|
scenes?: Criterion<CriterionValue>[];
|
||||||
images?: Criterion<CriterionValue>[];
|
images?: Criterion<CriterionValue>[];
|
||||||
galleries?: Criterion<CriterionValue>[];
|
galleries?: Criterion<CriterionValue>[];
|
||||||
movies?: Criterion<CriterionValue>[];
|
groups?: Criterion<CriterionValue>[];
|
||||||
performer?: ILabeledId;
|
performer?: ILabeledId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,17 +179,17 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderGroupsPopoverButton() {
|
function maybeRenderGroupsPopoverButton() {
|
||||||
if (!performer.movie_count) return;
|
if (!performer.group_count) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverCountButton
|
<PopoverCountButton
|
||||||
className="group-count"
|
className="group-count"
|
||||||
type="group"
|
type="group"
|
||||||
count={performer.movie_count}
|
count={performer.group_count}
|
||||||
url={NavUtils.makePerformerGroupsUrl(
|
url={NavUtils.makePerformerGroupsUrl(
|
||||||
performer,
|
performer,
|
||||||
extraCriteria?.performer,
|
extraCriteria?.performer,
|
||||||
extraCriteria?.movies
|
extraCriteria?.groups
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -202,7 +202,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
performer.gallery_count ||
|
performer.gallery_count ||
|
||||||
performer.tags.length > 0 ||
|
performer.tags.length > 0 ||
|
||||||
performer.o_counter ||
|
performer.o_counter ||
|
||||||
performer.movie_count
|
performer.group_count
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
|||||||
ret = "galleries";
|
ret = "galleries";
|
||||||
} else if (performer.image_count != 0) {
|
} else if (performer.image_count != 0) {
|
||||||
ret = "images";
|
ret = "images";
|
||||||
} else if (performer.movie_count != 0) {
|
} else if (performer.group_count != 0) {
|
||||||
ret = "groups";
|
ret = "groups";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,7 +325,7 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
|||||||
{intl.formatMessage({ id: "groups" })}
|
{intl.formatMessage({ id: "groups" })}
|
||||||
<Counter
|
<Counter
|
||||||
abbreviateCounter={abbreviateCounter}
|
abbreviateCounter={abbreviateCounter}
|
||||||
count={performer.movie_count}
|
count={performer.group_count}
|
||||||
hideZero
|
hideZero
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -387,23 +387,23 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderGroupPopoverButton(scene: GQL.SlimSceneDataFragment) {
|
function maybeRenderGroupPopoverButton(scene: GQL.SlimSceneDataFragment) {
|
||||||
if (scene.movies.length <= 0) return;
|
if (scene.groups.length <= 0) return;
|
||||||
|
|
||||||
const popoverContent = scene.movies.map((sceneMovie) => (
|
const popoverContent = scene.groups.map((sceneGroup) => (
|
||||||
<div className="group-tag-container row" key={sceneMovie.movie.id}>
|
<div className="group-tag-container row" key={sceneGroup.group.id}>
|
||||||
<Link
|
<Link
|
||||||
to={`/groups/${sceneMovie.movie.id}`}
|
to={`/groups/${sceneGroup.group.id}`}
|
||||||
className="group-tag col m-auto zoom-2"
|
className="group-tag col m-auto zoom-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="image-thumbnail"
|
className="image-thumbnail"
|
||||||
alt={sceneMovie.movie.name ?? ""}
|
alt={sceneGroup.group.name ?? ""}
|
||||||
src={sceneMovie.movie.front_image_path ?? ""}
|
src={sceneGroup.group.front_image_path ?? ""}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<GroupLink
|
<GroupLink
|
||||||
key={sceneMovie.movie.id}
|
key={sceneGroup.group.id}
|
||||||
group={sceneMovie.movie}
|
group={sceneGroup.group}
|
||||||
className="d-block"
|
className="d-block"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -417,7 +417,7 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Button className="minimal">
|
<Button className="minimal">
|
||||||
<Icon icon={faFilm} />
|
<Icon icon={faFilm} />
|
||||||
<span>{scene.movies.length}</span>
|
<span>{scene.groups.length}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</HoverPopover>
|
</HoverPopover>
|
||||||
);
|
);
|
||||||
@@ -511,7 +511,7 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||||||
if (
|
if (
|
||||||
scene.tags.length > 0 ||
|
scene.tags.length > 0 ||
|
||||||
scene.performers.length > 0 ||
|
scene.performers.length > 0 ||
|
||||||
scene.movies.length > 0 ||
|
scene.groups.length > 0 ||
|
||||||
scene.scene_markers.length > 0 ||
|
scene.scene_markers.length > 0 ||
|
||||||
scene?.o_counter ||
|
scene?.o_counter ||
|
||||||
scene.galleries.length > 0 ||
|
scene.galleries.length > 0 ||
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
aggregatePerformerIds
|
aggregatePerformerIds
|
||||||
);
|
);
|
||||||
sceneInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
sceneInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
|
||||||
sceneInput.movie_ids = getAggregateInputIDs(
|
sceneInput.group_ids = getAggregateInputIDs(
|
||||||
groupMode,
|
groupMode,
|
||||||
groupIds,
|
groupIds,
|
||||||
aggregateGroupIds
|
aggregateGroupIds
|
||||||
@@ -126,7 +126,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
.map((p) => p.id)
|
.map((p) => p.id)
|
||||||
.sort();
|
.sort();
|
||||||
const sceneTagIDs = (scene.tags ?? []).map((p) => p.id).sort();
|
const sceneTagIDs = (scene.tags ?? []).map((p) => p.id).sort();
|
||||||
const sceneGroupIDs = (scene.movies ?? []).map((m) => m.movie.id).sort();
|
const sceneGroupIDs = (scene.groups ?? []).map((m) => m.group.id).sort();
|
||||||
|
|
||||||
if (first) {
|
if (first) {
|
||||||
updateRating = sceneRating ?? undefined;
|
updateRating = sceneRating ?? undefined;
|
||||||
|
|||||||
@@ -144,23 +144,23 @@ const SceneCardPopovers = PatchComponent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderGroupPopoverButton() {
|
function maybeRenderGroupPopoverButton() {
|
||||||
if (props.scene.movies.length <= 0) return;
|
if (props.scene.groups.length <= 0) return;
|
||||||
|
|
||||||
const popoverContent = props.scene.movies.map((sceneGroup) => (
|
const popoverContent = props.scene.groups.map((sceneGroup) => (
|
||||||
<div className="group-tag-container row" key={sceneGroup.movie.id}>
|
<div className="group-tag-container row" key={sceneGroup.group.id}>
|
||||||
<Link
|
<Link
|
||||||
to={`/groups/${sceneGroup.movie.id}`}
|
to={`/groups/${sceneGroup.group.id}`}
|
||||||
className="group-tag col m-auto zoom-2"
|
className="group-tag col m-auto zoom-2"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="image-thumbnail"
|
className="image-thumbnail"
|
||||||
alt={sceneGroup.movie.name ?? ""}
|
alt={sceneGroup.group.name ?? ""}
|
||||||
src={sceneGroup.movie.front_image_path ?? ""}
|
src={sceneGroup.group.front_image_path ?? ""}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<GroupLink
|
<GroupLink
|
||||||
key={sceneGroup.movie.id}
|
key={sceneGroup.group.id}
|
||||||
group={sceneGroup.movie}
|
group={sceneGroup.group}
|
||||||
className="d-block"
|
className="d-block"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,7 +174,7 @@ const SceneCardPopovers = PatchComponent(
|
|||||||
>
|
>
|
||||||
<Button className="minimal">
|
<Button className="minimal">
|
||||||
<Icon icon={faFilm} />
|
<Icon icon={faFilm} />
|
||||||
<span>{props.scene.movies.length}</span>
|
<span>{props.scene.groups.length}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</HoverPopover>
|
</HoverPopover>
|
||||||
);
|
);
|
||||||
@@ -279,7 +279,7 @@ const SceneCardPopovers = PatchComponent(
|
|||||||
!props.compact &&
|
!props.compact &&
|
||||||
(props.scene.tags.length > 0 ||
|
(props.scene.tags.length > 0 ||
|
||||||
props.scene.performers.length > 0 ||
|
props.scene.performers.length > 0 ||
|
||||||
props.scene.movies.length > 0 ||
|
props.scene.groups.length > 0 ||
|
||||||
props.scene.scene_markers.length > 0 ||
|
props.scene.scene_markers.length > 0 ||
|
||||||
props.scene?.o_counter ||
|
props.scene?.o_counter ||
|
||||||
props.scene.galleries.length > 0 ||
|
props.scene.galleries.length > 0 ||
|
||||||
|
|||||||
@@ -441,12 +441,12 @@ const ScenePage: React.FC<IProps> = ({
|
|||||||
<FormattedMessage id="markers" />
|
<FormattedMessage id="markers" />
|
||||||
</Nav.Link>
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
{scene.movies.length > 0 ? (
|
{scene.groups.length > 0 ? (
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scene-group-panel">
|
<Nav.Link eventKey="scene-group-panel">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="countables.groups"
|
id="countables.groups"
|
||||||
values={{ count: scene.movies.length }}
|
values={{ count: scene.groups.length }}
|
||||||
/>
|
/>
|
||||||
</Nav.Link>
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}, [scene.performers]);
|
}, [scene.performers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGroups(scene.movies?.map((m) => m.movie) ?? []);
|
setGroups(scene.groups?.map((m) => m.group) ?? []);
|
||||||
}, [scene.movies]);
|
}, [scene.groups]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStudio(scene.studio ?? null);
|
setStudio(scene.studio ?? null);
|
||||||
@@ -125,10 +125,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
gallery_ids: yup.array(yup.string().required()).defined(),
|
gallery_ids: yup.array(yup.string().required()).defined(),
|
||||||
studio_id: yup.string().required().nullable(),
|
studio_id: yup.string().required().nullable(),
|
||||||
performer_ids: yup.array(yup.string().required()).defined(),
|
performer_ids: yup.array(yup.string().required()).defined(),
|
||||||
movies: yup
|
groups: yup
|
||||||
.array(
|
.array(
|
||||||
yup.object({
|
yup.object({
|
||||||
movie_id: yup.string().required(),
|
group_id: yup.string().required(),
|
||||||
scene_index: yup.number().integer().nullable().defined(),
|
scene_index: yup.number().integer().nullable().defined(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -149,8 +149,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
||||||
studio_id: scene.studio?.id ?? null,
|
studio_id: scene.studio?.id ?? null,
|
||||||
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
||||||
movies: (scene.movies ?? []).map((m) => {
|
groups: (scene.groups ?? []).map((m) => {
|
||||||
return { movie_id: m.movie.id, scene_index: m.scene_index ?? null };
|
return { group_id: m.group.id, scene_index: m.scene_index ?? null };
|
||||||
}),
|
}),
|
||||||
tag_ids: (scene.tags ?? []).map((t) => t.id),
|
tag_ids: (scene.tags ?? []).map((t) => t.id),
|
||||||
stash_ids: getStashIDs(scene.stash_ids),
|
stash_ids: getStashIDs(scene.stash_ids),
|
||||||
@@ -187,16 +187,16 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
return sceneImage;
|
return sceneImage;
|
||||||
}, [formik.values.cover_image, scene.paths?.screenshot]);
|
}, [formik.values.cover_image, scene.paths?.screenshot]);
|
||||||
|
|
||||||
const movieEntries = useMemo(() => {
|
const groupEntries = useMemo(() => {
|
||||||
return formik.values.movies
|
return formik.values.groups
|
||||||
.map((m) => {
|
.map((m) => {
|
||||||
return {
|
return {
|
||||||
movie: groups.find((mm) => mm.id === m.movie_id),
|
group: groups.find((mm) => mm.id === m.group_id),
|
||||||
scene_index: m.scene_index,
|
scene_index: m.scene_index,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((m) => m.movie !== undefined) as IGroupEntry[];
|
.filter((m) => m.group !== undefined) as IGroupEntry[];
|
||||||
}, [formik.values.movies, groups]);
|
}, [formik.values.groups, groups]);
|
||||||
|
|
||||||
function onSetGalleries(items: Gallery[]) {
|
function onSetGalleries(items: Gallery[]) {
|
||||||
setGalleries(items);
|
setGalleries(items);
|
||||||
@@ -256,21 +256,21 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
function onSetGroups(items: Group[]) {
|
function onSetGroups(items: Group[]) {
|
||||||
setGroups(items);
|
setGroups(items);
|
||||||
|
|
||||||
const existingMovies = formik.values.movies;
|
const existingGroups = formik.values.groups;
|
||||||
|
|
||||||
const newMovies = items.map((m) => {
|
const newGroups = items.map((m) => {
|
||||||
const existing = existingMovies.find((mm) => mm.movie_id === m.id);
|
const existing = existingGroups.find((mm) => mm.group_id === m.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
movie_id: m.id,
|
group_id: m.id,
|
||||||
scene_index: null,
|
scene_index: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
formik.setFieldValue("movies", newMovies);
|
formik.setFieldValue("groups", newGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSave(input: InputValues) {
|
async function onSave(input: InputValues) {
|
||||||
@@ -568,8 +568,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.movies && updatedScene.movies.length > 0) {
|
if (updatedScene.groups && updatedScene.groups.length > 0) {
|
||||||
const idMovis = updatedScene.movies.filter((p) => {
|
const idMovis = updatedScene.groups.filter((p) => {
|
||||||
return p.stored_id !== undefined && p.stored_id !== null;
|
return p.stored_id !== undefined && p.stored_id !== null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -725,24 +725,24 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
return renderField("performer_ids", title, control, fullWidthProps);
|
return renderField("performer_ids", title, control, fullWidthProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSetMovieEntries(input: IGroupEntry[]) {
|
function onSetGroupEntries(input: IGroupEntry[]) {
|
||||||
setGroups(input.map((m) => m.movie));
|
setGroups(input.map((m) => m.group));
|
||||||
|
|
||||||
const newMovies = input.map((m) => ({
|
const newGroups = input.map((m) => ({
|
||||||
movie_id: m.movie.id,
|
group_id: m.group.id,
|
||||||
scene_index: m.scene_index,
|
scene_index: m.scene_index,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
formik.setFieldValue("movies", newMovies);
|
formik.setFieldValue("groups", newGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMoviesField() {
|
function renderGroupsField() {
|
||||||
const title = intl.formatMessage({ id: "groups" });
|
const title = intl.formatMessage({ id: "groups" });
|
||||||
const control = (
|
const control = (
|
||||||
<SceneGroupTable value={movieEntries} onUpdate={onSetMovieEntries} />
|
<SceneGroupTable value={groupEntries} onUpdate={onSetGroupEntries} />
|
||||||
);
|
);
|
||||||
|
|
||||||
return renderField("movies", title, control, fullWidthProps);
|
return renderField("groups", title, control, fullWidthProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTagsField() {
|
function renderTagsField() {
|
||||||
@@ -820,7 +820,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
{renderGalleriesField()}
|
{renderGalleriesField()}
|
||||||
{renderStudioField()}
|
{renderStudioField()}
|
||||||
{renderPerformersField()}
|
{renderPerformersField()}
|
||||||
{renderMoviesField()}
|
{renderGroupsField()}
|
||||||
{renderTagsField()}
|
{renderTagsField()}
|
||||||
|
|
||||||
{renderStashIDsField(
|
{renderStashIDsField(
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ interface ISceneGroupPanelProps {
|
|||||||
export const SceneGroupPanel: React.FC<ISceneGroupPanelProps> = (
|
export const SceneGroupPanel: React.FC<ISceneGroupPanelProps> = (
|
||||||
props: ISceneGroupPanelProps
|
props: ISceneGroupPanelProps
|
||||||
) => {
|
) => {
|
||||||
const cards = props.scene.movies.map((sceneGroup) => (
|
const cards = props.scene.groups.map((sceneGroup) => (
|
||||||
<GroupCard
|
<GroupCard
|
||||||
key={sceneGroup.movie.id}
|
key={sceneGroup.group.id}
|
||||||
group={sceneGroup.movie}
|
group={sceneGroup.group}
|
||||||
sceneIndex={sceneGroup.scene_index ?? undefined}
|
sceneIndex={sceneGroup.scene_index ?? undefined}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { Form, Row, Col } from "react-bootstrap";
|
|||||||
import { Group, GroupSelect } from "src/components/Movies/MovieSelect";
|
import { Group, GroupSelect } from "src/components/Movies/MovieSelect";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
||||||
export type MovieSceneIndexMap = Map<string, number | undefined>;
|
export type GroupSceneIndexMap = Map<string, number | undefined>;
|
||||||
|
|
||||||
export interface IGroupEntry {
|
export interface IGroupEntry {
|
||||||
movie: Group;
|
group: Group;
|
||||||
scene_index?: GQL.InputMaybe<number> | undefined;
|
scene_index?: GQL.InputMaybe<number> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
|
|||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const groupIDs = useMemo(() => value.map((m) => m.movie.id), [value]);
|
const groupIDs = useMemo(() => value.map((m) => m.group.id), [value]);
|
||||||
|
|
||||||
const updateFieldChanged = (index: number, sceneIndex: number | null) => {
|
const updateFieldChanged = (index: number, sceneIndex: number | null) => {
|
||||||
const newValues = value.map((existing, i) => {
|
const newValues = value.map((existing, i) => {
|
||||||
@@ -52,7 +52,7 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
|
|||||||
if (i === index) {
|
if (i === index) {
|
||||||
return {
|
return {
|
||||||
...existing,
|
...existing,
|
||||||
movie: group,
|
group: group,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return existing;
|
return existing;
|
||||||
@@ -71,7 +71,7 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
|
|||||||
const newValues = [
|
const newValues = [
|
||||||
...value,
|
...value,
|
||||||
{
|
{
|
||||||
movie: group,
|
group: group,
|
||||||
scene_index: null,
|
scene_index: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -83,11 +83,11 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{value.map((m, i) => (
|
{value.map((m, i) => (
|
||||||
<Row key={m.movie.id} className="group-row">
|
<Row key={m.group.id} className="group-row">
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<GroupSelect
|
<GroupSelect
|
||||||
onSelect={(items) => onGroupSet(i, items)}
|
onSelect={(items) => onGroupSet(i, items)}
|
||||||
values={[m.movie!]}
|
values={[m.group!]}
|
||||||
excludeIds={groupIDs}
|
excludeIds={groupIDs}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -115,20 +115,20 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [groups, setGroups] = useState<
|
const [groups, setGroups] = useState<
|
||||||
ObjectListScrapeResult<GQL.ScrapedMovie>
|
ObjectListScrapeResult<GQL.ScrapedGroup>
|
||||||
>(
|
>(
|
||||||
new ObjectListScrapeResult<GQL.ScrapedMovie>(
|
new ObjectListScrapeResult<GQL.ScrapedGroup>(
|
||||||
sortStoredIdObjects(
|
sortStoredIdObjects(
|
||||||
sceneGroups.map((p) => ({
|
sceneGroups.map((p) => ({
|
||||||
stored_id: p.id,
|
stored_id: p.id,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
sortStoredIdObjects(scraped.movies ?? undefined)
|
sortStoredIdObjects(scraped.groups ?? undefined)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const [newGroups, setNewGroups] = useState<GQL.ScrapedMovie[]>(
|
const [newGroups, setNewGroups] = useState<GQL.ScrapedGroup[]>(
|
||||||
scraped.movies?.filter((t) => !t.stored_id) ?? []
|
scraped.groups?.filter((t) => !t.stored_id) ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
const { tags, newTags, scrapedTagsRow } = useScrapedTags(
|
const { tags, newTags, scrapedTagsRow } = useScrapedTags(
|
||||||
@@ -202,7 +202,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
director: director.getNewValue(),
|
director: director.getNewValue(),
|
||||||
studio: newStudioValue,
|
studio: newStudioValue,
|
||||||
performers: performers.getNewValue(),
|
performers: performers.getNewValue(),
|
||||||
movies: groups.getNewValue(),
|
groups: groups.getNewValue(),
|
||||||
tags: tags.getNewValue(),
|
tags: tags.getNewValue(),
|
||||||
details: details.getNewValue(),
|
details: details.getNewValue(),
|
||||||
image: image.getNewValue(),
|
image: image.getNewValue(),
|
||||||
|
|||||||
@@ -126,10 +126,10 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
|||||||
|
|
||||||
const GroupCell = (scene: GQL.SlimSceneDataFragment) => (
|
const GroupCell = (scene: GQL.SlimSceneDataFragment) => (
|
||||||
<ul className="comma-list overflowable">
|
<ul className="comma-list overflowable">
|
||||||
{scene.movies.map((sceneGroup) => (
|
{scene.groups.map((sceneGroup) => (
|
||||||
<li key={sceneGroup.movie.id}>
|
<li key={sceneGroup.group.id}>
|
||||||
<Link to={NavUtils.makeGroupScenesUrl(sceneGroup.movie)}>
|
<Link to={NavUtils.makeGroupScenesUrl(sceneGroup.group)}>
|
||||||
<span className="ellips-data">{sceneGroup.movie.name}</span>
|
<span className="ellips-data">{sceneGroup.group.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -100,10 +100,10 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupToStoredID(o: { movie: { id: string; name: string } }) {
|
function groupToStoredID(o: { group: { id: string; name: string } }) {
|
||||||
return {
|
return {
|
||||||
stored_id: o.movie.id,
|
stored_id: o.group.id,
|
||||||
name: o.movie.name,
|
name: o.group.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,10 +142,10 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [groups, setGroups] = useState<
|
const [groups, setGroups] = useState<
|
||||||
ObjectListScrapeResult<GQL.ScrapedMovie>
|
ObjectListScrapeResult<GQL.ScrapedGroup>
|
||||||
>(
|
>(
|
||||||
new ObjectListScrapeResult<GQL.ScrapedMovie>(
|
new ObjectListScrapeResult<GQL.ScrapedGroup>(
|
||||||
sortStoredIdObjects(dest.movies.map(groupToStoredID))
|
sortStoredIdObjects(dest.groups.map(groupToStoredID))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -253,9 +253,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
setGroups(
|
setGroups(
|
||||||
new ObjectListScrapeResult<GQL.ScrapedMovie>(
|
new ObjectListScrapeResult<GQL.ScrapedGroup>(
|
||||||
sortStoredIdObjects(dest.movies.map(groupToStoredID)),
|
sortStoredIdObjects(dest.groups.map(groupToStoredID)),
|
||||||
uniqIDStoredIDs(all.map((s) => s.movies.map(groupToStoredID)).flat())
|
uniqIDStoredIDs(all.map((s) => s.groups.map(groupToStoredID)).flat())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -585,14 +585,14 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
gallery_ids: galleries.getNewValue(),
|
gallery_ids: galleries.getNewValue(),
|
||||||
studio_id: studio.getNewValue()?.stored_id,
|
studio_id: studio.getNewValue()?.stored_id,
|
||||||
performer_ids: performers.getNewValue()?.map((p) => p.stored_id!),
|
performer_ids: performers.getNewValue()?.map((p) => p.stored_id!),
|
||||||
movies: groups.getNewValue()?.map((m) => {
|
groups: groups.getNewValue()?.map((m) => {
|
||||||
// find the equivalent movie in the original scenes
|
// find the equivalent group in the original scenes
|
||||||
const found = all
|
const found = all
|
||||||
.map((s) => s.movies)
|
.map((s) => s.groups)
|
||||||
.flat()
|
.flat()
|
||||||
.find((mm) => mm.movie.id === m.stored_id);
|
.find((mm) => mm.group.id === m.stored_id);
|
||||||
return {
|
return {
|
||||||
movie_id: m.stored_id!,
|
group_id: m.stored_id!,
|
||||||
scene_index: found!.scene_index,
|
scene_index: found!.scene_index,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
useListMovieScrapers,
|
useListGroupScrapers,
|
||||||
useListPerformerScrapers,
|
useListPerformerScrapers,
|
||||||
useListSceneScrapers,
|
useListSceneScrapers,
|
||||||
useListGalleryScrapers,
|
useListGalleryScrapers,
|
||||||
@@ -80,7 +80,7 @@ export const SettingsScrapingPanel: React.FC = () => {
|
|||||||
const { data: galleryScrapers, loading: loadingGalleries } =
|
const { data: galleryScrapers, loading: loadingGalleries } =
|
||||||
useListGalleryScrapers();
|
useListGalleryScrapers();
|
||||||
const { data: groupScrapers, loading: loadingGroups } =
|
const { data: groupScrapers, loading: loadingGroups } =
|
||||||
useListMovieScrapers();
|
useListGroupScrapers();
|
||||||
|
|
||||||
const { general, scraping, loading, error, saveGeneral, saveScraping } =
|
const { general, scraping, loading, error, saveGeneral, saveScraping } =
|
||||||
useSettings();
|
useSettings();
|
||||||
@@ -251,9 +251,9 @@ export const SettingsScrapingPanel: React.FC = () => {
|
|||||||
<tr key={scraper.id}>
|
<tr key={scraper.id}>
|
||||||
<td>{scraper.name}</td>
|
<td>{scraper.name}</td>
|
||||||
<td>
|
<td>
|
||||||
{renderGroupScrapeTypes(scraper.movie?.supported_scrapes ?? [])}
|
{renderGroupScrapeTypes(scraper.group?.supported_scrapes ?? [])}
|
||||||
</td>
|
</td>
|
||||||
<td>{renderURLs(scraper.movie?.urls ?? [])}</td>
|
<td>{renderURLs(scraper.group?.urls ?? [])}</td>
|
||||||
</tr>
|
</tr>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export const ScrapedPerformersRow: React.FC<
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ScrapedGroupsRow: React.FC<
|
export const ScrapedGroupsRow: React.FC<
|
||||||
IScrapedObjectRowImpl<GQL.ScrapedMovie>
|
IScrapedObjectRowImpl<GQL.ScrapedGroup>
|
||||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||||
const groupsCopy = useMemo(() => {
|
const groupsCopy = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -209,9 +209,9 @@ export const ScrapedGroupsRow: React.FC<
|
|||||||
}, [newObjects]);
|
}, [newObjects]);
|
||||||
|
|
||||||
function renderScrapedGroups(
|
function renderScrapedGroups(
|
||||||
scrapeResult: ScrapeResult<GQL.ScrapedMovie[]>,
|
scrapeResult: ScrapeResult<GQL.ScrapedGroup[]>,
|
||||||
isNew?: boolean,
|
isNew?: boolean,
|
||||||
onChangeFn?: (value: GQL.ScrapedMovie[]) => void
|
onChangeFn?: (value: GQL.ScrapedGroup[]) => void
|
||||||
) {
|
) {
|
||||||
const resultValue = isNew
|
const resultValue = isNew
|
||||||
? scrapeResult.newValue
|
? scrapeResult.newValue
|
||||||
@@ -244,7 +244,7 @@ export const ScrapedGroupsRow: React.FC<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrapedObjectsRow<GQL.ScrapedMovie>
|
<ScrapedObjectsRow<GQL.ScrapedGroup>
|
||||||
title={title}
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderObjects={renderScrapedGroups}
|
renderObjects={renderScrapedGroups}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
useMovieCreate,
|
useGroupCreate,
|
||||||
usePerformerCreate,
|
usePerformerCreate,
|
||||||
useStudioCreate,
|
useStudioCreate,
|
||||||
useTagCreate,
|
useTagCreate,
|
||||||
@@ -124,12 +124,12 @@ export function useCreateScrapedPerformer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateScrapedGroup(
|
export function useCreateScrapedGroup(
|
||||||
props: IUseCreateNewObjectProps<GQL.ScrapedMovie>
|
props: IUseCreateNewObjectProps<GQL.ScrapedGroup>
|
||||||
) {
|
) {
|
||||||
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
|
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
|
||||||
const [createGroup] = useMovieCreate();
|
const [createGroup] = useGroupCreate();
|
||||||
|
|
||||||
async function createNewGroup(toCreate: GQL.ScrapedMovie) {
|
async function createNewGroup(toCreate: GQL.ScrapedGroup) {
|
||||||
const input = scrapedGroupToCreateInput(toCreate);
|
const input = scrapedGroupToCreateInput(toCreate);
|
||||||
|
|
||||||
const result = await createGroup({
|
const result = await createGroup({
|
||||||
@@ -137,10 +137,10 @@ export function useCreateScrapedGroup(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const newValue = [...(scrapeResult.newValue ?? [])];
|
const newValue = [...(scrapeResult.newValue ?? [])];
|
||||||
if (result.data?.movieCreate)
|
if (result.data?.groupCreate)
|
||||||
newValue.push({
|
newValue.push({
|
||||||
stored_id: result.data.movieCreate.id,
|
stored_id: result.data.groupCreate.id,
|
||||||
name: result.data.movieCreate.name,
|
name: result.data.groupCreate.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
// add the new object to the new object value
|
// add the new object to the new object value
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const Stats: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="stats-element">
|
<div className="stats-element">
|
||||||
<p className="title">
|
<p className="title">
|
||||||
<FormattedNumber value={data.stats.movie_count} />
|
<FormattedNumber value={data.stats.group_count} />
|
||||||
</p>
|
</p>
|
||||||
<p className="heading">
|
<p className="heading">
|
||||||
<FormattedMessage id="groups" />
|
<FormattedMessage id="groups" />
|
||||||
|
|||||||
@@ -143,13 +143,13 @@ export const StudioCard: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderGroupsPopoverButton() {
|
function maybeRenderGroupsPopoverButton() {
|
||||||
if (!studio.movie_count) return;
|
if (!studio.group_count) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverCountButton
|
<PopoverCountButton
|
||||||
className="group-count"
|
className="group-count"
|
||||||
type="group"
|
type="group"
|
||||||
count={studio.movie_count}
|
count={studio.group_count}
|
||||||
url={NavUtils.makeStudioGroupsUrl(studio)}
|
url={NavUtils.makeStudioGroupsUrl(studio)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -190,7 +190,7 @@ export const StudioCard: React.FC<IProps> = ({
|
|||||||
studio.scene_count ||
|
studio.scene_count ||
|
||||||
studio.image_count ||
|
studio.image_count ||
|
||||||
studio.gallery_count ||
|
studio.gallery_count ||
|
||||||
studio.movie_count ||
|
studio.group_count ||
|
||||||
studio.performer_count ||
|
studio.performer_count ||
|
||||||
studio.tags.length > 0
|
studio.tags.length > 0
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
|||||||
const performerCount =
|
const performerCount =
|
||||||
(showAllCounts ? studio.performer_count_all : studio.performer_count) ?? 0;
|
(showAllCounts ? studio.performer_count_all : studio.performer_count) ?? 0;
|
||||||
const groupCount =
|
const groupCount =
|
||||||
(showAllCounts ? studio.movie_count_all : studio.movie_count) ?? 0;
|
(showAllCounts ? studio.group_count_all : studio.group_count) ?? 0;
|
||||||
|
|
||||||
const populatedDefaultTab = useMemo(() => {
|
const populatedDefaultTab = useMemo(() => {
|
||||||
let ret: TabKey = "scenes";
|
let ret: TabKey = "scenes";
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const StudioPerformersPanel: React.FC<IStudioPerformersPanel> = ({
|
|||||||
scenes: [studioCriterion],
|
scenes: [studioCriterion],
|
||||||
images: [studioCriterion],
|
images: [studioCriterion],
|
||||||
galleries: [studioCriterion],
|
galleries: [studioCriterion],
|
||||||
movies: [studioCriterion],
|
groups: [studioCriterion],
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterHook = useStudioFilterHook(studio);
|
const filterHook = useStudioFilterHook(studio);
|
||||||
|
|||||||
@@ -237,13 +237,13 @@ export const TagCard: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderGroupsPopoverButton() {
|
function maybeRenderGroupsPopoverButton() {
|
||||||
if (!tag.movie_count) return;
|
if (!tag.group_count) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverCountButton
|
<PopoverCountButton
|
||||||
className="group-count"
|
className="group-count"
|
||||||
type="group"
|
type="group"
|
||||||
count={tag.movie_count}
|
count={tag.group_count}
|
||||||
url={NavUtils.makeTagGroupsUrl(tag)}
|
url={NavUtils.makeTagGroupsUrl(tag)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
|||||||
const galleryCount =
|
const galleryCount =
|
||||||
(showAllCounts ? tag.gallery_count_all : tag.gallery_count) ?? 0;
|
(showAllCounts ? tag.gallery_count_all : tag.gallery_count) ?? 0;
|
||||||
const groupCount =
|
const groupCount =
|
||||||
(showAllCounts ? tag.movie_count_all : tag.movie_count) ?? 0;
|
(showAllCounts ? tag.group_count_all : tag.group_count) ?? 0;
|
||||||
const sceneMarkerCount =
|
const sceneMarkerCount =
|
||||||
(showAllCounts ? tag.scene_marker_count_all : tag.scene_marker_count) ?? 0;
|
(showAllCounts ? tag.scene_marker_count_all : tag.scene_marker_count) ?? 0;
|
||||||
const performerCount =
|
const performerCount =
|
||||||
|
|||||||
@@ -210,43 +210,43 @@ export const queryFindImages = (filter: ListFilterModel) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useFindMovie = (id: string) => {
|
export const useFindGroup = (id: string) => {
|
||||||
const skip = id === "new" || id === "";
|
const skip = id === "new" || id === "";
|
||||||
return GQL.useFindMovieQuery({ variables: { id }, skip });
|
return GQL.useFindGroupQuery({ variables: { id }, skip });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFindMovies = (filter?: ListFilterModel) =>
|
export const useFindGroups = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindMoviesQuery({
|
GQL.useFindGroupsQuery({
|
||||||
skip: filter === undefined,
|
skip: filter === undefined,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter?.makeFindFilter(),
|
filter: filter?.makeFindFilter(),
|
||||||
movie_filter: filter?.makeFilter(),
|
group_filter: filter?.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryFindMovies = (filter: ListFilterModel) =>
|
export const queryFindGroups = (filter: ListFilterModel) =>
|
||||||
client.query<GQL.FindMoviesQuery>({
|
client.query<GQL.FindGroupsQuery>({
|
||||||
query: GQL.FindMoviesDocument,
|
query: GQL.FindGroupsDocument,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter.makeFindFilter(),
|
||||||
movie_filter: filter.makeFilter(),
|
group_filter: filter.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryFindMoviesByIDForSelect = (movieIDs: string[]) =>
|
export const queryFindGroupsByIDForSelect = (groupIDs: string[]) =>
|
||||||
client.query<GQL.FindMoviesForSelectQuery>({
|
client.query<GQL.FindGroupsForSelectQuery>({
|
||||||
query: GQL.FindMoviesForSelectDocument,
|
query: GQL.FindGroupsForSelectDocument,
|
||||||
variables: {
|
variables: {
|
||||||
ids: movieIDs,
|
ids: groupIDs,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryFindMoviesForSelect = (filter: ListFilterModel) =>
|
export const queryFindGroupsForSelect = (filter: ListFilterModel) =>
|
||||||
client.query<GQL.FindMoviesForSelectQuery>({
|
client.query<GQL.FindGroupsForSelectQuery>({
|
||||||
query: GQL.FindMoviesForSelectDocument,
|
query: GQL.FindGroupsForSelectDocument,
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter.makeFindFilter(),
|
||||||
movie_filter: filter.makeFilter(),
|
group_filter: filter.makeFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -485,13 +485,13 @@ function updateO(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sceneMutationImpactedTypeFields = {
|
const sceneMutationImpactedTypeFields = {
|
||||||
Movie: ["scenes", "scene_count"],
|
Group: ["scenes", "scene_count"],
|
||||||
Gallery: ["scenes"],
|
Gallery: ["scenes"],
|
||||||
Performer: [
|
Performer: [
|
||||||
"scenes",
|
"scenes",
|
||||||
"scene_count",
|
"scene_count",
|
||||||
"movies",
|
"groups",
|
||||||
"movie_count",
|
"group_count",
|
||||||
"performer_count",
|
"performer_count",
|
||||||
],
|
],
|
||||||
Studio: ["scene_count", "performer_count"],
|
Studio: ["scene_count", "performer_count"],
|
||||||
@@ -500,7 +500,7 @@ const sceneMutationImpactedTypeFields = {
|
|||||||
|
|
||||||
const sceneMutationImpactedQueries = [
|
const sceneMutationImpactedQueries = [
|
||||||
GQL.FindScenesDocument, // various filters
|
GQL.FindScenesDocument, // various filters
|
||||||
GQL.FindMoviesDocument, // is missing scenes
|
GQL.FindGroupsDocument, // is missing scenes
|
||||||
GQL.FindGalleriesDocument, // is missing scenes
|
GQL.FindGalleriesDocument, // is missing scenes
|
||||||
GQL.FindPerformersDocument, // filter by scene count
|
GQL.FindPerformersDocument, // filter by scene count
|
||||||
GQL.FindStudiosDocument, // filter by scene count
|
GQL.FindStudiosDocument, // filter by scene count
|
||||||
@@ -1273,98 +1273,98 @@ export const mutateImageSetPrimaryFile = (id: string, fileID: string) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const movieMutationImpactedTypeFields = {
|
const groupMutationImpactedTypeFields = {
|
||||||
Performer: ["movie_count"],
|
Performer: ["group_count"],
|
||||||
Studio: ["movie_count"],
|
Studio: ["group_count"],
|
||||||
};
|
};
|
||||||
|
|
||||||
const movieMutationImpactedQueries = [
|
const groupMutationImpactedQueries = [
|
||||||
GQL.FindMoviesDocument, // various filters
|
GQL.FindGroupsDocument, // various filters
|
||||||
];
|
];
|
||||||
|
|
||||||
export const useMovieCreate = () =>
|
export const useGroupCreate = () =>
|
||||||
GQL.useMovieCreateMutation({
|
GQL.useGroupCreateMutation({
|
||||||
update(cache, result) {
|
update(cache, result) {
|
||||||
const movie = result.data?.movieCreate;
|
const group = result.data?.groupCreate;
|
||||||
if (!movie) return;
|
if (!group) return;
|
||||||
|
|
||||||
// update stats
|
// update stats
|
||||||
updateStats(cache, "movie_count", 1);
|
updateStats(cache, "group_count", 1);
|
||||||
|
|
||||||
evictTypeFields(cache, movieMutationImpactedTypeFields);
|
evictTypeFields(cache, groupMutationImpactedTypeFields);
|
||||||
evictQueries(cache, movieMutationImpactedQueries);
|
evictQueries(cache, groupMutationImpactedQueries);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useMovieUpdate = () =>
|
export const useGroupUpdate = () =>
|
||||||
GQL.useMovieUpdateMutation({
|
GQL.useGroupUpdateMutation({
|
||||||
update(cache, result) {
|
update(cache, result) {
|
||||||
if (!result.data?.movieUpdate) return;
|
if (!result.data?.groupUpdate) return;
|
||||||
|
|
||||||
evictTypeFields(cache, movieMutationImpactedTypeFields);
|
evictTypeFields(cache, groupMutationImpactedTypeFields);
|
||||||
evictQueries(cache, movieMutationImpactedQueries);
|
evictQueries(cache, groupMutationImpactedQueries);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useBulkMovieUpdate = (input: GQL.BulkMovieUpdateInput) =>
|
export const useBulkGroupUpdate = (input: GQL.BulkGroupUpdateInput) =>
|
||||||
GQL.useBulkMovieUpdateMutation({
|
GQL.useBulkGroupUpdateMutation({
|
||||||
variables: { input },
|
variables: { input },
|
||||||
update(cache, result) {
|
update(cache, result) {
|
||||||
if (!result.data?.bulkMovieUpdate) return;
|
if (!result.data?.bulkGroupUpdate) return;
|
||||||
|
|
||||||
evictTypeFields(cache, movieMutationImpactedTypeFields);
|
evictTypeFields(cache, groupMutationImpactedTypeFields);
|
||||||
evictQueries(cache, movieMutationImpactedQueries);
|
evictQueries(cache, groupMutationImpactedQueries);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useMovieDestroy = (input: GQL.MovieDestroyInput) =>
|
export const useGroupDestroy = (input: GQL.GroupDestroyInput) =>
|
||||||
GQL.useMovieDestroyMutation({
|
GQL.useGroupDestroyMutation({
|
||||||
variables: input,
|
variables: input,
|
||||||
update(cache, result) {
|
update(cache, result) {
|
||||||
if (!result.data?.movieDestroy) return;
|
if (!result.data?.groupDestroy) return;
|
||||||
|
|
||||||
const obj = { __typename: "Movie", id: input.id };
|
const obj = { __typename: "Group", id: input.id };
|
||||||
deleteObject(cache, obj, GQL.FindMovieDocument);
|
deleteObject(cache, obj, GQL.FindGroupDocument);
|
||||||
|
|
||||||
// update stats
|
// update stats
|
||||||
updateStats(cache, "movie_count", -1);
|
updateStats(cache, "group_count", -1);
|
||||||
|
|
||||||
evictTypeFields(cache, {
|
evictTypeFields(cache, {
|
||||||
Scene: ["movies"],
|
Scene: ["groups"],
|
||||||
Performer: ["movie_count"],
|
Performer: ["group_count"],
|
||||||
Studio: ["movie_count"],
|
Studio: ["group_count"],
|
||||||
});
|
});
|
||||||
evictQueries(cache, [
|
evictQueries(cache, [
|
||||||
...movieMutationImpactedQueries,
|
...groupMutationImpactedQueries,
|
||||||
GQL.FindScenesDocument, // filter by movie
|
GQL.FindScenesDocument, // filter by group
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useMoviesDestroy = (input: GQL.MoviesDestroyMutationVariables) =>
|
export const useGroupsDestroy = (input: GQL.GroupsDestroyMutationVariables) =>
|
||||||
GQL.useMoviesDestroyMutation({
|
GQL.useGroupsDestroyMutation({
|
||||||
variables: input,
|
variables: input,
|
||||||
update(cache, result) {
|
update(cache, result) {
|
||||||
if (!result.data?.moviesDestroy) return;
|
if (!result.data?.groupsDestroy) return;
|
||||||
|
|
||||||
const { ids } = input;
|
const { ids } = input;
|
||||||
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const obj = { __typename: "Movie", id };
|
const obj = { __typename: "Group", id };
|
||||||
deleteObject(cache, obj, GQL.FindMovieDocument);
|
deleteObject(cache, obj, GQL.FindGroupDocument);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update stats
|
// update stats
|
||||||
updateStats(cache, "movie_count", -ids.length);
|
updateStats(cache, "group_count", -ids.length);
|
||||||
|
|
||||||
evictTypeFields(cache, {
|
evictTypeFields(cache, {
|
||||||
Scene: ["movies"],
|
Scene: ["groups"],
|
||||||
Performer: ["movie_count"],
|
Performer: ["group_count"],
|
||||||
Studio: ["movie_count"],
|
Studio: ["group_count"],
|
||||||
});
|
});
|
||||||
evictQueries(cache, [
|
evictQueries(cache, [
|
||||||
...movieMutationImpactedQueries,
|
...groupMutationImpactedQueries,
|
||||||
GQL.FindScenesDocument, // filter by movie
|
GQL.FindScenesDocument, // filter by group
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1678,7 +1678,7 @@ export const usePerformerDestroy = () =>
|
|||||||
evictQueries(cache, [
|
evictQueries(cache, [
|
||||||
...performerMutationImpactedQueries,
|
...performerMutationImpactedQueries,
|
||||||
GQL.FindPerformersDocument, // appears with
|
GQL.FindPerformersDocument, // appears with
|
||||||
GQL.FindMoviesDocument, // filter by performers
|
GQL.FindGroupsDocument, // filter by performers
|
||||||
GQL.FindSceneMarkersDocument, // filter by performers
|
GQL.FindSceneMarkersDocument, // filter by performers
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
@@ -1718,7 +1718,7 @@ export const usePerformersDestroy = (
|
|||||||
evictQueries(cache, [
|
evictQueries(cache, [
|
||||||
...performerMutationImpactedQueries,
|
...performerMutationImpactedQueries,
|
||||||
GQL.FindPerformersDocument, // appears with
|
GQL.FindPerformersDocument, // appears with
|
||||||
GQL.FindMoviesDocument, // filter by performers
|
GQL.FindGroupsDocument, // filter by performers
|
||||||
GQL.FindSceneMarkersDocument, // filter by performers
|
GQL.FindSceneMarkersDocument, // filter by performers
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
@@ -1731,7 +1731,7 @@ const studioMutationImpactedTypeFields = {
|
|||||||
export const studioMutationImpactedQueries = [
|
export const studioMutationImpactedQueries = [
|
||||||
GQL.FindScenesDocument, // filter by studio
|
GQL.FindScenesDocument, // filter by studio
|
||||||
GQL.FindImagesDocument, // filter by studio
|
GQL.FindImagesDocument, // filter by studio
|
||||||
GQL.FindMoviesDocument, // filter by studio
|
GQL.FindGroupsDocument, // filter by studio
|
||||||
GQL.FindGalleriesDocument, // filter by studio
|
GQL.FindGalleriesDocument, // filter by studio
|
||||||
GQL.FindPerformersDocument, // filter by studio
|
GQL.FindPerformersDocument, // filter by studio
|
||||||
GQL.FindStudiosDocument, // various filters
|
GQL.FindStudiosDocument, // various filters
|
||||||
@@ -2161,11 +2161,11 @@ export const mutateStashBoxBatchStudioTag = (
|
|||||||
variables: { input },
|
variables: { input },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useListMovieScrapers = () => GQL.useListMovieScrapersQuery();
|
export const useListGroupScrapers = () => GQL.useListGroupScrapersQuery();
|
||||||
|
|
||||||
export const queryScrapeMovieURL = (url: string) =>
|
export const queryScrapeGroupURL = (url: string) =>
|
||||||
client.query<GQL.ScrapeMovieUrlQuery>({
|
client.query<GQL.ScrapeGroupUrlQuery>({
|
||||||
query: GQL.ScrapeMovieUrlDocument,
|
query: GQL.ScrapeGroupUrlDocument,
|
||||||
variables: { url },
|
variables: { url },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
@@ -2261,7 +2261,7 @@ export const useLoggingSubscribe = () => GQL.useLoggingSubscribeSubscription();
|
|||||||
|
|
||||||
// all scraper-related queries
|
// all scraper-related queries
|
||||||
export const scraperMutationImpactedQueries = [
|
export const scraperMutationImpactedQueries = [
|
||||||
GQL.ListMovieScrapersDocument,
|
GQL.ListGroupScrapersDocument,
|
||||||
GQL.ListPerformerScrapersDocument,
|
GQL.ListPerformerScrapersDocument,
|
||||||
GQL.ListSceneScrapersDocument,
|
GQL.ListSceneScrapersDocument,
|
||||||
GQL.InstalledScraperPackagesDocument,
|
GQL.InstalledScraperPackagesDocument,
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export function generateDefaultFrontPageContent(intl: IntlShape) {
|
|||||||
return [
|
return [
|
||||||
recentlyReleased(intl, FilterMode.Scenes, "scenes"),
|
recentlyReleased(intl, FilterMode.Scenes, "scenes"),
|
||||||
recentlyAdded(intl, FilterMode.Studios, "studios"),
|
recentlyAdded(intl, FilterMode.Studios, "studios"),
|
||||||
recentlyReleased(intl, FilterMode.Movies, "groups"),
|
recentlyReleased(intl, FilterMode.Groups, "groups"),
|
||||||
recentlyAdded(intl, FilterMode.Performers, "performers"),
|
recentlyAdded(intl, FilterMode.Performers, "performers"),
|
||||||
recentlyReleased(intl, FilterMode.Galleries, "galleries"),
|
recentlyReleased(intl, FilterMode.Galleries, "galleries"),
|
||||||
];
|
];
|
||||||
@@ -156,8 +156,8 @@ export function generatePremadeFrontPageContent(intl: IntlShape) {
|
|||||||
recentlyReleased(intl, FilterMode.Galleries, "galleries"),
|
recentlyReleased(intl, FilterMode.Galleries, "galleries"),
|
||||||
recentlyAdded(intl, FilterMode.Galleries, "galleries"),
|
recentlyAdded(intl, FilterMode.Galleries, "galleries"),
|
||||||
recentlyAdded(intl, FilterMode.Images, "images"),
|
recentlyAdded(intl, FilterMode.Images, "images"),
|
||||||
recentlyReleased(intl, FilterMode.Movies, "groups"),
|
recentlyReleased(intl, FilterMode.Groups, "groups"),
|
||||||
recentlyAdded(intl, FilterMode.Movies, "groups"),
|
recentlyAdded(intl, FilterMode.Groups, "groups"),
|
||||||
recentlyAdded(intl, FilterMode.Studios, "studios"),
|
recentlyAdded(intl, FilterMode.Studios, "studios"),
|
||||||
recentlyAdded(intl, FilterMode.Performers, "performers"),
|
recentlyAdded(intl, FilterMode.Performers, "performers"),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ const typePolicies: TypePolicies = {
|
|||||||
findStudio: {
|
findStudio: {
|
||||||
read: readReference("Studio"),
|
read: readReference("Studio"),
|
||||||
},
|
},
|
||||||
findMovie: {
|
findGroup: {
|
||||||
read: readReference("Movie"),
|
read: readReference("Group"),
|
||||||
},
|
},
|
||||||
findGallery: {
|
findGallery: {
|
||||||
read: readReference("Gallery"),
|
read: readReference("Gallery"),
|
||||||
@@ -80,7 +80,7 @@ const typePolicies: TypePolicies = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Movie: {
|
Group: {
|
||||||
fields: {
|
fields: {
|
||||||
studio: {
|
studio: {
|
||||||
read: readDanglingNull,
|
read: readDanglingNull,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
|
|
||||||
export const scrapedGroupToCreateInput = (toCreate: GQL.ScrapedMovie) => {
|
export const scrapedGroupToCreateInput = (toCreate: GQL.ScrapedGroup) => {
|
||||||
const input: GQL.MovieCreateInput = {
|
const input: GQL.GroupCreateInput = {
|
||||||
name: toCreate.name ?? "",
|
name: toCreate.name ?? "",
|
||||||
url: toCreate.url,
|
urls: toCreate.urls,
|
||||||
aliases: toCreate.aliases,
|
aliases: toCreate.aliases,
|
||||||
front_image: toCreate.front_image,
|
front_image: toCreate.front_image,
|
||||||
back_image: toCreate.back_image,
|
back_image: toCreate.back_image,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const StudioIsMissingCriterionOption = new IsMissingCriterionOption(
|
|||||||
["image", "stash_id", "details"]
|
["image", "stash_id", "details"]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const MovieIsMissingCriterionOption = new IsMissingCriterionOption(
|
export const GroupIsMissingCriterionOption = new IsMissingCriterionOption(
|
||||||
"isMissing",
|
"isMissing",
|
||||||
"is_missing",
|
"is_missing",
|
||||||
["front_image", "back_image", "scenes"]
|
["front_image", "back_image", "scenes"]
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
|
|||||||
|
|
||||||
const inputType = "groups";
|
const inputType = "groups";
|
||||||
|
|
||||||
export const MoviesCriterionOption = new ILabeledIdCriterionOption(
|
export const GroupsCriterionOption = new ILabeledIdCriterionOption(
|
||||||
|
"groups",
|
||||||
"groups",
|
"groups",
|
||||||
"movies",
|
|
||||||
false,
|
false,
|
||||||
inputType,
|
inputType,
|
||||||
() => new MoviesCriterion()
|
() => new GroupsCriterion()
|
||||||
);
|
);
|
||||||
|
|
||||||
export class MoviesCriterion extends ILabeledIdCriterion {
|
export class GroupsCriterion extends ILabeledIdCriterion {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(MoviesCriterionOption);
|
super(GroupsCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user