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 {
...PerformerData
}
scene {
scenes {
id
title
path

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,41 +120,6 @@ func (r *queryResolver) MarkerStrings(ctx context.Context, q *string, sort *stri
return ret, nil
}
func (r *queryResolver) ValidGalleriesForScene(ctx context.Context, scene_id *string) ([]*models.Gallery, error) {
if scene_id == nil {
panic("nil scene id") // TODO make scene_id mandatory
}
sceneID, err := strconv.Atoi(*scene_id)
if err != nil {
return nil, err
}
var validGalleries []*models.Gallery
var sceneGallery *models.Gallery
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
sqb := repo.Scene()
scene, err := sqb.Find(sceneID)
if err != nil {
return err
}
qb := repo.Gallery()
validGalleries, err = qb.ValidGalleriesForScenePath(scene.Path)
if err != nil {
return err
}
sceneGallery, err = qb.FindBySceneID(sceneID)
return err
}); err != nil {
return nil, err
}
if sceneGallery != nil {
validGalleries = append(validGalleries, sceneGallery)
}
return validGalleries, nil
}
func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
var ret models.StatsResultType
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {

View File

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

View File

@@ -105,7 +105,7 @@ func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) (re
return ret, nil
}
func (r *sceneResolver) Gallery(ctx context.Context, obj *models.Scene) (ret *models.Gallery, err error) {
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
ret, err = repo.Gallery().FindBySceneID(obj.ID)
return err

View File

@@ -60,14 +60,6 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.Galle
newGallery.StudioID = sql.NullInt64{Valid: false}
}
if input.SceneID != nil {
sceneID, _ := strconv.ParseInt(*input.SceneID, 10, 64)
newGallery.SceneID = sql.NullInt64{Int64: sceneID, Valid: true}
} else {
// studio must be nullable
newGallery.SceneID = sql.NullInt64{Valid: false}
}
// Start the transaction and save the gallery
var gallery *models.Gallery
if err := r.withTxn(ctx, func(repo models.Repository) error {
@@ -88,6 +80,11 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.Galle
return err
}
// Save the scenes
if err := r.updateGalleryScenes(qb, gallery.ID, input.SceneIds); err != nil {
return err
}
return nil
}); err != nil {
return nil, err
@@ -112,6 +109,14 @@ func (r *mutationResolver) updateGalleryTags(qb models.GalleryReaderWriter, gall
return qb.UpdateTags(galleryID, ids)
}
func (r *mutationResolver) updateGalleryScenes(qb models.GalleryReaderWriter, galleryID int, sceneIDs []string) error {
ids, err := utils.StringSliceToIntSlice(sceneIDs)
if err != nil {
return err
}
return qb.UpdateScenes(galleryID, ids)
}
func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (ret *models.Gallery, err error) {
translator := changesetTranslator{
inputMap: getUpdateInputMap(ctx),
@@ -221,6 +226,13 @@ func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, transl
}
}
// Save the scenes
if translator.hasField("scene_ids") {
if err := r.updateGalleryScenes(qb, galleryID, input.SceneIds); err != nil {
return nil, err
}
}
return gallery, nil
}
@@ -241,7 +253,6 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B
updatedGallery.Date = translator.sqliteDate(input.Date, "date")
updatedGallery.Rating = translator.nullInt64(input.Rating, "rating")
updatedGallery.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id")
updatedGallery.SceneID = translator.nullInt64FromString(input.SceneID, "scene_id")
updatedGallery.Organized = input.Organized
ret := []*models.Gallery{}
@@ -284,6 +295,18 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.B
return err
}
}
// Save the scenes
if translator.hasField("scene_ids") {
sceneIDs, err := adjustGallerySceneIDs(qb, galleryID, *input.SceneIds)
if err != nil {
return err
}
if err := qb.UpdateScenes(galleryID, sceneIDs); err != nil {
return err
}
}
}
return nil
@@ -312,6 +335,15 @@ func adjustGalleryTagIDs(qb models.GalleryReader, galleryID int, ids models.Bulk
return adjustIDs(ret, ids), nil
}
func adjustGallerySceneIDs(qb models.GalleryReader, galleryID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetSceneIDs(galleryID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) {
galleryIDs, err := utils.StringSliceToIntSlice(input.Ids)
if err != nil {

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
if translator.hasField("performer_ids") {
if err := r.updateScenePerformers(qb, sceneID, input.PerformerIds); err != nil {
@@ -145,6 +122,13 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator
}
}
// Save the galleries
if translator.hasField("gallery_ids") {
if err := r.updateSceneGalleries(qb, sceneID, input.GalleryIds); err != nil {
return nil, err
}
}
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := models.StashIDsFromInput(input.StashIds)
@@ -206,6 +190,14 @@ func (r *mutationResolver) updateSceneTags(qb models.SceneReaderWriter, sceneID
return qb.UpdateTags(sceneID, ids)
}
func (r *mutationResolver) updateSceneGalleries(qb models.SceneReaderWriter, sceneID int, galleryIDs []string) error {
ids, err := utils.StringSliceToIntSlice(galleryIDs)
if err != nil {
return err
}
return qb.UpdateGalleries(sceneID, ids)
}
func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.BulkSceneUpdateInput) ([]*models.Scene, error) {
sceneIDs, err := utils.StringSliceToIntSlice(input.Ids)
if err != nil {
@@ -236,7 +228,6 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
// Start the transaction and save the scene marker
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Scene()
gqb := repo.Gallery()
for _, sceneID := range sceneIDs {
updatedScene.ID = sceneID
@@ -248,20 +239,6 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
ret = append(ret, scene)
if translator.hasField("gallery_id") {
// Save the gallery
galleryID, _ := strconv.Atoi(*input.GalleryID)
updatedGallery := models.GalleryPartial{
ID: galleryID,
SceneID: &sql.NullInt64{Int64: int64(sceneID), Valid: true},
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
if _, err := gqb.UpdatePartial(updatedGallery); err != nil {
return err
}
}
// Save the performers
if translator.hasField("performer_ids") {
performerIDs, err := adjustScenePerformerIDs(qb, sceneID, *input.PerformerIds)
@@ -285,6 +262,18 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
return err
}
}
// Save the galleries
if translator.hasField("gallery_ids") {
galleryIDs, err := adjustSceneGalleryIDs(qb, sceneID, *input.GalleryIds)
if err != nil {
return err
}
if err := qb.UpdateGalleries(sceneID, galleryIDs); err != nil {
return err
}
}
}
return nil
@@ -350,6 +339,15 @@ func adjustSceneTagIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdate
return adjustIDs(ret, ids), nil
}
func adjustSceneGalleryIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
ret, err = qb.GetGalleryIDs(sceneID)
if err != nil {
return nil, err
}
return adjustIDs(ret, ids), nil
}
func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) {
sceneID, err := strconv.Atoi(input.ID)
if err != nil {

View File

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

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
}
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"`
OCounter int `json:"o_counter,omitempty"`
Details string `json:"details,omitempty"`
Gallery string `json:"gallery,omitempty"`
Galleries []string `json:"galleries,omitempty"`
Performers []string `json:"performers,omitempty"`
Movies []SceneMovie `json:"movies,omitempty"`
Tags []string `json:"tags,omitempty"`

View File

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

View File

@@ -382,15 +382,13 @@ func exportScene(wg *sync.WaitGroup, jobChan <-chan *models.Scene, repo models.R
continue
}
sceneGallery, err := galleryReader.FindBySceneID(s.ID)
galleries, err := galleryReader.FindBySceneID(s.ID)
if err != nil {
logger.Errorf("[scenes] <%s> error getting scene gallery: %s", sceneHash, err.Error())
logger.Errorf("[scenes] <%s> error getting scene gallery checksums: %s", sceneHash, err.Error())
continue
}
if sceneGallery != nil {
newSceneJSON.Gallery = sceneGallery.Checksum
}
newSceneJSON.Galleries = gallery.GetChecksums(galleries)
performers, err := performerReader.FindBySceneID(s.ID)
if err != nil {
@@ -423,9 +421,7 @@ func exportScene(wg *sync.WaitGroup, jobChan <-chan *models.Scene, repo models.R
t.studios.IDs = utils.IntAppendUnique(t.studios.IDs, int(s.StudioID.Int64))
}
if sceneGallery != nil {
t.galleries.IDs = utils.IntAppendUnique(t.galleries.IDs, sceneGallery.ID)
}
t.galleries.IDs = utils.IntAppendUniques(t.galleries.IDs, gallery.GetIDs(galleries))
tagIDs, err := scene.GetDependentTagIDs(tagReader, sceneMarkerReader, s)
if err != nil {

View File

@@ -300,41 +300,26 @@ func (t *ScanTask) associateGallery(wg *sizedwaitgroup.SizedWaitGroup) {
return nil
}
// gallery has no SceneID
if !g.SceneID.Valid {
basename := strings.TrimSuffix(t.FilePath, filepath.Ext(t.FilePath))
var relatedFiles []string
vExt := config.GetVideoExtensions()
// make a list of media files that can be related to the gallery
for _, ext := range vExt {
related := basename + "." + ext
// exclude gallery extensions from the related files
if !isGallery(related) {
relatedFiles = append(relatedFiles, related)
}
basename := strings.TrimSuffix(t.FilePath, filepath.Ext(t.FilePath))
var relatedFiles []string
vExt := config.GetVideoExtensions()
// make a list of media files that can be related to the gallery
for _, ext := range vExt {
related := basename + "." + ext
// exclude gallery extensions from the related files
if !isGallery(related) {
relatedFiles = append(relatedFiles, related)
}
for _, scenePath := range relatedFiles {
s, err := sqb.FindByPath(scenePath)
if err != nil {
}
for _, scenePath := range relatedFiles {
scene, _ := sqb.FindByPath(scenePath)
// found related Scene
if scene != nil {
logger.Infof("associate: Gallery %s is related to scene: %d", t.FilePath, scene.ID)
if err := sqb.UpdateGalleries(scene.ID, []int{g.ID}); err != nil {
return err
}
// found related Scene
if s != nil {
logger.Infof("associate: Gallery %s is related to scene: %d", t.FilePath, s.ID)
g.SceneID.Int64 = int64(s.ID)
g.SceneID.Valid = true
_, err = qb.Update(*g)
if err != nil {
return fmt.Errorf("associate: Error updating gallery sceneId %s", err)
}
// since a gallery can have only one related scene
// only first found is associated
break
}
}
}

View File

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

View File

@@ -35,20 +35,6 @@ func (_m *GalleryReaderWriter) All() ([]*models.Gallery, error) {
return r0, r1
}
// ClearGalleryId provides a mock function with given fields: sceneID
func (_m *GalleryReaderWriter) ClearGalleryId(sceneID int) error {
ret := _m.Called(sceneID)
var r0 error
if rf, ok := ret.Get(0).(func(int) error); ok {
r0 = rf(sceneID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Count provides a mock function with given fields:
func (_m *GalleryReaderWriter) Count() (int, error) {
ret := _m.Called()
@@ -153,6 +139,29 @@ func (_m *GalleryReaderWriter) FindByChecksum(checksum string) (*models.Gallery,
return r0, r1
}
// FindByChecksums provides a mock function with given fields: checksums
func (_m *GalleryReaderWriter) FindByChecksums(checksums []string) ([]*models.Gallery, error) {
ret := _m.Called(checksums)
var r0 []*models.Gallery
if rf, ok := ret.Get(0).(func([]string) []*models.Gallery); ok {
r0 = rf(checksums)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Gallery)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(checksums)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindByImageID provides a mock function with given fields: imageID
func (_m *GalleryReaderWriter) FindByImageID(imageID int) ([]*models.Gallery, error) {
ret := _m.Called(imageID)
@@ -200,15 +209,15 @@ func (_m *GalleryReaderWriter) FindByPath(path string) (*models.Gallery, error)
}
// FindBySceneID provides a mock function with given fields: sceneID
func (_m *GalleryReaderWriter) FindBySceneID(sceneID int) (*models.Gallery, error) {
func (_m *GalleryReaderWriter) FindBySceneID(sceneID int) ([]*models.Gallery, error) {
ret := _m.Called(sceneID)
var r0 *models.Gallery
if rf, ok := ret.Get(0).(func(int) *models.Gallery); ok {
var r0 []*models.Gallery
if rf, ok := ret.Get(0).(func(int) []*models.Gallery); ok {
r0 = rf(sceneID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.Gallery)
r0 = ret.Get(0).([]*models.Gallery)
}
}
@@ -314,6 +323,29 @@ func (_m *GalleryReaderWriter) GetTagIDs(galleryID int) ([]int, error) {
return r0, r1
}
// GetSceneIDs provides a mock function with given fields: galleryID
func (_m *GalleryReaderWriter) GetSceneIDs(galleryID int) ([]int, error) {
ret := _m.Called(galleryID)
var r0 []int
if rf, ok := ret.Get(0).(func(int) []int); ok {
r0 = rf(galleryID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int) error); ok {
r1 = rf(galleryID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Query provides a mock function with given fields: galleryFilter, findFilter
func (_m *GalleryReaderWriter) Query(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) {
ret := _m.Called(galleryFilter, findFilter)
@@ -446,25 +478,16 @@ func (_m *GalleryReaderWriter) UpdateTags(galleryID int, tagIDs []int) error {
return r0
}
// ValidGalleriesForScenePath provides a mock function with given fields: scenePath
func (_m *GalleryReaderWriter) ValidGalleriesForScenePath(scenePath string) ([]*models.Gallery, error) {
ret := _m.Called(scenePath)
// UpdateScenes provides a mock function with given fields: galleryID, sceneIDs
func (_m *GalleryReaderWriter) UpdateScenes(galleryID int, sceneIDs []int) error {
ret := _m.Called(galleryID, sceneIDs)
var r0 []*models.Gallery
if rf, ok := ret.Get(0).(func(string) []*models.Gallery); ok {
r0 = rf(scenePath)
var r0 error
if rf, ok := ret.Get(0).(func(int, []int) error); ok {
r0 = rf(galleryID, sceneIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Gallery)
}
r0 = ret.Error(0)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(scenePath)
} else {
r1 = ret.Error(1)
}
return r0, r1
return r0
}

View File

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

View File

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

View File

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

View File

@@ -127,21 +127,6 @@ func GetStudioName(reader models.StudioReader, scene *models.Scene) (string, err
return "", nil
}
// GetGalleryChecksum returns the checksum of the provided gallery. It returns an
// empty string if there is no gallery assigned to the scene.
func GetGalleryChecksum(reader models.GalleryReader, scene *models.Scene) (string, error) {
gallery, err := reader.FindBySceneID(scene.ID)
if err != nil {
return "", fmt.Errorf("error getting scene gallery: %s", err.Error())
}
if gallery != nil {
return gallery.Checksum, nil
}
return "", nil
}
// GetTagNames returns a slice of tag names corresponding to the provided
// scene's tags.
func GetTagNames(reader models.TagReader, scene *models.Scene) ([]string, error) {

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

View File

@@ -25,7 +25,7 @@ type Importer struct {
ID int
scene models.Scene
gallery *models.Gallery
galleries []*models.Gallery
performers []*models.Performer
movies []models.MoviesScenes
tags []*models.Tag
@@ -39,7 +39,7 @@ func (i *Importer) PreImport() error {
return err
}
if err := i.populateGallery(); err != nil {
if err := i.populateGalleries(); err != nil {
return err
}
@@ -174,25 +174,32 @@ func (i *Importer) createStudio(name string) (int, error) {
return created.ID, nil
}
func (i *Importer) populateGallery() error {
if i.Input.Gallery != "" {
gallery, err := i.GalleryWriter.FindByChecksum(i.Input.Gallery)
func (i *Importer) populateGalleries() error {
if len(i.Input.Galleries) > 0 {
checksums := i.Input.Galleries
galleries, err := i.GalleryWriter.FindByChecksums(checksums)
if err != nil {
return fmt.Errorf("error finding gallery: %s", err.Error())
return err
}
if gallery == nil {
var pluckedChecksums []string
for _, gallery := range galleries {
pluckedChecksums = append(pluckedChecksums, gallery.Checksum)
}
missingGalleries := utils.StrFilter(checksums, func(checksum string) bool {
return !utils.StrInclude(pluckedChecksums, checksum)
})
if len(missingGalleries) > 0 {
if i.MissingRefBehaviour == models.ImportMissingRefEnumFail {
return fmt.Errorf("scene gallery '%s' not found", i.Input.Studio)
return fmt.Errorf("scene galleries [%s] not found", strings.Join(missingGalleries, ", "))
}
// we don't create galleries - just ignore
if i.MissingRefBehaviour == models.ImportMissingRefEnumIgnore || i.MissingRefBehaviour == models.ImportMissingRefEnumCreate {
return nil
}
} else {
i.gallery = gallery
}
i.galleries = galleries
}
return nil
@@ -333,11 +340,14 @@ func (i *Importer) PostImport(id int) error {
}
}
if i.gallery != nil {
i.gallery.SceneID = sql.NullInt64{Int64: int64(id), Valid: true}
_, err := i.GalleryWriter.Update(*i.gallery)
if err != nil {
return fmt.Errorf("failed to update gallery: %s", err.Error())
if len(i.galleries) > 0 {
var galleryIDs []int
for _, gallery := range i.galleries {
galleryIDs = append(galleryIDs, gallery.ID)
}
if err := i.ReaderWriter.UpdateGalleries(id, galleryIDs); err != nil {
return fmt.Errorf("failed to associate galleries: %s", err.Error())
}
}

View File

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

View File

@@ -83,3 +83,23 @@ func AddTag(qb models.SceneReaderWriter, id int, tagID int) (bool, error) {
return false, nil
}
func AddGallery(qb models.SceneReaderWriter, id int, galleryID int) (bool, error) {
galleryIDs, err := qb.GetGalleryIDs(id)
if err != nil {
return false, err
}
oldLen := len(galleryIDs)
galleryIDs = utils.IntAppendUnique(galleryIDs, galleryID)
if len(galleryIDs) != oldLen {
if err := qb.UpdateGalleries(id, galleryIDs); err != nil {
return false, err
}
return true, nil
}
return false, nil
}

View File

@@ -146,34 +146,15 @@ type SceneFragment struct {
Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\""
Fingerprints []*FingerprintFragment "json:\"fingerprints\" graphql:\"fingerprints\""
}
type GalleryFragment struct {
ID string "json:\"id\" graphql:\"id\""
Title *string "json:\"title\" graphql:\"title\""
Details *string "json:\"details\" graphql:\"details\""
Duration *int "json:\"duration\" graphql:\"duration\""
Date *string "json:\"date\" graphql:\"date\""
Urls []*URLFragment "json:\"urls\" graphql:\"urls\""
Images []*ImageFragment "json:\"images\" graphql:\"images\""
Studio *StudioFragment "json:\"studio\" graphql:\"studio\""
Tags []*TagFragment "json:\"tags\" graphql:\"tags\""
Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\""
Fingerprints []*FingerprintFragment "json:\"fingerprints\" graphql:\"fingerprints\""
}
type FindSceneByFingerprint struct {
FindSceneByFingerprint []*SceneFragment "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\""
}
type FindScenesByFingerprints struct {
FindScenesByFingerprints []*SceneFragment "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\""
}
type FindGalleriesByFingerprints struct {
FindGalleriesByFingerprints []*GalleryFragment `json:"findGalleriesByFingerprints" graphql:"findGalleriesByFingerprints"`
}
type SearchScene struct {
SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\""
}
type SearchGallery struct {
SearchGallery []*GalleryFragment `json:"searchScene" graphql:"searchScene"`
}
type SubmitFingerprintPayload struct {
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
}
@@ -208,9 +189,15 @@ fragment SceneFragment on Scene {
... FingerprintFragment
}
}
fragment TagFragment on Tag {
name
id
fragment URLFragment on URL {
url
type
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment PerformerFragment on Performer {
id
@@ -245,25 +232,15 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment
}
}
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
@@ -280,15 +257,19 @@ fragment StudioFragment on Studio {
... ImageFragment
}
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
fragment TagFragment on Tag {
name
id
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
}
`
@@ -310,61 +291,6 @@ const FindScenesByFingerprintsQuery = `query FindScenesByFingerprints ($fingerpr
... SceneFragment
}
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
... URLFragment
}
images {
... ImageFragment
}
}
fragment TagFragment on Tag {
name
id
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment SceneFragment on Scene {
id
title
details
duration
date
urls {
... URLFragment
}
images {
... ImageFragment
}
studio {
... StudioFragment
}
tags {
... TagFragment
}
performers {
... PerformerAppearanceFragment
}
fingerprints {
... FingerprintFragment
}
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
@@ -404,19 +330,74 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment
}
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment SceneFragment on Scene {
id
title
details
duration
date
urls {
... URLFragment
}
images {
... ImageFragment
}
studio {
... StudioFragment
}
tags {
... TagFragment
}
performers {
... PerformerAppearanceFragment
}
fingerprints {
... FingerprintFragment
}
}
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment TagFragment on Tag {
name
id
}
fragment StudioFragment on Studio {
name
id
urls {
... URLFragment
}
images {
... ImageFragment
}
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment BodyModificationFragment on BodyModification {
location
description
}
`
func (c *Client) FindScenesByFingerprints(ctx context.Context, fingerprints []string, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFingerprints, error) {
@@ -437,6 +418,53 @@ const SearchSceneQuery = `query SearchScene ($term: String!) {
... SceneFragment
}
}
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment TagFragment on Tag {
name
id
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
urls {
... URLFragment
}
images {
... ImageFragment
}
birthdate {
... FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
... MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
... BodyModificationFragment
}
piercings {
... BodyModificationFragment
}
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
@@ -447,11 +475,6 @@ fragment MeasurementsFragment on Measurements {
waist
hip
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment SceneFragment on Scene {
id
title
@@ -477,59 +500,6 @@ fragment SceneFragment on Scene {
... FingerprintFragment
}
}
fragment TagFragment on Tag {
name
id
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
urls {
... URLFragment
}
images {
... ImageFragment
}
birthdate {
... FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
... MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
... BodyModificationFragment
}
piercings {
... BodyModificationFragment
}
}
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
@@ -540,137 +510,21 @@ fragment StudioFragment on Studio {
... ImageFragment
}
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment BodyModificationFragment on BodyModification {
location
description
}
`
func (c *Client) FindGalleriesByFingerprints(ctx context.Context, fingerprints []string, httpRequestOptions ...client.HTTPRequestOption) (*FindGalleriesByFingerprints, error) {
vars := map[string]interface{}{
"fingerprints": fingerprints,
}
var res FindGalleriesByFingerprints
if err := c.Client.Post(ctx, FindScenesByFingerprintsQuery, &res, vars, httpRequestOptions...); err != nil {
return nil, err
}
return &res, nil
}
const SearchGalleryQuery = `query SearchGallery ($term: String!) {
searchGallery(term: $term) {
... GalleryFragment
}
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment GalleryFragment on Gallery {
id
title
details
duration
date
urls {
... URLFragment
}
images {
... ImageFragment
}
studio {
... StudioFragment
}
tags {
... TagFragment
}
performers {
... PerformerAppearanceFragment
}
fingerprints {
... FingerprintFragment
}
}
fragment TagFragment on Tag {
name
id
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
urls {
... URLFragment
}
images {
... ImageFragment
}
birthdate {
... FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
... MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
... BodyModificationFragment
}
piercings {
... BodyModificationFragment
}
}
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
... URLFragment
}
images {
... ImageFragment
}
}
fragment BodyModificationFragment on BodyModification {
location
description
}
`
func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) {
@@ -686,19 +540,6 @@ func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOption
return &res, nil
}
func (c *Client) SearchGallery(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchGallery, error) {
vars := map[string]interface{}{
"term": term,
}
var res SearchGallery
if err := c.Client.Post(ctx, SearchGalleryQuery, &res, vars, httpRequestOptions...); err != nil {
return nil, err
}
return &res, nil
}
const SubmitFingerprintQuery = `mutation SubmitFingerprint ($input: FingerprintSubmission!) {
submitFingerprint(input: $input)
}

View File

@@ -3,7 +3,6 @@ package sqlite
import (
"database/sql"
"fmt"
"path/filepath"
"strconv"
"github.com/stashapp/stash/pkg/models"
@@ -14,6 +13,7 @@ const galleryTable = "galleries"
const performersGalleriesTable = "performers_galleries"
const galleriesTagsTable = "galleries_tags"
const galleriesImagesTable = "galleries_images"
const galleriesScenesTable = "scenes_galleries"
const galleryIDColumn = "gallery_id"
type galleryQueryBuilder struct {
@@ -73,23 +73,6 @@ func (qb *galleryQueryBuilder) Destroy(id int) error {
return qb.destroyExisting([]int{id})
}
type GalleryNullSceneID struct {
SceneID sql.NullInt64
}
func (qb *galleryQueryBuilder) ClearGalleryId(sceneID int) error {
_, err := qb.tx.NamedExec(
`UPDATE galleries SET scene_id = null WHERE scene_id = :sceneid`,
GalleryNullSceneID{
SceneID: sql.NullInt64{
Int64: int64(sceneID),
Valid: true,
},
},
)
return err
}
func (qb *galleryQueryBuilder) Find(id int) (*models.Gallery, error) {
var ret models.Gallery
if err := qb.get(id, &ret); err != nil {
@@ -125,22 +108,29 @@ func (qb *galleryQueryBuilder) FindByChecksum(checksum string) (*models.Gallery,
return qb.queryGallery(query, args)
}
func (qb *galleryQueryBuilder) FindByChecksums(checksums []string) ([]*models.Gallery, error) {
query := "SELECT * FROM galleries WHERE checksum IN " + getInBinding(len(checksums))
var args []interface{}
for _, checksum := range checksums {
args = append(args, checksum)
}
return qb.queryGalleries(query, args)
}
func (qb *galleryQueryBuilder) FindByPath(path string) (*models.Gallery, error) {
query := "SELECT * FROM galleries WHERE path = ? LIMIT 1"
args := []interface{}{path}
return qb.queryGallery(query, args)
}
func (qb *galleryQueryBuilder) FindBySceneID(sceneID int) (*models.Gallery, error) {
query := "SELECT galleries.* FROM galleries WHERE galleries.scene_id = ? LIMIT 1"
func (qb *galleryQueryBuilder) FindBySceneID(sceneID int) ([]*models.Gallery, error) {
query := selectAll(galleryTable) + `
LEFT JOIN scenes_galleries as scenes_join on scenes_join.gallery_id = galleries.id
WHERE scenes_join.scene_id = ?
GROUP BY galleries.id
`
args := []interface{}{sceneID}
return qb.queryGallery(query, args)
}
func (qb *galleryQueryBuilder) ValidGalleriesForScenePath(scenePath string) ([]*models.Gallery, error) {
sceneDirPath := filepath.Dir(scenePath)
query := "SELECT galleries.* FROM galleries WHERE galleries.scene_id IS NULL AND galleries.path LIKE '" + sceneDirPath + "%' ORDER BY path ASC"
return qb.queryGalleries(query, nil)
return qb.queryGalleries(query, args)
}
func (qb *galleryQueryBuilder) FindByImageID(imageID int) ([]*models.Gallery, error) {
@@ -182,6 +172,7 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
query.body = selectDistinctIDs("galleries")
query.body += `
left join performers_galleries as performers_join on performers_join.gallery_id = galleries.id
left join scenes_galleries as scenes_join on scenes_join.gallery_id = galleries.id
left join studios as studio on studio.id = galleries.studio_id
left join galleries_tags as tags_join on tags_join.gallery_id = galleries.id
left join galleries_images as images_join on images_join.gallery_id = galleries.id
@@ -189,7 +180,7 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
`
if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"galleries.path", "galleries.checksum"}
searchColumns := []string{"galleries.title", "galleries.path", "galleries.checksum"}
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
query.addWhere(clause)
query.addArg(thisArgs...)
@@ -221,8 +212,8 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter {
case "scene":
query.addWhere("galleries.scene_id IS NULL")
case "scenes":
query.addWhere("scenes_join.gallery_id IS NULL")
case "studio":
query.addWhere("galleries.studio_id IS NULL")
case "performers":
@@ -442,3 +433,23 @@ func (qb *galleryQueryBuilder) UpdateImages(galleryID int, imageIDs []int) error
// Delete the existing joins and then create new ones
return qb.imagesRepository().replace(galleryID, imageIDs)
}
func (qb *galleryQueryBuilder) scenesRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: galleriesScenesTable,
idColumn: galleryIDColumn,
},
fkColumn: sceneIDColumn,
}
}
func (qb *galleryQueryBuilder) GetSceneIDs(galleryID int) ([]int, error) {
return qb.scenesRepository().getIDs(galleryID)
}
func (qb *galleryQueryBuilder) UpdateScenes(galleryID int, sceneIDs []int) error {
// Delete the existing joins and then create new ones
return qb.scenesRepository().replace(galleryID, sceneIDs)
}

View File

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

View File

@@ -13,6 +13,7 @@ const sceneTable = "scenes"
const sceneIDColumn = "scene_id"
const performersScenesTable = "performers_scenes"
const scenesTagsTable = "scenes_tags"
const scenesGalleriesTable = "scenes_galleries"
const moviesScenesTable = "movies_scenes"
var scenesForPerformerQuery = selectAll(sceneTable) + `
@@ -44,6 +45,12 @@ WHERE scenes_tags.tag_id = ?
GROUP BY scenes_tags.scene_id
`
var scenesForGalleryQuery = selectAll(sceneTable) + `
LEFT JOIN scenes_galleries as galleries_join on galleries_join.scene_id = scenes.id
WHERE galleries_join.gallery_id = ?
GROUP BY scenes.id
`
var countScenesForMissingChecksumQuery = `
SELECT id FROM scenes
WHERE scenes.checksum is null
@@ -221,6 +228,11 @@ func (qb *sceneQueryBuilder) FindByPerformerID(performerID int) ([]*models.Scene
return qb.queryScenes(scenesForPerformerQuery, args)
}
func (qb *sceneQueryBuilder) FindByGalleryID(galleryID int) ([]*models.Scene, error) {
args := []interface{}{galleryID}
return qb.queryScenes(scenesForGalleryQuery, args)
}
func (qb *sceneQueryBuilder) CountByPerformerID(performerID int) (int, error) {
args := []interface{}{performerID}
return qb.runCountQuery(qb.buildCountQuery(countScenesForPerformerQuery), args)
@@ -293,7 +305,7 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
left join performers_scenes as performers_join on performers_join.scene_id = scenes.id
left join movies_scenes as movies_join on movies_join.scene_id = scenes.id
left join studios as studio on studio.id = scenes.studio_id
left join galleries as gallery on gallery.scene_id = scenes.id
left join scenes_galleries as galleries_join on galleries_join.scene_id = scenes.id
left join scenes_tags as tags_join on tags_join.scene_id = scenes.id
left join scene_stash_ids on scene_stash_ids.scene_id = scenes.id
`
@@ -368,8 +380,8 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
if isMissingFilter := sceneFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter {
case "gallery":
query.addWhere("gallery.scene_id IS NULL")
case "galleries":
query.addWhere("galleries_join.scene_id IS NULL")
case "studio":
query.addWhere("scenes.studio_id IS NULL")
case "movie":
@@ -683,6 +695,26 @@ func (qb *sceneQueryBuilder) UpdateTags(id int, tagIDs []int) error {
return qb.tagsRepository().replace(id, tagIDs)
}
func (qb *sceneQueryBuilder) galleriesRepository() *joinRepository {
return &joinRepository{
repository: repository{
tx: qb.tx,
tableName: scenesGalleriesTable,
idColumn: sceneIDColumn,
},
fkColumn: galleryIDColumn,
}
}
func (qb *sceneQueryBuilder) GetGalleryIDs(id int) ([]int, error) {
return qb.galleriesRepository().getIDs(id)
}
func (qb *sceneQueryBuilder) UpdateGalleries(id int, galleryIDs []int) error {
// Delete the existing joins and then create new ones
return qb.galleriesRepository().replace(id, galleryIDs)
}
func (qb *sceneQueryBuilder) stashIDRepository() *stashIDRepository {
return &stashIDRepository{
repository{

View File

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

View File

@@ -5,7 +5,6 @@ package sqlite_test
import (
"context"
"database/sql"
"errors"
"fmt"
"io/ioutil"
"os"
@@ -192,7 +191,7 @@ func populateDB() error {
return fmt.Errorf("error creating studios: %s", err.Error())
}
if err := linkSceneGallery(r.Gallery(), sceneIdxWithGallery, galleryIdxWithScene); err != nil {
if err := linkSceneGallery(r.Scene(), sceneIdxWithGallery, galleryIdxWithScene); err != nil {
return fmt.Errorf("error linking scene to gallery: %s", err.Error())
}
@@ -623,20 +622,8 @@ func linkScenePerformer(qb models.SceneReaderWriter, sceneIndex, performerIndex
return err
}
func linkSceneGallery(gqb models.GalleryReaderWriter, sceneIndex, galleryIndex int) error {
gallery, err := gqb.Find(galleryIDs[galleryIndex])
if err != nil {
return fmt.Errorf("error finding gallery: %s", err.Error())
}
if gallery == nil {
return errors.New("gallery is nil")
}
gallery.SceneID = sql.NullInt64{Int64: int64(sceneIDs[sceneIndex]), Valid: true}
_, err = gqb.Update(*gallery)
func linkSceneGallery(qb models.SceneReaderWriter, sceneIndex, galleryIndex int) error {
_, err := scene.AddGallery(qb, sceneIDs[sceneIndex], galleryIDs[galleryIndex])
return err
}

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.
### ✨ New Features
* Add support for multiple galleries per scene, and vice-versa.
* Add backup database functionality to Settings/Tasks.
* Add gallery wall view.
* Add organized flag for scenes, galleries and images.

View File

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

View File

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

View File

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

View File

@@ -12,12 +12,13 @@ import {
import {
PerformerSelect,
TagSelect,
SceneSelect,
StudioSelect,
Icon,
LoadingIndicator,
} from "src/components/Shared";
import { useToast } from "src/hooks";
import { FormUtils, EditableTextUtils } from "src/utils";
import { FormUtils, EditableTextUtils, TextUtils } from "src/utils";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
@@ -49,6 +50,7 @@ export const GalleryEditPanel: React.FC<
const [studioId, setStudioId] = useState<string>();
const [performerIds, setPerformerIds] = useState<string[]>();
const [tagIds, setTagIds] = useState<string[]>();
const [scenes, setScenes] = useState<{ id: string; title: string }[]>([]);
const Scrapers = useListGalleryScrapers();
@@ -117,6 +119,12 @@ export const GalleryEditPanel: React.FC<
setStudioId(state?.studio?.id ?? undefined);
setPerformerIds(perfIds);
setTagIds(tIds);
setScenes(
(state?.scenes ?? []).map((s) => ({
id: s.id,
title: s.title ?? TextUtils.fileNameFromPath(s.path ?? ""),
}))
);
}
useEffect(() => {
@@ -135,6 +143,7 @@ export const GalleryEditPanel: React.FC<
studio_id: studioId ?? null,
performer_ids: performerIds,
tag_ids: tagIds,
scene_ids: scenes.map((s) => s.id),
};
}
@@ -390,6 +399,23 @@ export const GalleryEditPanel: React.FC<
/>
</Col>
</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 className="col-12 col-lg-6 col-xl-12">
<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 * as GQL from "src/core/generated-graphql";
import { useFindGallery } from "src/core/StashService";
import { useLightbox } from "src/hooks";
import { LoadingIndicator } from "src/components/Shared";
import "flexbin/flexbin.css";
interface IProps {
gallery: GQL.GalleryDataFragment;
galleryId: string;
}
export const GalleryViewer: React.FC<IProps> = ({ gallery }) => {
const images = gallery?.images ?? [];
export const GalleryViewer: React.FC<IProps> = ({ galleryId }) => {
const { data, loading } = useFindGallery(galleryId);
const images = data?.findGallery?.images ?? [];
const showLightbox = useLightbox({ images, showNavigation: false });
if (loading) return <LoadingIndicator />;
const thumbs = images.map((file, index) => (
<div
role="link"

View File

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

View File

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

View File

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

View File

@@ -22,13 +22,13 @@ import {
PerformerSelect,
TagSelect,
StudioSelect,
SceneGallerySelect,
GallerySelect,
Icon,
LoadingIndicator,
ImageInput,
} from "src/components/Shared";
import { useToast } from "src/hooks";
import { ImageUtils, FormUtils, EditableTextUtils } from "src/utils";
import { ImageUtils, FormUtils, EditableTextUtils, TextUtils } from "src/utils";
import { MovieSelect } from "src/components/Shared/Select";
import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable";
import { RatingStars } from "./RatingStars";
@@ -47,7 +47,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
const [url, setUrl] = useState<string>();
const [date, setDate] = useState<string>();
const [rating, setRating] = useState<number>();
const [galleryId, setGalleryId] = useState<string>();
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
[]
);
const [studioId, setStudioId] = useState<string>();
const [performerIds, setPerformerIds] = useState<string[]>();
const [movieIds, setMovieIds] = useState<string[] | undefined>(undefined);
@@ -171,7 +173,12 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
setUrl(state.url ?? undefined);
setDate(state.date ?? undefined);
setRating(state.rating === null ? NaN : state.rating);
setGalleryId(state?.gallery?.id ?? undefined);
setGalleries(
(state?.galleries ?? []).map((g) => ({
id: g.id,
title: g.title ?? TextUtils.fileNameFromPath(g.path ?? ""),
}))
);
setStudioId(state?.studio?.id ?? undefined);
setMovieIds(moviIds);
setMovieSceneIndexes(movieSceneIdx);
@@ -196,7 +203,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
url,
date,
rating: rating ?? null,
gallery_id: galleryId ?? null,
gallery_ids: galleries.map((g) => g.id),
studio_id: studioId ?? null,
performer_ids: performerIds,
movies: makeMovieInputs(),
@@ -596,15 +603,14 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
/>
</Col>
</Form.Group>
<Form.Group controlId="gallery" as={Row}>
<Form.Group controlId="galleries" as={Row}>
{FormUtils.renderLabel({
title: "Gallery",
title: "Galleries",
})}
<Col xs={9}>
<SceneGallerySelect
sceneId={props.scene.id}
gallery={props.scene.gallery ?? undefined}
onSelect={(item) => setGalleryId(item ? item.id : undefined)}
<GallerySelect
galleries={galleries}
onSelect={(items) => setGalleries(items)}
/>
</Col>
</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,
useStudioCreate,
usePerformerCreate,
useFindGalleries,
} from "src/core/StashService";
import { useToast } from "src/hooks";
import { ListFilterModel } from "src/models/list-filter/filter";
import { FilterMode } from "src/models/list-filter/types";
import { TextUtils } from "src/utils";
export type ValidTypes =
| GQL.SlimPerformerDataFragment
@@ -72,23 +70,28 @@ interface IFilterComponentProps extends IFilterProps {
interface IFilterSelectProps<T extends boolean>
extends Omit<ISelectProps<T>, "onChange" | "items" | "onCreateOption"> {}
interface ISceneGallerySelect {
gallery?: Pick<GQL.Gallery, "title" | "path" | "id">;
sceneId: string;
onSelect: (
item:
| GQL.ValidGalleriesForSceneQuery["validGalleriesForScene"][0]
| undefined
) => void;
type Gallery = { id: string; title: string };
interface IGallerySelect {
galleries: Gallery[];
onSelect: (items: Gallery[]) => void;
}
const getSelectedValues = (selectedItems: ValueType<Option, boolean>) =>
type Scene = { id: string; title: string };
interface ISceneSelect {
scenes: Scene[];
onSelect: (items: Scene[]) => void;
}
const getSelectedItems = (selectedItems: ValueType<Option, boolean>) =>
selectedItems
? (Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map(
(item) => item.value
)
? Array.isArray(selectedItems)
? selectedItems
: [selectedItems]
: [];
const getSelectedValues = (selectedItems: ValueType<Option, boolean>) =>
getSelectedItems(selectedItems).map((item) => item.value);
const SelectComponent = <T extends boolean>({
type,
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 { data, loading } = useFindGalleries(getFilter());
const [selectedOption, setSelectedOption] = useState<Option>();
const { data, loading } = GQL.useFindGalleriesQuery({
skip: query === "",
variables: {
filter: {
q: query,
},
},
});
const galleries = data?.findGalleries.galleries ?? [];
const items = galleries.map((g) => ({
label: g.title ?? g.path ?? "",
label: g.title ?? TextUtils.fileNameFromPath(g.path ?? ""),
value: g.id,
}));
function getFilter() {
const ret = new ListFilterModel(FilterMode.Galleries);
ret.searchTerm = query;
return ret;
}
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
const onChange = (selectedItem: ValueType<Option, false>) => {
setSelectedOption(selectedItem ?? undefined);
const onChange = (selectedItems: ValueType<Option, boolean>) => {
const selected = getSelectedItems(selectedItems);
props.onSelect(
selectedItem
? galleries.find((g) => g.id === selectedItem.value)
: undefined
selected.map((s) => ({
id: s.value,
title: s.label,
}))
);
};
const option =
selectedOption ??
(props.gallery
? {
value: props.gallery.id,
label: props.gallery.title ?? props.gallery.path ?? "Unknown",
}
: undefined);
const options = props.galleries.map((g) => ({
value: g.id,
label: g.title ?? "Unknown",
}));
return (
<SelectComponent
isMulti={false}
onChange={onChange}
onInputChange={onInputChange}
isLoading={loading}
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 {
SceneGallerySelect,
GallerySelect,
ScrapePerformerSuggest,
MarkerTitleSuggest,
FilterSelect,
PerformerSelect,
StudioSelect,
TagSelect,
SceneSelect,
} from "./Select";
export { default as Icon } from "./Icon";

View File

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

View File

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

View File

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