Skip to content

Commit

Permalink
Support for assigning any image from a gallery as the cover (stashapp…
Browse files Browse the repository at this point in the history
…#5053)

Co-authored-by: WithoutPants <[email protected]>
  • Loading branch information
sezzim and WithoutPants authored Aug 29, 2024
1 parent 8133aa8 commit 68738bd
Show file tree
Hide file tree
Showing 23 changed files with 383 additions and 10 deletions.
2 changes: 2 additions & 0 deletions graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ type Mutation {

addGalleryImages(input: GalleryAddInput!): Boolean!
removeGalleryImages(input: GalleryRemoveInput!): Boolean!
setGalleryCover(input: GallerySetCoverInput!): Boolean!
resetGalleryCover(input: GalleryResetCoverInput!): Boolean!

galleryChapterCreate(input: GalleryChapterCreateInput!): GalleryChapter
galleryChapterUpdate(input: GalleryChapterUpdateInput!): GalleryChapter
Expand Down
9 changes: 9 additions & 0 deletions graphql/schema/types/gallery.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,12 @@ input GalleryRemoveInput {
gallery_id: ID!
image_ids: [ID!]!
}

input GallerySetCoverInput {
gallery_id: ID!
cover_image_id: ID!
}

input GalleryResetCoverInput {
gallery_id: ID!
}
55 changes: 55 additions & 0 deletions internal/api/resolver_mutation_gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,61 @@ func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input Galler
return true, nil
}

func (r *mutationResolver) SetGalleryCover(ctx context.Context, input GallerySetCoverInput) (bool, error) {
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, fmt.Errorf("converting gallery id: %w", err)
}

coverImageID, err := strconv.Atoi(input.CoverImageID)
if err != nil {
return false, fmt.Errorf("converting cover image id: %w", err)
}

if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
gallery, err := qb.Find(ctx, galleryID)
if err != nil {
return err
}

if gallery == nil {
return fmt.Errorf("gallery with id %d not found", galleryID)
}

return r.galleryService.SetCover(ctx, gallery, coverImageID)
}); err != nil {
return false, err
}

return true, nil
}

func (r *mutationResolver) ResetGalleryCover(ctx context.Context, input GalleryResetCoverInput) (bool, error) {
galleryID, err := strconv.Atoi(input.GalleryID)
if err != nil {
return false, fmt.Errorf("converting gallery id: %w", err)
}

if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
gallery, err := qb.Find(ctx, galleryID)
if err != nil {
return err
}

if gallery == nil {
return fmt.Errorf("gallery with id %d not found", galleryID)
}

return r.galleryService.ResetCover(ctx, gallery)
}); err != nil {
return false, err
}

return true, nil
}

func (r *mutationResolver) getGalleryChapter(ctx context.Context, id int) (ret *models.GalleryChapter, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.GalleryChapter.Find(ctx, id)
Expand Down
3 changes: 3 additions & 0 deletions internal/manager/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type GalleryService interface {
AddImages(ctx context.Context, g *models.Gallery, toAdd ...int) error
RemoveImages(ctx context.Context, g *models.Gallery, toRemove ...int) error

SetCover(ctx context.Context, g *models.Gallery, coverImageId int) error
ResetCover(ctx context.Context, g *models.Gallery) error

Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error)

ValidateImageGalleryChange(ctx context.Context, i *models.Image, updateIDs models.UpdateIDs) error
Expand Down
16 changes: 16 additions & 0 deletions pkg/gallery/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ func (s *Service) RemoveImages(ctx context.Context, g *models.Gallery, toRemove
return s.Updated(ctx, g.ID)
}

func (s *Service) SetCover(ctx context.Context, g *models.Gallery, coverImageID int) error {
if err := s.Repository.SetCover(ctx, g.ID, coverImageID); err != nil {
return fmt.Errorf("failed to set cover: %w", err)
}

return s.Updated(ctx, g.ID)
}

func (s *Service) ResetCover(ctx context.Context, g *models.Gallery) error {
if err := s.Repository.ResetCover(ctx, g.ID); err != nil {
return fmt.Errorf("failed to reset cover: %w", err)
}

return s.Updated(ctx, g.ID)
}

func AddPerformer(ctx context.Context, qb models.GalleryUpdater, o *models.Gallery, performerID int) error {
galleryPartial := models.NewGalleryPartial()
galleryPartial.PerformerIDs = &models.UpdateIDs{
Expand Down
7 changes: 7 additions & 0 deletions pkg/image/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ func FindGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int,
}

func findGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int, useCoverJpg bool, galleryCoverRegex string) (*models.Image, error) {
img, err := r.CoverByGalleryID(ctx, galleryID)
if err != nil {
return nil, err
} else if img != nil {
return img, nil
}

// try to find cover.jpg in the gallery
perPage := 1
sortBy := "path"
Expand Down
28 changes: 28 additions & 0 deletions pkg/models/mocks/GalleryReaderWriter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions pkg/models/mocks/ImageReaderWriter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/models/repository_gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ type GalleryWriter interface {
AddFileID(ctx context.Context, id int, fileID FileID) error
AddImages(ctx context.Context, galleryID int, imageIDs ...int) error
RemoveImages(ctx context.Context, galleryID int, imageIDs ...int) error
SetCover(ctx context.Context, galleryID int, coverImageID int) error
ResetCover(ctx context.Context, galleryID int) error
}

// GalleryReaderWriter provides all gallery methods.
Expand Down
1 change: 1 addition & 0 deletions pkg/models/repository_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type ImageFinder interface {
type ImageQueryer interface {
Query(ctx context.Context, options ImageQueryOptions) (*ImageQueryResult, error)
QueryCount(ctx context.Context, imageFilter *ImageFilterType, findFilter *FindFilterType) (int, error)
CoverByGalleryID(ctx context.Context, galleryId int) (*Image, error)
}

// ImageCounter provides methods to count images.
Expand Down
2 changes: 1 addition & 1 deletion pkg/sqlite/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (
dbConnTimeout = 30
)

var appSchemaVersion uint = 65
var appSchemaVersion uint = 66

//go:embed migrations/*.sql
var migrationsBox embed.FS
Expand Down
8 changes: 8 additions & 0 deletions pkg/sqlite/gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,14 @@ func (qb *GalleryStore) UpdateImages(ctx context.Context, galleryID int, imageID
return galleryRepository.images.replace(ctx, galleryID, imageIDs)
}

func (qb *GalleryStore) SetCover(ctx context.Context, galleryID int, coverImageID int) error {
return imageGalleriesTableMgr.setCover(ctx, coverImageID, galleryID)
}

func (qb *GalleryStore) ResetCover(ctx context.Context, galleryID int) error {
return imageGalleriesTableMgr.resetCover(ctx, galleryID)
}

func (qb *GalleryStore) GetSceneIDs(ctx context.Context, id int) ([]int, error) {
return galleryRepository.scenes.getIDs(ctx, id)
}
28 changes: 28 additions & 0 deletions pkg/sqlite/gallery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2973,6 +2973,34 @@ func TestGalleryQueryHasChapters(t *testing.T) {
})
}

func TestGallerySetAndResetCover(t *testing.T) {
withTxn(func(ctx context.Context) error {
sqb := db.Gallery

imagePath2 := getFilePath(folderIdxWithImageFiles, getImageBasename(imageIdx2WithGallery))

result, err := db.Image.CoverByGalleryID(ctx, galleryIDs[galleryIdxWithTwoImages])
assert.Nil(t, err)
assert.Nil(t, result)

err = sqb.SetCover(ctx, galleryIDs[galleryIdxWithTwoImages], imageIDs[imageIdx2WithGallery])
assert.Nil(t, err)

result, err = db.Image.CoverByGalleryID(ctx, galleryIDs[galleryIdxWithTwoImages])
assert.Nil(t, err)
assert.Equal(t, result.Path, imagePath2)

err = sqb.ResetCover(ctx, galleryIDs[galleryIdxWithTwoImages])
assert.Nil(t, err)

result, err = db.Image.CoverByGalleryID(ctx, galleryIDs[galleryIdxWithTwoImages])
assert.Nil(t, err)
assert.Nil(t, result)

return nil
})
}

// TODO Count
// TODO All
// TODO Query
Expand Down
36 changes: 36 additions & 0 deletions pkg/sqlite/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,42 @@ func (qb *ImageStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*mo
return ret, nil
}

// Returns the custom cover for the gallery, if one has been set.
func (qb *ImageStore) CoverByGalleryID(ctx context.Context, galleryID int) (*models.Image, error) {
table := qb.table()

sq := dialect.From(table).
InnerJoin(
galleriesImagesJoinTable,
goqu.On(table.Col(idColumn).Eq(galleriesImagesJoinTable.Col(imageIDColumn))),
).
Select(table.Col(idColumn)).
Where(goqu.And(
galleriesImagesJoinTable.Col("gallery_id").Eq(galleryID),
galleriesImagesJoinTable.Col("cover").Eq(true),
))

q := qb.selectDataset().Prepared(true).Where(
table.Col(idColumn).Eq(
sq,
),
)

ret, err := qb.getMany(ctx, q)
if err != nil {
return nil, fmt.Errorf("getting cover for gallery %d: %w", galleryID, err)
}

switch {
case len(ret) > 1:
return nil, fmt.Errorf("internal error: multiple covers returned for gallery %d", galleryID)
case len(ret) == 1:
return ret[0], nil
default:
return nil, nil
}
}

func (qb *ImageStore) GetFiles(ctx context.Context, id int) ([]models.File, error) {
fileIDs, err := imageRepository.files.get(ctx, id)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions pkg/sqlite/migrations/66_gallery_cover.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `galleries_images` ADD COLUMN `cover` BOOLEAN NOT NULL DEFAULT 0;
CREATE UNIQUE INDEX `index_galleries_images_gallery_id_cover` on `galleries_images` (`gallery_id`, `cover`) WHERE `cover` = 1;
39 changes: 39 additions & 0 deletions pkg/sqlite/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,45 @@ func (t *scenesGroupsTable) modifyJoins(ctx context.Context, id int, v []models.
return nil
}

type imageGalleriesTable struct {
joinTable
}

func (t *imageGalleriesTable) setCover(ctx context.Context, id int, galleryID int) error {
if err := t.resetCover(ctx, galleryID); err != nil {
return err
}

table := t.table.table

q := dialect.Update(table).Prepared(true).Set(goqu.Record{
"cover": true,
}).Where(t.idColumn.Eq(id), table.Col(galleryIDColumn).Eq(galleryID))

if _, err := exec(ctx, q); err != nil {
return fmt.Errorf("setting cover flag in %s: %w", t.table.table.GetTable(), err)
}

return nil
}

func (t *imageGalleriesTable) resetCover(ctx context.Context, galleryID int) error {
table := t.table.table

q := dialect.Update(table).Prepared(true).Set(goqu.Record{
"cover": false,
}).Where(
table.Col(galleryIDColumn).Eq(galleryID),
table.Col("cover").Eq(true),
)

if _, err := exec(ctx, q); err != nil {
return fmt.Errorf("unsetting cover flags in %s: %w", t.table.table.GetTable(), err)
}

return nil
}

type relatedFilesTable struct {
table
}
Expand Down
12 changes: 7 additions & 5 deletions pkg/sqlite/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ var (
},
}

imageGalleriesTableMgr = &joinTable{
table: table{
table: galleriesImagesJoinTable,
idColumn: galleriesImagesJoinTable.Col(imageIDColumn),
imageGalleriesTableMgr = &imageGalleriesTable{
joinTable: joinTable{
table: table{
table: galleriesImagesJoinTable,
idColumn: galleriesImagesJoinTable.Col(imageIDColumn),
},
fkColumn: galleriesImagesJoinTable.Col(galleryIDColumn),
},
fkColumn: galleriesImagesJoinTable.Col(galleryIDColumn),
}

imagesTagsTableMgr = &joinTable{
Expand Down
10 changes: 10 additions & 0 deletions ui/v2.5/graphql/mutations/gallery.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,13 @@ mutation AddGalleryImages($gallery_id: ID!, $image_ids: [ID!]!) {
mutation RemoveGalleryImages($gallery_id: ID!, $image_ids: [ID!]!) {
removeGalleryImages(input: { gallery_id: $gallery_id, image_ids: $image_ids })
}

mutation SetGalleryCover($gallery_id: ID!, $cover_image_id: ID!) {
setGalleryCover(
input: { gallery_id: $gallery_id, cover_image_id: $cover_image_id }
)
}

mutation ResetGalleryCover($gallery_id: ID!) {
resetGalleryCover(input: { gallery_id: $gallery_id })
}
Loading

0 comments on commit 68738bd

Please sign in to comment.