Decouple galleries from scenes (#1057)

This commit is contained in:
InfiniteTF
2021-02-01 21:56:54 +01:00
committed by GitHub
parent 86bfb64a0d
commit 4fd022a93b
54 changed files with 952 additions and 755 deletions

View File

@@ -21,7 +21,7 @@ fragment GallerySlimData on Gallery {
performers { performers {
...PerformerData ...PerformerData
} }
scene { scenes {
id id
title title
path path

View File

@@ -24,9 +24,7 @@ fragment GalleryData on Gallery {
performers { performers {
...PerformerData ...PerformerData
} }
scene { scenes {
id ...SceneData
title
path
} }
} }

View File

@@ -37,7 +37,7 @@ fragment SlimSceneData on Scene {
seconds seconds
} }
gallery { galleries {
id id
path path
title title

View File

@@ -35,8 +35,8 @@ fragment SceneData on Scene {
...SceneMarkerData ...SceneMarkerData
} }
gallery { galleries {
...GalleryData ...GallerySlimData
} }
studio { studio {

View File

@@ -36,14 +36,6 @@ query AllTagsForFilter {
} }
} }
query ValidGalleriesForScene($scene_id: ID!) {
validGalleriesForScene(scene_id: $scene_id) {
id
path
title
}
}
query Stats { query Stats {
stats { stats {
scene_count, scene_count,

View File

@@ -45,7 +45,7 @@ query ParseSceneFilenames($filter: FindFilterType!, $config: SceneParserInput!)
date date
rating rating
studio_id studio_id
gallery_id gallery_ids
movies { movies {
movie_id movie_id
} }

View File

@@ -50,8 +50,6 @@ type Query {
"""Get marker strings""" """Get marker strings"""
markerStrings(q: String, sort: String): [MarkerStringsResultType]! markerStrings(q: String, sort: String): [MarkerStringsResultType]!
"""Get the list of valid galleries for a given scene ID"""
validGalleriesForScene(scene_id: ID): [Gallery!]!
"""Get stats""" """Get stats"""
stats: StatsResultType! stats: StatsResultType!
"""Organize scene markers by tag for a given scene ID""" """Organize scene markers by tag for a given scene ID"""

View File

@@ -9,7 +9,7 @@ type Gallery {
details: String details: String
rating: Int rating: Int
organized: Boolean! organized: Boolean!
scene: Scene scenes: [Scene!]!
studio: Studio studio: Studio
image_count: Int! image_count: Int!
tags: [Tag!]! tags: [Tag!]!
@@ -33,7 +33,7 @@ input GalleryCreateInput {
details: String details: String
rating: Int rating: Int
organized: Boolean organized: Boolean
scene_id: ID scene_ids: [ID!]
studio_id: ID studio_id: ID
tag_ids: [ID!] tag_ids: [ID!]
performer_ids: [ID!] performer_ids: [ID!]
@@ -48,7 +48,7 @@ input GalleryUpdateInput {
details: String details: String
rating: Int rating: Int
organized: Boolean organized: Boolean
scene_id: ID scene_ids: [ID!]
studio_id: ID studio_id: ID
tag_ids: [ID!] tag_ids: [ID!]
performer_ids: [ID!] performer_ids: [ID!]
@@ -62,7 +62,7 @@ input BulkGalleryUpdateInput {
details: String details: String
rating: Int rating: Int
organized: Boolean organized: Boolean
scene_id: ID scene_ids: BulkUpdateIds
studio_id: ID studio_id: ID
tag_ids: BulkUpdateIds tag_ids: BulkUpdateIds
performer_ids: BulkUpdateIds performer_ids: BulkUpdateIds

View File

@@ -40,7 +40,7 @@ type Scene {
paths: ScenePathsType! # Resolver paths: ScenePathsType! # Resolver
scene_markers: [SceneMarker!]! scene_markers: [SceneMarker!]!
gallery: Gallery galleries: [Gallery!]!
studio: Studio studio: Studio
movies: [SceneMovie!]! movies: [SceneMovie!]!
tags: [Tag!]! tags: [Tag!]!
@@ -63,7 +63,7 @@ input SceneUpdateInput {
rating: Int rating: Int
organized: Boolean organized: Boolean
studio_id: ID studio_id: ID
gallery_id: ID gallery_ids: [ID!]
performer_ids: [ID!] performer_ids: [ID!]
movies: [SceneMovieInput!] movies: [SceneMovieInput!]
tag_ids: [ID!] tag_ids: [ID!]
@@ -93,7 +93,7 @@ input BulkSceneUpdateInput {
rating: Int rating: Int
organized: Boolean organized: Boolean
studio_id: ID studio_id: ID
gallery_id: ID gallery_ids: BulkUpdateIds
performer_ids: BulkUpdateIds performer_ids: BulkUpdateIds
tag_ids: BulkUpdateIds tag_ids: BulkUpdateIds
} }
@@ -134,7 +134,7 @@ type SceneParserResult {
date: String date: String
rating: Int rating: Int
studio_id: ID studio_id: ID
gallery_id: ID gallery_ids: [ID!]
performer_ids: [ID!] performer_ids: [ID!]
movies: [SceneMovieID!] movies: [SceneMovieID!]
tag_ids: [ID!] tag_ids: [ID!]

View File

@@ -120,41 +120,6 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri
return ret, nil 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) { func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
var ret models.StatsResultType var ret models.StatsResultType
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {

View File

@@ -90,15 +90,10 @@ func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int
return nil, nil return nil, nil
} }
func (r *galleryResolver) Scene(ctx context.Context, obj *models.Gallery) (ret *models.Scene, err error) { func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
if !obj.SceneID.Valid {
return nil, nil
}
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
var err error var err error
ret, err = repo.Scene().Find(int(obj.SceneID.Int64)) ret, err = repo.Scene().FindByGalleryID(obj.ID)
return err return err
}); err != nil { }); err != nil {
return nil, err return nil, err

View File

@@ -105,7 +105,7 @@ func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (re
return ret, nil 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 { if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Gallery().FindBySceneID(obj.ID) ret, err = repo.Gallery().FindBySceneID(obj.ID)
return err return err

View File

@@ -60,14 +60,6 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.Galle
newGallery.StudioID = sql.NullInt64{Valid: false} 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 // Start the transaction and save the gallery
var gallery *models.Gallery var gallery *models.Gallery
if err := r.withTxn(ctx, func(repo models.Repository) error { 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 return err
} }
// Save the scenes
if err := r.updateGalleryScenes(qb, gallery.ID, input.SceneIds); err != nil {
return err
}
return nil return nil
}); err != nil { }); err != nil {
return nil, err return nil, err
@@ -112,6 +109,14 @@ func (r *mutationResolver) updateGalleryTags(qb models.GalleryReaderWriter, gall
return qb.UpdateTags(galleryID, ids) 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) { func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (ret *models.Gallery, err error) {
translator := changesetTranslator{ translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx), 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 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.Date = translator.sqliteDate(input.Date, "date")
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating") updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id") updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedGallery.SceneID = translator.nullInt64FromString(input.SceneID, "scene_id")
updatedGallery.Organized = input.Organized updatedGallery.Organized = input.Organized
ret := []*models.Gallery{} ret := []*models.Gallery{}
@@ -284,6 +295,18 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B
return err 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 return nil
@@ -312,6 +335,15 @@ func adjustGalleryTagIDs(qb models.GalleryReader, galleryID int, ids models.Bulk
return adjustIDs(ret, ids), nil 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) { func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) {
galleryIDs, err := utils.StringSliceToIntSlice(input.Ids) galleryIDs, err := utils.StringSliceToIntSlice(input.Ids)
if err != nil { if err != nil {

View File

@@ -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 // Save the performers
if translator.hasField("performer_ids") { if translator.hasField("performer_ids") {
if err := r.updateScenePerformers(qb, sceneID, input.PerformerIds); err != nil { 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 // Save the stash_ids
if translator.hasField("stash_ids") { if translator.hasField("stash_ids") {
stashIDJoins := models.StashIDsFromInput(input.StashIds) stashIDJoins := models.StashIDsFromInput(input.StashIds)
@@ -206,6 +190,14 @@ func (r *mutationResolver) updateSceneTags(qb models.SceneReaderWriter, sceneID
return qb.UpdateTags(sceneID, ids) 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) { func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.BulkSceneUpdateInput) ([]*models.Scene, error) {
sceneIDs, err := utils.StringSliceToIntSlice(input.Ids) sceneIDs, err := utils.StringSliceToIntSlice(input.Ids)
if err != nil { 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 // Start the transaction and save the scene marker
if err := r.withTxn(ctx, func(repo models.Repository) error { if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene() qb := repo.Scene()
gqb := repo.Gallery()
for _, sceneID := range sceneIDs { for _, sceneID := range sceneIDs {
updatedScene.ID = sceneID updatedScene.ID = sceneID
@@ -248,20 +239,6 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
ret = append(ret, scene) 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 // Save the performers
if translator.hasField("performer_ids") { if translator.hasField("performer_ids") {
performerIDs, err := adjustScenePerformerIDs(qb, sceneID, *input.PerformerIds) performerIDs, err := adjustScenePerformerIDs(qb, sceneID, *input.PerformerIds)
@@ -285,6 +262,18 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
return err 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 return nil
@@ -350,6 +339,15 @@ func adjustSceneTagIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdate
return adjustIDs(ret, ids), nil 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) { func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
sceneID, err := strconv.Atoi(input.ID) sceneID, err := strconv.Atoi(input.ID)
if err != nil { if err != nil {

View File

@@ -21,7 +21,7 @@ import (
var DB *sqlx.DB var DB *sqlx.DB
var dbPath string var dbPath string
var appSchemaVersion uint = 17 var appSchemaVersion uint = 18
var databaseSchemaVersion uint var databaseSchemaVersion uint
const sqlite3Driver = "sqlite3ex" const sqlite3Driver = "sqlite3ex"

View File

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

View File

@@ -74,3 +74,14 @@ func GetIDs(galleries []*models.Gallery) []int {
return results 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
}

View File

@@ -46,7 +46,7 @@ type Scene struct {
Organized bool `json:"organized,omitempty"` Organized bool `json:"organized,omitempty"`
OCounter int `json:"o_counter,omitempty"` OCounter int `json:"o_counter,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
Gallery string `json:"gallery,omitempty"` Galleries []string `json:"galleries,omitempty"`
Performers []string `json:"performers,omitempty"` Performers []string `json:"performers,omitempty"`
Movies []SceneMovie `json:"movies,omitempty"` Movies []SceneMovie `json:"movies,omitempty"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`

View File

@@ -17,11 +17,6 @@ import (
func DestroyScene(scene *models.Scene, repo models.Repository) (func(), error) { func DestroyScene(scene *models.Scene, repo models.Repository) (func(), error) {
qb := repo.Scene() qb := repo.Scene()
mqb := repo.SceneMarker() mqb := repo.SceneMarker()
gqb := repo.Gallery()
if err := gqb.ClearGalleryId(scene.ID); err != nil {
return nil, err
}
markers, err := mqb.FindBySceneID(scene.ID) markers, err := mqb.FindBySceneID(scene.ID)
if err != nil { if err != nil {

View File

@@ -382,15 +382,13 @@ func exportScene(wg *sync.WaitGroup, jobChan <-chan *models.Scene, repo models.R
continue continue
} }
sceneGallery, err := galleryReader.FindBySceneID(s.ID) galleries, err := galleryReader.FindBySceneID(s.ID)
if err != nil { 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 continue
} }
if sceneGallery != nil { newSceneJSON.Galleries = gallery.GetChecksums(galleries)
newSceneJSON.Gallery = sceneGallery.Checksum
}
performers, err := performerReader.FindBySceneID(s.ID) performers, err := performerReader.FindBySceneID(s.ID)
if err != nil { 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)) t.studios.IDs = utils.IntAppendUnique(t.studios.IDs, int(s.StudioID.Int64))
} }
if sceneGallery != nil { t.galleries.IDs = utils.IntAppendUniques(t.galleries.IDs, gallery.GetIDs(galleries))
t.galleries.IDs = utils.IntAppendUnique(t.galleries.IDs, sceneGallery.ID)
}
tagIDs, err := scene.GetDependentTagIDs(tagReader, sceneMarkerReader, s) tagIDs, err := scene.GetDependentTagIDs(tagReader, sceneMarkerReader, s)
if err != nil { if err != nil {

View File

@@ -300,41 +300,26 @@ func (t *ScanTask) associateGallery(wg *sizedwaitgroup.SizedWaitGroup) {
return nil return nil
} }
// gallery has no SceneID basename := strings.TrimSuffix(t.FilePath, filepath.Ext(t.FilePath))
if !g.SceneID.Valid { var relatedFiles []string
basename := strings.TrimSuffix(t.FilePath, filepath.Ext(t.FilePath)) vExt := config.GetVideoExtensions()
var relatedFiles []string // make a list of media files that can be related to the gallery
vExt := config.GetVideoExtensions() for _, ext := range vExt {
// make a list of media files that can be related to the gallery related := basename + "." + ext
for _, ext := range vExt { // exclude gallery extensions from the related files
related := basename + "." + ext if !isGallery(related) {
// exclude gallery extensions from the related files relatedFiles = append(relatedFiles, related)
if !isGallery(related) {
relatedFiles = append(relatedFiles, related)
}
} }
for _, scenePath := range relatedFiles { }
s, err := sqb.FindByPath(scenePath) for _, scenePath := range relatedFiles {
if err != nil { 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 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
}
} }
} }

View File

@@ -4,15 +4,16 @@ type GalleryReader interface {
Find(id int) (*Gallery, error) Find(id int) (*Gallery, error)
FindMany(ids []int) ([]*Gallery, error) FindMany(ids []int) ([]*Gallery, error)
FindByChecksum(checksum string) (*Gallery, error) FindByChecksum(checksum string) (*Gallery, error)
FindByChecksums(checksums []string) ([]*Gallery, error)
FindByPath(path string) (*Gallery, error) FindByPath(path string) (*Gallery, error)
FindBySceneID(sceneID int) (*Gallery, error) FindBySceneID(sceneID int) ([]*Gallery, error)
FindByImageID(imageID int) ([]*Gallery, error) FindByImageID(imageID int) ([]*Gallery, error)
ValidGalleriesForScenePath(scenePath string) ([]*Gallery, error)
Count() (int, error) Count() (int, error)
All() ([]*Gallery, error) All() ([]*Gallery, error)
Query(galleryFilter *GalleryFilterType, findFilter *FindFilterType) ([]*Gallery, int, error) Query(galleryFilter *GalleryFilterType, findFilter *FindFilterType) ([]*Gallery, int, error)
GetPerformerIDs(galleryID int) ([]int, error) GetPerformerIDs(galleryID int) ([]int, error)
GetTagIDs(galleryID int) ([]int, error) GetTagIDs(galleryID int) ([]int, error)
GetSceneIDs(galleryID int) ([]int, error)
GetImageIDs(galleryID int) ([]int, error) GetImageIDs(galleryID int) ([]int, error)
} }
@@ -22,9 +23,9 @@ type GalleryWriter interface {
UpdatePartial(updatedGallery GalleryPartial) (*Gallery, error) UpdatePartial(updatedGallery GalleryPartial) (*Gallery, error)
UpdateFileModTime(id int, modTime NullSQLiteTimestamp) error UpdateFileModTime(id int, modTime NullSQLiteTimestamp) error
Destroy(id int) error Destroy(id int) error
ClearGalleryId(sceneID int) error
UpdatePerformers(galleryID int, performerIDs []int) error UpdatePerformers(galleryID int, performerIDs []int) error
UpdateTags(galleryID int, tagIDs []int) error UpdateTags(galleryID int, tagIDs []int) error
UpdateScenes(galleryID int, sceneIDs []int) error
UpdateImages(galleryID int, imageIDs []int) error UpdateImages(galleryID int, imageIDs []int) error
} }

View File

@@ -35,20 +35,6 @@ func (_m *GalleryReaderWriter) All() ([]*models.Gallery, error) {
return r0, r1 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: // Count provides a mock function with given fields:
func (_m *GalleryReaderWriter) Count() (int, error) { func (_m *GalleryReaderWriter) Count() (int, error) {
ret := _m.Called() ret := _m.Called()
@@ -153,6 +139,29 @@ func (_m *GalleryReaderWriter) FindByChecksum(checksum string) (*models.Gallery,
return r0, r1 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 // FindByImageID provides a mock function with given fields: imageID
func (_m *GalleryReaderWriter) FindByImageID(imageID int) ([]*models.Gallery, error) { func (_m *GalleryReaderWriter) FindByImageID(imageID int) ([]*models.Gallery, error) {
ret := _m.Called(imageID) 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 // 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) ret := _m.Called(sceneID)
var r0 *models.Gallery var r0 []*models.Gallery
if rf, ok := ret.Get(0).(func(int) *models.Gallery); ok { if rf, ok := ret.Get(0).(func(int) []*models.Gallery); ok {
r0 = rf(sceneID) r0 = rf(sceneID)
} else { } else {
if ret.Get(0) != nil { 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 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 // Query provides a mock function with given fields: galleryFilter, findFilter
func (_m *GalleryReaderWriter) Query(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) { func (_m *GalleryReaderWriter) Query(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) {
ret := _m.Called(galleryFilter, findFilter) ret := _m.Called(galleryFilter, findFilter)
@@ -446,25 +478,16 @@ func (_m *GalleryReaderWriter) UpdateTags(galleryID int, tagIDs []int) error {
return r0 return r0
} }
// ValidGalleriesForScenePath provides a mock function with given fields: scenePath // UpdateScenes provides a mock function with given fields: galleryID, sceneIDs
func (_m *GalleryReaderWriter) ValidGalleriesForScenePath(scenePath string) ([]*models.Gallery, error) { func (_m *GalleryReaderWriter) UpdateScenes(galleryID int, sceneIDs []int) error {
ret := _m.Called(scenePath) ret := _m.Called(galleryID, sceneIDs)
var r0 []*models.Gallery var r0 error
if rf, ok := ret.Get(0).(func(string) []*models.Gallery); ok { if rf, ok := ret.Get(0).(func(int, []int) error); ok {
r0 = rf(scenePath) r0 = rf(galleryID, sceneIDs)
} else { } else {
if ret.Get(0) != nil { r0 = ret.Error(0)
r0 = ret.Get(0).([]*models.Gallery)
}
} }
var r1 error return r0
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(scenePath)
} else {
r1 = ret.Error(1)
}
return r0, r1
} }

View File

@@ -392,6 +392,29 @@ func (_m *SceneReaderWriter) FindByPerformerID(performerID int) ([]*models.Scene
return r0, r1 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 // FindMany provides a mock function with given fields: ids
func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) { func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) {
ret := _m.Called(ids) ret := _m.Called(ids)
@@ -461,13 +484,13 @@ func (_m *SceneReaderWriter) GetMovies(sceneID int) ([]models.MoviesScenes, erro
return r0, r1 return r0, r1
} }
// GetPerformerIDs provides a mock function with given fields: imageID // GetPerformerIDs provides a mock function with given fields: sceneID
func (_m *SceneReaderWriter) GetPerformerIDs(imageID int) ([]int, error) { func (_m *SceneReaderWriter) GetPerformerIDs(sceneID int) ([]int, error) {
ret := _m.Called(imageID) ret := _m.Called(sceneID)
var r0 []int var r0 []int
if rf, ok := ret.Get(0).(func(int) []int); ok { if rf, ok := ret.Get(0).(func(int) []int); ok {
r0 = rf(imageID) r0 = rf(sceneID)
} else { } else {
if ret.Get(0) != nil { if ret.Get(0) != nil {
r0 = ret.Get(0).([]int) r0 = ret.Get(0).([]int)
@@ -476,7 +499,30 @@ func (_m *SceneReaderWriter) GetPerformerIDs(imageID int) ([]int, error) {
var r1 error var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok { 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 { } else {
r1 = ret.Error(1) r1 = ret.Error(1)
} }
@@ -778,6 +824,20 @@ func (_m *SceneReaderWriter) UpdatePerformers(sceneID int, performerIDs []int) e
return r0 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 // UpdateStashIDs provides a mock function with given fields: sceneID, stashIDs
func (_m *SceneReaderWriter) UpdateStashIDs(sceneID int, stashIDs []models.StashID) error { func (_m *SceneReaderWriter) UpdateStashIDs(sceneID int, stashIDs []models.StashID) error {
ret := _m.Called(sceneID, stashIDs) ret := _m.Called(sceneID, stashIDs)

View File

@@ -16,7 +16,6 @@ type Gallery struct {
Rating sql.NullInt64 `db:"rating" json:"rating"` Rating sql.NullInt64 `db:"rating" json:"rating"`
Organized bool `db:"organized" json:"organized"` Organized bool `db:"organized" json:"organized"`
StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"` 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"` FileModTime NullSQLiteTimestamp `db:"file_mod_time" json:"file_mod_time"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
@@ -35,7 +34,6 @@ type GalleryPartial struct {
Rating *sql.NullInt64 `db:"rating" json:"rating"` Rating *sql.NullInt64 `db:"rating" json:"rating"`
Organized *bool `db:"organized" json:"organized"` Organized *bool `db:"organized" json:"organized"`
StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"` 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"` FileModTime *NullSQLiteTimestamp `db:"file_mod_time" json:"file_mod_time"`
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`

View File

@@ -7,6 +7,7 @@ type SceneReader interface {
FindByOSHash(oshash string) (*Scene, error) FindByOSHash(oshash string) (*Scene, error)
FindByPath(path string) (*Scene, error) FindByPath(path string) (*Scene, error)
FindByPerformerID(performerID int) ([]*Scene, error) FindByPerformerID(performerID int) ([]*Scene, error)
FindByGalleryID(performerID int) ([]*Scene, error)
CountByPerformerID(performerID int) (int, error) CountByPerformerID(performerID int) (int, error)
// FindByStudioID(studioID int) ([]*Scene, error) // FindByStudioID(studioID int) ([]*Scene, error)
FindByMovieID(movieID int) ([]*Scene, error) FindByMovieID(movieID int) ([]*Scene, error)
@@ -25,9 +26,10 @@ type SceneReader interface {
QueryByPathRegex(findFilter *FindFilterType) ([]*Scene, int, error) QueryByPathRegex(findFilter *FindFilterType) ([]*Scene, int, error)
GetCover(sceneID int) ([]byte, error) GetCover(sceneID int) ([]byte, error)
GetMovies(sceneID int) ([]MoviesScenes, error) GetMovies(sceneID int) ([]MoviesScenes, error)
GetTagIDs(imageID int) ([]int, error) GetTagIDs(sceneID int) ([]int, error)
GetPerformerIDs(imageID int) ([]int, error) GetGalleryIDs(sceneID int) ([]int, error)
GetStashIDs(performerID int) ([]*StashID, error) GetPerformerIDs(sceneID int) ([]int, error)
GetStashIDs(sceneID int) ([]*StashID, error)
} }
type SceneWriter interface { type SceneWriter interface {
@@ -43,6 +45,7 @@ type SceneWriter interface {
DestroyCover(sceneID int) error DestroyCover(sceneID int) error
UpdatePerformers(sceneID int, performerIDs []int) error UpdatePerformers(sceneID int, performerIDs []int) error
UpdateTags(sceneID int, tagIDs []int) error UpdateTags(sceneID int, tagIDs []int) error
UpdateGalleries(sceneID int, galleryIDs []int) error
UpdateMovies(sceneID int, movies []MoviesScenes) error UpdateMovies(sceneID int, movies []MoviesScenes) error
UpdateStashIDs(sceneID int, stashIDs []StashID) error UpdateStashIDs(sceneID int, stashIDs []StashID) error
} }

View File

@@ -127,21 +127,6 @@ func GetStudioName(reader models.StudioReader, scene *models.Scene) (string, err
return "", nil 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 // GetTagNames returns a slice of tag names corresponding to the provided
// scene's tags. // scene's tags.
func GetTagNames(reader models.TagReader, scene *models.Scene) ([]string, error) { func GetTagNames(reader models.TagReader, scene *models.Scene) ([]string, error) {

View File

@@ -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 { type stringSliceTestScenario struct {
input models.Scene input models.Scene
expected []string expected []string

View File

@@ -25,7 +25,7 @@ type Importer struct {
ID int ID int
scene models.Scene scene models.Scene
gallery *models.Gallery galleries []*models.Gallery
performers []*models.Performer performers []*models.Performer
movies []models.MoviesScenes movies []models.MoviesScenes
tags []*models.Tag tags []*models.Tag
@@ -39,7 +39,7 @@ func (i *Importer) PreImport() error {
return err return err
} }
if err := i.populateGallery(); err != nil { if err := i.populateGalleries(); err != nil {
return err return err
} }
@@ -174,25 +174,32 @@ func (i *Importer) createStudio(name string) (int, error) {
return created.ID, nil return created.ID, nil
} }
func (i *Importer) populateGallery() error { func (i *Importer) populateGalleries() error {
if i.Input.Gallery != "" { if len(i.Input.Galleries) > 0 {
gallery, err := i.GalleryWriter.FindByChecksum(i.Input.Gallery) checksums := i.Input.Galleries
galleries, err := i.GalleryWriter.FindByChecksums(checksums)
if err != nil { 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 { 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 // 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 return nil
@@ -333,11 +340,14 @@ func (i *Importer) PostImport(id int) error {
} }
} }
if i.gallery != nil { if len(i.galleries) > 0 {
i.gallery.SceneID = sql.NullInt64{Int64: int64(id), Valid: true} var galleryIDs []int
_, err := i.GalleryWriter.Update(*i.gallery) for _, gallery := range i.galleries {
if err != nil { galleryIDs = append(galleryIDs, gallery.ID)
return fmt.Errorf("failed to update gallery: %s", err.Error()) }
if err := i.ReaderWriter.UpdateGalleries(id, galleryIDs); err != nil {
return fmt.Errorf("failed to associate galleries: %s", err.Error())
} }
} }

View File

@@ -47,6 +47,7 @@ const (
missingTagName = "missingTagName" missingTagName = "missingTagName"
errPerformersID = 200 errPerformersID = 200
errGalleriesID = 201
missingChecksum = "missingChecksum" missingChecksum = "missingChecksum"
missingOSHash = "missingOSHash" missingOSHash = "missingOSHash"
@@ -162,23 +163,30 @@ func TestImporterPreImportWithGallery(t *testing.T) {
galleryReaderWriter := &mocks.GalleryReaderWriter{} galleryReaderWriter := &mocks.GalleryReaderWriter{}
i := Importer{ i := Importer{
GalleryWriter: galleryReaderWriter, GalleryWriter: galleryReaderWriter,
Path: path, Path: path,
MissingRefBehaviour: models.ImportMissingRefEnumFail,
Input: jsonschema.Scene{ Input: jsonschema.Scene{
Gallery: existingGalleryChecksum, Galleries: []string{
existingGalleryChecksum,
},
}, },
} }
galleryReaderWriter.On("FindByChecksum", existingGalleryChecksum).Return(&models.Gallery{ galleryReaderWriter.On("FindByChecksums", []string{existingGalleryChecksum}).Return([]*models.Gallery{
ID: existingGalleryID, {
ID: existingGalleryID,
Checksum: existingGalleryChecksum,
},
}, nil).Once() }, 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() err := i.PreImport()
assert.Nil(t, err) 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() err = i.PreImport()
assert.NotNil(t, err) assert.NotNil(t, err)
@@ -192,12 +200,14 @@ func TestImporterPreImportWithMissingGallery(t *testing.T) {
Path: path, Path: path,
GalleryWriter: galleryReaderWriter, GalleryWriter: galleryReaderWriter,
Input: jsonschema.Scene{ Input: jsonschema.Scene{
Gallery: missingGalleryChecksum, Galleries: []string{
missingGalleryChecksum,
},
}, },
MissingRefBehaviour: models.ImportMissingRefEnumFail, 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() err := i.PreImport()
assert.NotNil(t, err) assert.NotNil(t, err)
@@ -205,12 +215,10 @@ func TestImporterPreImportWithMissingGallery(t *testing.T) {
i.MissingRefBehaviour = models.ImportMissingRefEnumIgnore i.MissingRefBehaviour = models.ImportMissingRefEnumIgnore
err = i.PreImport() err = i.PreImport()
assert.Nil(t, err) assert.Nil(t, err)
assert.Nil(t, i.gallery)
i.MissingRefBehaviour = models.ImportMissingRefEnumCreate i.MissingRefBehaviour = models.ImportMissingRefEnumCreate
err = i.PreImport() err = i.PreImport()
assert.Nil(t, err) assert.Nil(t, err)
assert.Nil(t, i.gallery)
galleryReaderWriter.AssertExpectations(t) galleryReaderWriter.AssertExpectations(t)
} }
@@ -506,33 +514,30 @@ func TestImporterPostImport(t *testing.T) {
readerWriter.AssertExpectations(t) readerWriter.AssertExpectations(t)
} }
func TestImporterPostImportUpdateGallery(t *testing.T) { func TestImporterPostImportUpdateGalleries(t *testing.T) {
galleryReaderWriter := &mocks.GalleryReaderWriter{} sceneReaderWriter := &mocks.SceneReaderWriter{}
i := Importer{ i := Importer{
GalleryWriter: galleryReaderWriter, ReaderWriter: sceneReaderWriter,
gallery: &models.Gallery{ galleries: []*models.Gallery{
ID: existingGalleryID, {
ID: existingGalleryID,
},
}, },
} }
updateErr := errors.New("Update error") updateErr := errors.New("UpdateGalleries error")
updateArg := *i.gallery sceneReaderWriter.On("UpdateGalleries", sceneID, []int{existingGalleryID}).Return(nil).Once()
updateArg.SceneID = models.NullInt64(sceneID) sceneReaderWriter.On("UpdateGalleries", errGalleriesID, mock.AnythingOfType("[]int")).Return(updateErr).Once()
galleryReaderWriter.On("Update", updateArg).Return(nil, nil).Once()
updateArg.SceneID = models.NullInt64(errGalleryID)
galleryReaderWriter.On("Update", updateArg).Return(nil, updateErr).Once()
err := i.PostImport(sceneID) err := i.PostImport(sceneID)
assert.Nil(t, err) assert.Nil(t, err)
err = i.PostImport(errGalleryID) err = i.PostImport(errGalleriesID)
assert.NotNil(t, err) assert.NotNil(t, err)
galleryReaderWriter.AssertExpectations(t) sceneReaderWriter.AssertExpectations(t)
} }
func TestImporterPostImportUpdatePerformers(t *testing.T) { func TestImporterPostImportUpdatePerformers(t *testing.T) {

View File

@@ -83,3 +83,23 @@ func AddTag(qb models.SceneReaderWriter, id int, tagID int) (bool, error) {
return false, nil 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
}

View File

@@ -146,34 +146,15 @@ type SceneFragment struct {
Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\"" Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\""
Fingerprints []*FingerprintFragment "json:\"fingerprints\" graphql:\"fingerprints\"" 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 { type FindSceneByFingerprint struct {
FindSceneByFingerprint []*SceneFragment "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" FindSceneByFingerprint []*SceneFragment "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\""
} }
type FindScenesByFingerprints struct { type FindScenesByFingerprints struct {
FindScenesByFingerprints []*SceneFragment "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" FindScenesByFingerprints []*SceneFragment "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\""
} }
type FindGalleriesByFingerprints struct {
FindGalleriesByFingerprints []*GalleryFragment `json:"findGalleriesByFingerprints" graphql:"findGalleriesByFingerprints"`
}
type SearchScene struct { type SearchScene struct {
SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\"" SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\""
} }
type SearchGallery struct {
SearchGallery []*GalleryFragment `json:"searchScene" graphql:"searchScene"`
}
type SubmitFingerprintPayload struct { type SubmitFingerprintPayload struct {
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
} }
@@ -208,9 +189,15 @@ fragment SceneFragment on Scene {
... FingerprintFragment ... FingerprintFragment
} }
} }
fragment TagFragment on Tag { fragment URLFragment on URL {
name url
id type
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
} }
fragment PerformerFragment on Performer { fragment PerformerFragment on Performer {
id id
@@ -245,25 +232,15 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment ... BodyModificationFragment
} }
} }
fragment BodyModificationFragment on BodyModification { fragment FuzzyDateFragment on FuzzyDate {
location date
description accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
} }
fragment FingerprintFragment on Fingerprint { fragment FingerprintFragment on Fingerprint {
algorithm algorithm
hash hash
duration duration
} }
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image { fragment ImageFragment on Image {
id id
url url
@@ -280,15 +257,19 @@ fragment StudioFragment on Studio {
... ImageFragment ... ImageFragment
} }
} }
fragment PerformerAppearanceFragment on PerformerAppearance { fragment TagFragment on Tag {
as name
performer { id
... PerformerFragment
}
} }
fragment FuzzyDateFragment on FuzzyDate { fragment MeasurementsFragment on Measurements {
date band_size
accuracy cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
} }
` `
@@ -310,61 +291,6 @@ const FindScenesByFingerprintsQuery = `query FindScenesByFingerprints ($fingerpr
... SceneFragment ... 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 { fragment PerformerAppearanceFragment on PerformerAppearance {
as as
performer { performer {
@@ -404,19 +330,74 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment ... BodyModificationFragment
} }
} }
fragment FuzzyDateFragment on FuzzyDate { fragment MeasurementsFragment on Measurements {
date band_size
accuracy cup_size
waist
hip
} }
fragment FingerprintFragment on Fingerprint { fragment FingerprintFragment on Fingerprint {
algorithm algorithm
hash hash
duration 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 { fragment URLFragment on URL {
url url
type 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) { 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 ... 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 { fragment FuzzyDateFragment on FuzzyDate {
date date
accuracy accuracy
@@ -447,11 +475,6 @@ fragment MeasurementsFragment on Measurements {
waist waist
hip hip
} }
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment SceneFragment on Scene { fragment SceneFragment on Scene {
id id
title title
@@ -477,59 +500,6 @@ fragment SceneFragment on Scene {
... FingerprintFragment ... 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 { fragment StudioFragment on Studio {
name name
id id
@@ -540,137 +510,21 @@ fragment StudioFragment on Studio {
... ImageFragment ... ImageFragment
} }
} }
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment BodyModificationFragment on BodyModification { fragment BodyModificationFragment on BodyModification {
location location
description 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 { fragment FingerprintFragment on Fingerprint {
algorithm algorithm
hash hash
duration 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) { 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 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!) { const SubmitFingerprintQuery = `mutation SubmitFingerprint ($input: FingerprintSubmission!) {
submitFingerprint(input: $input) submitFingerprint(input: $input)
} }

View File

@@ -3,7 +3,6 @@ package sqlite
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"path/filepath"
"strconv" "strconv"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
@@ -14,6 +13,7 @@ const galleryTable = "galleries"
const performersGalleriesTable = "performers_galleries" const performersGalleriesTable = "performers_galleries"
const galleriesTagsTable = "galleries_tags" const galleriesTagsTable = "galleries_tags"
const galleriesImagesTable = "galleries_images" const galleriesImagesTable = "galleries_images"
const galleriesScenesTable = "scenes_galleries"
const galleryIDColumn = "gallery_id" const galleryIDColumn = "gallery_id"
type galleryQueryBuilder struct { type galleryQueryBuilder struct {
@@ -73,23 +73,6 @@ func (qb *galleryQueryBuilder) Destroy(id int) error {
return qb.destroyExisting([]int{id}) 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) { func (qb *galleryQueryBuilder) Find(id int) (*models.Gallery, error) {
var ret models.Gallery var ret models.Gallery
if err := qb.get(id, &ret); err != nil { 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) 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) { func (qb *galleryQueryBuilder) FindByPath(path string) (*models.Gallery, error) {
query := "SELECT * FROM galleries WHERE path = ? LIMIT 1" query := "SELECT * FROM galleries WHERE path = ? LIMIT 1"
args := []interface{}{path} args := []interface{}{path}
return qb.queryGallery(query, args) return qb.queryGallery(query, args)
} }
func (qb *galleryQueryBuilder) FindBySceneID(sceneID int) (*models.Gallery, error) { func (qb *galleryQueryBuilder) FindBySceneID(sceneID int) ([]*models.Gallery, error) {
query := "SELECT galleries.* FROM galleries WHERE galleries.scene_id = ? LIMIT 1" 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} args := []interface{}{sceneID}
return qb.queryGallery(query, args) return qb.queryGalleries(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)
} }
func (qb *galleryQueryBuilder) FindByImageID(imageID int) ([]*models.Gallery, error) { 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 = selectDistinctIDs("galleries")
query.body += ` query.body += `
left join performers_galleries as performers_join on performers_join.gallery_id = galleries.id 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 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_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 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 != "" { 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) clause, thisArgs := getSearchBinding(searchColumns, *q, false)
query.addWhere(clause) query.addWhere(clause)
query.addArg(thisArgs...) query.addArg(thisArgs...)
@@ -221,8 +212,8 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" { if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter { switch *isMissingFilter {
case "scene": case "scenes":
query.addWhere("galleries.scene_id IS NULL") query.addWhere("scenes_join.gallery_id IS NULL")
case "studio": case "studio":
query.addWhere("galleries.studio_id IS NULL") query.addWhere("galleries.studio_id IS NULL")
case "performers": case "performers":
@@ -442,3 +433,23 @@ func (qb *galleryQueryBuilder) UpdateImages(galleryID int, imageIDs []int) error
// Delete the existing joins and then create new ones // Delete the existing joins and then create new ones
return qb.imagesRepository().replace(galleryID, imageIDs) 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)
}

View File

@@ -94,21 +94,21 @@ func TestGalleryFindBySceneID(t *testing.T) {
gqb := r.Gallery() gqb := r.Gallery()
sceneID := sceneIDs[sceneIdxWithGallery] sceneID := sceneIDs[sceneIdxWithGallery]
gallery, err := gqb.FindBySceneID(sceneID) galleries, err := gqb.FindBySceneID(sceneID)
if err != nil { if err != nil {
t.Errorf("Error finding gallery: %s", err.Error()) 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 { if err != nil {
t.Errorf("Error finding gallery: %s", err.Error()) t.Errorf("Error finding gallery: %s", err.Error())
} }
assert.Nil(t, gallery) assert.Nil(t, galleries)
return nil return nil
}) })
@@ -233,7 +233,7 @@ func verifyGalleriesRating(t *testing.T, ratingCriterion models.IntCriterionInpu
func TestGalleryQueryIsMissingScene(t *testing.T) { func TestGalleryQueryIsMissingScene(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
qb := r.Gallery() qb := r.Gallery()
isMissing := "scene" isMissing := "scenes"
galleryFilter := models.GalleryFilterType{ galleryFilter := models.GalleryFilterType{
IsMissing: &isMissing, IsMissing: &isMissing,
} }
@@ -265,10 +265,8 @@ func TestGalleryQueryIsMissingScene(t *testing.T) {
}) })
} }
// TODO ValidGalleriesForScenePath
// TODO Count // TODO Count
// TODO All // TODO All
// TODO Query // TODO Query
// TODO Update // TODO Update
// TODO Destroy // TODO Destroy
// TODO ClearGalleryId

View File

@@ -13,6 +13,7 @@ const sceneTable = "scenes"
const sceneIDColumn = "scene_id" const sceneIDColumn = "scene_id"
const performersScenesTable = "performers_scenes" const performersScenesTable = "performers_scenes"
const scenesTagsTable = "scenes_tags" const scenesTagsTable = "scenes_tags"
const scenesGalleriesTable = "scenes_galleries"
const moviesScenesTable = "movies_scenes" const moviesScenesTable = "movies_scenes"
var scenesForPerformerQuery = selectAll(sceneTable) + ` var scenesForPerformerQuery = selectAll(sceneTable) + `
@@ -44,6 +45,12 @@ WHERE scenes_tags.tag_id = ?
GROUP BY scenes_tags.scene_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 = ` var countScenesForMissingChecksumQuery = `
SELECT id FROM scenes SELECT id FROM scenes
WHERE scenes.checksum is null WHERE scenes.checksum is null
@@ -221,6 +228,11 @@ func (qb *sceneQueryBuilder) FindByPerformerID(performerID int) ([]*models.Scene
return qb.queryScenes(scenesForPerformerQuery, args) 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) { func (qb *sceneQueryBuilder) CountByPerformerID(performerID int) (int, error) {
args := []interface{}{performerID} args := []interface{}{performerID}
return qb.runCountQuery(qb.buildCountQuery(countScenesForPerformerQuery), args) 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 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 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 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 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 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 != "" { if isMissingFilter := sceneFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter { switch *isMissingFilter {
case "gallery": case "galleries":
query.addWhere("gallery.scene_id IS NULL") query.addWhere("galleries_join.scene_id IS NULL")
case "studio": case "studio":
query.addWhere("scenes.studio_id IS NULL") query.addWhere("scenes.studio_id IS NULL")
case "movie": case "movie":
@@ -683,6 +695,26 @@ func (qb *sceneQueryBuilder) UpdateTags(id int, tagIDs []int) error {
return qb.tagsRepository().replace(id, tagIDs) 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 { func (qb *sceneQueryBuilder) stashIDRepository() *stashIDRepository {
return &stashIDRepository{ return &stashIDRepository{
repository{ repository{

View File

@@ -494,7 +494,7 @@ func TestSceneQueryHasMarkers(t *testing.T) {
func TestSceneQueryIsMissingGallery(t *testing.T) { func TestSceneQueryIsMissingGallery(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
sqb := r.Scene() sqb := r.Scene()
isMissing := "gallery" isMissing := "galleries"
sceneFilter := models.SceneFilterType{ sceneFilter := models.SceneFilterType{
IsMissing: &isMissing, IsMissing: &isMissing,
} }

View File

@@ -5,7 +5,6 @@ package sqlite_test
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -192,7 +191,7 @@ func populateDB() error {
return fmt.Errorf("error creating studios: %s", err.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()) return fmt.Errorf("error linking scene to gallery: %s", err.Error())
} }
@@ -623,20 +622,8 @@ func linkScenePerformer(qb models.SceneReaderWriter, sceneIndex, performerIndex
return err return err
} }
func linkSceneGallery(gqb models.GalleryReaderWriter, sceneIndex, galleryIndex int) error { func linkSceneGallery(qb models.SceneReaderWriter, sceneIndex, galleryIndex int) error {
gallery, err := gqb.Find(galleryIDs[galleryIndex]) _, err := scene.AddGallery(qb, sceneIDs[sceneIndex], 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)
return err return err
} }

View File

@@ -1,6 +1,7 @@
#### 💥 Note: After upgrading, all scene file sizes will be 0B until a new [scan](/settings?tab=tasks) is run. #### 💥 Note: After upgrading, all scene file sizes will be 0B until a new [scan](/settings?tab=tasks) is run.
### ✨ New Features ### ✨ New Features
* Add support for multiple galleries per scene, and vice-versa.
* Add backup database functionality to Settings/Tasks. * Add backup database functionality to Settings/Tasks.
* Add gallery wall view. * Add gallery wall view.
* Add organized flag for scenes, galleries and images. * Add organized flag for scenes, galleries and images.

View File

@@ -7,7 +7,7 @@ import { useToast } from "src/hooks";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
interface IDeleteGalleryDialogProps { interface IDeleteGalleryDialogProps {
selected: Partial<GQL.GalleryDataFragment>[]; selected: Pick<GQL.Gallery, "id">[];
onClose: (confirmed: boolean) => void; onClose: (confirmed: boolean) => void;
} }

View File

@@ -16,9 +16,9 @@ import { TextUtils } from "src/utils";
interface IProps { interface IProps {
gallery: GQL.GallerySlimDataFragment; gallery: GQL.GallerySlimDataFragment;
selecting?: boolean; selecting?: boolean;
selected: boolean | undefined; selected?: boolean | undefined;
zoomIndex: number; zoomIndex?: number;
onSelectedChanged: (selected: boolean, shiftKey: boolean) => void; onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
} }
export const GalleryCard: React.FC<IProps> = (props) => { export const GalleryCard: React.FC<IProps> = (props) => {
@@ -27,19 +27,18 @@ export const GalleryCard: React.FC<IProps> = (props) => {
config?.data?.configuration.interface.showStudioAsText ?? false; config?.data?.configuration.interface.showStudioAsText ?? false;
function maybeRenderScenePopoverButton() { function maybeRenderScenePopoverButton() {
if (!props.gallery.scene) return; if (props.gallery.scenes.length === 0) return;
const popoverContent = ( const popoverContent = props.gallery.scenes.map((scene) => (
<TagLink key={props.gallery.scene.id} scene={props.gallery.scene} /> <TagLink key={scene.id} scene={scene} />
); ));
return ( return (
<HoverPopover placement="bottom" content={popoverContent}> <HoverPopover placement="bottom" content={popoverContent}>
<Link to={`/scenes/${props.gallery.scene.id}`}> <Button className="minimal">
<Button className="minimal"> <Icon icon="play-circle" />
<Icon icon="play-circle" /> <span>{props.gallery.scenes.length}</span>
</Button> </Button>
</Link>
</HoverPopover> </HoverPopover>
); );
} }
@@ -124,7 +123,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
function maybeRenderPopoverButtonGroup() { function maybeRenderPopoverButtonGroup() {
if ( if (
props.gallery.scene || props.gallery.scenes.length > 0 ||
props.gallery.performers.length > 0 || props.gallery.performers.length > 0 ||
props.gallery.tags.length > 0 || props.gallery.tags.length > 0 ||
props.gallery.organized props.gallery.organized

View File

@@ -13,6 +13,7 @@ import { DeleteGalleriesDialog } from "../DeleteGalleriesDialog";
import { GalleryImagesPanel } from "./GalleryImagesPanel"; import { GalleryImagesPanel } from "./GalleryImagesPanel";
import { GalleryAddPanel } from "./GalleryAddPanel"; import { GalleryAddPanel } from "./GalleryAddPanel";
import { GalleryFileInfoPanel } from "./GalleryFileInfoPanel"; import { GalleryFileInfoPanel } from "./GalleryFileInfoPanel";
import { GalleryScenesPanel } from "./GalleryScenesPanel";
interface IGalleryParams { interface IGalleryParams {
id?: string; id?: string;
@@ -118,6 +119,11 @@ export const Gallery: React.FC = () => {
<Nav.Item> <Nav.Item>
<Nav.Link eventKey="gallery-details-panel">Details</Nav.Link> <Nav.Link eventKey="gallery-details-panel">Details</Nav.Link>
</Nav.Item> </Nav.Item>
{gallery.scenes.length > 0 && (
<Nav.Item>
<Nav.Link eventKey="gallery-scenes-panel">Scenes</Nav.Link>
</Nav.Item>
)}
{gallery.path ? ( {gallery.path ? (
<Nav.Item> <Nav.Item>
<Nav.Link eventKey="gallery-file-info-panel"> <Nav.Link eventKey="gallery-file-info-panel">
@@ -157,6 +163,11 @@ export const Gallery: React.FC = () => {
onDelete={() => setIsDeleteAlertOpen(true)} onDelete={() => setIsDeleteAlertOpen(true)}
/> />
</Tab.Pane> </Tab.Pane>
{gallery.scenes.length > 0 && (
<Tab.Pane eventKey="gallery-scenes-panel">
<GalleryScenesPanel scenes={gallery.scenes} />
</Tab.Pane>
)}
</Tab.Content> </Tab.Content>
</Tab.Container> </Tab.Container>
); );

View File

@@ -12,12 +12,13 @@ import {
import { import {
PerformerSelect, PerformerSelect,
TagSelect, TagSelect,
SceneSelect,
StudioSelect, StudioSelect,
Icon, Icon,
LoadingIndicator, LoadingIndicator,
} from "src/components/Shared"; } from "src/components/Shared";
import { useToast } from "src/hooks"; 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 { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
import { GalleryScrapeDialog } from "./GalleryScrapeDialog"; import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
@@ -49,6 +50,7 @@ export const GalleryEditPanel: React.FC<
const [studioId, setStudioId] = useState<string>(); const [studioId, setStudioId] = useState<string>();
const [performerIds, setPerformerIds] = useState<string[]>(); const [performerIds, setPerformerIds] = useState<string[]>();
const [tagIds, setTagIds] = useState<string[]>(); const [tagIds, setTagIds] = useState<string[]>();
const [scenes, setScenes] = useState<{ id: string; title: string }[]>([]);
const Scrapers = useListGalleryScrapers(); const Scrapers = useListGalleryScrapers();
@@ -117,6 +119,12 @@ export const GalleryEditPanel: React.FC<
setStudioId(state?.studio?.id ?? undefined); setStudioId(state?.studio?.id ?? undefined);
setPerformerIds(perfIds); setPerformerIds(perfIds);
setTagIds(tIds); setTagIds(tIds);
setScenes(
(state?.scenes ?? []).map((s) => ({
id: s.id,
title: s.title ?? TextUtils.fileNameFromPath(s.path ?? ""),
}))
);
} }
useEffect(() => { useEffect(() => {
@@ -135,6 +143,7 @@ export const GalleryEditPanel: React.FC<
studio_id: studioId ?? null, studio_id: studioId ?? null,
performer_ids: performerIds, performer_ids: performerIds,
tag_ids: tagIds, tag_ids: tagIds,
scene_ids: scenes.map((s) => s.id),
}; };
} }
@@ -390,6 +399,23 @@ export const GalleryEditPanel: React.FC<
/> />
</Col> </Col>
</Form.Group> </Form.Group>
<Form.Group controlId="scenes" as={Row}>
{FormUtils.renderLabel({
title: "Scenes",
labelProps: {
column: true,
sm: 3,
xl: 12,
},
})}
<Col sm={9} xl={12}>
<SceneSelect
scenes={scenes}
onSelect={(items) => setScenes(items)}
/>
</Col>
</Form.Group>
</div> </div>
<div className="col-12 col-lg-6 col-xl-12"> <div className="col-12 col-lg-6 col-xl-12">
<Form.Group controlId="details"> <Form.Group controlId="details">

View File

@@ -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<IGalleryScenesPanelProps> = ({
scenes,
}) => (
<div className="container gallery-scenes">
{scenes.map((scene) => (
<SceneCard scene={scene} />
))}
</div>
);

View File

@@ -1,16 +1,20 @@
import React from "react"; import React from "react";
import * as GQL from "src/core/generated-graphql"; import { useFindGallery } from "src/core/StashService";
import { useLightbox } from "src/hooks"; import { useLightbox } from "src/hooks";
import { LoadingIndicator } from "src/components/Shared";
import "flexbin/flexbin.css"; import "flexbin/flexbin.css";
interface IProps { interface IProps {
gallery: GQL.GalleryDataFragment; galleryId: string;
} }
export const GalleryViewer: React.FC<IProps> = ({ gallery }) => { export const GalleryViewer: React.FC<IProps> = ({ galleryId }) => {
const images = gallery?.images ?? []; const { data, loading } = useFindGallery(galleryId);
const images = data?.findGallery?.images ?? [];
const showLightbox = useLightbox({ images, showNavigation: false }); const showLightbox = useLightbox({ images, showNavigation: false });
if (loading) return <LoadingIndicator />;
const thumbs = images.map((file, index) => ( const thumbs = images.map((file, index) => (
<div <div
role="link" role="link"

View File

@@ -90,6 +90,11 @@ $galleryTabWidth: 450px;
&-gallery { &-gallery {
height: 22.5rem; height: 22.5rem;
} }
&-image {
height: 22.5rem;
width: 15rem;
}
} }
} }
} }

View File

@@ -66,9 +66,9 @@ export const ScenePreview: React.FC<IScenePreviewProps> = ({
interface ISceneCardProps { interface ISceneCardProps {
scene: GQL.SlimSceneDataFragment; scene: GQL.SlimSceneDataFragment;
selecting?: boolean; selecting?: boolean;
selected: boolean | undefined; selected?: boolean | undefined;
zoomIndex: number; zoomIndex?: number;
onSelectedChanged: (selected: boolean, shiftKey: boolean) => void; onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
} }
export const SceneCard: React.FC<ISceneCardProps> = ( export const SceneCard: React.FC<ISceneCardProps> = (
@@ -257,17 +257,20 @@ export const SceneCard: React.FC<ISceneCardProps> = (
} }
function maybeRenderGallery() { function maybeRenderGallery() {
if (props.scene.gallery) { if (props.scene.galleries.length <= 0) return;
return (
<div> const popoverContent = props.scene.galleries.map((gallery) => (
<Link to={`/galleries/${props.scene.gallery.id}`}> <TagLink key={gallery.id} gallery={gallery} />
<Button className="minimal"> ));
<Icon icon="image" />
</Button> return (
</Link> <HoverPopover placement="bottom" content={popoverContent}>
</div> <Button className="minimal">
); <Icon icon="image" />
} <span>{props.scene.galleries.length}</span>
</Button>
</HoverPopover>
);
} }
function maybeRenderOrganized() { function maybeRenderOrganized() {
@@ -289,7 +292,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
props.scene.movies.length > 0 || props.scene.movies.length > 0 ||
props.scene.scene_markers.length > 0 || props.scene.scene_markers.length > 0 ||
props.scene?.o_counter || props.scene?.o_counter ||
props.scene.gallery || props.scene.galleries.length > 0 ||
props.scene.organized props.scene.organized
) { ) {
return ( return (
@@ -314,7 +317,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
) { ) {
const { shiftKey } = event; const { shiftKey } = event;
if (props.selecting) { if (props.selecting && props.onSelectedChanged) {
props.onSelectedChanged(!props.selected, shiftKey); props.onSelectedChanged(!props.selected, shiftKey);
event.preventDefault(); event.preventDefault();
} }
@@ -331,7 +334,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
const ev = event; const ev = event;
const shiftKey = false; const shiftKey = false;
if (props.selecting && !props.selected) { if (props.selecting && props.onSelectedChanged && !props.selected) {
props.onSelectedChanged(true, shiftKey); props.onSelectedChanged(true, shiftKey);
} }
@@ -354,7 +357,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
type="checkbox" type="checkbox"
className="scene-card-check" className="scene-card-check"
checked={props.selected} checked={props.selected}
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)} onChange={() => props.onSelectedChanged?.(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => { onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
// eslint-disable-next-line prefer-destructuring // eslint-disable-next-line prefer-destructuring
shiftKey = event.shiftKey; shiftKey = event.shiftKey;

View File

@@ -24,6 +24,7 @@ import { SceneEditPanel } from "./SceneEditPanel";
import { SceneDetailPanel } from "./SceneDetailPanel"; import { SceneDetailPanel } from "./SceneDetailPanel";
import { OCounterButton } from "./OCounterButton"; import { OCounterButton } from "./OCounterButton";
import { SceneMoviePanel } from "./SceneMoviePanel"; import { SceneMoviePanel } from "./SceneMoviePanel";
import { SceneGalleriesPanel } from "./SceneGalleriesPanel";
import { DeleteScenesDialog } from "../DeleteScenesDialog"; import { DeleteScenesDialog } from "../DeleteScenesDialog";
import { SceneGenerateDialog } from "../SceneGenerateDialog"; import { SceneGenerateDialog } from "../SceneGenerateDialog";
import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel"; import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel";
@@ -243,13 +244,16 @@ export const Scene: React.FC = () => {
) : ( ) : (
"" ""
)} )}
{scene.gallery ? ( {scene.galleries.length === 1 ? (
<Nav.Item> <Nav.Item>
<Nav.Link eventKey="scene-gallery-panel">Gallery</Nav.Link> <Nav.Link eventKey="scene-gallery-panel">Gallery</Nav.Link>
</Nav.Item> </Nav.Item>
) : ( ) : undefined}
"" {scene.galleries.length > 1 ? (
)} <Nav.Item>
<Nav.Link eventKey="scene-galleries-panel">Galleries</Nav.Link>
</Nav.Item>
) : undefined}
<Nav.Item> <Nav.Item>
<Nav.Link eventKey="scene-video-filter-panel">Filters</Nav.Link> <Nav.Link eventKey="scene-video-filter-panel">Filters</Nav.Link>
</Nav.Item> </Nav.Item>
@@ -295,12 +299,15 @@ export const Scene: React.FC = () => {
<Tab.Pane eventKey="scene-movie-panel"> <Tab.Pane eventKey="scene-movie-panel">
<SceneMoviePanel scene={scene} /> <SceneMoviePanel scene={scene} />
</Tab.Pane> </Tab.Pane>
{scene.gallery ? ( {scene.galleries.length === 1 && (
<Tab.Pane eventKey="scene-gallery-panel"> <Tab.Pane eventKey="scene-gallery-panel">
<GalleryViewer gallery={scene.gallery} /> <GalleryViewer galleryId={scene.galleries[0].id} />
</Tab.Pane>
)}
{scene.galleries.length > 1 && (
<Tab.Pane eventKey="scene-galleries-panel">
<SceneGalleriesPanel galleries={scene.galleries} />
</Tab.Pane> </Tab.Pane>
) : (
""
)} )}
<Tab.Pane eventKey="scene-video-filter-panel"> <Tab.Pane eventKey="scene-video-filter-panel">
<SceneVideoFilterPanel scene={scene} /> <SceneVideoFilterPanel scene={scene} />

View File

@@ -22,13 +22,13 @@ import {
PerformerSelect, PerformerSelect,
TagSelect, TagSelect,
StudioSelect, StudioSelect,
SceneGallerySelect, GallerySelect,
Icon, Icon,
LoadingIndicator, LoadingIndicator,
ImageInput, ImageInput,
} from "src/components/Shared"; } from "src/components/Shared";
import { useToast } from "src/hooks"; 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 { MovieSelect } from "src/components/Shared/Select";
import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable"; import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable";
import { RatingStars } from "./RatingStars"; import { RatingStars } from "./RatingStars";
@@ -47,7 +47,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
const [url, setUrl] = useState<string>(); const [url, setUrl] = useState<string>();
const [date, setDate] = useState<string>(); const [date, setDate] = useState<string>();
const [rating, setRating] = useState<number>(); const [rating, setRating] = useState<number>();
const [galleryId, setGalleryId] = useState<string>(); const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
[]
);
const [studioId, setStudioId] = useState<string>(); const [studioId, setStudioId] = useState<string>();
const [performerIds, setPerformerIds] = useState<string[]>(); const [performerIds, setPerformerIds] = useState<string[]>();
const [movieIds, setMovieIds] = useState<string[] | undefined>(undefined); const [movieIds, setMovieIds] = useState<string[] | undefined>(undefined);
@@ -171,7 +173,12 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
setUrl(state.url ?? undefined); setUrl(state.url ?? undefined);
setDate(state.date ?? undefined); setDate(state.date ?? undefined);
setRating(state.rating === null ? NaN : state.rating); 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); setStudioId(state?.studio?.id ?? undefined);
setMovieIds(moviIds); setMovieIds(moviIds);
setMovieSceneIndexes(movieSceneIdx); setMovieSceneIndexes(movieSceneIdx);
@@ -196,7 +203,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
url, url,
date, date,
rating: rating ?? null, rating: rating ?? null,
gallery_id: galleryId ?? null, gallery_ids: galleries.map((g) => g.id),
studio_id: studioId ?? null, studio_id: studioId ?? null,
performer_ids: performerIds, performer_ids: performerIds,
movies: makeMovieInputs(), movies: makeMovieInputs(),
@@ -596,15 +603,14 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
/> />
</Col> </Col>
</Form.Group> </Form.Group>
<Form.Group controlId="gallery" as={Row}> <Form.Group controlId="galleries" as={Row}>
{FormUtils.renderLabel({ {FormUtils.renderLabel({
title: "Gallery", title: "Galleries",
})} })}
<Col xs={9}> <Col xs={9}>
<SceneGallerySelect <GallerySelect
sceneId={props.scene.id} galleries={galleries}
gallery={props.scene.gallery ?? undefined} onSelect={(items) => setGalleries(items)}
onSelect={(item) => setGalleryId(item ? item.id : undefined)}
/> />
</Col> </Col>
</Form.Group> </Form.Group>

View File

@@ -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<ISceneGalleriesPanelProps> = ({
galleries,
}) => {
const cards = galleries.map((gallery) => (
<GalleryCard key={gallery.id} gallery={gallery} selecting={false} />
));
return <div className="row justify-content-center">{cards}</div>;
};

View File

@@ -14,11 +14,9 @@ import {
useTagCreate, useTagCreate,
useStudioCreate, useStudioCreate,
usePerformerCreate, usePerformerCreate,
useFindGalleries,
} from "src/core/StashService"; } from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter"; import { TextUtils } from "src/utils";
import { FilterMode } from "src/models/list-filter/types";
export type ValidTypes = export type ValidTypes =
| GQL.SlimPerformerDataFragment | GQL.SlimPerformerDataFragment
@@ -72,23 +70,28 @@ interface IFilterComponentProps extends IFilterProps {
interface IFilterSelectProps<T extends boolean> interface IFilterSelectProps<T extends boolean>
extends Omit<ISelectProps<T>, "onChange" | "items" | "onCreateOption"> {} extends Omit<ISelectProps<T>, "onChange" | "items" | "onCreateOption"> {}
interface ISceneGallerySelect { type Gallery = { id: string; title: string };
gallery?: Pick<GQL.Gallery, "title" | "path" | "id">; interface IGallerySelect {
sceneId: string; galleries: Gallery[];
onSelect: ( onSelect: (items: Gallery[]) => void;
item:
| GQL.ValidGalleriesForSceneQuery["validGalleriesForScene"][0]
| undefined
) => void;
} }
const getSelectedValues = (selectedItems: ValueType<Option, boolean>) => type Scene = { id: string; title: string };
interface ISceneSelect {
scenes: Scene[];
onSelect: (items: Scene[]) => void;
}
const getSelectedItems = (selectedItems: ValueType<Option, boolean>) =>
selectedItems selectedItems
? (Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map( ? Array.isArray(selectedItems)
(item) => item.value ? selectedItems
) : [selectedItems]
: []; : [];
const getSelectedValues = (selectedItems: ValueType<Option, boolean>) =>
getSelectedItems(selectedItems).map((item) => item.value);
const SelectComponent = <T extends boolean>({ const SelectComponent = <T extends boolean>({
type, type,
initialIds, initialIds,
@@ -231,53 +234,104 @@ const FilterSelectComponent = <T extends boolean>(
); );
}; };
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => { export const GallerySelect: React.FC<IGallerySelect> = (props) => {
const [query, setQuery] = useState<string>(""); const [query, setQuery] = useState<string>("");
const { data, loading } = useFindGalleries(getFilter()); const { data, loading } = GQL.useFindGalleriesQuery({
const [selectedOption, setSelectedOption] = useState<Option>(); skip: query === "",
variables: {
filter: {
q: query,
},
},
});
const galleries = data?.findGalleries.galleries ?? []; const galleries = data?.findGalleries.galleries ?? [];
const items = galleries.map((g) => ({ const items = galleries.map((g) => ({
label: g.title ?? g.path ?? "", label: g.title ?? TextUtils.fileNameFromPath(g.path ?? ""),
value: g.id, value: g.id,
})); }));
function getFilter() {
const ret = new ListFilterModel(FilterMode.Galleries);
ret.searchTerm = query;
return ret;
}
const onInputChange = debounce((input: string) => { const onInputChange = debounce((input: string) => {
setQuery(input); setQuery(input);
}, 500); }, 500);
const onChange = (selectedItem: ValueType<Option, false>) => { const onChange = (selectedItems: ValueType<Option, boolean>) => {
setSelectedOption(selectedItem ?? undefined); const selected = getSelectedItems(selectedItems);
props.onSelect( props.onSelect(
selectedItem selected.map((s) => ({
? galleries.find((g) => g.id === selectedItem.value) id: s.value,
: undefined title: s.label,
}))
); );
}; };
const option = const options = props.galleries.map((g) => ({
selectedOption ?? value: g.id,
(props.gallery label: g.title ?? "Unknown",
? { }));
value: props.gallery.id,
label: props.gallery.title ?? props.gallery.path ?? "Unknown",
}
: undefined);
return ( return (
<SelectComponent <SelectComponent
isMulti={false}
onChange={onChange} onChange={onChange}
onInputChange={onInputChange} onInputChange={onInputChange}
isLoading={loading} isLoading={loading}
items={items} items={items}
selectedOptions={option} selectedOptions={options}
isMulti
placeholder="Search for gallery..."
noOptionsMessage={query === "" ? null : "No galleries found."}
showDropdown={false}
/>
);
};
export const SceneSelect: React.FC<ISceneSelect> = (props) => {
const [query, setQuery] = useState<string>("");
const { data, loading } = GQL.useFindScenesQuery({
skip: query === "",
variables: {
filter: {
q: query,
},
},
});
const scenes = data?.findScenes.scenes ?? [];
const items = scenes.map((s) => ({
label: s.title ?? TextUtils.fileNameFromPath(s.path ?? ""),
value: s.id,
}));
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
const onChange = (selectedItems: ValueType<Option, true>) => {
const selected = getSelectedItems(selectedItems);
props.onSelect(
(selected ?? []).map((s) => ({
id: s.value,
title: s.label,
}))
);
};
const options = props.scenes.map((s) => ({
value: s.id,
label: s.title,
}));
return (
<SelectComponent
onChange={onChange}
onInputChange={onInputChange}
isLoading={loading}
items={items}
selectedOptions={options}
isMulti
placeholder="Search for scene..."
noOptionsMessage={query === "" ? null : "No scenes found."}
showDropdown={false}
/> />
); );
}; };

View File

@@ -1,11 +1,12 @@
export { export {
SceneGallerySelect, GallerySelect,
ScrapePerformerSuggest, ScrapePerformerSuggest,
MarkerTitleSuggest, MarkerTitleSuggest,
FilterSelect, FilterSelect,
PerformerSelect, PerformerSelect,
StudioSelect, StudioSelect,
TagSelect, TagSelect,
SceneSelect,
} from "./Select"; } from "./Select";
export { default as Icon } from "./Icon"; export { default as Icon } from "./Icon";

View File

@@ -33,13 +33,13 @@
padding: 1rem 0; padding: 1rem 0;
&:hover { &:hover {
background-color: hsl(204, 20, 30); background-color: hsl(204, 20%, 30%);
cursor: pointer; cursor: pointer;
} }
} }
.selected-result { .selected-result {
background-color: hsl(204, 20, 30); background-color: hsl(204, 20%, 30%);
border-radius: 3px; border-radius: 3px;
&:hover { &:hover {

View File

@@ -264,10 +264,6 @@ export const useAllPerformersForFilter = () =>
GQL.useAllPerformersForFilterQuery(); GQL.useAllPerformersForFilterQuery();
export const useAllStudiosForFilter = () => GQL.useAllStudiosForFilterQuery(); export const useAllStudiosForFilter = () => GQL.useAllStudiosForFilterQuery();
export const useAllMoviesForFilter = () => GQL.useAllMoviesForFilterQuery(); export const useAllMoviesForFilter = () => GQL.useAllMoviesForFilterQuery();
export const useValidGalleriesForScene = (sceneId: string) =>
GQL.useValidGalleriesForSceneQuery({
variables: { scene_id: sceneId },
});
export const useStats = () => GQL.useStatsQuery(); export const useStats = () => GQL.useStatsQuery();
export const useVersion = () => GQL.useVersionQuery(); export const useVersion = () => GQL.useVersionQuery();
export const useLatestVersion = () => export const useLatestVersion = () =>

View File

@@ -15,7 +15,7 @@ export class SceneIsMissingCriterion extends IsMissingCriterion {
"details", "details",
"url", "url",
"date", "date",
"gallery", "galleries",
"studio", "studio",
"movie", "movie",
"performers", "performers",
@@ -83,7 +83,7 @@ export class GalleryIsMissingCriterion extends IsMissingCriterion {
"studio", "studio",
"performers", "performers",
"tags", "tags",
"scene", "scenes",
]; ];
} }