mirror of
https://github.com/stashapp/stash.git
synced 2026-02-28 02:02:57 +01:00
Backend support for gallery custom fields (#6592)
This commit is contained in:
parent
076032ff8b
commit
47dcdd439c
24 changed files with 528 additions and 50 deletions
|
|
@ -596,6 +596,8 @@ input GalleryFilterType {
|
|||
files_filter: FileFilterType
|
||||
"Filter by related folders that meet this criteria"
|
||||
folders_filter: FolderFilterType
|
||||
|
||||
custom_fields: [CustomFieldCriterionInput!]
|
||||
}
|
||||
|
||||
input TagFilterType {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ type Gallery {
|
|||
cover: Image
|
||||
|
||||
paths: GalleryPathsType! # Resolver
|
||||
custom_fields: Map!
|
||||
image(index: Int!): Image!
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +51,8 @@ input GalleryCreateInput {
|
|||
studio_id: ID
|
||||
tag_ids: [ID!]
|
||||
performer_ids: [ID!]
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
|
||||
input GalleryUpdateInput {
|
||||
|
|
@ -71,6 +74,8 @@ input GalleryUpdateInput {
|
|||
performer_ids: [ID!]
|
||||
|
||||
primary_file_id: ID
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input BulkGalleryUpdateInput {
|
||||
|
|
@ -89,6 +94,8 @@ input BulkGalleryUpdateInput {
|
|||
studio_id: ID
|
||||
tag_ids: BulkUpdateIds
|
||||
performer_ids: BulkUpdateIds
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input GalleryDestroyInput {
|
||||
|
|
|
|||
|
|
@ -54,8 +54,9 @@ type Loaders struct {
|
|||
ImageFiles *ImageFileIDsLoader
|
||||
GalleryFiles *GalleryFileIDsLoader
|
||||
|
||||
GalleryByID *GalleryLoader
|
||||
ImageByID *ImageLoader
|
||||
GalleryByID *GalleryLoader
|
||||
GalleryCustomFields *CustomFieldsLoader
|
||||
ImageByID *ImageLoader
|
||||
|
||||
PerformerByID *PerformerLoader
|
||||
PerformerCustomFields *CustomFieldsLoader
|
||||
|
|
@ -88,6 +89,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
|||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGalleries(ctx),
|
||||
},
|
||||
GalleryCustomFields: &CustomFieldsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGalleryCustomFields(ctx),
|
||||
},
|
||||
ImageByID: &ImageLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
|
|
@ -319,6 +325,18 @@ func (m Middleware) fetchTagCustomFields(ctx context.Context) func(keys []int) (
|
|||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGalleryCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Gallery.GetCustomFieldsBulk(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGroups(ctx context.Context) func(keys []int) ([]*models.Group, []error) {
|
||||
return func(keys []int) (ret []*models.Group, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
|
|
|
|||
|
|
@ -216,3 +216,16 @@ func (r *galleryResolver) Image(ctx context.Context, obj *models.Gallery, index
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *galleryResolver) CustomFields(ctx context.Context, obj *models.Gallery) (map[string]interface{}, error) {
|
||||
m, err := loaders.From(ctx).GalleryCustomFields.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
|||
}
|
||||
|
||||
// Populate a new gallery from the input
|
||||
newGallery := models.NewGallery()
|
||||
newGallery := models.CreateGalleryInput{
|
||||
Gallery: &models.Gallery{},
|
||||
}
|
||||
*newGallery.Gallery = models.NewGallery()
|
||||
|
||||
newGallery.Title = strings.TrimSpace(input.Title)
|
||||
newGallery.Code = translator.string(input.Code)
|
||||
|
|
@ -81,10 +84,12 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
|
|||
newGallery.URLs = models.NewRelatedStrings([]string{strings.TrimSpace(*input.URL)})
|
||||
}
|
||||
|
||||
newGallery.CustomFields = convertMapJSONNumbers(input.CustomFields)
|
||||
|
||||
// Start the transaction and save the gallery
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.Gallery
|
||||
if err := qb.Create(ctx, &newGallery, nil); err != nil {
|
||||
if err := qb.Create(ctx, &newGallery); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -241,6 +246,10 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
|||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
|
||||
if input.CustomFields != nil {
|
||||
updatedGallery.CustomFields = handleUpdateCustomFields(*input.CustomFields)
|
||||
}
|
||||
|
||||
// gallery scene is set from the scene only
|
||||
|
||||
gallery, err := qb.UpdatePartial(ctx, galleryID, updatedGallery)
|
||||
|
|
@ -293,6 +302,10 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
|
|||
return nil, fmt.Errorf("converting scene ids: %w", err)
|
||||
}
|
||||
|
||||
if input.CustomFields != nil {
|
||||
updatedGallery.CustomFields = handleUpdateCustomFields(*input.CustomFields)
|
||||
}
|
||||
|
||||
ret := []*models.Gallery{}
|
||||
|
||||
// Start the transaction and save the galleries
|
||||
|
|
|
|||
|
|
@ -468,7 +468,10 @@ func makeGallery(expectedResult bool) *models.Gallery {
|
|||
}
|
||||
|
||||
func createGallery(ctx context.Context, w models.GalleryWriter, o *models.Gallery, f *models.BaseFile) error {
|
||||
err := w.Create(ctx, o, []models.FileID{f.ID})
|
||||
err := w.Create(ctx, &models.CreateGalleryInput{
|
||||
Gallery: o,
|
||||
FileIDs: []models.FileID{f.ID},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create gallery with path '%s': %s", f.Path, err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -779,6 +779,7 @@ func (t *ExportTask) exportGallery(ctx context.Context, wg *sync.WaitGroup, jobC
|
|||
studioReader := r.Studio
|
||||
performerReader := r.Performer
|
||||
tagReader := r.Tag
|
||||
galleryReader := r.Gallery
|
||||
galleryChapterReader := r.GalleryChapter
|
||||
|
||||
for g := range jobChan {
|
||||
|
|
@ -847,6 +848,12 @@ func (t *ExportTask) exportGallery(ctx context.Context, wg *sync.WaitGroup, jobC
|
|||
|
||||
newGalleryJSON.Tags = tag.GetNames(tags)
|
||||
|
||||
newGalleryJSON.CustomFields, err = galleryReader.GetCustomFields(ctx, g.ID)
|
||||
if err != nil {
|
||||
logger.Errorf("[galleries] <%s> error getting gallery custom fields: %v", g.DisplayName(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if t.includeDependencies {
|
||||
if g.StudioID != nil {
|
||||
t.studios.IDs = sliceutil.AppendUnique(t.studios.IDs, *g.StudioID)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,9 @@ type Importer struct {
|
|||
Input jsonschema.Gallery
|
||||
MissingRefBehaviour models.ImportMissingRefEnum
|
||||
|
||||
ID int
|
||||
gallery models.Gallery
|
||||
ID int
|
||||
gallery models.Gallery
|
||||
customFields map[string]interface{}
|
||||
}
|
||||
|
||||
func (i *Importer) PreImport(ctx context.Context) error {
|
||||
|
|
@ -51,6 +52,8 @@ func (i *Importer) PreImport(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
i.customFields = i.Input.CustomFields
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -356,7 +359,11 @@ func (i *Importer) Create(ctx context.Context) (*int, error) {
|
|||
for _, f := range i.gallery.Files.List() {
|
||||
fileIDs = append(fileIDs, f.Base().ID)
|
||||
}
|
||||
err := i.ReaderWriter.Create(ctx, &i.gallery, fileIDs)
|
||||
err := i.ReaderWriter.Create(ctx, &models.CreateGalleryInput{
|
||||
Gallery: &i.gallery,
|
||||
FileIDs: fileIDs,
|
||||
CustomFields: i.customFields,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating gallery: %v", err)
|
||||
}
|
||||
|
|
@ -368,7 +375,12 @@ func (i *Importer) Create(ctx context.Context) (*int, error) {
|
|||
func (i *Importer) Update(ctx context.Context, id int) error {
|
||||
gallery := i.gallery
|
||||
gallery.ID = id
|
||||
err := i.ReaderWriter.Update(ctx, &gallery)
|
||||
err := i.ReaderWriter.Update(ctx, &models.UpdateGalleryInput{
|
||||
Gallery: &gallery,
|
||||
CustomFields: models.CustomFieldsInput{
|
||||
Full: i.customFields,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating existing gallery: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ type ScanCreatorUpdater interface {
|
|||
FindByFingerprints(ctx context.Context, fp []models.Fingerprint) ([]*models.Gallery, error)
|
||||
GetFiles(ctx context.Context, relatedID int) ([]models.File, error)
|
||||
|
||||
Create(ctx context.Context, newGallery *models.Gallery, fileIDs []models.FileID) error
|
||||
models.GalleryCreator
|
||||
UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error)
|
||||
AddFileID(ctx context.Context, id int, fileID models.FileID) error
|
||||
}
|
||||
|
|
@ -80,7 +80,10 @@ func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models.
|
|||
|
||||
logger.Infof("%s doesn't exist. Creating new gallery...", f.Base().Path)
|
||||
|
||||
if err := h.CreatorUpdater.Create(ctx, &newGallery, []models.FileID{baseFile.ID}); err != nil {
|
||||
if err := h.CreatorUpdater.Create(ctx, &models.CreateGalleryInput{
|
||||
Gallery: &newGallery,
|
||||
FileIDs: []models.FileID{baseFile.ID},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("creating new gallery: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ type ScanCreatorUpdater interface {
|
|||
type GalleryFinderCreator interface {
|
||||
FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Gallery, error)
|
||||
FindByFolderID(ctx context.Context, folderID models.FolderID) ([]*models.Gallery, error)
|
||||
Create(ctx context.Context, newObject *models.Gallery, fileIDs []models.FileID) error
|
||||
models.GalleryCreator
|
||||
UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error)
|
||||
}
|
||||
|
||||
|
|
@ -252,9 +252,13 @@ func (h *ScanHandler) getOrCreateFolderBasedGallery(ctx context.Context, f model
|
|||
newGallery := models.NewGallery()
|
||||
newGallery.FolderID = &folderID
|
||||
|
||||
input := models.CreateGalleryInput{
|
||||
Gallery: &newGallery,
|
||||
}
|
||||
|
||||
logger.Infof("Creating folder-based gallery for %s", filepath.Dir(f.Base().Path))
|
||||
|
||||
if err := h.GalleryFinder.Create(ctx, &newGallery, nil); err != nil {
|
||||
if err := h.GalleryFinder.Create(ctx, &input); err != nil {
|
||||
return nil, fmt.Errorf("creating folder based gallery: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +312,12 @@ func (h *ScanHandler) getOrCreateZipBasedGallery(ctx context.Context, zipFile mo
|
|||
|
||||
logger.Infof("%s doesn't exist. Creating new gallery...", zipFile.Base().Path)
|
||||
|
||||
if err := h.GalleryFinder.Create(ctx, &newGallery, []models.FileID{zipFile.Base().ID}); err != nil {
|
||||
input := models.CreateGalleryInput{
|
||||
Gallery: &newGallery,
|
||||
FileIDs: []models.FileID{zipFile.Base().ID},
|
||||
}
|
||||
|
||||
if err := h.GalleryFinder.Create(ctx, &input); err != nil {
|
||||
return nil, fmt.Errorf("creating zip-based gallery: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ type GalleryFilterType struct {
|
|||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
UpdatedAt *TimestampCriterionInput `json:"updated_at"`
|
||||
|
||||
// Filter by custom fields
|
||||
CustomFields []CustomFieldCriterionInput `json:"custom_fields"`
|
||||
}
|
||||
|
||||
type GalleryUpdateInput struct {
|
||||
|
|
@ -86,6 +89,8 @@ type GalleryUpdateInput struct {
|
|||
PerformerIds []string `json:"performer_ids"`
|
||||
PrimaryFileID *string `json:"primary_file_id"`
|
||||
|
||||
CustomFields *CustomFieldsInput `json:"custom_fields"`
|
||||
|
||||
// deprecated
|
||||
URL *string `json:"url"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,22 +18,23 @@ type GalleryChapter struct {
|
|||
}
|
||||
|
||||
type Gallery struct {
|
||||
ZipFiles []string `json:"zip_files,omitempty"`
|
||||
FolderPath string `json:"folder_path,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
Date string `json:"date,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Photographer string `json:"photographer,omitempty"`
|
||||
Rating int `json:"rating,omitempty"`
|
||||
Organized bool `json:"organized,omitempty"`
|
||||
Chapters []GalleryChapter `json:"chapters,omitempty"`
|
||||
Studio string `json:"studio,omitempty"`
|
||||
Performers []string `json:"performers,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
||||
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
|
||||
ZipFiles []string `json:"zip_files,omitempty"`
|
||||
FolderPath string `json:"folder_path,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
Date string `json:"date,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Photographer string `json:"photographer,omitempty"`
|
||||
Rating int `json:"rating,omitempty"`
|
||||
Organized bool `json:"organized,omitempty"`
|
||||
Chapters []GalleryChapter `json:"chapters,omitempty"`
|
||||
Studio string `json:"studio,omitempty"`
|
||||
Performers []string `json:"performers,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
CreatedAt json.JSONTime `json:"created_at,omitempty"`
|
||||
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
|
||||
CustomFields map[string]interface{} `json:"custom_fields,omitempty"`
|
||||
|
||||
// deprecated - for import only
|
||||
URL string `json:"url,omitempty"`
|
||||
|
|
|
|||
|
|
@ -114,13 +114,13 @@ func (_m *GalleryReaderWriter) CountByFileID(ctx context.Context, fileID models.
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, newGallery, fileIDs
|
||||
func (_m *GalleryReaderWriter) Create(ctx context.Context, newGallery *models.Gallery, fileIDs []models.FileID) error {
|
||||
ret := _m.Called(ctx, newGallery, fileIDs)
|
||||
// Create provides a mock function with given fields: ctx, newGallery
|
||||
func (_m *GalleryReaderWriter) Create(ctx context.Context, newGallery *models.CreateGalleryInput) error {
|
||||
ret := _m.Called(ctx, newGallery)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Gallery, []models.FileID) error); ok {
|
||||
r0 = rf(ctx, newGallery, fileIDs)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.CreateGalleryInput) error); ok {
|
||||
r0 = rf(ctx, newGallery)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
|
@ -395,6 +395,52 @@ func (_m *GalleryReaderWriter) FindUserGalleryByTitle(ctx context.Context, title
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// GetCustomFields provides a mock function with given fields: ctx, id
|
||||
func (_m *GalleryReaderWriter) GetCustomFields(ctx context.Context, id int) (map[string]interface{}, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 map[string]interface{}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int) map[string]interface{}); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetCustomFieldsBulk provides a mock function with given fields: ctx, ids
|
||||
func (_m *GalleryReaderWriter) GetCustomFieldsBulk(ctx context.Context, ids []int) ([]models.CustomFieldMap, error) {
|
||||
ret := _m.Called(ctx, ids)
|
||||
|
||||
var r0 []models.CustomFieldMap
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []int) []models.CustomFieldMap); ok {
|
||||
r0 = rf(ctx, ids)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]models.CustomFieldMap)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
|
||||
r1 = rf(ctx, ids)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetFiles provides a mock function with given fields: ctx, relatedID
|
||||
func (_m *GalleryReaderWriter) GetFiles(ctx context.Context, relatedID int) ([]models.File, error) {
|
||||
ret := _m.Called(ctx, relatedID)
|
||||
|
|
@ -656,12 +702,26 @@ func (_m *GalleryReaderWriter) SetCover(ctx context.Context, galleryID int, cove
|
|||
return r0
|
||||
}
|
||||
|
||||
// SetCustomFields provides a mock function with given fields: ctx, id, fields
|
||||
func (_m *GalleryReaderWriter) SetCustomFields(ctx context.Context, id int, fields models.CustomFieldsInput) error {
|
||||
ret := _m.Called(ctx, id, fields)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int, models.CustomFieldsInput) error); ok {
|
||||
r0 = rf(ctx, id, fields)
|
||||
} 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 {
|
||||
func (_m *GalleryReaderWriter) Update(ctx context.Context, updatedGallery *models.UpdateGalleryInput) error {
|
||||
ret := _m.Called(ctx, updatedGallery)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Gallery) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.UpdateGalleryInput) error); ok {
|
||||
r0 = rf(ctx, updatedGallery)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,20 @@ func NewGallery() Gallery {
|
|||
}
|
||||
}
|
||||
|
||||
type CreateGalleryInput struct {
|
||||
*Gallery
|
||||
|
||||
FileIDs []FileID
|
||||
CustomFields map[string]interface{} `json:"custom_fields"`
|
||||
}
|
||||
|
||||
type UpdateGalleryInput struct {
|
||||
*Gallery
|
||||
|
||||
FileIDs []FileID
|
||||
CustomFields CustomFieldsInput `json:"custom_fields"`
|
||||
}
|
||||
|
||||
// GalleryPartial represents part of a Gallery object. It is used to update
|
||||
// the database entry. Only non-nil fields will be updated.
|
||||
type GalleryPartial struct {
|
||||
|
|
@ -70,6 +84,8 @@ type GalleryPartial struct {
|
|||
TagIDs *UpdateIDs
|
||||
PerformerIDs *UpdateIDs
|
||||
PrimaryFileID *FileID
|
||||
|
||||
CustomFields CustomFieldsInput
|
||||
}
|
||||
|
||||
func NewGalleryPartial() GalleryPartial {
|
||||
|
|
|
|||
|
|
@ -37,12 +37,12 @@ type GalleryCounter interface {
|
|||
|
||||
// GalleryCreator provides methods to create galleries.
|
||||
type GalleryCreator interface {
|
||||
Create(ctx context.Context, newGallery *Gallery, fileIDs []FileID) error
|
||||
Create(ctx context.Context, newGallery *CreateGalleryInput) error
|
||||
}
|
||||
|
||||
// GalleryUpdater provides methods to update galleries.
|
||||
type GalleryUpdater interface {
|
||||
Update(ctx context.Context, updatedGallery *Gallery) error
|
||||
Update(ctx context.Context, updatedGallery *UpdateGalleryInput) error
|
||||
UpdatePartial(ctx context.Context, id int, updatedGallery GalleryPartial) (*Gallery, error)
|
||||
UpdateImages(ctx context.Context, galleryID int, imageIDs []int) error
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ type GalleryReader interface {
|
|||
PerformerIDLoader
|
||||
TagIDLoader
|
||||
FileLoader
|
||||
CustomFieldsReader
|
||||
|
||||
All(ctx context.Context) ([]*Gallery, error)
|
||||
}
|
||||
|
|
@ -80,6 +81,8 @@ type GalleryWriter interface {
|
|||
GalleryUpdater
|
||||
GalleryDestroyer
|
||||
|
||||
CustomFieldsWriter
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -522,6 +522,10 @@ func (db *Anonymiser) anonymiseGalleries(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := db.anonymiseCustomFields(ctx, goqu.T(galleriesCustomFieldsTable.GetTable()), "gallery_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -240,3 +240,9 @@ func TestSceneSetCustomFields(t *testing.T) {
|
|||
|
||||
testSetCustomFields(t, "Scene", db.Scene, sceneIDs[sceneIdx], getSceneCustomFields(sceneIdx))
|
||||
}
|
||||
|
||||
func TestGallerySetCustomFields(t *testing.T) {
|
||||
galleryIdx := galleryIdxWithScene
|
||||
|
||||
testSetCustomFields(t, "Gallery", db.Gallery, galleryIDs[galleryIdx], getGalleryCustomFields(galleryIdx))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const (
|
|||
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 80
|
||||
var appSchemaVersion uint = 81
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
|
|
|||
|
|
@ -183,6 +183,8 @@ var (
|
|||
)
|
||||
|
||||
type GalleryStore struct {
|
||||
customFieldsStore
|
||||
|
||||
tableMgr *table
|
||||
|
||||
fileStore *FileStore
|
||||
|
|
@ -191,6 +193,10 @@ type GalleryStore struct {
|
|||
|
||||
func NewGalleryStore(fileStore *FileStore, folderStore *FolderStore) *GalleryStore {
|
||||
return &GalleryStore{
|
||||
customFieldsStore: customFieldsStore{
|
||||
table: galleriesCustomFieldsTable,
|
||||
fk: galleriesCustomFieldsTable.Col(galleryIDColumn),
|
||||
},
|
||||
tableMgr: galleryTableMgr,
|
||||
fileStore: fileStore,
|
||||
folderStore: folderStore,
|
||||
|
|
@ -231,18 +237,18 @@ func (qb *GalleryStore) selectDataset() *goqu.SelectDataset {
|
|||
)
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, fileIDs []models.FileID) error {
|
||||
func (qb *GalleryStore) Create(ctx context.Context, newObject *models.CreateGalleryInput) error {
|
||||
var r galleryRow
|
||||
r.fromGallery(*newObject)
|
||||
r.fromGallery(*newObject.Gallery)
|
||||
|
||||
id, err := qb.tableMgr.insertID(ctx, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(fileIDs) > 0 {
|
||||
if len(newObject.FileIDs) > 0 {
|
||||
const firstPrimary = true
|
||||
if err := galleriesFilesTableMgr.insertJoins(ctx, id, firstPrimary, fileIDs); err != nil {
|
||||
if err := galleriesFilesTableMgr.insertJoins(ctx, id, firstPrimary, newObject.FileIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -269,19 +275,24 @@ func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, f
|
|||
}
|
||||
}
|
||||
|
||||
const partial = false
|
||||
if err := qb.setCustomFields(ctx, id, newObject.CustomFields, partial); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updated, err := qb.find(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding after create: %w", err)
|
||||
}
|
||||
|
||||
*newObject = *updated
|
||||
*newObject.Gallery = *updated
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Gallery) error {
|
||||
func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.UpdateGalleryInput) error {
|
||||
var r galleryRow
|
||||
r.fromGallery(*updatedObject)
|
||||
r.fromGallery(*updatedObject.Gallery)
|
||||
|
||||
if err := qb.tableMgr.updateByID(ctx, updatedObject.ID, r); err != nil {
|
||||
return err
|
||||
|
|
@ -319,6 +330,10 @@ func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Galler
|
|||
}
|
||||
}
|
||||
|
||||
if err := qb.SetCustomFields(ctx, updatedObject.ID, updatedObject.CustomFields); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -364,6 +379,10 @@ func (qb *GalleryStore) UpdatePartial(ctx context.Context, id int, partial model
|
|||
}
|
||||
}
|
||||
|
||||
if err := qb.SetCustomFields(ctx, id, partial.CustomFields); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return qb.find(ctx, id)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,13 @@ func (qb *galleryFilterHandler) criterionHandler() criterionHandler {
|
|||
×tampCriterionHandler{filter.CreatedAt, "galleries.created_at", nil},
|
||||
×tampCriterionHandler{filter.UpdatedAt, "galleries.updated_at", nil},
|
||||
|
||||
&customFieldsFilterHandler{
|
||||
table: galleriesCustomFieldsTable.GetTable(),
|
||||
fkCol: galleryIDColumn,
|
||||
c: filter.CustomFields,
|
||||
idCol: "galleries.id",
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "scenes_galleries.scene_id",
|
||||
relatedRepo: sceneRepository.repository,
|
||||
|
|
|
|||
|
|
@ -160,7 +160,10 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
|
|||
fileIDs = []models.FileID{s.Files.List()[0].Base().ID}
|
||||
}
|
||||
|
||||
if err := qb.Create(ctx, &s, fileIDs); (err != nil) != tt.wantErr {
|
||||
if err := qb.Create(ctx, &models.CreateGalleryInput{
|
||||
Gallery: &s,
|
||||
FileIDs: fileIDs,
|
||||
}); (err != nil) != tt.wantErr {
|
||||
t.Errorf("galleryQueryBuilder.Create() error = %v, wantErr = %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
|
|
@ -360,7 +363,9 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
|
|||
|
||||
copy := *tt.updatedObject
|
||||
|
||||
if err := qb.Update(ctx, tt.updatedObject); (err != nil) != tt.wantErr {
|
||||
if err := qb.Update(ctx, &models.UpdateGalleryInput{
|
||||
Gallery: tt.updatedObject,
|
||||
}); (err != nil) != tt.wantErr {
|
||||
t.Errorf("galleryQueryBuilder.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
|
|
@ -3001,6 +3006,245 @@ func TestGallerySetAndResetCover(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGalleryQueryCustomFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filter *models.GalleryFilterType
|
||||
includeIdxs []int
|
||||
excludeIdxs []int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"equals",
|
||||
&models.GalleryFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
Value: []any{getGalleryStringValue(galleryIdxWithImage, "custom")},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{galleryIdxWithImage},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"not equals",
|
||||
&models.GalleryFilterType{
|
||||
Title: &models.StringCriterionInput{
|
||||
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierNotEquals,
|
||||
Value: []any{getGalleryStringValue(galleryIdxWithImage, "custom")},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[]int{galleryIdxWithImage},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"includes",
|
||||
&models.GalleryFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
Value: []any{getGalleryStringValue(galleryIdxWithImage, "custom")[9:]},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{galleryIdxWithImage},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"excludes",
|
||||
&models.GalleryFilterType{
|
||||
Title: &models.StringCriterionInput{
|
||||
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
Value: []any{getGalleryStringValue(galleryIdxWithImage, "custom")[9:]},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[]int{galleryIdxWithImage},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"regex",
|
||||
&models.GalleryFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
Value: []any{".*17_custom"},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{galleryIdxWithPerformerTag},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid regex",
|
||||
&models.GalleryFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
Value: []any{"["},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"not matches regex",
|
||||
&models.GalleryFilterType{
|
||||
Title: &models.StringCriterionInput{
|
||||
Value: getGalleryStringValue(galleryIdxWithPerformerTag, titleField),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierNotMatchesRegex,
|
||||
Value: []any{".*17_custom"},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[]int{galleryIdxWithPerformerTag},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid not matches regex",
|
||||
&models.GalleryFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierNotMatchesRegex,
|
||||
Value: []any{"["},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"null",
|
||||
&models.GalleryFilterType{
|
||||
Title: &models.StringCriterionInput{
|
||||
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "not existing",
|
||||
Modifier: models.CriterionModifierIsNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{galleryIdxWithImage},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"not null",
|
||||
&models.GalleryFilterType{
|
||||
Title: &models.StringCriterionInput{
|
||||
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierNotNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{galleryIdxWithImage},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"between",
|
||||
&models.GalleryFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "real",
|
||||
Modifier: models.CriterionModifierBetween,
|
||||
Value: []any{0.15, 0.25},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{galleryIdxWithImage},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"not between",
|
||||
&models.GalleryFilterType{
|
||||
Title: &models.StringCriterionInput{
|
||||
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "real",
|
||||
Modifier: models.CriterionModifierNotBetween,
|
||||
Value: []any{0.15, 0.25},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[]int{galleryIdxWithImage},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||
assert := assert.New(t)
|
||||
|
||||
galleries, _, err := db.Gallery.Query(ctx, tt.filter, nil)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GalleryStore.Query() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ids := galleriesToIDs(galleries)
|
||||
include := indexesToIDs(galleryIDs, tt.includeIdxs)
|
||||
exclude := indexesToIDs(galleryIDs, tt.excludeIdxs)
|
||||
|
||||
for _, i := range include {
|
||||
assert.Contains(ids, i)
|
||||
}
|
||||
for _, e := range exclude {
|
||||
assert.NotContains(ids, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Count
|
||||
// TODO All
|
||||
// TODO Query
|
||||
|
|
|
|||
9
pkg/sqlite/migrations/81_gallery_custom_fields.up.sql
Normal file
9
pkg/sqlite/migrations/81_gallery_custom_fields.up.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
CREATE TABLE `gallery_custom_fields` (
|
||||
`gallery_id` integer NOT NULL,
|
||||
`field` varchar(64) NOT NULL,
|
||||
`value` BLOB NOT NULL,
|
||||
PRIMARY KEY (`gallery_id`, `field`),
|
||||
foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX `index_gallery_custom_fields_field_value` ON `gallery_custom_fields` (`field`, `value`);
|
||||
|
|
@ -1389,6 +1389,18 @@ func makeGallery(i int, includeScenes bool) *models.Gallery {
|
|||
return ret
|
||||
}
|
||||
|
||||
func getGalleryCustomFields(index int) map[string]interface{} {
|
||||
if index%5 == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"string": getGalleryStringValue(index, "custom"),
|
||||
"int": int64(index % 5),
|
||||
"real": float64(index) / 10,
|
||||
}
|
||||
}
|
||||
|
||||
func createGalleries(ctx context.Context, n int) error {
|
||||
gqb := db.Gallery
|
||||
fqb := db.File
|
||||
|
|
@ -1410,7 +1422,11 @@ func createGalleries(ctx context.Context, n int) error {
|
|||
const includeScenes = false
|
||||
gallery := makeGallery(i, includeScenes)
|
||||
|
||||
err := gqb.Create(ctx, gallery, fileIDs)
|
||||
err := gqb.Create(ctx, &models.CreateGalleryInput{
|
||||
Gallery: gallery,
|
||||
FileIDs: fileIDs,
|
||||
CustomFields: getGalleryCustomFields(i),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating gallery %v+: %s", gallery, err.Error())
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ var (
|
|||
performersGalleriesJoinTable = goqu.T(performersGalleriesTable)
|
||||
galleriesScenesJoinTable = goqu.T(galleriesScenesTable)
|
||||
galleriesURLsJoinTable = goqu.T(galleriesURLsTable)
|
||||
galleriesCustomFieldsTable = goqu.T("gallery_custom_fields")
|
||||
|
||||
scenesFilesJoinTable = goqu.T(scenesFilesTable)
|
||||
scenesTagsJoinTable = goqu.T(scenesTagsTable)
|
||||
|
|
|
|||
Loading…
Reference in a new issue