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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
108 changed files with 1437 additions and 567 deletions

View File

@ -51,6 +51,11 @@ models:
fieldName: DurationFinite
frame_rate:
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
BlobsStorageType:
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType

View File

@ -77,13 +77,22 @@ type Query {
): FindStudiosResultType!
"Find a movie by ID"
findMovie(id: ID!): Movie
findMovie(id: ID!): Movie @deprecated(reason: "Use findGroup instead")
"A function which queries Movie objects"
findMovies(
movie_filter: MovieFilterType
filter: FindFilterType
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
findGalleries(
@ -156,7 +165,13 @@ type Query {
scrapeSingleMovie(
source: ScraperSourceInput!
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"
scrapeURL(url: String!, ty: ScrapeContentType!): ScrapedContent
@ -169,6 +184,9 @@ type Query {
scrapeGalleryURL(url: String!): ScrapedGallery
"Scrapes a complete movie record based on a URL"
scrapeMovieURL(url: String!): ScrapedMovie
@deprecated(reason: "Use scrapeGroupURL instead")
"Scrapes a complete group record based on a URL"
scrapeGroupURL(url: String!): ScrapedGroup
# Plugins
"List loaded plugins"
@ -214,7 +232,7 @@ type Query {
allPerformers: [Performer!]!
allTags: [Tag!]! @deprecated(reason: "Use findTags 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
@ -316,10 +334,21 @@ type Mutation {
studiosDestroy(ids: [ID!]!): Boolean!
movieCreate(input: MovieCreateInput!): Movie
@deprecated(reason: "Use groupCreate instead")
movieUpdate(input: MovieUpdateInput!): Movie
@deprecated(reason: "Use groupUpdate instead")
movieDestroy(input: MovieDestroyInput!): Boolean!
@deprecated(reason: "Use groupDestroy instead")
moviesDestroy(ids: [ID!]!): Boolean!
@deprecated(reason: "Use groupsDestroy instead")
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
tagUpdate(input: TagUpdateInput!): Tag

View File

@ -257,7 +257,9 @@ input SceneFilterType {
"Filter to only include scenes with this studio"
studios: HierarchicalMultiCriterionInput
"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"
galleries: MultiCriterionInput
"Filter to only include scenes with these tags"
@ -309,6 +311,9 @@ input SceneFilterType {
tags_filter: TagFilterType
"Filter by related movies that meet this criteria"
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"
markers_filter: SceneMarkerFilterType
}
@ -351,6 +356,44 @@ input MovieFilterType {
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 {
AND: StudioFilterType
OR: StudioFilterType
@ -508,6 +551,9 @@ input TagFilterType {
"Filter by number of movies with this tag"
movie_count: IntCriterionInput
"Filter by number of group with this tag"
group_count: IntCriterionInput
"Filter by number of markers with this tag"
marker_count: IntCriterionInput
@ -702,6 +748,7 @@ enum FilterMode {
GALLERIES
SCENE_MARKERS
MOVIES
GROUPS
TAGS
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
performers: ExportObjectTypeInput
tags: ExportObjectTypeInput
movies: ExportObjectTypeInput
groups: ExportObjectTypeInput
movies: ExportObjectTypeInput @deprecated(reason: "Use groups instead")
galleries: ExportObjectTypeInput
includeDependencies: Boolean
}

View File

@ -42,7 +42,8 @@ type Performer {
scene_count: Int! # Resolver
image_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
o_counter: Int # Resolver
scenes: [Scene!]!
@ -55,7 +56,8 @@ type Performer {
weight: Int
created_at: Time!
updated_at: Time!
movies: [Movie!]!
groups: [Group!]! @deprecated(reason: "use groups instead")
movies: [Movie!]! @deprecated(reason: "use groups instead")
}
input PerformerCreateInput {

View File

@ -26,6 +26,11 @@ type SceneMovie {
scene_index: Int
}
type SceneGroup {
group: Group!
scene_index: Int
}
type VideoCaption {
language_code: String!
caption_type: String!
@ -68,7 +73,8 @@ type Scene {
scene_markers: [SceneMarker!]!
galleries: [Gallery!]!
studio: Studio
movies: [SceneMovie!]!
groups: [SceneGroup!]!
movies: [SceneMovie!]! @deprecated(reason: "Use groups")
tags: [Tag!]!
performers: [Performer!]!
stash_ids: [StashID!]!
@ -82,6 +88,11 @@ input SceneMovieInput {
scene_index: Int
}
input SceneGroupInput {
group_id: ID!
scene_index: Int
}
input SceneCreateInput {
title: String
code: String
@ -96,7 +107,8 @@ input SceneCreateInput {
studio_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
movies: [SceneMovieInput!]
groups: [SceneGroupInput!]
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
cover_image: String
@ -128,7 +140,8 @@ input SceneUpdateInput {
studio_id: ID
gallery_ids: [ID!]
performer_ids: [ID!]
movies: [SceneMovieInput!]
groups: [SceneGroupInput!]
movies: [SceneMovieInput!] @deprecated(reason: "Use groups")
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
cover_image: String
@ -175,7 +188,8 @@ input BulkSceneUpdateInput {
gallery_ids: BulkUpdateIds
performer_ids: BulkUpdateIds
tag_ids: BulkUpdateIds
movie_ids: BulkUpdateIds
group_ids: BulkUpdateIds
movie_ids: BulkUpdateIds @deprecated(reason: "Use group_ids")
}
input SceneDestroyInput {

View File

@ -31,3 +31,35 @@ input ScrapedMovieInput {
synopsis: String
# 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 {
GALLERY
MOVIE
GROUP
PERFORMER
SCENE
}
@ -22,6 +23,7 @@ union ScrapedContent =
| ScrapedScene
| ScrapedGallery
| ScrapedMovie
| ScrapedGroup
| ScrapedPerformer
type ScraperSpec {
@ -40,7 +42,9 @@ type Scraper {
"Details for gallery scraper"
gallery: ScraperSpec
"Details for movie scraper"
movie: ScraperSpec
movie: ScraperSpec @deprecated(reason: "use group")
"Details for group scraper"
group: ScraperSpec
}
type ScrapedStudio {
@ -76,7 +80,8 @@ type ScrapedScene {
studio: ScrapedStudio
tags: [ScrapedTag!]
performers: [ScrapedPerformer!]
movies: [ScrapedMovie!]
movies: [ScrapedMovie!] @deprecated(reason: "use groups")
groups: [ScrapedGroup!]
remote_site_id: String
duration: Int
@ -190,10 +195,19 @@ input ScrapeSingleMovieInput {
query: String
"Instructs to query by movie id"
movie_id: ID
"Instructs to query by gallery fragment"
"Instructs to query by movie fragment"
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 {
"Index of the configured stash-box instance to use"
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")

View File

@ -7,7 +7,8 @@ type StatsResultType {
gallery_count: Int!
performer_count: Int!
studio_count: Int!
movie_count: Int!
group_count: Int!
movie_count: Int! @deprecated(reason: "use group_count instead")
tag_count: Int!
total_o_count: Int!
total_play_duration: Float!

View File

@ -13,7 +13,8 @@ type Studio {
image_count(depth: Int): Int! # Resolver
gallery_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!]!
# rating expressed as 1-100
rating100: Int
@ -21,7 +22,8 @@ type Studio {
details: String
created_at: Time!
updated_at: Time!
movies: [Movie!]!
groups: [Group!]!
movies: [Movie!]! @deprecated(reason: "use groups instead")
}
input StudioCreateInput {

View File

@ -14,7 +14,8 @@ type Tag {
gallery_count(depth: Int): Int! # Resolver
performer_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!]!
children: [Tag!]!

View File

@ -355,6 +355,33 @@ func (t changesetTranslator) relatedMovies(value []models.SceneMovieInput) (mode
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) {
if !t.hasField(field) {
return nil, nil
@ -371,6 +398,22 @@ func (t changesetTranslator) updateMovieIDs(value []models.SceneMovieInput, fiel
}, 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) {
if !t.hasField(field) || value == nil {
return nil, nil

View File

@ -72,9 +72,14 @@ func (r *Resolver) SceneMarker() SceneMarkerResolver {
func (r *Resolver) Studio() StudioResolver {
return &studioResolver{r}
}
func (r *Resolver) Group() GroupResolver {
return &groupResolver{&movieResolver{r}}
}
func (r *Resolver) Movie() MovieResolver {
return &movieResolver{r}
}
func (r *Resolver) Subscription() SubscriptionResolver {
return &subscriptionResolver{r}
}
@ -111,7 +116,11 @@ type sceneResolver struct{ *Resolver }
type sceneMarkerResolver struct{ *Resolver }
type imageResolver struct{ *Resolver }
type studioResolver struct{ *Resolver }
// group is movie under the hood
type movieResolver struct{ *Resolver }
type groupResolver struct{ *movieResolver }
type tagResolver struct{ *Resolver }
type galleryFileResolver struct{ *Resolver }
type videoFileResolver struct{ *Resolver }
@ -218,7 +227,7 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
return err
}
moviesCount, err := movieQB.Count(ctx)
groupsCount, err := movieQB.Count(ctx)
if err != nil {
return err
}
@ -262,7 +271,8 @@ func (r *queryResolver) Stats(ctx context.Context) (*StatsResultType, error) {
GalleryCount: galleryCount,
PerformerCount: performersCount,
StudioCount: studiosCount,
MovieCount: moviesCount,
GroupCount: groupsCount,
MovieCount: groupsCount,
TagCount: tagsCount,
TotalOCount: totalOCount,
TotalPlayDuration: totalPlayDuration,

View File

@ -179,7 +179,7 @@ func (r *performerResolver) GalleryCount(ctx context.Context, obj *models.Perfor
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 {
ret, err = r.repository.Movie.CountByPerformerID(ctx, obj.ID)
return err
@ -190,6 +190,11 @@ func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performe
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) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
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
}
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 {
ret, err = r.repository.Movie.FindByPerformerID(ctx, obj.ID)
return err
@ -262,3 +267,8 @@ func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (
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
}
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) {
if !obj.TagIDs.Loaded() {
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
}
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 {
ret, err = movie.CountByStudioID(ctx, r.repository.Movie, obj.ID, depth)
return err
@ -109,6 +109,11 @@ func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, dep
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) {
if obj.ParentID == nil {
return nil, nil
@ -144,7 +149,7 @@ func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*in
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 {
ret, err = r.repository.Movie.FindByStudioID(ctx, obj.ID)
return err
@ -154,3 +159,8 @@ func (r *studioResolver) Movies(ctx context.Context, obj *models.Studio) (ret []
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
}
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 {
ret, err = movie.CountByTagID(ctx, r.repository.Movie, obj.ID, depth)
return err
@ -131,6 +131,10 @@ func (r *tagResolver) MovieCount(ctx context.Context, obj *models.Tag, depth *in
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) {
var hasImage bool
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
}
// 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)
}
@ -197,6 +199,8 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
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)
}
@ -250,6 +254,8 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
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)
@ -275,6 +281,8 @@ func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyI
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
@ -300,6 +308,8 @@ func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string)
}
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)
}

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)
}
newScene.Movies, err = translator.relatedMovies(input.Movies)
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
// prefer groups over movies
if len(input.Groups) > 0 {
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
@ -216,9 +224,16 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
updatedScene.MovieIDs, err = translator.updateMovieIDs(input.Movies, "movies")
if err != nil {
return nil, fmt.Errorf("converting movies: %w", err)
if translator.hasField("groups") {
updatedScene.MovieIDs, err = translator.updateMovieIDsFromGroups(input.Groups, "groups")
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
@ -358,9 +373,16 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
return nil, fmt.Errorf("converting gallery ids: %w", err)
}
updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(input.MovieIds, "movie_ids")
if err != nil {
return nil, fmt.Errorf("converting movie ids: %w", err)
if translator.hasField("groups") {
updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(input.GroupIds, "group_ids")
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{}

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
}
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) {
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) {
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
images *exportSpec
performers *exportSpec
movies *exportSpec
groups *exportSpec
tags *exportSpec
studios *exportSpec
galleries *exportSpec
@ -63,7 +63,8 @@ type ExportObjectsInput struct {
Studios *ExportObjectTypeInput `json:"studios"`
Performers *ExportObjectTypeInput `json:"performers"`
Tags *ExportObjectTypeInput `json:"tags"`
Movies *ExportObjectTypeInput `json:"movies"`
Groups *ExportObjectTypeInput `json:"groups"`
Movies *ExportObjectTypeInput `json:"movies"` // deprecated
Galleries *ExportObjectTypeInput `json:"galleries"`
IncludeDependencies *bool `json:"includeDependencies"`
}
@ -97,13 +98,19 @@ func CreateExportTask(a models.HashAlgorithm, input ExportObjectsInput) *ExportT
includeDeps = *input.IncludeDependencies
}
// handle deprecated Movies field
groupSpec := input.Groups
if groupSpec == nil && input.Movies != nil {
groupSpec = input.Movies
}
return &ExportTask{
repository: GetInstance().Repository,
fileNamingAlgorithm: a,
scenes: newExportSpec(input.Scenes),
images: newExportSpec(input.Images),
performers: newExportSpec(input.Performers),
movies: newExportSpec(input.Movies),
groups: newExportSpec(groupSpec),
tags: newExportSpec(input.Tags),
studios: newExportSpec(input.Studios),
galleries: newExportSpec(input.Galleries),
@ -282,11 +289,11 @@ func (t *ExportTask) populateMovieScenes(ctx context.Context) {
var movies []*models.Movie
var err error
all := t.full || (t.movies != nil && t.movies.all)
all := t.full || (t.groups != nil && t.groups.all)
if all {
movies, err = reader.All(ctx)
} else if t.movies != nil && len(t.movies.IDs) > 0 {
movies, err = reader.FindMany(ctx, t.movies.IDs)
} else if t.groups != nil && len(t.groups.IDs) > 0 {
movies, err = reader.FindMany(ctx, t.groups.IDs)
}
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)
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))
}
@ -1080,11 +1087,11 @@ func (t *ExportTask) ExportMovies(ctx context.Context, workers int) {
reader := t.repository.Movie
var movies []*models.Movie
var err error
all := t.full || (t.movies != nil && t.movies.all)
all := t.full || (t.groups != nil && t.groups.all)
if all {
movies, err = reader.All(ctx)
} else if t.movies != nil && len(t.movies.IDs) > 0 {
movies, err = reader.FindMany(ctx, t.movies.IDs)
} else if t.groups != nil && len(t.groups.IDs) > 0 {
movies, err = reader.FindMany(ctx, t.groups.IDs)
}
if err != nil {

View File

@ -15,6 +15,7 @@ const (
FilterModeGalleries FilterMode = "GALLERIES"
FilterModeSceneMarkers FilterMode = "SCENE_MARKERS"
FilterModeMovies FilterMode = "MOVIES"
FilterModeGroups FilterMode = "GROUPS"
FilterModeTags FilterMode = "TAGS"
FilterModeImages FilterMode = "IMAGES"
)
@ -25,6 +26,7 @@ var AllFilterMode = []FilterMode{
FilterModeStudios,
FilterModeGalleries,
FilterModeSceneMarkers,
FilterModeGroups,
FilterModeMovies,
FilterModeTags,
FilterModeImages,
@ -32,7 +34,7 @@ var AllFilterMode = []FilterMode{
func (e FilterMode) IsValid() bool {
switch e {
case FilterModeScenes, FilterModePerformers, FilterModeStudios, FilterModeGalleries, FilterModeSceneMarkers, FilterModeMovies, FilterModeTags, FilterModeImages:
case FilterModeScenes, FilterModePerformers, FilterModeStudios, FilterModeGalleries, FilterModeSceneMarkers, FilterModeMovies, FilterModeGroups, FilterModeTags, FilterModeImages:
return true
}
return false

View File

@ -414,3 +414,24 @@ type ScrapedMovie struct {
}
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"`
// Filter to only include scenes with this studio
Studios *HierarchicalMultiCriterionInput `json:"studios"`
// Filter to only include scenes with this group
Groups *MultiCriterionInput `json:"groups"`
// Filter to only include scenes with this movie
Movies *MultiCriterionInput `json:"movies"`
// Filter to only include scenes with this gallery
@ -103,6 +105,8 @@ type SceneFilterType struct {
StudiosFilter *StudioFilterType `json:"studios_filter"`
// Filter by related tags that meet this criteria
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
MoviesFilter *MovieFilterType `json:"movies_filter"`
// Filter by related markers that meet this criteria
@ -131,11 +135,17 @@ type SceneQueryResult struct {
resolveErr error
}
// SceneMovieInput is used for groups and movies
type SceneMovieInput struct {
MovieID string `json:"movie_id"`
SceneIndex *int `json:"scene_index"`
}
type SceneGroupInput struct {
GroupID string `json:"group_id"`
SceneIndex *int `json:"scene_index"`
}
type SceneCreateInput struct {
Title *string `json:"title"`
Code *string `json:"code"`
@ -150,6 +160,7 @@ type SceneCreateInput struct {
GalleryIds []string `json:"gallery_ids"`
PerformerIds []string `json:"performer_ids"`
Movies []SceneMovieInput `json:"movies"`
Groups []SceneGroupInput `json:"groups"`
TagIds []string `json:"tag_ids"`
// This should be a URL or a base64 encoded data URL
CoverImage *string `json:"cover_image"`
@ -177,6 +188,7 @@ type SceneUpdateInput struct {
GalleryIds []string `json:"gallery_ids"`
PerformerIds []string `json:"performer_ids"`
Movies []SceneMovieInput `json:"movies"`
Groups []SceneGroupInput `json:"groups"`
TagIds []string `json:"tag_ids"`
// This should be a URL or a base64 encoded data URL
CoverImage *string `json:"cover_image"`

View File

@ -22,6 +22,8 @@ type TagFilterType struct {
PerformerCount *IntCriterionInput `json:"performer_count"`
// Filter by number of studios with this tag
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
MovieCount *IntCriterionInput `json:"movie_count"`
// Filter by number of markers with this tag

View File

@ -26,10 +26,16 @@ const (
GalleryChapterUpdatePost TriggerEnum = "GalleryChapter.Update.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"
MovieUpdatePost TriggerEnum = "Movie.Update.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"
PerformerUpdatePost TriggerEnum = "Performer.Update.Post"
PerformerDestroyPost TriggerEnum = "Performer.Destroy.Post"

View File

@ -299,6 +299,7 @@ func (c config) spec() Scraper {
if len(movie.SupportedScrapes) > 0 {
ret.Movie = &movie
ret.Group = &movie
}
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
case ScrapeContentTypeGallery:
return c.GalleryByFragment != nil || len(c.GalleryByURL) > 0
case ScrapeContentTypeMovie:
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
return len(c.MovieByURL) > 0
}
@ -339,7 +340,7 @@ func (c config) matchesURL(url string, ty ScrapeContentType) bool {
return true
}
}
case ScrapeContentTypeMovie:
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
for _, scraper := range c.MovieByURL {
if scraper.matchesURL(url) {
return true

View File

@ -81,7 +81,7 @@ func loadUrlCandidates(c config, ty ScrapeContentType) []*scrapeByURLConfig {
return c.PerformerByURL
case ScrapeContentTypeScene:
return c.SceneByURL
case ScrapeContentTypeMovie:
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
return c.MovieByURL
case ScrapeContentTypeGallery:
return c.GalleryByURL

View File

@ -102,7 +102,7 @@ func (s *jsonScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCont
return nil, err
}
return ret, nil
case ScrapeContentTypeMovie:
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
ret, err := scraper.scrapeMovie(ctx, q)
if err != nil || ret == nil {
return nil, err

View File

@ -18,6 +18,7 @@ type ScrapedScene struct {
Studio *models.ScrapedStudio `json:"studio"`
Tags []*models.ScrapedTag `json:"tags"`
Performers []*models.ScrapedPerformer `json:"performers"`
Groups []*models.ScrapedGroup `json:"groups"`
Movies []*models.ScrapedMovie `json:"movies"`
RemoteSiteID *string `json:"remote_site_id"`
Duration *int `json:"duration"`

View File

@ -31,6 +31,7 @@ type ScrapeContentType string
const (
ScrapeContentTypeGallery ScrapeContentType = "GALLERY"
ScrapeContentTypeMovie ScrapeContentType = "MOVIE"
ScrapeContentTypeGroup ScrapeContentType = "GROUP"
ScrapeContentTypePerformer ScrapeContentType = "PERFORMER"
ScrapeContentTypeScene ScrapeContentType = "SCENE"
)
@ -38,13 +39,14 @@ const (
var AllScrapeContentType = []ScrapeContentType{
ScrapeContentTypeGallery,
ScrapeContentTypeMovie,
ScrapeContentTypeGroup,
ScrapeContentTypePerformer,
ScrapeContentTypeScene,
}
func (e ScrapeContentType) IsValid() bool {
switch e {
case ScrapeContentTypeGallery, ScrapeContentTypeMovie, ScrapeContentTypePerformer, ScrapeContentTypeScene:
case ScrapeContentTypeGallery, ScrapeContentTypeMovie, ScrapeContentTypeGroup, ScrapeContentTypePerformer, ScrapeContentTypeScene:
return true
}
return false
@ -81,6 +83,8 @@ type Scraper struct {
// Details for gallery scraper
Gallery *ScraperSpec `json:"gallery"`
// Details for movie scraper
Group *ScraperSpec `json:"group"`
// Details for movie scraper
Movie *ScraperSpec `json:"movie"`
}

View File

@ -384,7 +384,7 @@ func (s *scriptScraper) scrape(ctx context.Context, input string, ty ScrapeConte
var scene *ScrapedScene
err := s.runScraperScript(ctx, input, &scene)
return scene, err
case ScrapeContentTypeMovie:
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
var movie *models.ScrapedMovie
err := s.runScraperScript(ctx, input, &movie)
return movie, err

View File

@ -83,7 +83,7 @@ func (s *xpathScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCon
return nil, err
}
return ret, nil
case ScrapeContentTypeMovie:
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
ret, err := scraper.scrapeMovie(ctx, q)
if err != nil || ret == nil {
return nil, err

View File

@ -1881,7 +1881,7 @@ func TestGalleryQueryIsMissingPerformers(t *testing.T) {
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 {
assert.NotEqual(t, galleryIDs[galleryIdxWithPerformer], gallery.ID)
}

View File

@ -2053,7 +2053,7 @@ func TestImageQueryIsMissingPerformers(t *testing.T) {
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 {
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 {
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) {
// SELECT * FROM %s WHERE mode = ? AND name != ? ORDER BY name ASC
table := qb.table()
sq := qb.selectDataset().Prepared(true).Where(
table.Col("mode").Eq(mode),
table.Col("name").Neq(savedFilterDefaultName),
).Order(table.Col("name").Asc())
// TODO - querying on groups needs to include movies
// remove this when we migrate to remove the movies filter mode in the database
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)
if err != nil {

View File

@ -1074,6 +1074,7 @@ var sceneSortOptions = sortOptions{
"duration",
"file_mod_time",
"framerate",
"group_scene_number",
"id",
"interactive",
"interactive_speed",
@ -1140,7 +1141,7 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
direction := findFilter.GetDirection()
switch sort {
case "movie_scene_number":
case "movie_scene_number", "group_scene_number":
query.join(moviesScenesTable, "", "scenes.id = movies_scenes.scene_id")
query.sortAndPagination += getSort("scene_index", direction, moviesScenesTable)
case "tag_count":

View File

@ -147,7 +147,10 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
qb.performersCriterionHandler(sceneFilter.Performers),
qb.performerCountCriterionHandler(sceneFilter.PerformerCount),
studioCriterionHandler(sceneTable, sceneFilter.Studios),
qb.moviesCriterionHandler(sceneFilter.Movies),
qb.groupsCriterionHandler(sceneFilter.Groups),
qb.groupsCriterionHandler(sceneFilter.Movies),
qb.galleriesCriterionHandler(sceneFilter.Galleries),
qb.performerTagsCriterionHandler(sceneFilter.PerformerTags),
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) {
sceneRepository.movies.join(f, "", "scenes.id")
f.addLeftJoin("movies", "", "movies_scenes.movie_id = movies.id")

View File

@ -278,9 +278,7 @@ const (
)
const (
savedFilterIdxDefaultScene = iota
savedFilterIdxDefaultImage
savedFilterIdxScene
savedFilterIdxScene = iota
savedFilterIdxImage
// new indexes above
@ -1777,9 +1775,9 @@ func createChapter(ctx context.Context, mqb models.GalleryChapterReaderWriter, c
func getSavedFilterMode(index int) models.FilterMode {
switch index {
case savedFilterIdxScene, savedFilterIdxDefaultScene:
case savedFilterIdxScene:
return models.FilterModeScenes
case savedFilterIdxImage, savedFilterIdxDefaultImage:
case savedFilterIdxImage:
return models.FilterModeImages
default:
return models.FilterModeScenes
@ -1787,11 +1785,6 @@ func getSavedFilterMode(index int) models.FilterMode {
}
func getSavedFilterName(index int) string {
if index <= savedFilterIdxDefaultImage {
// empty string for default filters
return ""
}
if index <= savedFilterIdxImage {
// use the same name for the first two - should be possible
return firstSavedFilterName

View File

@ -683,7 +683,7 @@ func (qb *TagStore) getTagSort(query *queryBuilder, findFilter *models.FindFilte
sortQuery += getCountSort(tagTable, performersTagsTable, tagIDColumn, direction)
case "studios_count":
sortQuery += getCountSort(tagTable, studiosTagsTable, tagIDColumn, direction)
case "movies_count":
case "movies_count", "groups_count":
sortQuery += getCountSort(tagTable, moviesTagsTable, tagIDColumn, direction)
default:
sortQuery += getSort(sort, direction, "tags")

View File

@ -67,7 +67,10 @@ func (qb *tagFilterHandler) criterionHandler() criterionHandler {
qb.galleryCountCriterionHandler(tagFilter.GalleryCount),
qb.performerCountCriterionHandler(tagFilter.PerformerCount),
qb.studioCountCriterionHandler(tagFilter.StudioCount),
qb.movieCountCriterionHandler(tagFilter.MovieCount),
qb.groupCountCriterionHandler(tagFilter.GroupCount),
qb.groupCountCriterionHandler(tagFilter.MovieCount),
qb.markerCountCriterionHandler(tagFilter.MarkerCount),
qb.parentsCriterionHandler(tagFilter.Parents),
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) {
if movieCount != nil {
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
name
front_image_path
rating100
}
fragment SelectMovieData on Movie {
fragment SelectGroupData on Group {
id
name
aliases

View File

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

View File

@ -23,7 +23,7 @@ fragment PerformerData on Performer {
scene_count
image_count
gallery_count
movie_count
group_count
performer_count
o_counter

View File

@ -58,8 +58,8 @@ fragment SlimSceneData on Scene {
image_path
}
movies {
movie {
groups {
group {
id
name
front_image_path

View File

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

View File

@ -73,13 +73,13 @@ fragment ScrapedScenePerformerData on ScrapedPerformer {
weight
}
fragment ScrapedMovieStudioData on ScrapedStudio {
fragment ScrapedGroupStudioData on ScrapedStudio {
stored_id
name
url
}
fragment ScrapedMovieData on ScrapedMovie {
fragment ScrapedGroupData on ScrapedGroup {
name
aliases
duration
@ -92,14 +92,14 @@ fragment ScrapedMovieData on ScrapedMovie {
back_image
studio {
...ScrapedMovieStudioData
...ScrapedGroupStudioData
}
tags {
...ScrapedSceneTagData
}
}
fragment ScrapedSceneMovieData on ScrapedMovie {
fragment ScrapedSceneGroupData on ScrapedGroup {
stored_id
name
aliases
@ -113,7 +113,7 @@ fragment ScrapedSceneMovieData on ScrapedMovie {
back_image
studio {
...ScrapedMovieStudioData
...ScrapedGroupStudioData
}
tags {
...ScrapedSceneTagData
@ -173,8 +173,8 @@ fragment ScrapedSceneData on ScrapedScene {
...ScrapedScenePerformerData
}
movies {
...ScrapedSceneMovieData
groups {
...ScrapedSceneGroupData
}
fingerprints {
@ -245,8 +245,8 @@ fragment ScrapedStashBoxSceneData on ScrapedScene {
...ScrapedScenePerformerData
}
movies {
...ScrapedSceneMovieData
groups {
...ScrapedSceneGroupData
}
}

View File

@ -23,8 +23,8 @@ fragment StudioData on Studio {
gallery_count_all: gallery_count(depth: -1)
performer_count
performer_count_all: performer_count(depth: -1)
movie_count
movie_count_all: movie_count(depth: -1)
group_count
group_count_all: group_count(depth: -1)
stash_ids {
stash_id
endpoint

View File

@ -18,8 +18,8 @@ fragment TagData on Tag {
performer_count_all: performer_count(depth: -1)
studio_count
studio_count_all: studio_count(depth: -1)
movie_count
movie_count_all: movie_count(depth: -1)
group_count
group_count_all: group_count(depth: -1)
parents {
...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
performer_count
studio_count
movie_count
group_count
tag_count
total_o_count
total_play_duration

View File

@ -1,27 +1,27 @@
query FindMovies($filter: FindFilterType, $movie_filter: MovieFilterType) {
findMovies(filter: $filter, movie_filter: $movie_filter) {
query FindGroups($filter: FindFilterType, $group_filter: GroupFilterType) {
findGroups(filter: $filter, group_filter: $group_filter) {
count
movies {
...MovieData
groups {
...GroupData
}
}
}
query FindMovie($id: ID!) {
findMovie(id: $id) {
...MovieData
query FindGroup($id: ID!) {
findGroup(id: $id) {
...GroupData
}
}
query FindMoviesForSelect(
query FindGroupsForSelect(
$filter: FindFilterType
$movie_filter: MovieFilterType
$group_filter: GroupFilterType
$ids: [ID!]
) {
findMovies(filter: $filter, movie_filter: $movie_filter, ids: $ids) {
findGroups(filter: $filter, group_filter: $group_filter, ids: $ids) {
count
movies {
...SelectMovieData
groups {
...SelectGroupData
}
}
}

View File

@ -31,11 +31,11 @@ query ListGalleryScrapers {
}
}
query ListMovieScrapers {
listScrapers(types: [MOVIE]) {
query ListGroupScrapers {
listScrapers(types: [GROUP]) {
id
name
movie {
group {
urls
supported_scrapes
}
@ -114,9 +114,9 @@ query ScrapeGalleryURL($url: String!) {
}
}
query ScrapeMovieURL($url: String!) {
scrapeMovieURL(url: $url) {
...ScrapedMovieData
query ScrapeGroupURL($url: String!) {
scrapeGroupURL(url: $url) {
...ScrapedGroupData
}
}

View File

@ -44,6 +44,7 @@ const RecommendationRow: React.FC<IFilter> = ({ mode, filter, header }) => {
/>
);
case GQL.FilterMode.Movies:
case GQL.FilterMode.Groups:
return (
<GroupRecommendationRow
isTouch={isTouch}

View File

@ -23,6 +23,7 @@ const FilterModeToMessageID = {
[GQL.FilterMode.Galleries]: "galleries",
[GQL.FilterMode.Images]: "images",
[GQL.FilterMode.Movies]: "groups",
[GQL.FilterMode.Groups]: "groups",
[GQL.FilterMode.Performers]: "performers",
[GQL.FilterMode.SceneMarkers]: "markers",
[GQL.FilterMode.Scenes]: "scenes",

View File

@ -194,6 +194,7 @@ const FilterModeToConfigKey = {
[FilterMode.Galleries]: "galleries",
[FilterMode.Images]: "images",
[FilterMode.Movies]: "groups",
[FilterMode.Groups]: "groups",
[FilterMode.Performers]: "performers",
[FilterMode.SceneMarkers]: "sceneMarkers",
[FilterMode.Scenes]: "scenes",

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { Form, Col, Row } from "react-bootstrap";
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 { ModalComponent } from "../Shared/Modal";
import { StudioSelect } from "../Shared/Select";
@ -20,7 +20,7 @@ import { isEqual } from "lodash-es";
import { MultiSet } from "../Shared/MultiSet";
interface IListOperationProps {
selected: GQL.MovieDataFragment[];
selected: GQL.GroupDataFragment[];
onClose: (applied: boolean) => void;
}
@ -39,32 +39,32 @@ export const EditGroupsDialog: React.FC<IListOperationProps> = (
const [tagIds, setTagIds] = useState<string[]>();
const [existingTagIds, setExistingTagIds] = useState<string[]>();
const [updateMovies] = useBulkMovieUpdate(getMovieInput());
const [updateGroups] = useBulkGroupUpdate(getGroupInput());
const [isUpdating, setIsUpdating] = useState(false);
function getMovieInput(): GQL.BulkMovieUpdateInput {
function getGroupInput(): GQL.BulkGroupUpdateInput {
const aggregateRating = getAggregateRating(props.selected);
const aggregateStudioId = getAggregateStudioId(props.selected);
const aggregateTagIds = getAggregateTagIds(props.selected);
const movieInput: GQL.BulkMovieUpdateInput = {
ids: props.selected.map((movie) => movie.id),
const groupInput: GQL.BulkGroupUpdateInput = {
ids: props.selected.map((group) => group.id),
director,
};
// if rating is undefined
movieInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
movieInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
movieInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
groupInput.rating100 = getAggregateInputValue(rating100, aggregateRating);
groupInput.studio_id = getAggregateInputValue(studioId, aggregateStudioId);
groupInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
return movieInput;
return groupInput;
}
async function onSave() {
setIsUpdating(true);
try {
await updateMovies();
await updateGroups();
Toast.success(
intl.formatMessage(
{ id: "toast.updated_entity" },
@ -88,26 +88,26 @@ export const EditGroupsDialog: React.FC<IListOperationProps> = (
let updateDirector: string | undefined;
let first = true;
state.forEach((movie: GQL.MovieDataFragment) => {
const movieTagIDs = (movie.tags ?? []).map((p) => p.id).sort();
state.forEach((group: GQL.GroupDataFragment) => {
const groupTagIDs = (group.tags ?? []).map((p) => p.id).sort();
if (first) {
first = false;
updateRating = movie.rating100 ?? undefined;
updateStudioId = movie.studio?.id ?? undefined;
updateTagIds = movieTagIDs;
updateDirector = movie.director ?? undefined;
updateRating = group.rating100 ?? undefined;
updateStudioId = group.studio?.id ?? undefined;
updateTagIds = groupTagIDs;
updateDirector = group.director ?? undefined;
} else {
if (movie.rating100 !== updateRating) {
if (group.rating100 !== updateRating) {
updateRating = undefined;
}
if (movie.studio?.id !== updateStudioId) {
if (group.studio?.id !== updateStudioId) {
updateStudioId = undefined;
}
if (movie.director !== updateDirector) {
if (group.director !== updateDirector) {
updateDirector = undefined;
}
if (!isEqual(movieTagIDs, updateTagIds)) {
if (!isEqual(groupTagIDs, updateTagIds)) {
updateTagIds = [];
}
}

View File

@ -12,7 +12,7 @@ import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
import ScreenUtils from "src/utils/screen";
interface IProps {
group: GQL.MovieDataFragment;
group: GQL.GroupDataFragment;
containerWidth?: number;
sceneIndex?: number;
selecting?: boolean;

View File

@ -4,7 +4,7 @@ import { GroupCard } from "./MovieCard";
import { useContainerDimensions } from "../Shared/GridCard/GridCard";
interface IGroupCardGrid {
groups: GQL.MovieDataFragment[];
groups: GQL.GroupDataFragment[];
selectedIds: Set<string>;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
}

View File

@ -6,9 +6,9 @@ import cx from "classnames";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
import {
useFindMovie,
useMovieUpdate,
useMovieDestroy,
useFindGroup,
useGroupUpdate,
useGroupDestroy,
} from "src/core/StashService";
import { useHistory, RouteComponentProps } from "react-router-dom";
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 { GroupScenesPanel } from "./MovieScenesPanel";
import {
CompressedMovieDetailsPanel,
CompressedGroupDetailsPanel,
GroupDetailsPanel,
} from "./MovieDetailsPanel";
import { GroupEditPanel } from "./MovieEditPanel";
@ -38,7 +38,7 @@ import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { ExternalLinksButton } from "src/components/Shared/ExternalLinksButton";
interface IProps {
group: GQL.MovieDataFragment;
group: GQL.GroupDataFragment;
}
interface IGroupParams {
@ -64,7 +64,7 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
const [isEditing, setIsEditing] = useState<boolean>(false);
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
// Editing movie state
// Editing group state
const [frontImage, setFrontImage] = useState<string | null>();
const [backImage, setBackImage] = useState<string | null>();
const [encodingImage, setEncodingImage] = useState<boolean>(false);
@ -106,8 +106,8 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
images: lightboxImages,
});
const [updateMovie, { loading: updating }] = useMovieUpdate();
const [deleteMovie, { loading: deleting }] = useMovieDestroy({
const [updateGroup, { loading: updating }] = useGroupUpdate();
const [deleteGroup, { loading: deleting }] = useGroupDestroy({
id: group.id,
});
@ -131,8 +131,8 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
setRating
);
async function onSave(input: GQL.MovieCreateInput) {
await updateMovie({
async function onSave(input: GQL.GroupCreateInput) {
await updateGroup({
variables: {
input: {
id: group.id,
@ -151,12 +151,12 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
async function onDelete() {
try {
await deleteMovie();
await deleteGroup();
} catch (e) {
Toast.error(e);
}
// redirect to movies page
// redirect to groups page
history.push(`/groups`);
}
@ -287,7 +287,7 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
function setRating(v: number | null) {
if (group.id) {
updateMovie({
updateGroup({
variables: {
input: {
id: group.id,
@ -343,7 +343,7 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
function maybeRenderCompressedDetails() {
if (!isEditing && loadStickyHeader) {
return <CompressedMovieDetailsPanel group={group} />;
return <CompressedGroupDetailsPanel group={group} />;
}
}
@ -441,16 +441,16 @@ const GroupLoader: React.FC<RouteComponentProps<IGroupParams>> = ({
match,
}) => {
const { id } = match.params;
const { data, loading, error } = useFindMovie(id);
const { data, loading, error } = useFindGroup(id);
useScrollToTopOnMount();
if (loading) return <LoadingIndicator />;
if (error) return <ErrorMessage error={error.message} />;
if (!data?.findMovie)
return <ErrorMessage error={`No movie found with id ${id}.`} />;
if (!data?.findGroup)
return <ErrorMessage error={`No group found with id ${id}.`} />;
return <GroupPage group={data.findMovie} />;
return <GroupPage group={data.findGroup} />;
};
export default GroupLoader;

View File

@ -1,6 +1,6 @@
import React, { useMemo, useState } from "react";
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 { useIntl } from "react-intl";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
@ -18,23 +18,23 @@ const GroupCreate: React.FC = () => {
name: query.get("q") ?? undefined,
};
// Editing movie state
// Editing group state
const [frontImage, setFrontImage] = useState<string | null>();
const [backImage, setBackImage] = useState<string | null>();
const [encodingImage, setEncodingImage] = useState<boolean>(false);
const [createMovie] = useMovieCreate();
const [createGroup] = useGroupCreate();
async function onSave(input: GQL.MovieCreateInput) {
const result = await createMovie({
async function onSave(input: GQL.GroupCreateInput) {
const result = await createGroup({
variables: { input },
});
if (result.data?.movieCreate?.id) {
history.push(`/groups/${result.data.movieCreate.id}`);
if (result.data?.groupCreate?.id) {
history.push(`/groups/${result.data.groupCreate.id}`);
Toast.success(
intl.formatMessage(
{ 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";
interface IGroupDetailsPanel {
group: GQL.MovieDataFragment;
group: GQL.GroupDataFragment;
collapsed?: 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,
}) => {
function scrollToTop() {

View File

@ -4,8 +4,8 @@ import * as GQL from "src/core/generated-graphql";
import * as yup from "yup";
import Mousetrap from "mousetrap";
import {
queryScrapeMovieURL,
useListMovieScrapers,
queryScrapeGroupURL,
useListGroupScrapers,
} from "src/core/StashService";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
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";
interface IGroupEditPanel {
group: Partial<GQL.MovieDataFragment>;
onSubmit: (movie: GQL.MovieCreateInput) => Promise<void>;
group: Partial<GQL.GroupDataFragment>;
onSubmit: (group: GQL.GroupCreateInput) => Promise<void>;
onCancel: () => void;
onDelete: () => void;
setFrontImage: (image?: string | null) => void;
@ -56,8 +56,8 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
const [imageClipboard, setImageClipboard] = useState<string>();
const Scrapers = useListMovieScrapers();
const [scrapedGroup, setScrapedGroup] = useState<GQL.ScrapedMovie>();
const Scrapers = useListGroupScrapers();
const [scrapedGroup, setScrapedGroup] = useState<GQL.ScrapedGroup>();
const [studio, setStudio] = useState<Studio | null>(null);
@ -129,7 +129,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
});
function updateGroupEditStateFromScraper(
state: Partial<GQL.ScrapedMovieDataFragment>
state: Partial<GQL.ScrapedGroupDataFragment>
) {
if (state.name) {
formik.setFieldValue("name", state.name);
@ -190,21 +190,21 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
setIsLoading(false);
}
async function onScrapeMovieURL(url: string) {
async function onScrapeGroupURL(url: string) {
if (!url) return;
setIsLoading(true);
try {
const result = await queryScrapeMovieURL(url);
if (!result.data || !result.data.scrapeMovieURL) {
const result = await queryScrapeGroupURL(url);
if (!result.data || !result.data.scrapeGroupURL) {
return;
}
// if this is a new group, just dump the data
if (isNew) {
updateGroupEditStateFromScraper(result.data.scrapeMovieURL);
updateGroupEditStateFromScraper(result.data.scrapeGroupURL);
} else {
setScrapedGroup(result.data.scrapeMovieURL);
setScrapedGroup(result.data.scrapeGroupURL);
}
} catch (e) {
Toast.error(e);
@ -217,7 +217,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
return (
!!scrapedUrl &&
(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) {
updateGroupEditStateFromScraper(p);
}
@ -381,7 +381,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
<Prompt
when={formik.dirty}
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/"))
return true;
@ -396,7 +396,7 @@ export const GroupEditPanel: React.FC<IGroupEditPanel> = ({
{renderDateField("date")}
{renderStudioField()}
{renderInputField("director")}
{renderURLListField("urls", onScrapeMovieURL, urlScrapable)}
{renderURLListField("urls", onScrapeGroupURL, urlScrapable)}
{renderInputField("synopsis", "textarea")}
{renderTagsField()}
</Form>

View File

@ -1,13 +1,13 @@
import React from "react";
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 { SceneList } from "src/components/Scenes/SceneList";
import { View } from "src/components/List/views";
interface IGroupScenesPanel {
active: boolean;
group: GQL.MovieDataFragment;
group: GQL.GroupDataFragment;
}
export const GroupScenesPanel: React.FC<IGroupScenesPanel> = ({
@ -15,32 +15,32 @@ export const GroupScenesPanel: React.FC<IGroupScenesPanel> = ({
group,
}) => {
function filterHook(filter: ListFilterModel) {
const movieValue = { id: group.id, label: group.name };
// if movie is already present, then we modify it, otherwise add
let movieCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "movies";
}) as MoviesCriterion | undefined;
const groupValue = { id: group.id, label: group.name };
// if group is already present, then we modify it, otherwise add
let groupCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "groups";
}) as GroupsCriterion | undefined;
if (
movieCriterion &&
(movieCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
movieCriterion.modifier === GQL.CriterionModifier.Includes)
groupCriterion &&
(groupCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
groupCriterion.modifier === GQL.CriterionModifier.Includes)
) {
// add the movie if not present
// add the group if not present
if (
!movieCriterion.value.find((p) => {
!groupCriterion.value.find((p) => {
return p.id === group.id;
})
) {
movieCriterion.value.push(movieValue);
groupCriterion.value.push(groupValue);
}
movieCriterion.modifier = GQL.CriterionModifier.IncludesAll;
groupCriterion.modifier = GQL.CriterionModifier.IncludesAll;
} else {
// overwrite
movieCriterion = new MoviesCriterion();
movieCriterion.value = [movieValue];
filter.criteria.push(movieCriterion);
groupCriterion = new GroupsCriterion();
groupCriterion.value = [groupValue];
filter.criteria.push(groupCriterion);
}
return filter;
@ -50,7 +50,7 @@ export const GroupScenesPanel: React.FC<IGroupScenesPanel> = ({
return (
<SceneList
filterHook={filterHook}
defaultSort="movie_scene_number"
defaultSort="group_scene_number"
alterQuery={active}
view={View.GroupScenes}
/>

View File

@ -21,12 +21,12 @@ import { Tag } from "src/components/Tags/TagSelect";
import { useScrapedTags } from "src/components/Shared/ScrapeDialog/scrapedTags";
interface IGroupScrapeDialogProps {
group: Partial<GQL.MovieUpdateInput>;
group: Partial<GQL.GroupUpdateInput>;
groupStudio: Studio | null;
groupTags: Tag[];
scraped: GQL.ScrapedMovie;
scraped: GQL.ScrapedGroup;
onClose: (scrapedMovie?: GQL.ScrapedMovie) => void;
onClose: (scrapedGroup?: GQL.ScrapedGroup) => void;
}
export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
@ -126,7 +126,7 @@ export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
return <></>;
}
function makeNewScrapedItem(): GQL.ScrapedMovie {
function makeNewScrapedItem(): GQL.ScrapedGroup {
const newStudioValue = studio.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 * as GQL from "src/core/generated-graphql";
import {
queryFindMovies,
useFindMovies,
useMoviesDestroy,
queryFindGroups,
useFindGroups,
useGroupsDestroy,
} from "src/core/StashService";
import { makeItemList, showWhenSelected } from "../List/ItemList";
import { ExportDialog } from "../Shared/ExportDialog";
@ -19,13 +19,13 @@ import { EditGroupsDialog } from "./EditMoviesDialog";
import { View } from "../List/views";
const GroupItemList = makeItemList({
filterMode: GQL.FilterMode.Movies,
useResult: useFindMovies,
getItems(result: GQL.FindMoviesQueryResult) {
return result?.data?.findMovies?.movies ?? [];
filterMode: GQL.FilterMode.Groups,
useResult: useFindGroups,
getItems(result: GQL.FindGroupsQueryResult) {
return result?.data?.findGroups?.groups ?? [];
},
getCount(result: GQL.FindMoviesQueryResult) {
return result?.data?.findMovies?.count ?? 0;
getCount(result: GQL.FindGroupsQueryResult) {
return result?.data?.findGroups?.count ?? 0;
},
});
@ -62,7 +62,7 @@ export const GroupList: React.FC<IGroupList> = ({
];
function addKeybinds(
result: GQL.FindMoviesQueryResult,
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
@ -75,21 +75,21 @@ export const GroupList: React.FC<IGroupList> = ({
}
async function viewRandom(
result: GQL.FindMoviesQueryResult,
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel
) {
// query for a random image
if (result.data?.findMovies) {
const { count } = result.data.findMovies;
if (result.data?.findGroups) {
const { count } = result.data.findGroups;
const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindMovies(filterCopy);
if (singleResult.data.findMovies.movies.length === 1) {
const { id } = singleResult.data.findMovies.movies[0];
// navigate to the movie page
const singleResult = await queryFindGroups(filterCopy);
if (singleResult.data.findGroups.groups.length === 1) {
const { id } = singleResult.data.findGroups.groups[0];
// navigate to the group page
history.push(`/groups/${id}`);
}
}
@ -106,7 +106,7 @@ export const GroupList: React.FC<IGroupList> = ({
}
function renderContent(
result: GQL.FindMoviesQueryResult,
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
@ -116,7 +116,7 @@ export const GroupList: React.FC<IGroupList> = ({
return (
<ExportDialog
exportInput={{
movies: {
groups: {
ids: Array.from(selectedIds.values()),
all: isExportAll,
},
@ -128,12 +128,12 @@ export const GroupList: React.FC<IGroupList> = ({
}
function renderGroups() {
if (!result.data?.findMovies) return;
if (!result.data?.findGroups) return;
if (filter.displayMode === DisplayMode.Grid) {
return (
<GroupCardGrid
groups={result.data.findMovies.movies}
groups={result.data.findGroups.groups}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
@ -152,14 +152,14 @@ export const GroupList: React.FC<IGroupList> = ({
}
function renderEditDialog(
selectedGroups: GQL.MovieDataFragment[],
selectedGroups: GQL.GroupDataFragment[],
onClose: (applied: boolean) => void
) {
return <EditGroupsDialog selected={selectedGroups} onClose={onClose} />;
}
function renderDeleteDialog(
selectedGroups: GQL.SlimMovieDataFragment[],
selectedGroups: GQL.SlimGroupDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
@ -168,7 +168,7 @@ export const GroupList: React.FC<IGroupList> = ({
onClose={onClose}
singularEntity={intl.formatMessage({ id: "group" })}
pluralEntity={intl.formatMessage({ id: "groups" })}
destroyMutation={useMoviesDestroy}
destroyMutation={useGroupsDestroy}
/>
);
}

View File

@ -1,6 +1,6 @@
import React from "react";
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 { GroupCard } from "./MovieCard";
import { ListFilterModel } from "src/models/list-filter/filter";
@ -15,8 +15,8 @@ interface IProps {
}
export const GroupRecommendationRow: React.FC<IProps> = (props: IProps) => {
const result = useFindMovies(props.filter);
const cardCount = result.data?.findMovies.count;
const result = useFindGroups(props.filter);
const cardCount = result.data?.findGroups.count;
if (!result.loading && !cardCount) {
return null;
@ -42,8 +42,8 @@ export const GroupRecommendationRow: React.FC<IProps> = (props: IProps) => {
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={`_${i}`} className="group-skeleton skeleton-card"></div>
))
: result.data?.findMovies.movies.map((m) => (
<GroupCard key={m.id} group={m} />
: result.data?.findGroups.groups.map((g) => (
<GroupCard key={g.id} group={g} />
))}
</Slider>
</RecommendationRow>

View File

@ -9,9 +9,9 @@ import cx from "classnames";
import * as GQL from "src/core/generated-graphql";
import {
queryFindMoviesForSelect,
queryFindMoviesByIDForSelect,
useMovieCreate,
queryFindGroupsForSelect,
queryFindGroupsByIDForSelect,
useGroupCreate,
} from "src/core/StashService";
import { ConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl";
@ -31,29 +31,29 @@ import { PatchComponent, PatchFunction } from "src/patch";
import { TruncatedText } from "../Shared/TruncatedText";
export type Group = Pick<
GQL.Movie,
GQL.Group,
"id" | "name" | "date" | "front_image_path" | "aliases"
> & {
studio?: Pick<GQL.Studio, "name"> | null;
};
type Option = SelectOption<Group>;
type FindMoviesResult = Awaited<
ReturnType<typeof queryFindMoviesForSelect>
>["data"]["findMovies"]["movies"];
type FindGroupsResult = Awaited<
ReturnType<typeof queryFindGroupsForSelect>
>["data"]["findGroups"]["groups"];
function sortMoviesByRelevance(input: string, movies: FindMoviesResult) {
function sortGroupsByRelevance(input: string, groups: FindGroupsResult) {
return sortByRelevance(
input,
movies,
groups,
(m) => m.name,
(m) => (m.aliases ? [m.aliases] : [])
);
}
const movieSelectSort = PatchFunction(
"MovieSelect.sort",
sortMoviesByRelevance
const groupSelectSort = PatchFunction(
"GroupSelect.sort",
sortGroupsByRelevance
);
const _GroupSelect: React.FC<
@ -63,7 +63,7 @@ const _GroupSelect: React.FC<
excludeIds?: string[];
}
> = (props) => {
const [createMovie] = useMovieCreate();
const [createGroup] = useGroupCreate();
const { configuration } = React.useContext(ConfigurationContext);
const intl = useIntl();
@ -74,23 +74,23 @@ const _GroupSelect: React.FC<
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
async function loadMovies(input: string): Promise<Option[]> {
const filter = new ListFilterModel(GQL.FilterMode.Movies);
async function loadGroups(input: string): Promise<Option[]> {
const filter = new ListFilterModel(GQL.FilterMode.Groups);
filter.searchTerm = input;
filter.currentPage = 1;
filter.itemsPerPage = maxOptionsShown;
filter.sortBy = "name";
filter.sortDirection = GQL.SortDirectionEnum.Asc;
const query = await queryFindMoviesForSelect(filter);
let ret = query.data.findMovies.movies.filter((movie) => {
const query = await queryFindGroupsForSelect(filter);
let ret = query.data.findGroups.groups.filter((group) => {
// HACK - we should probably exclude these in the backend query, but
// 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) => ({
value: movie.id,
object: movie,
return groupSelectSort(input, ret).map((group) => ({
value: group.id,
object: group,
}));
}
@ -184,12 +184,12 @@ const _GroupSelect: React.FC<
};
const onCreate = async (name: string) => {
const result = await createMovie({
const result = await createGroup({
variables: { input: { name } },
});
return {
value: result.data!.movieCreate!.id,
item: result.data!.movieCreate!,
value: result.data!.groupCreate!.id,
item: result.data!.groupCreate!,
message: "Created group",
};
};
@ -230,7 +230,7 @@ const _GroupSelect: React.FC<
},
props.className
)}
loadOptions={loadMovies}
loadOptions={loadGroups}
getNamedObject={getNamedObject}
isValidNewOption={isValidNewOption}
components={{
@ -273,10 +273,10 @@ const _GroupIDSelect: React.FC<IFilterProps & IFilterIDProps<Group>> = (
}
async function loadObjectsByID(idsToLoad: string[]): Promise<Group[]> {
const query = await queryFindMoviesByIDForSelect(idsToLoad);
const { movies: loadedMovies } = query.data.findMovies;
const query = await queryFindGroupsByIDForSelect(idsToLoad);
const { groups: loadedGroups } = query.data.findGroups;
return loadedMovies;
return loadedGroups;
}
useEffect(() => {

View File

@ -28,7 +28,7 @@ export interface IPerformerCardExtraCriteria {
scenes?: Criterion<CriterionValue>[];
images?: Criterion<CriterionValue>[];
galleries?: Criterion<CriterionValue>[];
movies?: Criterion<CriterionValue>[];
groups?: Criterion<CriterionValue>[];
performer?: ILabeledId;
}
@ -179,17 +179,17 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
}
function maybeRenderGroupsPopoverButton() {
if (!performer.movie_count) return;
if (!performer.group_count) return;
return (
<PopoverCountButton
className="group-count"
type="group"
count={performer.movie_count}
count={performer.group_count}
url={NavUtils.makePerformerGroupsUrl(
performer,
extraCriteria?.performer,
extraCriteria?.movies
extraCriteria?.groups
)}
/>
);
@ -202,7 +202,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
performer.gallery_count ||
performer.tags.length > 0 ||
performer.o_counter ||
performer.movie_count
performer.group_count
) {
return (
<>

View File

@ -145,7 +145,7 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
ret = "galleries";
} else if (performer.image_count != 0) {
ret = "images";
} else if (performer.movie_count != 0) {
} else if (performer.group_count != 0) {
ret = "groups";
}
}
@ -325,7 +325,7 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
{intl.formatMessage({ id: "groups" })}
<Counter
abbreviateCounter={abbreviateCounter}
count={performer.movie_count}
count={performer.group_count}
hideZero
/>
</>

View File

@ -387,23 +387,23 @@ export const SceneDuplicateChecker: React.FC = () => {
}
function maybeRenderGroupPopoverButton(scene: GQL.SlimSceneDataFragment) {
if (scene.movies.length <= 0) return;
if (scene.groups.length <= 0) return;
const popoverContent = scene.movies.map((sceneMovie) => (
<div className="group-tag-container row" key={sceneMovie.movie.id}>
const popoverContent = scene.groups.map((sceneGroup) => (
<div className="group-tag-container row" key={sceneGroup.group.id}>
<Link
to={`/groups/${sceneMovie.movie.id}`}
to={`/groups/${sceneGroup.group.id}`}
className="group-tag col m-auto zoom-2"
>
<img
className="image-thumbnail"
alt={sceneMovie.movie.name ?? ""}
src={sceneMovie.movie.front_image_path ?? ""}
alt={sceneGroup.group.name ?? ""}
src={sceneGroup.group.front_image_path ?? ""}
/>
</Link>
<GroupLink
key={sceneMovie.movie.id}
group={sceneMovie.movie}
key={sceneGroup.group.id}
group={sceneGroup.group}
className="d-block"
/>
</div>
@ -417,7 +417,7 @@ export const SceneDuplicateChecker: React.FC = () => {
>
<Button className="minimal">
<Icon icon={faFilm} />
<span>{scene.movies.length}</span>
<span>{scene.groups.length}</span>
</Button>
</HoverPopover>
);
@ -511,7 +511,7 @@ export const SceneDuplicateChecker: React.FC = () => {
if (
scene.tags.length > 0 ||
scene.performers.length > 0 ||
scene.movies.length > 0 ||
scene.groups.length > 0 ||
scene.scene_markers.length > 0 ||
scene?.o_counter ||
scene.galleries.length > 0 ||

View File

@ -79,7 +79,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
aggregatePerformerIds
);
sceneInput.tag_ids = getAggregateInputIDs(tagMode, tagIds, aggregateTagIds);
sceneInput.movie_ids = getAggregateInputIDs(
sceneInput.group_ids = getAggregateInputIDs(
groupMode,
groupIds,
aggregateGroupIds
@ -126,7 +126,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
.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) {
updateRating = sceneRating ?? undefined;

View File

@ -144,23 +144,23 @@ const SceneCardPopovers = PatchComponent(
}
function maybeRenderGroupPopoverButton() {
if (props.scene.movies.length <= 0) return;
if (props.scene.groups.length <= 0) return;
const popoverContent = props.scene.movies.map((sceneGroup) => (
<div className="group-tag-container row" key={sceneGroup.movie.id}>
const popoverContent = props.scene.groups.map((sceneGroup) => (
<div className="group-tag-container row" key={sceneGroup.group.id}>
<Link
to={`/groups/${sceneGroup.movie.id}`}
to={`/groups/${sceneGroup.group.id}`}
className="group-tag col m-auto zoom-2"
>
<img
className="image-thumbnail"
alt={sceneGroup.movie.name ?? ""}
src={sceneGroup.movie.front_image_path ?? ""}
alt={sceneGroup.group.name ?? ""}
src={sceneGroup.group.front_image_path ?? ""}
/>
</Link>
<GroupLink
key={sceneGroup.movie.id}
group={sceneGroup.movie}
key={sceneGroup.group.id}
group={sceneGroup.group}
className="d-block"
/>
</div>
@ -174,7 +174,7 @@ const SceneCardPopovers = PatchComponent(
>
<Button className="minimal">
<Icon icon={faFilm} />
<span>{props.scene.movies.length}</span>
<span>{props.scene.groups.length}</span>
</Button>
</HoverPopover>
);
@ -279,7 +279,7 @@ const SceneCardPopovers = PatchComponent(
!props.compact &&
(props.scene.tags.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?.o_counter ||
props.scene.galleries.length > 0 ||

View File

@ -441,12 +441,12 @@ const ScenePage: React.FC<IProps> = ({
<FormattedMessage id="markers" />
</Nav.Link>
</Nav.Item>
{scene.movies.length > 0 ? (
{scene.groups.length > 0 ? (
<Nav.Item>
<Nav.Link eventKey="scene-group-panel">
<FormattedMessage
id="countables.groups"
values={{ count: scene.movies.length }}
values={{ count: scene.groups.length }}
/>
</Nav.Link>
</Nav.Item>

View File

@ -104,8 +104,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
}, [scene.performers]);
useEffect(() => {
setGroups(scene.movies?.map((m) => m.movie) ?? []);
}, [scene.movies]);
setGroups(scene.groups?.map((m) => m.group) ?? []);
}, [scene.groups]);
useEffect(() => {
setStudio(scene.studio ?? null);
@ -125,10 +125,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
gallery_ids: yup.array(yup.string().required()).defined(),
studio_id: yup.string().required().nullable(),
performer_ids: yup.array(yup.string().required()).defined(),
movies: yup
groups: yup
.array(
yup.object({
movie_id: yup.string().required(),
group_id: yup.string().required(),
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),
studio_id: scene.studio?.id ?? null,
performer_ids: (scene.performers ?? []).map((p) => p.id),
movies: (scene.movies ?? []).map((m) => {
return { movie_id: m.movie.id, scene_index: m.scene_index ?? null };
groups: (scene.groups ?? []).map((m) => {
return { group_id: m.group.id, scene_index: m.scene_index ?? null };
}),
tag_ids: (scene.tags ?? []).map((t) => t.id),
stash_ids: getStashIDs(scene.stash_ids),
@ -187,16 +187,16 @@ export const SceneEditPanel: React.FC<IProps> = ({
return sceneImage;
}, [formik.values.cover_image, scene.paths?.screenshot]);
const movieEntries = useMemo(() => {
return formik.values.movies
const groupEntries = useMemo(() => {
return formik.values.groups
.map((m) => {
return {
movie: groups.find((mm) => mm.id === m.movie_id),
group: groups.find((mm) => mm.id === m.group_id),
scene_index: m.scene_index,
};
})
.filter((m) => m.movie !== undefined) as IGroupEntry[];
}, [formik.values.movies, groups]);
.filter((m) => m.group !== undefined) as IGroupEntry[];
}, [formik.values.groups, groups]);
function onSetGalleries(items: Gallery[]) {
setGalleries(items);
@ -256,21 +256,21 @@ export const SceneEditPanel: React.FC<IProps> = ({
function onSetGroups(items: Group[]) {
setGroups(items);
const existingMovies = formik.values.movies;
const existingGroups = formik.values.groups;
const newMovies = items.map((m) => {
const existing = existingMovies.find((mm) => mm.movie_id === m.id);
const newGroups = items.map((m) => {
const existing = existingGroups.find((mm) => mm.group_id === m.id);
if (existing) {
return existing;
}
return {
movie_id: m.id,
group_id: m.id,
scene_index: null,
};
});
formik.setFieldValue("movies", newMovies);
formik.setFieldValue("groups", newGroups);
}
async function onSave(input: InputValues) {
@ -568,8 +568,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
}
}
if (updatedScene.movies && updatedScene.movies.length > 0) {
const idMovis = updatedScene.movies.filter((p) => {
if (updatedScene.groups && updatedScene.groups.length > 0) {
const idMovis = updatedScene.groups.filter((p) => {
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);
}
function onSetMovieEntries(input: IGroupEntry[]) {
setGroups(input.map((m) => m.movie));
function onSetGroupEntries(input: IGroupEntry[]) {
setGroups(input.map((m) => m.group));
const newMovies = input.map((m) => ({
movie_id: m.movie.id,
const newGroups = input.map((m) => ({
group_id: m.group.id,
scene_index: m.scene_index,
}));
formik.setFieldValue("movies", newMovies);
formik.setFieldValue("groups", newGroups);
}
function renderMoviesField() {
function renderGroupsField() {
const title = intl.formatMessage({ id: "groups" });
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() {
@ -820,7 +820,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
{renderGalleriesField()}
{renderStudioField()}
{renderPerformersField()}
{renderMoviesField()}
{renderGroupsField()}
{renderTagsField()}
{renderStashIDsField(

View File

@ -9,10 +9,10 @@ interface ISceneGroupPanelProps {
export const SceneGroupPanel: React.FC<ISceneGroupPanelProps> = (
props: ISceneGroupPanelProps
) => {
const cards = props.scene.movies.map((sceneGroup) => (
const cards = props.scene.groups.map((sceneGroup) => (
<GroupCard
key={sceneGroup.movie.id}
group={sceneGroup.movie}
key={sceneGroup.group.id}
group={sceneGroup.group}
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 cx from "classnames";
export type MovieSceneIndexMap = Map<string, number | undefined>;
export type GroupSceneIndexMap = Map<string, number | undefined>;
export interface IGroupEntry {
movie: Group;
group: Group;
scene_index?: GQL.InputMaybe<number> | undefined;
}
@ -22,7 +22,7 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
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 newValues = value.map((existing, i) => {
@ -52,7 +52,7 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
if (i === index) {
return {
...existing,
movie: group,
group: group,
};
}
return existing;
@ -71,7 +71,7 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
const newValues = [
...value,
{
movie: group,
group: group,
scene_index: null,
},
];
@ -83,11 +83,11 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
return (
<>
{value.map((m, i) => (
<Row key={m.movie.id} className="group-row">
<Row key={m.group.id} className="group-row">
<Col xs={9}>
<GroupSelect
onSelect={(items) => onGroupSet(i, items)}
values={[m.movie!]}
values={[m.group!]}
excludeIds={groupIDs}
/>
</Col>

View File

@ -115,20 +115,20 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
);
const [groups, setGroups] = useState<
ObjectListScrapeResult<GQL.ScrapedMovie>
ObjectListScrapeResult<GQL.ScrapedGroup>
>(
new ObjectListScrapeResult<GQL.ScrapedMovie>(
new ObjectListScrapeResult<GQL.ScrapedGroup>(
sortStoredIdObjects(
sceneGroups.map((p) => ({
stored_id: p.id,
name: p.name,
}))
),
sortStoredIdObjects(scraped.movies ?? undefined)
sortStoredIdObjects(scraped.groups ?? undefined)
)
);
const [newGroups, setNewGroups] = useState<GQL.ScrapedMovie[]>(
scraped.movies?.filter((t) => !t.stored_id) ?? []
const [newGroups, setNewGroups] = useState<GQL.ScrapedGroup[]>(
scraped.groups?.filter((t) => !t.stored_id) ?? []
);
const { tags, newTags, scrapedTagsRow } = useScrapedTags(
@ -202,7 +202,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
director: director.getNewValue(),
studio: newStudioValue,
performers: performers.getNewValue(),
movies: groups.getNewValue(),
groups: groups.getNewValue(),
tags: tags.getNewValue(),
details: details.getNewValue(),
image: image.getNewValue(),

View File

@ -126,10 +126,10 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
const GroupCell = (scene: GQL.SlimSceneDataFragment) => (
<ul className="comma-list overflowable">
{scene.movies.map((sceneGroup) => (
<li key={sceneGroup.movie.id}>
<Link to={NavUtils.makeGroupScenesUrl(sceneGroup.movie)}>
<span className="ellips-data">{sceneGroup.movie.name}</span>
{scene.groups.map((sceneGroup) => (
<li key={sceneGroup.group.id}>
<Link to={NavUtils.makeGroupScenesUrl(sceneGroup.group)}>
<span className="ellips-data">{sceneGroup.group.name}</span>
</Link>
</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 {
stored_id: o.movie.id,
name: o.movie.name,
stored_id: o.group.id,
name: o.group.name,
};
}
@ -142,10 +142,10 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
);
const [groups, setGroups] = useState<
ObjectListScrapeResult<GQL.ScrapedMovie>
ObjectListScrapeResult<GQL.ScrapedGroup>
>(
new ObjectListScrapeResult<GQL.ScrapedMovie>(
sortStoredIdObjects(dest.movies.map(groupToStoredID))
new ObjectListScrapeResult<GQL.ScrapedGroup>(
sortStoredIdObjects(dest.groups.map(groupToStoredID))
)
);
@ -253,9 +253,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
);
setGroups(
new ObjectListScrapeResult<GQL.ScrapedMovie>(
sortStoredIdObjects(dest.movies.map(groupToStoredID)),
uniqIDStoredIDs(all.map((s) => s.movies.map(groupToStoredID)).flat())
new ObjectListScrapeResult<GQL.ScrapedGroup>(
sortStoredIdObjects(dest.groups.map(groupToStoredID)),
uniqIDStoredIDs(all.map((s) => s.groups.map(groupToStoredID)).flat())
)
);
@ -585,14 +585,14 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
gallery_ids: galleries.getNewValue(),
studio_id: studio.getNewValue()?.stored_id,
performer_ids: performers.getNewValue()?.map((p) => p.stored_id!),
movies: groups.getNewValue()?.map((m) => {
// find the equivalent movie in the original scenes
groups: groups.getNewValue()?.map((m) => {
// find the equivalent group in the original scenes
const found = all
.map((s) => s.movies)
.map((s) => s.groups)
.flat()
.find((mm) => mm.movie.id === m.stored_id);
.find((mm) => mm.group.id === m.stored_id);
return {
movie_id: m.stored_id!,
group_id: m.stored_id!,
scene_index: found!.scene_index,
};
}),

View File

@ -3,7 +3,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import { Button } from "react-bootstrap";
import {
mutateReloadScrapers,
useListMovieScrapers,
useListGroupScrapers,
useListPerformerScrapers,
useListSceneScrapers,
useListGalleryScrapers,
@ -80,7 +80,7 @@ export const SettingsScrapingPanel: React.FC = () => {
const { data: galleryScrapers, loading: loadingGalleries } =
useListGalleryScrapers();
const { data: groupScrapers, loading: loadingGroups } =
useListMovieScrapers();
useListGroupScrapers();
const { general, scraping, loading, error, saveGeneral, saveScraping } =
useSettings();
@ -251,9 +251,9 @@ export const SettingsScrapingPanel: React.FC = () => {
<tr key={scraper.id}>
<td>{scraper.name}</td>
<td>
{renderGroupScrapeTypes(scraper.movie?.supported_scrapes ?? [])}
{renderGroupScrapeTypes(scraper.group?.supported_scrapes ?? [])}
</td>
<td>{renderURLs(scraper.movie?.urls ?? [])}</td>
<td>{renderURLs(scraper.group?.urls ?? [])}</td>
</tr>
));

View File

@ -197,7 +197,7 @@ export const ScrapedPerformersRow: React.FC<
};
export const ScrapedGroupsRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedMovie>
IScrapedObjectRowImpl<GQL.ScrapedGroup>
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
const groupsCopy = useMemo(() => {
return (
@ -209,9 +209,9 @@ export const ScrapedGroupsRow: React.FC<
}, [newObjects]);
function renderScrapedGroups(
scrapeResult: ScrapeResult<GQL.ScrapedMovie[]>,
scrapeResult: ScrapeResult<GQL.ScrapedGroup[]>,
isNew?: boolean,
onChangeFn?: (value: GQL.ScrapedMovie[]) => void
onChangeFn?: (value: GQL.ScrapedGroup[]) => void
) {
const resultValue = isNew
? scrapeResult.newValue
@ -244,7 +244,7 @@ export const ScrapedGroupsRow: React.FC<
}
return (
<ScrapedObjectsRow<GQL.ScrapedMovie>
<ScrapedObjectsRow<GQL.ScrapedGroup>
title={title}
result={result}
renderObjects={renderScrapedGroups}

View File

@ -1,7 +1,7 @@
import { useToast } from "src/hooks/Toast";
import * as GQL from "src/core/generated-graphql";
import {
useMovieCreate,
useGroupCreate,
usePerformerCreate,
useStudioCreate,
useTagCreate,
@ -124,12 +124,12 @@ export function useCreateScrapedPerformer(
}
export function useCreateScrapedGroup(
props: IUseCreateNewObjectProps<GQL.ScrapedMovie>
props: IUseCreateNewObjectProps<GQL.ScrapedGroup>
) {
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 result = await createGroup({
@ -137,10 +137,10 @@ export function useCreateScrapedGroup(
});
const newValue = [...(scrapeResult.newValue ?? [])];
if (result.data?.movieCreate)
if (result.data?.groupCreate)
newValue.push({
stored_id: result.data.movieCreate.id,
name: result.data.movieCreate.name,
stored_id: result.data.groupCreate.id,
name: result.data.groupCreate.name,
});
// add the new object to the new object value

View File

@ -50,7 +50,7 @@ export const Stats: React.FC = () => {
</div>
<div className="stats-element">
<p className="title">
<FormattedNumber value={data.stats.movie_count} />
<FormattedNumber value={data.stats.group_count} />
</p>
<p className="heading">
<FormattedMessage id="groups" />

View File

@ -143,13 +143,13 @@ export const StudioCard: React.FC<IProps> = ({
}
function maybeRenderGroupsPopoverButton() {
if (!studio.movie_count) return;
if (!studio.group_count) return;
return (
<PopoverCountButton
className="group-count"
type="group"
count={studio.movie_count}
count={studio.group_count}
url={NavUtils.makeStudioGroupsUrl(studio)}
/>
);
@ -190,7 +190,7 @@ export const StudioCard: React.FC<IProps> = ({
studio.scene_count ||
studio.image_count ||
studio.gallery_count ||
studio.movie_count ||
studio.group_count ||
studio.performer_count ||
studio.tags.length > 0
) {

View File

@ -109,7 +109,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
const performerCount =
(showAllCounts ? studio.performer_count_all : studio.performer_count) ?? 0;
const groupCount =
(showAllCounts ? studio.movie_count_all : studio.movie_count) ?? 0;
(showAllCounts ? studio.group_count_all : studio.group_count) ?? 0;
const populatedDefaultTab = useMemo(() => {
let ret: TabKey = "scenes";

View File

@ -25,7 +25,7 @@ export const StudioPerformersPanel: React.FC<IStudioPerformersPanel> = ({
scenes: [studioCriterion],
images: [studioCriterion],
galleries: [studioCriterion],
movies: [studioCriterion],
groups: [studioCriterion],
};
const filterHook = useStudioFilterHook(studio);

View File

@ -237,13 +237,13 @@ export const TagCard: React.FC<IProps> = ({
}
function maybeRenderGroupsPopoverButton() {
if (!tag.movie_count) return;
if (!tag.group_count) return;
return (
<PopoverCountButton
className="group-count"
type="group"
count={tag.movie_count}
count={tag.group_count}
url={NavUtils.makeTagGroupsUrl(tag)}
/>
);

View File

@ -106,7 +106,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
const galleryCount =
(showAllCounts ? tag.gallery_count_all : tag.gallery_count) ?? 0;
const groupCount =
(showAllCounts ? tag.movie_count_all : tag.movie_count) ?? 0;
(showAllCounts ? tag.group_count_all : tag.group_count) ?? 0;
const sceneMarkerCount =
(showAllCounts ? tag.scene_marker_count_all : tag.scene_marker_count) ?? 0;
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 === "";
return GQL.useFindMovieQuery({ variables: { id }, skip });
return GQL.useFindGroupQuery({ variables: { id }, skip });
};
export const useFindMovies = (filter?: ListFilterModel) =>
GQL.useFindMoviesQuery({
export const useFindGroups = (filter?: ListFilterModel) =>
GQL.useFindGroupsQuery({
skip: filter === undefined,
variables: {
filter: filter?.makeFindFilter(),
movie_filter: filter?.makeFilter(),
group_filter: filter?.makeFilter(),
},
});
export const queryFindMovies = (filter: ListFilterModel) =>
client.query<GQL.FindMoviesQuery>({
query: GQL.FindMoviesDocument,
export const queryFindGroups = (filter: ListFilterModel) =>
client.query<GQL.FindGroupsQuery>({
query: GQL.FindGroupsDocument,
variables: {
filter: filter.makeFindFilter(),
movie_filter: filter.makeFilter(),
group_filter: filter.makeFilter(),
},
});
export const queryFindMoviesByIDForSelect = (movieIDs: string[]) =>
client.query<GQL.FindMoviesForSelectQuery>({
query: GQL.FindMoviesForSelectDocument,
export const queryFindGroupsByIDForSelect = (groupIDs: string[]) =>
client.query<GQL.FindGroupsForSelectQuery>({
query: GQL.FindGroupsForSelectDocument,
variables: {
ids: movieIDs,
ids: groupIDs,
},
});
export const queryFindMoviesForSelect = (filter: ListFilterModel) =>
client.query<GQL.FindMoviesForSelectQuery>({
query: GQL.FindMoviesForSelectDocument,
export const queryFindGroupsForSelect = (filter: ListFilterModel) =>
client.query<GQL.FindGroupsForSelectQuery>({
query: GQL.FindGroupsForSelectDocument,
variables: {
filter: filter.makeFindFilter(),
movie_filter: filter.makeFilter(),
group_filter: filter.makeFilter(),
},
});
@ -485,13 +485,13 @@ function updateO(
}
const sceneMutationImpactedTypeFields = {
Movie: ["scenes", "scene_count"],
Group: ["scenes", "scene_count"],
Gallery: ["scenes"],
Performer: [
"scenes",
"scene_count",
"movies",
"movie_count",
"groups",
"group_count",
"performer_count",
],
Studio: ["scene_count", "performer_count"],
@ -500,7 +500,7 @@ const sceneMutationImpactedTypeFields = {
const sceneMutationImpactedQueries = [
GQL.FindScenesDocument, // various filters
GQL.FindMoviesDocument, // is missing scenes
GQL.FindGroupsDocument, // is missing scenes
GQL.FindGalleriesDocument, // is missing scenes
GQL.FindPerformersDocument, // filter by scene count
GQL.FindStudiosDocument, // filter by scene count
@ -1273,98 +1273,98 @@ export const mutateImageSetPrimaryFile = (id: string, fileID: string) =>
},
});
const movieMutationImpactedTypeFields = {
Performer: ["movie_count"],
Studio: ["movie_count"],
const groupMutationImpactedTypeFields = {
Performer: ["group_count"],
Studio: ["group_count"],
};
const movieMutationImpactedQueries = [
GQL.FindMoviesDocument, // various filters
const groupMutationImpactedQueries = [
GQL.FindGroupsDocument, // various filters
];
export const useMovieCreate = () =>
GQL.useMovieCreateMutation({
export const useGroupCreate = () =>
GQL.useGroupCreateMutation({
update(cache, result) {
const movie = result.data?.movieCreate;
if (!movie) return;
const group = result.data?.groupCreate;
if (!group) return;
// update stats
updateStats(cache, "movie_count", 1);
updateStats(cache, "group_count", 1);
evictTypeFields(cache, movieMutationImpactedTypeFields);
evictQueries(cache, movieMutationImpactedQueries);
evictTypeFields(cache, groupMutationImpactedTypeFields);
evictQueries(cache, groupMutationImpactedQueries);
},
});
export const useMovieUpdate = () =>
GQL.useMovieUpdateMutation({
export const useGroupUpdate = () =>
GQL.useGroupUpdateMutation({
update(cache, result) {
if (!result.data?.movieUpdate) return;
if (!result.data?.groupUpdate) return;
evictTypeFields(cache, movieMutationImpactedTypeFields);
evictQueries(cache, movieMutationImpactedQueries);
evictTypeFields(cache, groupMutationImpactedTypeFields);
evictQueries(cache, groupMutationImpactedQueries);
},
});
export const useBulkMovieUpdate = (input: GQL.BulkMovieUpdateInput) =>
GQL.useBulkMovieUpdateMutation({
export const useBulkGroupUpdate = (input: GQL.BulkGroupUpdateInput) =>
GQL.useBulkGroupUpdateMutation({
variables: { input },
update(cache, result) {
if (!result.data?.bulkMovieUpdate) return;
if (!result.data?.bulkGroupUpdate) return;
evictTypeFields(cache, movieMutationImpactedTypeFields);
evictQueries(cache, movieMutationImpactedQueries);
evictTypeFields(cache, groupMutationImpactedTypeFields);
evictQueries(cache, groupMutationImpactedQueries);
},
});
export const useMovieDestroy = (input: GQL.MovieDestroyInput) =>
GQL.useMovieDestroyMutation({
export const useGroupDestroy = (input: GQL.GroupDestroyInput) =>
GQL.useGroupDestroyMutation({
variables: input,
update(cache, result) {
if (!result.data?.movieDestroy) return;
if (!result.data?.groupDestroy) return;
const obj = { __typename: "Movie", id: input.id };
deleteObject(cache, obj, GQL.FindMovieDocument);
const obj = { __typename: "Group", id: input.id };
deleteObject(cache, obj, GQL.FindGroupDocument);
// update stats
updateStats(cache, "movie_count", -1);
updateStats(cache, "group_count", -1);
evictTypeFields(cache, {
Scene: ["movies"],
Performer: ["movie_count"],
Studio: ["movie_count"],
Scene: ["groups"],
Performer: ["group_count"],
Studio: ["group_count"],
});
evictQueries(cache, [
...movieMutationImpactedQueries,
GQL.FindScenesDocument, // filter by movie
...groupMutationImpactedQueries,
GQL.FindScenesDocument, // filter by group
]);
},
});
export const useMoviesDestroy = (input: GQL.MoviesDestroyMutationVariables) =>
GQL.useMoviesDestroyMutation({
export const useGroupsDestroy = (input: GQL.GroupsDestroyMutationVariables) =>
GQL.useGroupsDestroyMutation({
variables: input,
update(cache, result) {
if (!result.data?.moviesDestroy) return;
if (!result.data?.groupsDestroy) return;
const { ids } = input;
for (const id of ids) {
const obj = { __typename: "Movie", id };
deleteObject(cache, obj, GQL.FindMovieDocument);
const obj = { __typename: "Group", id };
deleteObject(cache, obj, GQL.FindGroupDocument);
}
// update stats
updateStats(cache, "movie_count", -ids.length);
updateStats(cache, "group_count", -ids.length);
evictTypeFields(cache, {
Scene: ["movies"],
Performer: ["movie_count"],
Studio: ["movie_count"],
Scene: ["groups"],
Performer: ["group_count"],
Studio: ["group_count"],
});
evictQueries(cache, [
...movieMutationImpactedQueries,
GQL.FindScenesDocument, // filter by movie
...groupMutationImpactedQueries,
GQL.FindScenesDocument, // filter by group
]);
},
});
@ -1678,7 +1678,7 @@ export const usePerformerDestroy = () =>
evictQueries(cache, [
...performerMutationImpactedQueries,
GQL.FindPerformersDocument, // appears with
GQL.FindMoviesDocument, // filter by performers
GQL.FindGroupsDocument, // filter by performers
GQL.FindSceneMarkersDocument, // filter by performers
]);
},
@ -1718,7 +1718,7 @@ export const usePerformersDestroy = (
evictQueries(cache, [
...performerMutationImpactedQueries,
GQL.FindPerformersDocument, // appears with
GQL.FindMoviesDocument, // filter by performers
GQL.FindGroupsDocument, // filter by performers
GQL.FindSceneMarkersDocument, // filter by performers
]);
},
@ -1731,7 +1731,7 @@ const studioMutationImpactedTypeFields = {
export const studioMutationImpactedQueries = [
GQL.FindScenesDocument, // filter by studio
GQL.FindImagesDocument, // filter by studio
GQL.FindMoviesDocument, // filter by studio
GQL.FindGroupsDocument, // filter by studio
GQL.FindGalleriesDocument, // filter by studio
GQL.FindPerformersDocument, // filter by studio
GQL.FindStudiosDocument, // various filters
@ -2161,11 +2161,11 @@ export const mutateStashBoxBatchStudioTag = (
variables: { input },
});
export const useListMovieScrapers = () => GQL.useListMovieScrapersQuery();
export const useListGroupScrapers = () => GQL.useListGroupScrapersQuery();
export const queryScrapeMovieURL = (url: string) =>
client.query<GQL.ScrapeMovieUrlQuery>({
query: GQL.ScrapeMovieUrlDocument,
export const queryScrapeGroupURL = (url: string) =>
client.query<GQL.ScrapeGroupUrlQuery>({
query: GQL.ScrapeGroupUrlDocument,
variables: { url },
fetchPolicy: "network-only",
});
@ -2261,7 +2261,7 @@ export const useLoggingSubscribe = () => GQL.useLoggingSubscribeSubscription();
// all scraper-related queries
export const scraperMutationImpactedQueries = [
GQL.ListMovieScrapersDocument,
GQL.ListGroupScrapersDocument,
GQL.ListPerformerScrapersDocument,
GQL.ListSceneScrapersDocument,
GQL.InstalledScraperPackagesDocument,

View File

@ -143,7 +143,7 @@ export function generateDefaultFrontPageContent(intl: IntlShape) {
return [
recentlyReleased(intl, FilterMode.Scenes, "scenes"),
recentlyAdded(intl, FilterMode.Studios, "studios"),
recentlyReleased(intl, FilterMode.Movies, "groups"),
recentlyReleased(intl, FilterMode.Groups, "groups"),
recentlyAdded(intl, FilterMode.Performers, "performers"),
recentlyReleased(intl, FilterMode.Galleries, "galleries"),
];
@ -156,8 +156,8 @@ export function generatePremadeFrontPageContent(intl: IntlShape) {
recentlyReleased(intl, FilterMode.Galleries, "galleries"),
recentlyAdded(intl, FilterMode.Galleries, "galleries"),
recentlyAdded(intl, FilterMode.Images, "images"),
recentlyReleased(intl, FilterMode.Movies, "groups"),
recentlyAdded(intl, FilterMode.Movies, "groups"),
recentlyReleased(intl, FilterMode.Groups, "groups"),
recentlyAdded(intl, FilterMode.Groups, "groups"),
recentlyAdded(intl, FilterMode.Studios, "studios"),
recentlyAdded(intl, FilterMode.Performers, "performers"),
];

View File

@ -46,8 +46,8 @@ const typePolicies: TypePolicies = {
findStudio: {
read: readReference("Studio"),
},
findMovie: {
read: readReference("Movie"),
findGroup: {
read: readReference("Group"),
},
findGallery: {
read: readReference("Gallery"),
@ -80,7 +80,7 @@ const typePolicies: TypePolicies = {
},
},
},
Movie: {
Group: {
fields: {
studio: {
read: readDanglingNull,

View File

@ -1,10 +1,10 @@
import * as GQL from "src/core/generated-graphql";
import TextUtils from "src/utils/text";
export const scrapedGroupToCreateInput = (toCreate: GQL.ScrapedMovie) => {
const input: GQL.MovieCreateInput = {
export const scrapedGroupToCreateInput = (toCreate: GQL.ScrapedGroup) => {
const input: GQL.GroupCreateInput = {
name: toCreate.name ?? "",
url: toCreate.url,
urls: toCreate.urls,
aliases: toCreate.aliases,
front_image: toCreate.front_image,
back_image: toCreate.back_image,

View File

@ -87,7 +87,7 @@ export const StudioIsMissingCriterionOption = new IsMissingCriterionOption(
["image", "stash_id", "details"]
);
export const MovieIsMissingCriterionOption = new IsMissingCriterionOption(
export const GroupIsMissingCriterionOption = new IsMissingCriterionOption(
"isMissing",
"is_missing",
["front_image", "back_image", "scenes"]

View File

@ -2,16 +2,16 @@ import { ILabeledIdCriterion, ILabeledIdCriterionOption } from "./criterion";
const inputType = "groups";
export const MoviesCriterionOption = new ILabeledIdCriterionOption(
export const GroupsCriterionOption = new ILabeledIdCriterionOption(
"groups",
"groups",
"movies",
false,
inputType,
() => new MoviesCriterion()
() => new GroupsCriterion()
);
export class MoviesCriterion extends ILabeledIdCriterion {
export class GroupsCriterion extends ILabeledIdCriterion {
constructor() {
super(MoviesCriterionOption);
super(GroupsCriterionOption);
}
}

Some files were not shown because too many files have changed in this diff Show More