diff --git a/graphql/documents/data/gallery-slim.graphql b/graphql/documents/data/gallery-slim.graphql index 8b73aa3db..51dbc3484 100644 --- a/graphql/documents/data/gallery-slim.graphql +++ b/graphql/documents/data/gallery-slim.graphql @@ -21,7 +21,7 @@ fragment GallerySlimData on Gallery { performers { ...PerformerData } - scene { + scenes { id title path diff --git a/graphql/documents/data/gallery.graphql b/graphql/documents/data/gallery.graphql index 188f08625..7c7fd8e24 100644 --- a/graphql/documents/data/gallery.graphql +++ b/graphql/documents/data/gallery.graphql @@ -24,9 +24,7 @@ fragment GalleryData on Gallery { performers { ...PerformerData } - scene { - id - title - path + scenes { + ...SceneData } } diff --git a/graphql/documents/data/scene-slim.graphql b/graphql/documents/data/scene-slim.graphql index 3d3bf66b7..473012d55 100644 --- a/graphql/documents/data/scene-slim.graphql +++ b/graphql/documents/data/scene-slim.graphql @@ -37,7 +37,7 @@ fragment SlimSceneData on Scene { seconds } - gallery { + galleries { id path title diff --git a/graphql/documents/data/scene.graphql b/graphql/documents/data/scene.graphql index e6ad62f20..1f8061e06 100644 --- a/graphql/documents/data/scene.graphql +++ b/graphql/documents/data/scene.graphql @@ -35,8 +35,8 @@ fragment SceneData on Scene { ...SceneMarkerData } - gallery { - ...GalleryData + galleries { + ...GallerySlimData } studio { diff --git a/graphql/documents/queries/misc.graphql b/graphql/documents/queries/misc.graphql index aad4dcfea..89983d5ba 100644 --- a/graphql/documents/queries/misc.graphql +++ b/graphql/documents/queries/misc.graphql @@ -36,14 +36,6 @@ query AllTagsForFilter { } } -query ValidGalleriesForScene($scene_id: ID!) { - validGalleriesForScene(scene_id: $scene_id) { - id - path - title - } -} - query Stats { stats { scene_count, diff --git a/graphql/documents/queries/scene.graphql b/graphql/documents/queries/scene.graphql index ff02ee4fb..87bb3fd7d 100644 --- a/graphql/documents/queries/scene.graphql +++ b/graphql/documents/queries/scene.graphql @@ -45,7 +45,7 @@ query ParseSceneFilenames($filter: FindFilterType!, $config: SceneParserInput!) date rating studio_id - gallery_id + gallery_ids movies { movie_id } diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index b320a10f8..9364d69a5 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -50,8 +50,6 @@ type Query { """Get marker strings""" markerStrings(q: String, sort: String): [MarkerStringsResultType]! - """Get the list of valid galleries for a given scene ID""" - validGalleriesForScene(scene_id: ID): [Gallery!]! """Get stats""" stats: StatsResultType! """Organize scene markers by tag for a given scene ID""" diff --git a/graphql/schema/types/gallery.graphql b/graphql/schema/types/gallery.graphql index 04fc53805..18301a647 100644 --- a/graphql/schema/types/gallery.graphql +++ b/graphql/schema/types/gallery.graphql @@ -9,7 +9,7 @@ type Gallery { details: String rating: Int organized: Boolean! - scene: Scene + scenes: [Scene!]! studio: Studio image_count: Int! tags: [Tag!]! @@ -33,7 +33,7 @@ input GalleryCreateInput { details: String rating: Int organized: Boolean - scene_id: ID + scene_ids: [ID!] studio_id: ID tag_ids: [ID!] performer_ids: [ID!] @@ -48,7 +48,7 @@ input GalleryUpdateInput { details: String rating: Int organized: Boolean - scene_id: ID + scene_ids: [ID!] studio_id: ID tag_ids: [ID!] performer_ids: [ID!] @@ -62,7 +62,7 @@ input BulkGalleryUpdateInput { details: String rating: Int organized: Boolean - scene_id: ID + scene_ids: BulkUpdateIds studio_id: ID tag_ids: BulkUpdateIds performer_ids: BulkUpdateIds diff --git a/graphql/schema/types/scene.graphql b/graphql/schema/types/scene.graphql index 2372331e5..c72ae17ef 100644 --- a/graphql/schema/types/scene.graphql +++ b/graphql/schema/types/scene.graphql @@ -40,7 +40,7 @@ type Scene { paths: ScenePathsType! # Resolver scene_markers: [SceneMarker!]! - gallery: Gallery + galleries: [Gallery!]! studio: Studio movies: [SceneMovie!]! tags: [Tag!]! @@ -63,7 +63,7 @@ input SceneUpdateInput { rating: Int organized: Boolean studio_id: ID - gallery_id: ID + gallery_ids: [ID!] performer_ids: [ID!] movies: [SceneMovieInput!] tag_ids: [ID!] @@ -93,7 +93,7 @@ input BulkSceneUpdateInput { rating: Int organized: Boolean studio_id: ID - gallery_id: ID + gallery_ids: BulkUpdateIds performer_ids: BulkUpdateIds tag_ids: BulkUpdateIds } @@ -134,7 +134,7 @@ type SceneParserResult { date: String rating: Int studio_id: ID - gallery_id: ID + gallery_ids: [ID!] performer_ids: [ID!] movies: [SceneMovieID!] tag_ids: [ID!] diff --git a/pkg/api/resolver.go b/pkg/api/resolver.go index b253a07df..93972358f 100644 --- a/pkg/api/resolver.go +++ b/pkg/api/resolver.go @@ -120,41 +120,6 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri return ret, nil } -func (r *queryResolver) ValidGalleriesForScene(ctx context.Context, scene_id *string) ([]*models.Gallery, error) { - if scene_id == nil { - panic("nil scene id") // TODO make scene_id mandatory - } - sceneID, err := strconv.Atoi(*scene_id) - if err != nil { - return nil, err - } - - var validGalleries []*models.Gallery - var sceneGallery *models.Gallery - if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { - sqb := repo.Scene() - scene, err := sqb.Find(sceneID) - if err != nil { - return err - } - - qb := repo.Gallery() - validGalleries, err = qb.ValidGalleriesForScenePath(scene.Path) - if err != nil { - return err - } - sceneGallery, err = qb.FindBySceneID(sceneID) - return err - }); err != nil { - return nil, err - } - - if sceneGallery != nil { - validGalleries = append(validGalleries, sceneGallery) - } - return validGalleries, nil -} - func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) { var ret models.StatsResultType if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { diff --git a/pkg/api/resolver_model_gallery.go b/pkg/api/resolver_model_gallery.go index 4b646cd02..a0d5cf7c5 100644 --- a/pkg/api/resolver_model_gallery.go +++ b/pkg/api/resolver_model_gallery.go @@ -90,15 +90,10 @@ func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int return nil, nil } -func (r *galleryResolver) Scene(ctx context.Context, obj *models.Gallery) (ret *models.Scene, err error) { - if !obj.SceneID.Valid { - return nil, nil - } - +func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) { if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { var err error - ret, err = repo.Scene().Find(int(obj.SceneID.Int64)) - + ret, err = repo.Scene().FindByGalleryID(obj.ID) return err }); err != nil { return nil, err diff --git a/pkg/api/resolver_model_scene.go b/pkg/api/resolver_model_scene.go index c81e82489..960c561ff 100644 --- a/pkg/api/resolver_model_scene.go +++ b/pkg/api/resolver_model_scene.go @@ -105,7 +105,7 @@ func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (re return ret, nil } -func (r *sceneResolver) Gallery(ctx context.Context, obj *models.Scene) (ret *models.Gallery, err error) { +func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) { if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { ret, err = repo.Gallery().FindBySceneID(obj.ID) return err diff --git a/pkg/api/resolver_mutation_gallery.go b/pkg/api/resolver_mutation_gallery.go index 68ed48e54..30100fa2e 100644 --- a/pkg/api/resolver_mutation_gallery.go +++ b/pkg/api/resolver_mutation_gallery.go @@ -60,14 +60,6 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.Galle newGallery.StudioID = sql.NullInt64{Valid: false} } - if input.SceneID != nil { - sceneID, _ := strconv.ParseInt(*input.SceneID, 10, 64) - newGallery.SceneID = sql.NullInt64{Int64: sceneID, Valid: true} - } else { - // studio must be nullable - newGallery.SceneID = sql.NullInt64{Valid: false} - } - // Start the transaction and save the gallery var gallery *models.Gallery if err := r.withTxn(ctx, func(repo models.Repository) error { @@ -88,6 +80,11 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.Galle return err } + // Save the scenes + if err := r.updateGalleryScenes(qb, gallery.ID, input.SceneIds); err != nil { + return err + } + return nil }); err != nil { return nil, err @@ -112,6 +109,14 @@ func (r *mutationResolver) updateGalleryTags(qb models.GalleryReaderWriter, gall return qb.UpdateTags(galleryID, ids) } +func (r *mutationResolver) updateGalleryScenes(qb models.GalleryReaderWriter, galleryID int, sceneIDs []string) error { + ids, err := utils.StringSliceToIntSlice(sceneIDs) + if err != nil { + return err + } + return qb.UpdateScenes(galleryID, ids) +} + func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (ret *models.Gallery, err error) { translator := changesetTranslator{ inputMap: getUpdateInputMap(ctx), @@ -221,6 +226,13 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, transl } } + // Save the scenes + if translator.hasField("scene_ids") { + if err := r.updateGalleryScenes(qb, galleryID, input.SceneIds); err != nil { + return nil, err + } + } + return gallery, nil } @@ -241,7 +253,6 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B updatedGallery.Date = translator.sqliteDate(input.Date, "date") updatedGallery.Rating = translator.nullInt64(input.Rating, "rating") updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id") - updatedGallery.SceneID = translator.nullInt64FromString(input.SceneID, "scene_id") updatedGallery.Organized = input.Organized ret := []*models.Gallery{} @@ -284,6 +295,18 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B return err } } + + // Save the scenes + if translator.hasField("scene_ids") { + sceneIDs, err := adjustGallerySceneIDs(qb, galleryID, *input.SceneIds) + if err != nil { + return err + } + + if err := qb.UpdateScenes(galleryID, sceneIDs); err != nil { + return err + } + } } return nil @@ -312,6 +335,15 @@ func adjustGalleryTagIDs(qb models.GalleryReader, galleryID int, ids models.Bulk return adjustIDs(ret, ids), nil } +func adjustGallerySceneIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) { + ret, err = qb.GetSceneIDs(galleryID) + if err != nil { + return nil, err + } + + return adjustIDs(ret, ids), nil +} + func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) { galleryIDs, err := utils.StringSliceToIntSlice(input.Ids) if err != nil { diff --git a/pkg/api/resolver_mutation_scene.go b/pkg/api/resolver_mutation_scene.go index 02b3920d1..5a0ba525f 100644 --- a/pkg/api/resolver_mutation_scene.go +++ b/pkg/api/resolver_mutation_scene.go @@ -101,29 +101,6 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator } } - // Clear the existing gallery value - if translator.hasField("gallery_id") { - gqb := repo.Gallery() - err = gqb.ClearGalleryId(sceneID) - if err != nil { - return nil, err - } - - if input.GalleryID != nil { - // Save the gallery - galleryID, _ := strconv.Atoi(*input.GalleryID) - updatedGallery := models.GalleryPartial{ - ID: galleryID, - SceneID: &sql.NullInt64{Int64: int64(sceneID), Valid: true}, - UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime}, - } - _, err := gqb.UpdatePartial(updatedGallery) - if err != nil { - return nil, err - } - } - } - // Save the performers if translator.hasField("performer_ids") { if err := r.updateScenePerformers(qb, sceneID, input.PerformerIds); err != nil { @@ -145,6 +122,13 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator } } + // Save the galleries + if translator.hasField("gallery_ids") { + if err := r.updateSceneGalleries(qb, sceneID, input.GalleryIds); err != nil { + return nil, err + } + } + // Save the stash_ids if translator.hasField("stash_ids") { stashIDJoins := models.StashIDsFromInput(input.StashIds) @@ -206,6 +190,14 @@ func (r *mutationResolver) updateSceneTags(qb models.SceneReaderWriter, sceneID return qb.UpdateTags(sceneID, ids) } +func (r *mutationResolver) updateSceneGalleries(qb models.SceneReaderWriter, sceneID int, galleryIDs []string) error { + ids, err := utils.StringSliceToIntSlice(galleryIDs) + if err != nil { + return err + } + return qb.UpdateGalleries(sceneID, ids) +} + func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.BulkSceneUpdateInput) ([]*models.Scene, error) { sceneIDs, err := utils.StringSliceToIntSlice(input.Ids) if err != nil { @@ -236,7 +228,6 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul // Start the transaction and save the scene marker if err := r.withTxn(ctx, func(repo models.Repository) error { qb := repo.Scene() - gqb := repo.Gallery() for _, sceneID := range sceneIDs { updatedScene.ID = sceneID @@ -248,20 +239,6 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul ret = append(ret, scene) - if translator.hasField("gallery_id") { - // Save the gallery - galleryID, _ := strconv.Atoi(*input.GalleryID) - updatedGallery := models.GalleryPartial{ - ID: galleryID, - SceneID: &sql.NullInt64{Int64: int64(sceneID), Valid: true}, - UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime}, - } - - if _, err := gqb.UpdatePartial(updatedGallery); err != nil { - return err - } - } - // Save the performers if translator.hasField("performer_ids") { performerIDs, err := adjustScenePerformerIDs(qb, sceneID, *input.PerformerIds) @@ -285,6 +262,18 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul return err } } + + // Save the galleries + if translator.hasField("gallery_ids") { + galleryIDs, err := adjustSceneGalleryIDs(qb, sceneID, *input.GalleryIds) + if err != nil { + return err + } + + if err := qb.UpdateGalleries(sceneID, galleryIDs); err != nil { + return err + } + } } return nil @@ -350,6 +339,15 @@ func adjustSceneTagIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdate return adjustIDs(ret, ids), nil } +func adjustSceneGalleryIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) { + ret, err = qb.GetGalleryIDs(sceneID) + if err != nil { + return nil, err + } + + return adjustIDs(ret, ids), nil +} + func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) { sceneID, err := strconv.Atoi(input.ID) if err != nil { diff --git a/pkg/database/database.go b/pkg/database/database.go index b22ce9e89..84fdcccf9 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -21,7 +21,7 @@ import ( var DB *sqlx.DB var dbPath string -var appSchemaVersion uint = 17 +var appSchemaVersion uint = 18 var databaseSchemaVersion uint const sqlite3Driver = "sqlite3ex" diff --git a/pkg/database/migrations/18_scene_galleries.up.sql b/pkg/database/migrations/18_scene_galleries.up.sql new file mode 100644 index 000000000..f85a4786d --- /dev/null +++ b/pkg/database/migrations/18_scene_galleries.up.sql @@ -0,0 +1,138 @@ +-- recreate the tables referencing galleries to correct their references +ALTER TABLE `galleries` rename to `_galleries_old`; +ALTER TABLE `galleries_images` rename to `_galleries_images_old`; +ALTER TABLE `galleries_tags` rename to `_galleries_tags_old`; +ALTER TABLE `performers_galleries` rename to `_performers_galleries_old`; + +CREATE TABLE `galleries` ( + `id` integer not null primary key autoincrement, + `path` varchar(510), + `checksum` varchar(255) not null, + `zip` boolean not null default '0', + `title` varchar(255), + `url` varchar(255), + `date` date, + `details` text, + `studio_id` integer, + `rating` tinyint, + `file_mod_time` datetime, + `organized` boolean not null default '0', + `created_at` datetime not null, + `updated_at` datetime not null, + foreign key(`studio_id`) references `studios`(`id`) on delete SET NULL +); + +DROP INDEX IF EXISTS `index_galleries_on_scene_id`; +DROP INDEX IF EXISTS `galleries_path_unique`; +DROP INDEX IF EXISTS `galleries_checksum_unique`; +DROP INDEX IF EXISTS `index_galleries_on_studio_id`; + +CREATE UNIQUE INDEX `galleries_path_unique` on `galleries` (`path`); +CREATE UNIQUE INDEX `galleries_checksum_unique` on `galleries` (`checksum`); +CREATE INDEX `index_galleries_on_studio_id` on `galleries` (`studio_id`); + +CREATE TABLE `scenes_galleries` ( + `scene_id` integer, + `gallery_id` integer, + foreign key(`scene_id`) references `scenes`(`id`) on delete CASCADE, + foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE +); + +CREATE INDEX `index_scenes_galleries_on_scene_id` on `scenes_galleries` (`scene_id`); +CREATE INDEX `index_scenes_galleries_on_gallery_id` on `scenes_galleries` (`gallery_id`); + +CREATE TABLE `galleries_images` ( + `gallery_id` integer, + `image_id` integer, + foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE, + foreign key(`image_id`) references `images`(`id`) on delete CASCADE +); + +DROP INDEX IF EXISTS `index_galleries_images_on_image_id`; +DROP INDEX IF EXISTS `index_galleries_images_on_gallery_id`; + +CREATE INDEX `index_galleries_images_on_image_id` on `galleries_images` (`image_id`); +CREATE INDEX `index_galleries_images_on_gallery_id` on `galleries_images` (`gallery_id`); + +CREATE TABLE `performers_galleries` ( + `performer_id` integer, + `gallery_id` integer, + foreign key(`performer_id`) references `performers`(`id`) on delete CASCADE, + foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE +); + +DROP INDEX IF EXISTS `index_performers_galleries_on_gallery_id`; +DROP INDEX IF EXISTS `index_performers_galleries_on_performer_id`; + +CREATE INDEX `index_performers_galleries_on_gallery_id` on `performers_galleries` (`gallery_id`); +CREATE INDEX `index_performers_galleries_on_performer_id` on `performers_galleries` (`performer_id`); + +CREATE TABLE `galleries_tags` ( + `gallery_id` integer, + `tag_id` integer, + foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE, + foreign key(`tag_id`) references `tags`(`id`) on delete CASCADE +); + +DROP INDEX IF EXISTS `index_galleries_tags_on_tag_id`; +DROP INDEX IF EXISTS `index_galleries_tags_on_gallery_id`; + +CREATE INDEX `index_galleries_tags_on_tag_id` on `galleries_tags` (`tag_id`); +CREATE INDEX `index_galleries_tags_on_gallery_id` on `galleries_tags` (`gallery_id`); + +-- populate from the old tables +INSERT INTO `galleries` + ( + `id`, + `path`, + `checksum`, + `zip`, + `title`, + `url`, + `date`, + `details`, + `studio_id`, + `rating`, + `file_mod_time`, + `organized`, + `created_at`, + `updated_at` + ) + SELECT + `id`, + `path`, + `checksum`, + `zip`, + `title`, + `url`, + `date`, + `details`, + `studio_id`, + `rating`, + `file_mod_time`, + `organized`, + `created_at`, + `updated_at` + FROM `_galleries_old`; + +INSERT INTO `scenes_galleries` + ( + `scene_id`, + `gallery_id` + ) + SELECT + `scene_id`, + `id` + FROM `_galleries_old` + WHERE scene_id IS NOT NULL; + +-- these tables are a direct copy +INSERT INTO `galleries_images` SELECT * from `_galleries_images_old`; +INSERT INTO `galleries_tags` SELECT * from `_galleries_tags_old`; +INSERT INTO `performers_galleries` SELECT * from `_performers_galleries_old`; + +-- drop old tables +DROP TABLE `_galleries_old`; +DROP TABLE `_galleries_images_old`; +DROP TABLE `_galleries_tags_old`; +DROP TABLE `_performers_galleries_old`; diff --git a/pkg/gallery/export.go b/pkg/gallery/export.go index c9dd2797d..e8347bb5c 100644 --- a/pkg/gallery/export.go +++ b/pkg/gallery/export.go @@ -74,3 +74,14 @@ func GetIDs(galleries []*models.Gallery) []int { return results } + +func GetChecksums(galleries []*models.Gallery) []string { + var results []string + for _, gallery := range galleries { + if gallery.Checksum != "" { + results = append(results, gallery.Checksum) + } + } + + return results +} diff --git a/pkg/manager/jsonschema/scene.go b/pkg/manager/jsonschema/scene.go index c4e70cc8d..79c466be6 100644 --- a/pkg/manager/jsonschema/scene.go +++ b/pkg/manager/jsonschema/scene.go @@ -46,7 +46,7 @@ type Scene struct { Organized bool `json:"organized,omitempty"` OCounter int `json:"o_counter,omitempty"` Details string `json:"details,omitempty"` - Gallery string `json:"gallery,omitempty"` + Galleries []string `json:"galleries,omitempty"` Performers []string `json:"performers,omitempty"` Movies []SceneMovie `json:"movies,omitempty"` Tags []string `json:"tags,omitempty"` diff --git a/pkg/manager/scene.go b/pkg/manager/scene.go index e5ddb0696..55495da81 100644 --- a/pkg/manager/scene.go +++ b/pkg/manager/scene.go @@ -17,11 +17,6 @@ import ( func DestroyScene(scene *models.Scene, repo models.Repository) (func(), error) { qb := repo.Scene() mqb := repo.SceneMarker() - gqb := repo.Gallery() - - if err := gqb.ClearGalleryId(scene.ID); err != nil { - return nil, err - } markers, err := mqb.FindBySceneID(scene.ID) if err != nil { diff --git a/pkg/manager/task_export.go b/pkg/manager/task_export.go index 011d739b8..dbcef2e46 100644 --- a/pkg/manager/task_export.go +++ b/pkg/manager/task_export.go @@ -382,15 +382,13 @@ func exportScene(wg *sync.WaitGroup, jobChan <-chan *models.Scene, repo models.R continue } - sceneGallery, err := galleryReader.FindBySceneID(s.ID) + galleries, err := galleryReader.FindBySceneID(s.ID) if err != nil { - logger.Errorf("[scenes] <%s> error getting scene gallery: %s", sceneHash, err.Error()) + logger.Errorf("[scenes] <%s> error getting scene gallery checksums: %s", sceneHash, err.Error()) continue } - if sceneGallery != nil { - newSceneJSON.Gallery = sceneGallery.Checksum - } + newSceneJSON.Galleries = gallery.GetChecksums(galleries) performers, err := performerReader.FindBySceneID(s.ID) if err != nil { @@ -423,9 +421,7 @@ func exportScene(wg *sync.WaitGroup, jobChan <-chan *models.Scene, repo models.R t.studios.IDs = utils.IntAppendUnique(t.studios.IDs, int(s.StudioID.Int64)) } - if sceneGallery != nil { - t.galleries.IDs = utils.IntAppendUnique(t.galleries.IDs, sceneGallery.ID) - } + t.galleries.IDs = utils.IntAppendUniques(t.galleries.IDs, gallery.GetIDs(galleries)) tagIDs, err := scene.GetDependentTagIDs(tagReader, sceneMarkerReader, s) if err != nil { diff --git a/pkg/manager/task_scan.go b/pkg/manager/task_scan.go index e3ba74ec0..6616832ff 100644 --- a/pkg/manager/task_scan.go +++ b/pkg/manager/task_scan.go @@ -300,41 +300,26 @@ func (t *ScanTask) associateGallery(wg *sizedwaitgroup.SizedWaitGroup) { return nil } - // gallery has no SceneID - if !g.SceneID.Valid { - basename := strings.TrimSuffix(t.FilePath, filepath.Ext(t.FilePath)) - var relatedFiles []string - vExt := config.GetVideoExtensions() - // make a list of media files that can be related to the gallery - for _, ext := range vExt { - related := basename + "." + ext - // exclude gallery extensions from the related files - if !isGallery(related) { - relatedFiles = append(relatedFiles, related) - } + basename := strings.TrimSuffix(t.FilePath, filepath.Ext(t.FilePath)) + var relatedFiles []string + vExt := config.GetVideoExtensions() + // make a list of media files that can be related to the gallery + for _, ext := range vExt { + related := basename + "." + ext + // exclude gallery extensions from the related files + if !isGallery(related) { + relatedFiles = append(relatedFiles, related) } - for _, scenePath := range relatedFiles { - s, err := sqb.FindByPath(scenePath) - if err != nil { + } + for _, scenePath := range relatedFiles { + scene, _ := sqb.FindByPath(scenePath) + // found related Scene + if scene != nil { + logger.Infof("associate: Gallery %s is related to scene: %d", t.FilePath, scene.ID) + + if err := sqb.UpdateGalleries(scene.ID, []int{g.ID}); err != nil { return err } - - // found related Scene - if s != nil { - logger.Infof("associate: Gallery %s is related to scene: %d", t.FilePath, s.ID) - - g.SceneID.Int64 = int64(s.ID) - g.SceneID.Valid = true - - _, err = qb.Update(*g) - if err != nil { - return fmt.Errorf("associate: Error updating gallery sceneId %s", err) - } - - // since a gallery can have only one related scene - // only first found is associated - break - } } } diff --git a/pkg/models/gallery.go b/pkg/models/gallery.go index f4c1b006d..71f19a666 100644 --- a/pkg/models/gallery.go +++ b/pkg/models/gallery.go @@ -4,15 +4,16 @@ type GalleryReader interface { Find(id int) (*Gallery, error) FindMany(ids []int) ([]*Gallery, error) FindByChecksum(checksum string) (*Gallery, error) + FindByChecksums(checksums []string) ([]*Gallery, error) FindByPath(path string) (*Gallery, error) - FindBySceneID(sceneID int) (*Gallery, error) + FindBySceneID(sceneID int) ([]*Gallery, error) FindByImageID(imageID int) ([]*Gallery, error) - ValidGalleriesForScenePath(scenePath string) ([]*Gallery, error) Count() (int, error) All() ([]*Gallery, error) Query(galleryFilter *GalleryFilterType, findFilter *FindFilterType) ([]*Gallery, int, error) GetPerformerIDs(galleryID int) ([]int, error) GetTagIDs(galleryID int) ([]int, error) + GetSceneIDs(galleryID int) ([]int, error) GetImageIDs(galleryID int) ([]int, error) } @@ -22,9 +23,9 @@ type GalleryWriter interface { UpdatePartial(updatedGallery GalleryPartial) (*Gallery, error) UpdateFileModTime(id int, modTime NullSQLiteTimestamp) error Destroy(id int) error - ClearGalleryId(sceneID int) error UpdatePerformers(galleryID int, performerIDs []int) error UpdateTags(galleryID int, tagIDs []int) error + UpdateScenes(galleryID int, sceneIDs []int) error UpdateImages(galleryID int, imageIDs []int) error } diff --git a/pkg/models/mocks/GalleryReaderWriter.go b/pkg/models/mocks/GalleryReaderWriter.go index 6869b9f6b..e3eb879d9 100644 --- a/pkg/models/mocks/GalleryReaderWriter.go +++ b/pkg/models/mocks/GalleryReaderWriter.go @@ -35,20 +35,6 @@ func (_m *GalleryReaderWriter) All() ([]*models.Gallery, error) { return r0, r1 } -// ClearGalleryId provides a mock function with given fields: sceneID -func (_m *GalleryReaderWriter) ClearGalleryId(sceneID int) error { - ret := _m.Called(sceneID) - - var r0 error - if rf, ok := ret.Get(0).(func(int) error); ok { - r0 = rf(sceneID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // Count provides a mock function with given fields: func (_m *GalleryReaderWriter) Count() (int, error) { ret := _m.Called() @@ -153,6 +139,29 @@ func (_m *GalleryReaderWriter) FindByChecksum(checksum string) (*models.Gallery, return r0, r1 } +// FindByChecksums provides a mock function with given fields: checksums +func (_m *GalleryReaderWriter) FindByChecksums(checksums []string) ([]*models.Gallery, error) { + ret := _m.Called(checksums) + + var r0 []*models.Gallery + if rf, ok := ret.Get(0).(func([]string) []*models.Gallery); ok { + r0 = rf(checksums) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Gallery) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]string) error); ok { + r1 = rf(checksums) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FindByImageID provides a mock function with given fields: imageID func (_m *GalleryReaderWriter) FindByImageID(imageID int) ([]*models.Gallery, error) { ret := _m.Called(imageID) @@ -200,15 +209,15 @@ func (_m *GalleryReaderWriter) FindByPath(path string) (*models.Gallery, error) } // FindBySceneID provides a mock function with given fields: sceneID -func (_m *GalleryReaderWriter) FindBySceneID(sceneID int) (*models.Gallery, error) { +func (_m *GalleryReaderWriter) FindBySceneID(sceneID int) ([]*models.Gallery, error) { ret := _m.Called(sceneID) - var r0 *models.Gallery - if rf, ok := ret.Get(0).(func(int) *models.Gallery); ok { + var r0 []*models.Gallery + if rf, ok := ret.Get(0).(func(int) []*models.Gallery); ok { r0 = rf(sceneID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Gallery) + r0 = ret.Get(0).([]*models.Gallery) } } @@ -314,6 +323,29 @@ func (_m *GalleryReaderWriter) GetTagIDs(galleryID int) ([]int, error) { return r0, r1 } +// GetSceneIDs provides a mock function with given fields: galleryID +func (_m *GalleryReaderWriter) GetSceneIDs(galleryID int) ([]int, error) { + ret := _m.Called(galleryID) + + var r0 []int + if rf, ok := ret.Get(0).(func(int) []int); ok { + r0 = rf(galleryID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(galleryID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Query provides a mock function with given fields: galleryFilter, findFilter func (_m *GalleryReaderWriter) Query(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) { ret := _m.Called(galleryFilter, findFilter) @@ -446,25 +478,16 @@ func (_m *GalleryReaderWriter) UpdateTags(galleryID int, tagIDs []int) error { return r0 } -// ValidGalleriesForScenePath provides a mock function with given fields: scenePath -func (_m *GalleryReaderWriter) ValidGalleriesForScenePath(scenePath string) ([]*models.Gallery, error) { - ret := _m.Called(scenePath) +// UpdateScenes provides a mock function with given fields: galleryID, sceneIDs +func (_m *GalleryReaderWriter) UpdateScenes(galleryID int, sceneIDs []int) error { + ret := _m.Called(galleryID, sceneIDs) - var r0 []*models.Gallery - if rf, ok := ret.Get(0).(func(string) []*models.Gallery); ok { - r0 = rf(scenePath) + var r0 error + if rf, ok := ret.Get(0).(func(int, []int) error); ok { + r0 = rf(galleryID, sceneIDs) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*models.Gallery) - } + r0 = ret.Error(0) } - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(scenePath) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } diff --git a/pkg/models/mocks/SceneReaderWriter.go b/pkg/models/mocks/SceneReaderWriter.go index 2c6862fb9..2a67e8891 100644 --- a/pkg/models/mocks/SceneReaderWriter.go +++ b/pkg/models/mocks/SceneReaderWriter.go @@ -392,6 +392,29 @@ func (_m *SceneReaderWriter) FindByPerformerID(performerID int) ([]*models.Scene return r0, r1 } +// FindByGalleryID provides a mock function with given fields: galleryID +func (_m *SceneReaderWriter) FindByGalleryID(galleryID int) ([]*models.Scene, error) { + ret := _m.Called(galleryID) + + var r0 []*models.Scene + if rf, ok := ret.Get(0).(func(int) []*models.Scene); ok { + r0 = rf(galleryID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Scene) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(galleryID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FindMany provides a mock function with given fields: ids func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) { ret := _m.Called(ids) @@ -461,13 +484,13 @@ func (_m *SceneReaderWriter) GetMovies(sceneID int) ([]models.MoviesScenes, erro return r0, r1 } -// GetPerformerIDs provides a mock function with given fields: imageID -func (_m *SceneReaderWriter) GetPerformerIDs(imageID int) ([]int, error) { - ret := _m.Called(imageID) +// GetPerformerIDs provides a mock function with given fields: sceneID +func (_m *SceneReaderWriter) GetPerformerIDs(sceneID int) ([]int, error) { + ret := _m.Called(sceneID) var r0 []int if rf, ok := ret.Get(0).(func(int) []int); ok { - r0 = rf(imageID) + r0 = rf(sceneID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]int) @@ -476,7 +499,30 @@ func (_m *SceneReaderWriter) GetPerformerIDs(imageID int) ([]int, error) { var r1 error if rf, ok := ret.Get(1).(func(int) error); ok { - r1 = rf(imageID) + r1 = rf(sceneID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetGalleryIDs provides a mock function with given fields: sceneID +func (_m *SceneReaderWriter) GetGalleryIDs(sceneID int) ([]int, error) { + ret := _m.Called(sceneID) + + var r0 []int + if rf, ok := ret.Get(0).(func(int) []int); ok { + r0 = rf(sceneID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(sceneID) } else { r1 = ret.Error(1) } @@ -778,6 +824,20 @@ func (_m *SceneReaderWriter) UpdatePerformers(sceneID int, performerIDs []int) e return r0 } +// UpdateGalleries provides a mock function with given fields: sceneID, galleryIDs +func (_m *SceneReaderWriter) UpdateGalleries(sceneID int, galleryIDs []int) error { + ret := _m.Called(sceneID, galleryIDs) + + var r0 error + if rf, ok := ret.Get(0).(func(int, []int) error); ok { + r0 = rf(sceneID, galleryIDs) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpdateStashIDs provides a mock function with given fields: sceneID, stashIDs func (_m *SceneReaderWriter) UpdateStashIDs(sceneID int, stashIDs []models.StashID) error { ret := _m.Called(sceneID, stashIDs) diff --git a/pkg/models/model_gallery.go b/pkg/models/model_gallery.go index acb0ca7c3..061dbf7d2 100644 --- a/pkg/models/model_gallery.go +++ b/pkg/models/model_gallery.go @@ -16,7 +16,6 @@ type Gallery struct { Rating sql.NullInt64 `db:"rating" json:"rating"` Organized bool `db:"organized" json:"organized"` StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"` - SceneID sql.NullInt64 `db:"scene_id,omitempty" json:"scene_id"` FileModTime NullSQLiteTimestamp `db:"file_mod_time" json:"file_mod_time"` CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` @@ -35,7 +34,6 @@ type GalleryPartial struct { Rating *sql.NullInt64 `db:"rating" json:"rating"` Organized *bool `db:"organized" json:"organized"` StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"` - SceneID *sql.NullInt64 `db:"scene_id,omitempty" json:"scene_id"` FileModTime *NullSQLiteTimestamp `db:"file_mod_time" json:"file_mod_time"` CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` diff --git a/pkg/models/scene.go b/pkg/models/scene.go index e86671e3b..cefc12461 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -7,6 +7,7 @@ type SceneReader interface { FindByOSHash(oshash string) (*Scene, error) FindByPath(path string) (*Scene, error) FindByPerformerID(performerID int) ([]*Scene, error) + FindByGalleryID(performerID int) ([]*Scene, error) CountByPerformerID(performerID int) (int, error) // FindByStudioID(studioID int) ([]*Scene, error) FindByMovieID(movieID int) ([]*Scene, error) @@ -25,9 +26,10 @@ type SceneReader interface { QueryByPathRegex(findFilter *FindFilterType) ([]*Scene, int, error) GetCover(sceneID int) ([]byte, error) GetMovies(sceneID int) ([]MoviesScenes, error) - GetTagIDs(imageID int) ([]int, error) - GetPerformerIDs(imageID int) ([]int, error) - GetStashIDs(performerID int) ([]*StashID, error) + GetTagIDs(sceneID int) ([]int, error) + GetGalleryIDs(sceneID int) ([]int, error) + GetPerformerIDs(sceneID int) ([]int, error) + GetStashIDs(sceneID int) ([]*StashID, error) } type SceneWriter interface { @@ -43,6 +45,7 @@ type SceneWriter interface { DestroyCover(sceneID int) error UpdatePerformers(sceneID int, performerIDs []int) error UpdateTags(sceneID int, tagIDs []int) error + UpdateGalleries(sceneID int, galleryIDs []int) error UpdateMovies(sceneID int, movies []MoviesScenes) error UpdateStashIDs(sceneID int, stashIDs []StashID) error } diff --git a/pkg/scene/export.go b/pkg/scene/export.go index 95af51119..9fcd6d096 100644 --- a/pkg/scene/export.go +++ b/pkg/scene/export.go @@ -127,21 +127,6 @@ func GetStudioName(reader models.StudioReader, scene *models.Scene) (string, err return "", nil } -// GetGalleryChecksum returns the checksum of the provided gallery. It returns an -// empty string if there is no gallery assigned to the scene. -func GetGalleryChecksum(reader models.GalleryReader, scene *models.Scene) (string, error) { - gallery, err := reader.FindBySceneID(scene.ID) - if err != nil { - return "", fmt.Errorf("error getting scene gallery: %s", err.Error()) - } - - if gallery != nil { - return gallery.Checksum, nil - } - - return "", nil -} - // GetTagNames returns a slice of tag names corresponding to the provided // scene's tags. func GetTagNames(reader models.TagReader, scene *models.Scene) ([]string, error) { diff --git a/pkg/scene/export_test.go b/pkg/scene/export_test.go index 682d0a9f6..2d30d9672 100644 --- a/pkg/scene/export_test.go +++ b/pkg/scene/export_test.go @@ -307,33 +307,6 @@ var getGalleryChecksumScenarios = []stringTestScenario{ }, } -func TestGetGalleryChecksum(t *testing.T) { - mockGalleryReader := &mocks.GalleryReaderWriter{} - - galleryErr := errors.New("error getting gallery") - - mockGalleryReader.On("FindBySceneID", sceneID).Return(&models.Gallery{ - Checksum: galleryChecksum, - }, nil).Once() - mockGalleryReader.On("FindBySceneID", noGalleryID).Return(nil, nil).Once() - mockGalleryReader.On("FindBySceneID", errGalleryID).Return(nil, galleryErr).Once() - - for i, s := range getGalleryChecksumScenarios { - scene := s.input - json, err := GetGalleryChecksum(mockGalleryReader, &scene) - - if !s.err && err != nil { - t.Errorf("[%d] unexpected error: %s", i, err.Error()) - } else if s.err && err == nil { - t.Errorf("[%d] expected error not returned", i) - } else { - assert.Equal(t, s.expected, json, "[%d]", i) - } - } - - mockGalleryReader.AssertExpectations(t) -} - type stringSliceTestScenario struct { input models.Scene expected []string diff --git a/pkg/scene/import.go b/pkg/scene/import.go index b3bc52420..eee87c8a8 100644 --- a/pkg/scene/import.go +++ b/pkg/scene/import.go @@ -25,7 +25,7 @@ type Importer struct { ID int scene models.Scene - gallery *models.Gallery + galleries []*models.Gallery performers []*models.Performer movies []models.MoviesScenes tags []*models.Tag @@ -39,7 +39,7 @@ func (i *Importer) PreImport() error { return err } - if err := i.populateGallery(); err != nil { + if err := i.populateGalleries(); err != nil { return err } @@ -174,25 +174,32 @@ func (i *Importer) createStudio(name string) (int, error) { return created.ID, nil } -func (i *Importer) populateGallery() error { - if i.Input.Gallery != "" { - gallery, err := i.GalleryWriter.FindByChecksum(i.Input.Gallery) +func (i *Importer) populateGalleries() error { + if len(i.Input.Galleries) > 0 { + checksums := i.Input.Galleries + galleries, err := i.GalleryWriter.FindByChecksums(checksums) if err != nil { - return fmt.Errorf("error finding gallery: %s", err.Error()) + return err } - if gallery == nil { + var pluckedChecksums []string + for _, gallery := range galleries { + pluckedChecksums = append(pluckedChecksums, gallery.Checksum) + } + + missingGalleries := utils.StrFilter(checksums, func(checksum string) bool { + return !utils.StrInclude(pluckedChecksums, checksum) + }) + + if len(missingGalleries) > 0 { if i.MissingRefBehaviour == models.ImportMissingRefEnumFail { - return fmt.Errorf("scene gallery '%s' not found", i.Input.Studio) + return fmt.Errorf("scene galleries [%s] not found", strings.Join(missingGalleries, ", ")) } // we don't create galleries - just ignore - if i.MissingRefBehaviour == models.ImportMissingRefEnumIgnore || i.MissingRefBehaviour == models.ImportMissingRefEnumCreate { - return nil - } - } else { - i.gallery = gallery } + + i.galleries = galleries } return nil @@ -333,11 +340,14 @@ func (i *Importer) PostImport(id int) error { } } - if i.gallery != nil { - i.gallery.SceneID = sql.NullInt64{Int64: int64(id), Valid: true} - _, err := i.GalleryWriter.Update(*i.gallery) - if err != nil { - return fmt.Errorf("failed to update gallery: %s", err.Error()) + if len(i.galleries) > 0 { + var galleryIDs []int + for _, gallery := range i.galleries { + galleryIDs = append(galleryIDs, gallery.ID) + } + + if err := i.ReaderWriter.UpdateGalleries(id, galleryIDs); err != nil { + return fmt.Errorf("failed to associate galleries: %s", err.Error()) } } diff --git a/pkg/scene/import_test.go b/pkg/scene/import_test.go index 971d7ae31..09f0bf38d 100644 --- a/pkg/scene/import_test.go +++ b/pkg/scene/import_test.go @@ -47,6 +47,7 @@ const ( missingTagName = "missingTagName" errPerformersID = 200 + errGalleriesID = 201 missingChecksum = "missingChecksum" missingOSHash = "missingOSHash" @@ -162,23 +163,30 @@ func TestImporterPreImportWithGallery(t *testing.T) { galleryReaderWriter := &mocks.GalleryReaderWriter{} i := Importer{ - GalleryWriter: galleryReaderWriter, - Path: path, + GalleryWriter: galleryReaderWriter, + Path: path, + MissingRefBehaviour: models.ImportMissingRefEnumFail, Input: jsonschema.Scene{ - Gallery: existingGalleryChecksum, + Galleries: []string{ + existingGalleryChecksum, + }, }, } - galleryReaderWriter.On("FindByChecksum", existingGalleryChecksum).Return(&models.Gallery{ - ID: existingGalleryID, + galleryReaderWriter.On("FindByChecksums", []string{existingGalleryChecksum}).Return([]*models.Gallery{ + { + ID: existingGalleryID, + Checksum: existingGalleryChecksum, + }, }, nil).Once() - galleryReaderWriter.On("FindByChecksum", existingGalleryErr).Return(nil, errors.New("FindByChecksum error")).Once() + + galleryReaderWriter.On("FindByChecksums", []string{existingGalleryErr}).Return(nil, errors.New("FindByChecksums error")).Once() err := i.PreImport() assert.Nil(t, err) - assert.Equal(t, existingGalleryID, i.gallery.ID) + assert.Equal(t, existingGalleryID, i.galleries[0].ID) - i.Input.Gallery = existingGalleryErr + i.Input.Galleries = []string{existingGalleryErr} err = i.PreImport() assert.NotNil(t, err) @@ -192,12 +200,14 @@ func TestImporterPreImportWithMissingGallery(t *testing.T) { Path: path, GalleryWriter: galleryReaderWriter, Input: jsonschema.Scene{ - Gallery: missingGalleryChecksum, + Galleries: []string{ + missingGalleryChecksum, + }, }, MissingRefBehaviour: models.ImportMissingRefEnumFail, } - galleryReaderWriter.On("FindByChecksum", missingGalleryChecksum).Return(nil, nil).Times(3) + galleryReaderWriter.On("FindByChecksums", []string{missingGalleryChecksum}).Return(nil, nil).Times(3) err := i.PreImport() assert.NotNil(t, err) @@ -205,12 +215,10 @@ func TestImporterPreImportWithMissingGallery(t *testing.T) { i.MissingRefBehaviour = models.ImportMissingRefEnumIgnore err = i.PreImport() assert.Nil(t, err) - assert.Nil(t, i.gallery) i.MissingRefBehaviour = models.ImportMissingRefEnumCreate err = i.PreImport() assert.Nil(t, err) - assert.Nil(t, i.gallery) galleryReaderWriter.AssertExpectations(t) } @@ -506,33 +514,30 @@ func TestImporterPostImport(t *testing.T) { readerWriter.AssertExpectations(t) } -func TestImporterPostImportUpdateGallery(t *testing.T) { - galleryReaderWriter := &mocks.GalleryReaderWriter{} +func TestImporterPostImportUpdateGalleries(t *testing.T) { + sceneReaderWriter := &mocks.SceneReaderWriter{} i := Importer{ - GalleryWriter: galleryReaderWriter, - gallery: &models.Gallery{ - ID: existingGalleryID, + ReaderWriter: sceneReaderWriter, + galleries: []*models.Gallery{ + { + ID: existingGalleryID, + }, }, } - updateErr := errors.New("Update error") + updateErr := errors.New("UpdateGalleries error") - updateArg := *i.gallery - updateArg.SceneID = models.NullInt64(sceneID) - - galleryReaderWriter.On("Update", updateArg).Return(nil, nil).Once() - - updateArg.SceneID = models.NullInt64(errGalleryID) - galleryReaderWriter.On("Update", updateArg).Return(nil, updateErr).Once() + sceneReaderWriter.On("UpdateGalleries", sceneID, []int{existingGalleryID}).Return(nil).Once() + sceneReaderWriter.On("UpdateGalleries", errGalleriesID, mock.AnythingOfType("[]int")).Return(updateErr).Once() err := i.PostImport(sceneID) assert.Nil(t, err) - err = i.PostImport(errGalleryID) + err = i.PostImport(errGalleriesID) assert.NotNil(t, err) - galleryReaderWriter.AssertExpectations(t) + sceneReaderWriter.AssertExpectations(t) } func TestImporterPostImportUpdatePerformers(t *testing.T) { diff --git a/pkg/scene/update.go b/pkg/scene/update.go index 940a1c92e..7dec4f415 100644 --- a/pkg/scene/update.go +++ b/pkg/scene/update.go @@ -83,3 +83,23 @@ func AddTag(qb models.SceneReaderWriter, id int, tagID int) (bool, error) { return false, nil } + +func AddGallery(qb models.SceneReaderWriter, id int, galleryID int) (bool, error) { + galleryIDs, err := qb.GetGalleryIDs(id) + if err != nil { + return false, err + } + + oldLen := len(galleryIDs) + galleryIDs = utils.IntAppendUnique(galleryIDs, galleryID) + + if len(galleryIDs) != oldLen { + if err := qb.UpdateGalleries(id, galleryIDs); err != nil { + return false, err + } + + return true, nil + } + + return false, nil +} diff --git a/pkg/scraper/stashbox/graphql/generated_client.go b/pkg/scraper/stashbox/graphql/generated_client.go index 26e4c7e39..0cd062fc1 100644 --- a/pkg/scraper/stashbox/graphql/generated_client.go +++ b/pkg/scraper/stashbox/graphql/generated_client.go @@ -146,34 +146,15 @@ type SceneFragment struct { Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\"" Fingerprints []*FingerprintFragment "json:\"fingerprints\" graphql:\"fingerprints\"" } -type GalleryFragment struct { - ID string "json:\"id\" graphql:\"id\"" - Title *string "json:\"title\" graphql:\"title\"" - Details *string "json:\"details\" graphql:\"details\"" - Duration *int "json:\"duration\" graphql:\"duration\"" - Date *string "json:\"date\" graphql:\"date\"" - Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" - Images []*ImageFragment "json:\"images\" graphql:\"images\"" - Studio *StudioFragment "json:\"studio\" graphql:\"studio\"" - Tags []*TagFragment "json:\"tags\" graphql:\"tags\"" - Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\"" - Fingerprints []*FingerprintFragment "json:\"fingerprints\" graphql:\"fingerprints\"" -} type FindSceneByFingerprint struct { FindSceneByFingerprint []*SceneFragment "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" } type FindScenesByFingerprints struct { FindScenesByFingerprints []*SceneFragment "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" } -type FindGalleriesByFingerprints struct { - FindGalleriesByFingerprints []*GalleryFragment `json:"findGalleriesByFingerprints" graphql:"findGalleriesByFingerprints"` -} type SearchScene struct { SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\"" } -type SearchGallery struct { - SearchGallery []*GalleryFragment `json:"searchScene" graphql:"searchScene"` -} type SubmitFingerprintPayload struct { SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" } @@ -208,9 +189,15 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment TagFragment on Tag { - name - id +fragment URLFragment on URL { + url + type +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } } fragment PerformerFragment on Performer { id @@ -245,25 +232,15 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy } fragment FingerprintFragment on Fingerprint { algorithm hash duration } -fragment URLFragment on URL { - url - type -} fragment ImageFragment on Image { id url @@ -280,15 +257,19 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } +fragment TagFragment on Tag { + name + id } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment BodyModificationFragment on BodyModification { + location + description } ` @@ -310,61 +291,6 @@ const FindScenesByFingerprintsQuery = `query FindScenesByFingerprints ($fingerpr ... SceneFragment } } -fragment ImageFragment on Image { - id - url - width - height -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } -} -fragment TagFragment on Tag { - name - id -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment SceneFragment on Scene { - id - title - details - duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} fragment PerformerAppearanceFragment on PerformerAppearance { as performer { @@ -404,19 +330,74 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip } fragment FingerprintFragment on Fingerprint { algorithm hash duration } +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} fragment URLFragment on URL { url type } +fragment ImageFragment on Image { + id + url + width + height +} +fragment TagFragment on Tag { + name + id +} +fragment StudioFragment on Studio { + name + id + urls { + ... URLFragment + } + images { + ... ImageFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment BodyModificationFragment on BodyModification { + location + description +} ` func (c *Client) FindScenesByFingerprints(ctx context.Context, fingerprints []string, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFingerprints, error) { @@ -437,6 +418,53 @@ const SearchSceneQuery = `query SearchScene ($term: String!) { ... SceneFragment } } +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} +fragment TagFragment on Tag { + name + id +} +fragment PerformerFragment on Performer { + id + name + disambiguation + aliases + gender + urls { + ... URLFragment + } + images { + ... ImageFragment + } + birthdate { + ... FuzzyDateFragment + } + ethnicity + country + eye_color + hair_color + height + measurements { + ... MeasurementsFragment + } + breast_type + career_start_year + career_end_year + tattoos { + ... BodyModificationFragment + } + piercings { + ... BodyModificationFragment + } +} fragment FuzzyDateFragment on FuzzyDate { date accuracy @@ -447,11 +475,6 @@ fragment MeasurementsFragment on Measurements { waist hip } -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} fragment SceneFragment on Scene { id title @@ -477,59 +500,6 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} -fragment PerformerFragment on Performer { - id - name - disambiguation - aliases - gender - urls { - ... URLFragment - } - images { - ... ImageFragment - } - birthdate { - ... FuzzyDateFragment - } - ethnicity - country - eye_color - hair_color - height - measurements { - ... MeasurementsFragment - } - breast_type - career_start_year - career_end_year - tattoos { - ... BodyModificationFragment - } - piercings { - ... BodyModificationFragment - } -} -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { - id - url - width - height -} fragment StudioFragment on Studio { name id @@ -540,137 +510,21 @@ fragment StudioFragment on Studio { ... ImageFragment } } +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} fragment BodyModificationFragment on BodyModification { location description } -` - -func (c *Client) FindGalleriesByFingerprints(ctx context.Context, fingerprints []string, httpRequestOptions ...client.HTTPRequestOption) (*FindGalleriesByFingerprints, error) { - vars := map[string]interface{}{ - "fingerprints": fingerprints, - } - - var res FindGalleriesByFingerprints - if err := c.Client.Post(ctx, FindScenesByFingerprintsQuery, &res, vars, httpRequestOptions...); err != nil { - return nil, err - } - - return &res, nil -} - -const SearchGalleryQuery = `query SearchGallery ($term: String!) { - searchGallery(term: $term) { - ... GalleryFragment - } -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} fragment FingerprintFragment on Fingerprint { algorithm hash duration } -fragment GalleryFragment on Gallery { - id - title - details - duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} -fragment PerformerFragment on Performer { - id - name - disambiguation - aliases - gender - urls { - ... URLFragment - } - images { - ... ImageFragment - } - birthdate { - ... FuzzyDateFragment - } - ethnicity - country - eye_color - hair_color - height - measurements { - ... MeasurementsFragment - } - breast_type - career_start_year - career_end_year - tattoos { - ... BodyModificationFragment - } - piercings { - ... BodyModificationFragment - } -} -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } -} -fragment BodyModificationFragment on BodyModification { - location - description -} ` func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) { @@ -686,19 +540,6 @@ func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOption return &res, nil } -func (c *Client) SearchGallery(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchGallery, error) { - vars := map[string]interface{}{ - "term": term, - } - - var res SearchGallery - if err := c.Client.Post(ctx, SearchGalleryQuery, &res, vars, httpRequestOptions...); err != nil { - return nil, err - } - - return &res, nil -} - const SubmitFingerprintQuery = `mutation SubmitFingerprint ($input: FingerprintSubmission!) { submitFingerprint(input: $input) } diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index cdc3ae0c3..a9b0f7d6c 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -3,7 +3,6 @@ package sqlite import ( "database/sql" "fmt" - "path/filepath" "strconv" "github.com/stashapp/stash/pkg/models" @@ -14,6 +13,7 @@ const galleryTable = "galleries" const performersGalleriesTable = "performers_galleries" const galleriesTagsTable = "galleries_tags" const galleriesImagesTable = "galleries_images" +const galleriesScenesTable = "scenes_galleries" const galleryIDColumn = "gallery_id" type galleryQueryBuilder struct { @@ -73,23 +73,6 @@ func (qb *galleryQueryBuilder) Destroy(id int) error { return qb.destroyExisting([]int{id}) } -type GalleryNullSceneID struct { - SceneID sql.NullInt64 -} - -func (qb *galleryQueryBuilder) ClearGalleryId(sceneID int) error { - _, err := qb.tx.NamedExec( - `UPDATE galleries SET scene_id = null WHERE scene_id = :sceneid`, - GalleryNullSceneID{ - SceneID: sql.NullInt64{ - Int64: int64(sceneID), - Valid: true, - }, - }, - ) - return err -} - func (qb *galleryQueryBuilder) Find(id int) (*models.Gallery, error) { var ret models.Gallery if err := qb.get(id, &ret); err != nil { @@ -125,22 +108,29 @@ func (qb *galleryQueryBuilder) FindByChecksum(checksum string) (*models.Gallery, return qb.queryGallery(query, args) } +func (qb *galleryQueryBuilder) FindByChecksums(checksums []string) ([]*models.Gallery, error) { + query := "SELECT * FROM galleries WHERE checksum IN " + getInBinding(len(checksums)) + var args []interface{} + for _, checksum := range checksums { + args = append(args, checksum) + } + return qb.queryGalleries(query, args) +} + func (qb *galleryQueryBuilder) FindByPath(path string) (*models.Gallery, error) { query := "SELECT * FROM galleries WHERE path = ? LIMIT 1" args := []interface{}{path} return qb.queryGallery(query, args) } -func (qb *galleryQueryBuilder) FindBySceneID(sceneID int) (*models.Gallery, error) { - query := "SELECT galleries.* FROM galleries WHERE galleries.scene_id = ? LIMIT 1" +func (qb *galleryQueryBuilder) FindBySceneID(sceneID int) ([]*models.Gallery, error) { + query := selectAll(galleryTable) + ` + LEFT JOIN scenes_galleries as scenes_join on scenes_join.gallery_id = galleries.id + WHERE scenes_join.scene_id = ? + GROUP BY galleries.id + ` args := []interface{}{sceneID} - return qb.queryGallery(query, args) -} - -func (qb *galleryQueryBuilder) ValidGalleriesForScenePath(scenePath string) ([]*models.Gallery, error) { - sceneDirPath := filepath.Dir(scenePath) - query := "SELECT galleries.* FROM galleries WHERE galleries.scene_id IS NULL AND galleries.path LIKE '" + sceneDirPath + "%' ORDER BY path ASC" - return qb.queryGalleries(query, nil) + return qb.queryGalleries(query, args) } func (qb *galleryQueryBuilder) FindByImageID(imageID int) ([]*models.Gallery, error) { @@ -182,6 +172,7 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi query.body = selectDistinctIDs("galleries") query.body += ` left join performers_galleries as performers_join on performers_join.gallery_id = galleries.id + left join scenes_galleries as scenes_join on scenes_join.gallery_id = galleries.id left join studios as studio on studio.id = galleries.studio_id left join galleries_tags as tags_join on tags_join.gallery_id = galleries.id left join galleries_images as images_join on images_join.gallery_id = galleries.id @@ -189,7 +180,7 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi ` if q := findFilter.Q; q != nil && *q != "" { - searchColumns := []string{"galleries.path", "galleries.checksum"} + searchColumns := []string{"galleries.title", "galleries.path", "galleries.checksum"} clause, thisArgs := getSearchBinding(searchColumns, *q, false) query.addWhere(clause) query.addArg(thisArgs...) @@ -221,8 +212,8 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" { switch *isMissingFilter { - case "scene": - query.addWhere("galleries.scene_id IS NULL") + case "scenes": + query.addWhere("scenes_join.gallery_id IS NULL") case "studio": query.addWhere("galleries.studio_id IS NULL") case "performers": @@ -442,3 +433,23 @@ func (qb *galleryQueryBuilder) UpdateImages(galleryID int, imageIDs []int) error // Delete the existing joins and then create new ones return qb.imagesRepository().replace(galleryID, imageIDs) } + +func (qb *galleryQueryBuilder) scenesRepository() *joinRepository { + return &joinRepository{ + repository: repository{ + tx: qb.tx, + tableName: galleriesScenesTable, + idColumn: galleryIDColumn, + }, + fkColumn: sceneIDColumn, + } +} + +func (qb *galleryQueryBuilder) GetSceneIDs(galleryID int) ([]int, error) { + return qb.scenesRepository().getIDs(galleryID) +} + +func (qb *galleryQueryBuilder) UpdateScenes(galleryID int, sceneIDs []int) error { + // Delete the existing joins and then create new ones + return qb.scenesRepository().replace(galleryID, sceneIDs) +} diff --git a/pkg/sqlite/gallery_test.go b/pkg/sqlite/gallery_test.go index 8ca671db2..9149c69d9 100644 --- a/pkg/sqlite/gallery_test.go +++ b/pkg/sqlite/gallery_test.go @@ -94,21 +94,21 @@ func TestGalleryFindBySceneID(t *testing.T) { gqb := r.Gallery() sceneID := sceneIDs[sceneIdxWithGallery] - gallery, err := gqb.FindBySceneID(sceneID) + galleries, err := gqb.FindBySceneID(sceneID) if err != nil { t.Errorf("Error finding gallery: %s", err.Error()) } - assert.Equal(t, getGalleryStringValue(galleryIdxWithScene, "Path"), gallery.Path.String) + assert.Equal(t, getGalleryStringValue(galleryIdxWithScene, "Path"), galleries[0].Path.String) - gallery, err = gqb.FindBySceneID(0) + galleries, err = gqb.FindBySceneID(0) if err != nil { t.Errorf("Error finding gallery: %s", err.Error()) } - assert.Nil(t, gallery) + assert.Nil(t, galleries) return nil }) @@ -233,7 +233,7 @@ func verifyGalleriesRating(t *testing.T, ratingCriterion models.IntCriterionInpu func TestGalleryQueryIsMissingScene(t *testing.T) { withTxn(func(r models.Repository) error { qb := r.Gallery() - isMissing := "scene" + isMissing := "scenes" galleryFilter := models.GalleryFilterType{ IsMissing: &isMissing, } @@ -265,10 +265,8 @@ func TestGalleryQueryIsMissingScene(t *testing.T) { }) } -// TODO ValidGalleriesForScenePath // TODO Count // TODO All // TODO Query // TODO Update // TODO Destroy -// TODO ClearGalleryId diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 3a8bf99bd..71d6492ba 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -13,6 +13,7 @@ const sceneTable = "scenes" const sceneIDColumn = "scene_id" const performersScenesTable = "performers_scenes" const scenesTagsTable = "scenes_tags" +const scenesGalleriesTable = "scenes_galleries" const moviesScenesTable = "movies_scenes" var scenesForPerformerQuery = selectAll(sceneTable) + ` @@ -44,6 +45,12 @@ WHERE scenes_tags.tag_id = ? GROUP BY scenes_tags.scene_id ` +var scenesForGalleryQuery = selectAll(sceneTable) + ` +LEFT JOIN scenes_galleries as galleries_join on galleries_join.scene_id = scenes.id +WHERE galleries_join.gallery_id = ? +GROUP BY scenes.id +` + var countScenesForMissingChecksumQuery = ` SELECT id FROM scenes WHERE scenes.checksum is null @@ -221,6 +228,11 @@ func (qb *sceneQueryBuilder) FindByPerformerID(performerID int) ([]*models.Scene return qb.queryScenes(scenesForPerformerQuery, args) } +func (qb *sceneQueryBuilder) FindByGalleryID(galleryID int) ([]*models.Scene, error) { + args := []interface{}{galleryID} + return qb.queryScenes(scenesForGalleryQuery, args) +} + func (qb *sceneQueryBuilder) CountByPerformerID(performerID int) (int, error) { args := []interface{}{performerID} return qb.runCountQuery(qb.buildCountQuery(countScenesForPerformerQuery), args) @@ -293,7 +305,7 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt left join performers_scenes as performers_join on performers_join.scene_id = scenes.id left join movies_scenes as movies_join on movies_join.scene_id = scenes.id left join studios as studio on studio.id = scenes.studio_id - left join galleries as gallery on gallery.scene_id = scenes.id + left join scenes_galleries as galleries_join on galleries_join.scene_id = scenes.id left join scenes_tags as tags_join on tags_join.scene_id = scenes.id left join scene_stash_ids on scene_stash_ids.scene_id = scenes.id ` @@ -368,8 +380,8 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt if isMissingFilter := sceneFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" { switch *isMissingFilter { - case "gallery": - query.addWhere("gallery.scene_id IS NULL") + case "galleries": + query.addWhere("galleries_join.scene_id IS NULL") case "studio": query.addWhere("scenes.studio_id IS NULL") case "movie": @@ -683,6 +695,26 @@ func (qb *sceneQueryBuilder) UpdateTags(id int, tagIDs []int) error { return qb.tagsRepository().replace(id, tagIDs) } +func (qb *sceneQueryBuilder) galleriesRepository() *joinRepository { + return &joinRepository{ + repository: repository{ + tx: qb.tx, + tableName: scenesGalleriesTable, + idColumn: sceneIDColumn, + }, + fkColumn: galleryIDColumn, + } +} + +func (qb *sceneQueryBuilder) GetGalleryIDs(id int) ([]int, error) { + return qb.galleriesRepository().getIDs(id) +} + +func (qb *sceneQueryBuilder) UpdateGalleries(id int, galleryIDs []int) error { + // Delete the existing joins and then create new ones + return qb.galleriesRepository().replace(id, galleryIDs) +} + func (qb *sceneQueryBuilder) stashIDRepository() *stashIDRepository { return &stashIDRepository{ repository{ diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index f6066b252..f8858bd08 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -494,7 +494,7 @@ func TestSceneQueryHasMarkers(t *testing.T) { func TestSceneQueryIsMissingGallery(t *testing.T) { withTxn(func(r models.Repository) error { sqb := r.Scene() - isMissing := "gallery" + isMissing := "galleries" sceneFilter := models.SceneFilterType{ IsMissing: &isMissing, } diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index aa663f159..5200dacc4 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -5,7 +5,6 @@ package sqlite_test import ( "context" "database/sql" - "errors" "fmt" "io/ioutil" "os" @@ -192,7 +191,7 @@ func populateDB() error { return fmt.Errorf("error creating studios: %s", err.Error()) } - if err := linkSceneGallery(r.Gallery(), sceneIdxWithGallery, galleryIdxWithScene); err != nil { + if err := linkSceneGallery(r.Scene(), sceneIdxWithGallery, galleryIdxWithScene); err != nil { return fmt.Errorf("error linking scene to gallery: %s", err.Error()) } @@ -623,20 +622,8 @@ func linkScenePerformer(qb models.SceneReaderWriter, sceneIndex, performerIndex return err } -func linkSceneGallery(gqb models.GalleryReaderWriter, sceneIndex, galleryIndex int) error { - gallery, err := gqb.Find(galleryIDs[galleryIndex]) - - if err != nil { - return fmt.Errorf("error finding gallery: %s", err.Error()) - } - - if gallery == nil { - return errors.New("gallery is nil") - } - - gallery.SceneID = sql.NullInt64{Int64: int64(sceneIDs[sceneIndex]), Valid: true} - _, err = gqb.Update(*gallery) - +func linkSceneGallery(qb models.SceneReaderWriter, sceneIndex, galleryIndex int) error { + _, err := scene.AddGallery(qb, sceneIDs[sceneIndex], galleryIDs[galleryIndex]) return err } diff --git a/ui/v2.5/src/components/Changelog/versions/v050.md b/ui/v2.5/src/components/Changelog/versions/v050.md index 45b244cf1..f30269d08 100644 --- a/ui/v2.5/src/components/Changelog/versions/v050.md +++ b/ui/v2.5/src/components/Changelog/versions/v050.md @@ -1,6 +1,7 @@ #### 💥 Note: After upgrading, all scene file sizes will be 0B until a new [scan](/settings?tab=tasks) is run. ### ✨ New Features +* Add support for multiple galleries per scene, and vice-versa. * Add backup database functionality to Settings/Tasks. * Add gallery wall view. * Add organized flag for scenes, galleries and images. diff --git a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx index 3f84649cc..59e8eabed 100644 --- a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx @@ -7,7 +7,7 @@ import { useToast } from "src/hooks"; import { FormattedMessage } from "react-intl"; interface IDeleteGalleryDialogProps { - selected: Partial[]; + selected: Pick[]; onClose: (confirmed: boolean) => void; } diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index ad29ab450..ce43c8eb3 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -16,9 +16,9 @@ import { TextUtils } from "src/utils"; interface IProps { gallery: GQL.GallerySlimDataFragment; selecting?: boolean; - selected: boolean | undefined; - zoomIndex: number; - onSelectedChanged: (selected: boolean, shiftKey: boolean) => void; + selected?: boolean | undefined; + zoomIndex?: number; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; } export const GalleryCard: React.FC = (props) => { @@ -27,19 +27,18 @@ export const GalleryCard: React.FC = (props) => { config?.data?.configuration.interface.showStudioAsText ?? false; function maybeRenderScenePopoverButton() { - if (!props.gallery.scene) return; + if (props.gallery.scenes.length === 0) return; - const popoverContent = ( - - ); + const popoverContent = props.gallery.scenes.map((scene) => ( + + )); return ( - - - + ); } @@ -124,7 +123,7 @@ export const GalleryCard: React.FC = (props) => { function maybeRenderPopoverButtonGroup() { if ( - props.gallery.scene || + props.gallery.scenes.length > 0 || props.gallery.performers.length > 0 || props.gallery.tags.length > 0 || props.gallery.organized diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index e41aa0fd5..b4353fdde 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -13,6 +13,7 @@ import { DeleteGalleriesDialog } from "../DeleteGalleriesDialog"; import { GalleryImagesPanel } from "./GalleryImagesPanel"; import { GalleryAddPanel } from "./GalleryAddPanel"; import { GalleryFileInfoPanel } from "./GalleryFileInfoPanel"; +import { GalleryScenesPanel } from "./GalleryScenesPanel"; interface IGalleryParams { id?: string; @@ -118,6 +119,11 @@ export const Gallery: React.FC = () => { Details + {gallery.scenes.length > 0 && ( + + Scenes + + )} {gallery.path ? ( @@ -157,6 +163,11 @@ export const Gallery: React.FC = () => { onDelete={() => setIsDeleteAlertOpen(true)} /> + {gallery.scenes.length > 0 && ( + + + + )} ); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index 84e5fab50..cd26f6b2b 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -12,12 +12,13 @@ import { import { PerformerSelect, TagSelect, + SceneSelect, StudioSelect, Icon, LoadingIndicator, } from "src/components/Shared"; import { useToast } from "src/hooks"; -import { FormUtils, EditableTextUtils } from "src/utils"; +import { FormUtils, EditableTextUtils, TextUtils } from "src/utils"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { GalleryScrapeDialog } from "./GalleryScrapeDialog"; @@ -49,6 +50,7 @@ export const GalleryEditPanel: React.FC< const [studioId, setStudioId] = useState(); const [performerIds, setPerformerIds] = useState(); const [tagIds, setTagIds] = useState(); + const [scenes, setScenes] = useState<{ id: string; title: string }[]>([]); const Scrapers = useListGalleryScrapers(); @@ -117,6 +119,12 @@ export const GalleryEditPanel: React.FC< setStudioId(state?.studio?.id ?? undefined); setPerformerIds(perfIds); setTagIds(tIds); + setScenes( + (state?.scenes ?? []).map((s) => ({ + id: s.id, + title: s.title ?? TextUtils.fileNameFromPath(s.path ?? ""), + })) + ); } useEffect(() => { @@ -135,6 +143,7 @@ export const GalleryEditPanel: React.FC< studio_id: studioId ?? null, performer_ids: performerIds, tag_ids: tagIds, + scene_ids: scenes.map((s) => s.id), }; } @@ -390,6 +399,23 @@ export const GalleryEditPanel: React.FC< /> + + + {FormUtils.renderLabel({ + title: "Scenes", + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + setScenes(items)} + /> + +
diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScenesPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScenesPanel.tsx new file mode 100644 index 000000000..8f21d9798 --- /dev/null +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScenesPanel.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import * as GQL from "src/core/generated-graphql"; +import { SceneCard } from "src/components/Scenes/SceneCard"; + +interface IGalleryScenesPanelProps { + scenes: GQL.SceneDataFragment[]; +} + +export const GalleryScenesPanel: React.FC = ({ + scenes, +}) => ( +
+ {scenes.map((scene) => ( + + ))} +
+); diff --git a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx index 5ed9f42f1..6a9ac5136 100644 --- a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx @@ -1,16 +1,20 @@ import React from "react"; -import * as GQL from "src/core/generated-graphql"; +import { useFindGallery } from "src/core/StashService"; import { useLightbox } from "src/hooks"; +import { LoadingIndicator } from "src/components/Shared"; import "flexbin/flexbin.css"; interface IProps { - gallery: GQL.GalleryDataFragment; + galleryId: string; } -export const GalleryViewer: React.FC = ({ gallery }) => { - const images = gallery?.images ?? []; +export const GalleryViewer: React.FC = ({ galleryId }) => { + const { data, loading } = useFindGallery(galleryId); + const images = data?.findGallery?.images ?? []; const showLightbox = useLightbox({ images, showNavigation: false }); + if (loading) return ; + const thumbs = images.map((file, index) => (
= ({ interface ISceneCardProps { scene: GQL.SlimSceneDataFragment; selecting?: boolean; - selected: boolean | undefined; - zoomIndex: number; - onSelectedChanged: (selected: boolean, shiftKey: boolean) => void; + selected?: boolean | undefined; + zoomIndex?: number; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; } export const SceneCard: React.FC = ( @@ -257,17 +257,20 @@ export const SceneCard: React.FC = ( } function maybeRenderGallery() { - if (props.scene.gallery) { - return ( -
- - - -
- ); - } + if (props.scene.galleries.length <= 0) return; + + const popoverContent = props.scene.galleries.map((gallery) => ( + + )); + + return ( + + + + ); } function maybeRenderOrganized() { @@ -289,7 +292,7 @@ export const SceneCard: React.FC = ( props.scene.movies.length > 0 || props.scene.scene_markers.length > 0 || props.scene?.o_counter || - props.scene.gallery || + props.scene.galleries.length > 0 || props.scene.organized ) { return ( @@ -314,7 +317,7 @@ export const SceneCard: React.FC = ( ) { const { shiftKey } = event; - if (props.selecting) { + if (props.selecting && props.onSelectedChanged) { props.onSelectedChanged(!props.selected, shiftKey); event.preventDefault(); } @@ -331,7 +334,7 @@ export const SceneCard: React.FC = ( const ev = event; const shiftKey = false; - if (props.selecting && !props.selected) { + if (props.selecting && props.onSelectedChanged && !props.selected) { props.onSelectedChanged(true, shiftKey); } @@ -354,7 +357,7 @@ export const SceneCard: React.FC = ( type="checkbox" className="scene-card-check" checked={props.selected} - onChange={() => props.onSelectedChanged(!props.selected, shiftKey)} + onChange={() => props.onSelectedChanged?.(!props.selected, shiftKey)} onClick={(event: React.MouseEvent) => { // eslint-disable-next-line prefer-destructuring shiftKey = event.shiftKey; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 149fa6fd3..133f3f5d7 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -24,6 +24,7 @@ import { SceneEditPanel } from "./SceneEditPanel"; import { SceneDetailPanel } from "./SceneDetailPanel"; import { OCounterButton } from "./OCounterButton"; import { SceneMoviePanel } from "./SceneMoviePanel"; +import { SceneGalleriesPanel } from "./SceneGalleriesPanel"; import { DeleteScenesDialog } from "../DeleteScenesDialog"; import { SceneGenerateDialog } from "../SceneGenerateDialog"; import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel"; @@ -243,13 +244,16 @@ export const Scene: React.FC = () => { ) : ( "" )} - {scene.gallery ? ( + {scene.galleries.length === 1 ? ( Gallery - ) : ( - "" - )} + ) : undefined} + {scene.galleries.length > 1 ? ( + + Galleries + + ) : undefined} Filters @@ -295,12 +299,15 @@ export const Scene: React.FC = () => { - {scene.gallery ? ( + {scene.galleries.length === 1 && ( - + + + )} + {scene.galleries.length > 1 && ( + + - ) : ( - "" )} diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index c28e2e40f..c9d6a75a4 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -22,13 +22,13 @@ import { PerformerSelect, TagSelect, StudioSelect, - SceneGallerySelect, + GallerySelect, Icon, LoadingIndicator, ImageInput, } from "src/components/Shared"; import { useToast } from "src/hooks"; -import { ImageUtils, FormUtils, EditableTextUtils } from "src/utils"; +import { ImageUtils, FormUtils, EditableTextUtils, TextUtils } from "src/utils"; import { MovieSelect } from "src/components/Shared/Select"; import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable"; import { RatingStars } from "./RatingStars"; @@ -47,7 +47,9 @@ export const SceneEditPanel: React.FC = (props: IProps) => { const [url, setUrl] = useState(); const [date, setDate] = useState(); const [rating, setRating] = useState(); - const [galleryId, setGalleryId] = useState(); + const [galleries, setGalleries] = useState<{ id: string; title: string }[]>( + [] + ); const [studioId, setStudioId] = useState(); const [performerIds, setPerformerIds] = useState(); const [movieIds, setMovieIds] = useState(undefined); @@ -171,7 +173,12 @@ export const SceneEditPanel: React.FC = (props: IProps) => { setUrl(state.url ?? undefined); setDate(state.date ?? undefined); setRating(state.rating === null ? NaN : state.rating); - setGalleryId(state?.gallery?.id ?? undefined); + setGalleries( + (state?.galleries ?? []).map((g) => ({ + id: g.id, + title: g.title ?? TextUtils.fileNameFromPath(g.path ?? ""), + })) + ); setStudioId(state?.studio?.id ?? undefined); setMovieIds(moviIds); setMovieSceneIndexes(movieSceneIdx); @@ -196,7 +203,7 @@ export const SceneEditPanel: React.FC = (props: IProps) => { url, date, rating: rating ?? null, - gallery_id: galleryId ?? null, + gallery_ids: galleries.map((g) => g.id), studio_id: studioId ?? null, performer_ids: performerIds, movies: makeMovieInputs(), @@ -596,15 +603,14 @@ export const SceneEditPanel: React.FC = (props: IProps) => { /> - + {FormUtils.renderLabel({ - title: "Gallery", + title: "Galleries", })} - setGalleryId(item ? item.id : undefined)} + setGalleries(items)} /> diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx new file mode 100644 index 000000000..eeb8c8392 --- /dev/null +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import * as GQL from "src/core/generated-graphql"; +import { GalleryCard } from "src/components/Galleries/GalleryCard"; + +interface ISceneGalleriesPanelProps { + galleries: GQL.GallerySlimDataFragment[]; +} + +export const SceneGalleriesPanel: React.FC = ({ + galleries, +}) => { + const cards = galleries.map((gallery) => ( + + )); + + return
{cards}
; +}; diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 93f0ac441..ae1088b7f 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -14,11 +14,9 @@ import { useTagCreate, useStudioCreate, usePerformerCreate, - useFindGalleries, } from "src/core/StashService"; import { useToast } from "src/hooks"; -import { ListFilterModel } from "src/models/list-filter/filter"; -import { FilterMode } from "src/models/list-filter/types"; +import { TextUtils } from "src/utils"; export type ValidTypes = | GQL.SlimPerformerDataFragment @@ -72,23 +70,28 @@ interface IFilterComponentProps extends IFilterProps { interface IFilterSelectProps extends Omit, "onChange" | "items" | "onCreateOption"> {} -interface ISceneGallerySelect { - gallery?: Pick; - sceneId: string; - onSelect: ( - item: - | GQL.ValidGalleriesForSceneQuery["validGalleriesForScene"][0] - | undefined - ) => void; +type Gallery = { id: string; title: string }; +interface IGallerySelect { + galleries: Gallery[]; + onSelect: (items: Gallery[]) => void; } -const getSelectedValues = (selectedItems: ValueType) => +type Scene = { id: string; title: string }; +interface ISceneSelect { + scenes: Scene[]; + onSelect: (items: Scene[]) => void; +} + +const getSelectedItems = (selectedItems: ValueType) => selectedItems - ? (Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map( - (item) => item.value - ) + ? Array.isArray(selectedItems) + ? selectedItems + : [selectedItems] : []; +const getSelectedValues = (selectedItems: ValueType) => + getSelectedItems(selectedItems).map((item) => item.value); + const SelectComponent = ({ type, initialIds, @@ -231,53 +234,104 @@ const FilterSelectComponent = ( ); }; -export const SceneGallerySelect: React.FC = (props) => { +export const GallerySelect: React.FC = (props) => { const [query, setQuery] = useState(""); - const { data, loading } = useFindGalleries(getFilter()); - const [selectedOption, setSelectedOption] = useState