Add group graphql interfaces (#5017)

* Deprecate movie and add group interfaces
* UI changes
This commit is contained in:
WithoutPants
2024-07-03 13:59:40 +10:00
committed by GitHub
parent f477b996b5
commit 2739696813
108 changed files with 1437 additions and 567 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
} }

View 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!]!
}

View File

@@ -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
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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!

View File

@@ -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 {

View File

@@ -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!]!

View File

@@ -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

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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 {

View 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
}

View File

@@ -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)
} }

View File

@@ -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{}

View 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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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() {}

View File

@@ -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"`

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"`

View File

@@ -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"`
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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)
} }
} }

View File

@@ -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 {

View File

@@ -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":

View File

@@ -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")

View File

@@ -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

View File

@@ -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")

View File

@@ -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")

View File

@@ -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

View File

@@ -1,4 +1,4 @@
fragment MovieData on Movie { fragment GroupData on Group {
id id
name name
aliases aliases

View File

@@ -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

View File

@@ -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

View File

@@ -53,9 +53,9 @@ fragment SceneData on Scene {
...SlimStudioData ...SlimStudioData
} }
movies { groups {
movie { group {
...MovieData ...GroupData
} }
scene_index scene_index
} }

View File

@@ -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
} }
} }

View File

@@ -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

View File

@@ -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

View 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)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
} }
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 = [];
} }
} }

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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() }
) )
); );
} }

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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}
/> />

View File

@@ -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();

View File

@@ -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}
/> />
); );
} }

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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 (
<> <>

View File

@@ -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
/> />
</> </>

View File

@@ -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 ||

View File

@@ -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;

View File

@@ -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 ||

View File

@@ -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>

View File

@@ -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(

View File

@@ -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}
/> />
)); ));

View File

@@ -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>

View File

@@ -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(),

View File

@@ -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>
))} ))}

View File

@@ -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,
}; };
}), }),

View File

@@ -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>
)); ));

View File

@@ -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}

View File

@@ -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

View File

@@ -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" />

View File

@@ -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
) { ) {

View File

@@ -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";

View File

@@ -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);

View File

@@ -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)}
/> />
); );

View File

@@ -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 =

View File

@@ -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,

View File

@@ -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"),
]; ];

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"]

View File

@@ -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