mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Support for assigning any image from a gallery as the cover (#5053)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -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{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -628,6 +628,34 @@ func (_m *GalleryReaderWriter) RemoveImages(ctx context.Context, galleryID int,
|
||||
return r0
|
||||
}
|
||||
|
||||
// ResetCover provides a mock function with given fields: ctx, galleryID
|
||||
func (_m *GalleryReaderWriter) ResetCover(ctx context.Context, galleryID int) error {
|
||||
ret := _m.Called(ctx, galleryID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int) error); ok {
|
||||
r0 = rf(ctx, galleryID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetCover provides a mock function with given fields: ctx, galleryID, coverImageID
|
||||
func (_m *GalleryReaderWriter) SetCover(ctx context.Context, galleryID int, coverImageID int) error {
|
||||
ret := _m.Called(ctx, galleryID, coverImageID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int, int) error); ok {
|
||||
r0 = rf(ctx, galleryID, coverImageID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, updatedGallery
|
||||
func (_m *GalleryReaderWriter) Update(ctx context.Context, updatedGallery *models.Gallery) error {
|
||||
ret := _m.Called(ctx, updatedGallery)
|
||||
|
||||
@@ -114,6 +114,29 @@ func (_m *ImageReaderWriter) CountByGalleryID(ctx context.Context, galleryID int
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CoverByGalleryID provides a mock function with given fields: ctx, galleryId
|
||||
func (_m *ImageReaderWriter) CoverByGalleryID(ctx context.Context, galleryId int) (*models.Image, error) {
|
||||
ret := _m.Called(ctx, galleryId)
|
||||
|
||||
var r0 *models.Image
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int) *models.Image); ok {
|
||||
r0 = rf(ctx, galleryId)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*models.Image)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||
r1 = rf(ctx, galleryId)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, newImage, fileIDs
|
||||
func (_m *ImageReaderWriter) Create(ctx context.Context, newImage *models.Image, fileIDs []models.FileID) error {
|
||||
ret := _m.Called(ctx, newImage, fileIDs)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -30,7 +30,7 @@ const (
|
||||
dbConnTimeout = 30
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 65
|
||||
var appSchemaVersion uint = 66
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
2
pkg/sqlite/migrations/66_gallery_cover.up.sql
Normal file
2
pkg/sqlite/migrations/66_gallery_cover.up.sql
Normal file
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user