Add Chapters for Galleries (#3289)

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
yoshnopa
2023-03-16 05:04:54 +01:00
committed by GitHub
parent 32c91c4855
commit 7e8f941155
58 changed files with 1685 additions and 133 deletions
+83
View File
@@ -0,0 +1,83 @@
package gallery
import (
"context"
"database/sql"
"fmt"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/jsonschema"
)
type ChapterCreatorUpdater interface {
Create(ctx context.Context, newGalleryChapter models.GalleryChapter) (*models.GalleryChapter, error)
Update(ctx context.Context, updatedGalleryChapter models.GalleryChapter) (*models.GalleryChapter, error)
FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error)
}
type ChapterImporter struct {
GalleryID int
ReaderWriter ChapterCreatorUpdater
Input jsonschema.GalleryChapter
MissingRefBehaviour models.ImportMissingRefEnum
chapter models.GalleryChapter
}
func (i *ChapterImporter) PreImport(ctx context.Context) error {
i.chapter = models.GalleryChapter{
Title: i.Input.Title,
ImageIndex: i.Input.ImageIndex,
GalleryID: sql.NullInt64{Int64: int64(i.GalleryID), Valid: true},
CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()},
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()},
}
return nil
}
func (i *ChapterImporter) Name() string {
return fmt.Sprintf("%s (%d)", i.Input.Title, i.Input.ImageIndex)
}
func (i *ChapterImporter) PostImport(ctx context.Context, id int) error {
return nil
}
func (i *ChapterImporter) FindExistingID(ctx context.Context) (*int, error) {
existingChapters, err := i.ReaderWriter.FindByGalleryID(ctx, i.GalleryID)
if err != nil {
return nil, err
}
for _, m := range existingChapters {
if m.ImageIndex == i.chapter.ImageIndex {
id := m.ID
return &id, nil
}
}
return nil, nil
}
func (i *ChapterImporter) Create(ctx context.Context) (*int, error) {
created, err := i.ReaderWriter.Create(ctx, i.chapter)
if err != nil {
return nil, fmt.Errorf("error creating chapter: %v", err)
}
id := created.ID
return &id, nil
}
func (i *ChapterImporter) Update(ctx context.Context, id int) error {
chapter := i.chapter
chapter.ID = id
_, err := i.ReaderWriter.Update(ctx, chapter)
if err != nil {
return fmt.Errorf("error updating existing chapter: %v", err)
}
return nil
}
+11
View File
@@ -11,6 +11,8 @@ import (
func (s *Service) Destroy(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error) {
var imgsDestroyed []*models.Image
// chapter deletion is done via delete cascade, so we don't need to do anything here
// if this is a zip-based gallery, delete the images as well first
zipImgsDestroyed, err := s.destroyZipFileImages(ctx, i, fileDeleter, deleteGenerated, deleteFile)
if err != nil {
@@ -39,6 +41,15 @@ func (s *Service) Destroy(ctx context.Context, i *models.Gallery, fileDeleter *i
return imgsDestroyed, nil
}
type ChapterDestroyer interface {
FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error)
Destroy(ctx context.Context, id int) error
}
func DestroyChapter(ctx context.Context, galleryChapter *models.GalleryChapter, qb ChapterDestroyer) error {
return qb.Destroy(ctx, galleryChapter.ID)
}
func (s *Service) destroyZipFileImages(ctx context.Context, i *models.Gallery, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) ([]*models.Image, error) {
if err := i.LoadFiles(ctx, s.Repository); err != nil {
return nil, err
+29
View File
@@ -2,6 +2,7 @@ package gallery
import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/json"
@@ -9,6 +10,10 @@ import (
"github.com/stashapp/stash/pkg/studio"
)
type ChapterFinder interface {
FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error)
}
// ToBasicJSON converts a gallery object into its JSON object equivalent. It
// does not convert the relationships to other objects.
func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) {
@@ -58,6 +63,30 @@ func GetStudioName(ctx context.Context, reader studio.Finder, gallery *models.Ga
return "", nil
}
// GetGalleryChaptersJSON returns a slice of GalleryChapter JSON representation
// objects corresponding to the provided gallery's chapters.
func GetGalleryChaptersJSON(ctx context.Context, chapterReader ChapterFinder, gallery *models.Gallery) ([]jsonschema.GalleryChapter, error) {
galleryChapters, err := chapterReader.FindByGalleryID(ctx, gallery.ID)
if err != nil {
return nil, fmt.Errorf("error getting gallery chapters: %v", err)
}
var results []jsonschema.GalleryChapter
for _, galleryChapter := range galleryChapters {
galleryChapterJSON := jsonschema.GalleryChapter{
Title: galleryChapter.Title,
ImageIndex: galleryChapter.ImageIndex,
CreatedAt: json.JSONTime{Time: galleryChapter.CreatedAt.Timestamp},
UpdatedAt: json.JSONTime{Time: galleryChapter.UpdatedAt.Timestamp},
}
results = append(results, galleryChapterJSON)
}
return results, nil
}
func GetIDs(galleries []*models.Gallery) []int {
var results []int
for _, gallery := range galleries {
+122
View File
@@ -22,6 +22,9 @@ const (
errStudioID = 6
// noTagsID = 11
noChaptersID = 7
errChaptersID = 8
errFindByChapterID = 9
)
var (
@@ -63,6 +66,19 @@ func createFullGallery(id int) models.Gallery {
}
}
func createEmptyGallery(id int) models.Gallery {
return models.Gallery{
ID: id,
Files: models.NewRelatedFiles([]file.File{
&file.BaseFile{
Path: path,
},
}),
CreatedAt: createTime,
UpdatedAt: updateTime,
}
}
func createFullJSONGallery() *jsonschema.Gallery {
return &jsonschema.Gallery{
Title: title,
@@ -168,3 +184,109 @@ func TestGetStudioName(t *testing.T) {
mockStudioReader.AssertExpectations(t)
}
const (
validChapterID1 = 1
validChapterID2 = 2
chapterTitle1 = "chapterTitle1"
chapterTitle2 = "chapterTitle2"
chapterImageIndex1 = 10
chapterImageIndex2 = 50
)
type galleryChaptersTestScenario struct {
input models.Gallery
expected []jsonschema.GalleryChapter
err bool
}
var getGalleryChaptersJSONScenarios = []galleryChaptersTestScenario{
{
createEmptyGallery(galleryID),
[]jsonschema.GalleryChapter{
{
Title: chapterTitle1,
ImageIndex: chapterImageIndex1,
CreatedAt: json.JSONTime{
Time: createTime,
},
UpdatedAt: json.JSONTime{
Time: updateTime,
},
},
{
Title: chapterTitle2,
ImageIndex: chapterImageIndex2,
CreatedAt: json.JSONTime{
Time: createTime,
},
UpdatedAt: json.JSONTime{
Time: updateTime,
},
},
},
false,
},
{
createEmptyGallery(noChaptersID),
nil,
false,
},
{
createEmptyGallery(errChaptersID),
nil,
true,
},
}
var validChapters = []*models.GalleryChapter{
{
ID: validChapterID1,
Title: chapterTitle1,
ImageIndex: chapterImageIndex1,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
},
{
ID: validChapterID2,
Title: chapterTitle2,
ImageIndex: chapterImageIndex2,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
},
}
func TestGetGalleryChaptersJSON(t *testing.T) {
mockChapterReader := &mocks.GalleryChapterReaderWriter{}
chaptersErr := errors.New("error getting gallery chapters")
mockChapterReader.On("FindByGalleryID", testCtx, galleryID).Return(validChapters, nil).Once()
mockChapterReader.On("FindByGalleryID", testCtx, noChaptersID).Return(nil, nil).Once()
mockChapterReader.On("FindByGalleryID", testCtx, errChaptersID).Return(nil, chaptersErr).Once()
for i, s := range getGalleryChaptersJSONScenarios {
gallery := s.input
json, err := GetGalleryChaptersJSON(testCtx, mockChapterReader, &gallery)
switch {
case !s.err && err != nil:
t.Errorf("[%d] unexpected error: %s", i, err.Error())
case s.err && err == nil:
t.Errorf("[%d] expected error not returned", i)
default:
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
}
+1
View File
@@ -24,6 +24,7 @@ type Importer struct {
Input jsonschema.Gallery
MissingRefBehaviour models.ImportMissingRefEnum
ID int
gallery models.Gallery
}
+7
View File
@@ -31,6 +31,13 @@ type ImageService interface {
DestroyZipImages(ctx context.Context, zipFile file.File, fileDeleter *image.FileDeleter, deleteGenerated bool) ([]*models.Image, error)
}
type ChapterRepository interface {
ChapterFinder
ChapterDestroyer
Update(ctx context.Context, updatedObject models.GalleryChapter) (*models.GalleryChapter, error)
}
type Service struct {
Repository Repository
ImageFinder ImageFinder
+2
View File
@@ -31,6 +31,8 @@ type GalleryFilterType struct {
Organized *bool `json:"organized"`
// Filter by average image resolution
AverageResolution *ResolutionCriterionInput `json:"average_resolution"`
// Filter to only include scenes which have chapters. `true` or `false`
HasChapters *string `json:"has_chapters"`
// Filter to only include galleries with this studio
Studios *HierarchicalMultiCriterionInput `json:"studios"`
// Filter to only include galleries with these tags
+20
View File
@@ -0,0 +1,20 @@
package models
import "context"
type GalleryChapterReader interface {
Find(ctx context.Context, id int) (*GalleryChapter, error)
FindMany(ctx context.Context, ids []int) ([]*GalleryChapter, error)
FindByGalleryID(ctx context.Context, galleryID int) ([]*GalleryChapter, error)
}
type GalleryChapterWriter interface {
Create(ctx context.Context, newGalleryChapter GalleryChapter) (*GalleryChapter, error)
Update(ctx context.Context, updatedGalleryChapter GalleryChapter) (*GalleryChapter, error)
Destroy(ctx context.Context, id int) error
}
type GalleryChapterReaderWriter interface {
GalleryChapterReader
GalleryChapterWriter
}
+19 -11
View File
@@ -10,22 +10,30 @@ import (
"github.com/stashapp/stash/pkg/models/json"
)
type Gallery struct {
ZipFiles []string `json:"zip_files,omitempty"`
FolderPath string `json:"folder_path,omitempty"`
type GalleryChapter struct {
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"`
Date string `json:"date,omitempty"`
Details string `json:"details,omitempty"`
Rating int `json:"rating,omitempty"`
Organized bool `json:"organized,omitempty"`
Studio string `json:"studio,omitempty"`
Performers []string `json:"performers,omitempty"`
Tags []string `json:"tags,omitempty"`
ImageIndex int `json:"image_index,omitempty"`
CreatedAt json.JSONTime `json:"created_at,omitempty"`
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
}
type Gallery struct {
ZipFiles []string `json:"zip_files,omitempty"`
FolderPath string `json:"folder_path,omitempty"`
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"`
Date string `json:"date,omitempty"`
Details string `json:"details,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"`
}
func (s Gallery) Filename(basename string, hash string) string {
ret := fsutil.SanitiseBasename(basename)
@@ -0,0 +1,144 @@
// Code generated by mockery v2.10.0. DO NOT EDIT.
package mocks
import (
context "context"
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
)
// GalleryChapterReaderWriter is an autogenerated mock type for the GalleryChapterReaderWriter type
type GalleryChapterReaderWriter struct {
mock.Mock
}
// Create provides a mock function with given fields: ctx, newGalleryChapter
func (_m *GalleryChapterReaderWriter) Create(ctx context.Context, newGalleryChapter models.GalleryChapter) (*models.GalleryChapter, error) {
ret := _m.Called(ctx, newGalleryChapter)
var r0 *models.GalleryChapter
if rf, ok := ret.Get(0).(func(context.Context, models.GalleryChapter) *models.GalleryChapter); ok {
r0 = rf(ctx, newGalleryChapter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.GalleryChapter)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, models.GalleryChapter) error); ok {
r1 = rf(ctx, newGalleryChapter)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Destroy provides a mock function with given fields: ctx, id
func (_m *GalleryChapterReaderWriter) Destroy(ctx context.Context, id int) error {
ret := _m.Called(ctx, id)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Find provides a mock function with given fields: ctx, id
func (_m *GalleryChapterReaderWriter) Find(ctx context.Context, id int) (*models.GalleryChapter, error) {
ret := _m.Called(ctx, id)
var r0 *models.GalleryChapter
if rf, ok := ret.Get(0).(func(context.Context, int) *models.GalleryChapter); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.GalleryChapter)
}
}
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
}
// FindByGalleryID provides a mock function with given fields: ctx, galleryID
func (_m *GalleryChapterReaderWriter) FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error) {
ret := _m.Called(ctx, galleryID)
var r0 []*models.GalleryChapter
if rf, ok := ret.Get(0).(func(context.Context, int) []*models.GalleryChapter); ok {
r0 = rf(ctx, galleryID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.GalleryChapter)
}
}
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
}
// FindMany provides a mock function with given fields: ctx, ids
func (_m *GalleryChapterReaderWriter) FindMany(ctx context.Context, ids []int) ([]*models.GalleryChapter, error) {
ret := _m.Called(ctx, ids)
var r0 []*models.GalleryChapter
if rf, ok := ret.Get(0).(func(context.Context, []int) []*models.GalleryChapter); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.GalleryChapter)
}
}
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
}
// Update provides a mock function with given fields: ctx, updatedGalleryChapter
func (_m *GalleryChapterReaderWriter) Update(ctx context.Context, updatedGalleryChapter models.GalleryChapter) (*models.GalleryChapter, error) {
ret := _m.Called(ctx, updatedGalleryChapter)
var r0 *models.GalleryChapter
if rf, ok := ret.Get(0).(func(context.Context, models.GalleryChapter) *models.GalleryChapter); ok {
r0 = rf(ctx, updatedGalleryChapter)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*models.GalleryChapter)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, models.GalleryChapter) error); ok {
r1 = rf(ctx, updatedGalleryChapter)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
+12 -11
View File
@@ -44,16 +44,17 @@ func (*TxnManager) Reset() error {
func NewTxnRepository() models.Repository {
return models.Repository{
TxnManager: &TxnManager{},
Gallery: &GalleryReaderWriter{},
Image: &ImageReaderWriter{},
Movie: &MovieReaderWriter{},
Performer: &PerformerReaderWriter{},
Scene: &SceneReaderWriter{},
SceneMarker: &SceneMarkerReaderWriter{},
ScrapedItem: &ScrapedItemReaderWriter{},
Studio: &StudioReaderWriter{},
Tag: &TagReaderWriter{},
SavedFilter: &SavedFilterReaderWriter{},
TxnManager: &TxnManager{},
Gallery: &GalleryReaderWriter{},
GalleryChapter: &GalleryChapterReaderWriter{},
Image: &ImageReaderWriter{},
Movie: &MovieReaderWriter{},
Performer: &PerformerReaderWriter{},
Scene: &SceneReaderWriter{},
SceneMarker: &SceneMarkerReaderWriter{},
ScrapedItem: &ScrapedItemReaderWriter{},
Studio: &StudioReaderWriter{},
Tag: &TagReaderWriter{},
SavedFilter: &SavedFilterReaderWriter{},
}
}
+24
View File
@@ -0,0 +1,24 @@
package models
import (
"database/sql"
)
type GalleryChapter struct {
ID int `db:"id" json:"id"`
Title string `db:"title" json:"title"`
ImageIndex int `db:"image_index" json:"image_index"`
GalleryID sql.NullInt64 `db:"gallery_id,omitempty" json:"gallery_id"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}
type GalleryChapters []*GalleryChapter
func (m *GalleryChapters) Append(o interface{}) {
*m = append(*m, o.(*GalleryChapter))
}
func (m *GalleryChapters) New() interface{} {
return &GalleryChapter{}
}
+13 -12
View File
@@ -14,16 +14,17 @@ type TxnManager interface {
type Repository struct {
TxnManager
File file.Store
Folder file.FolderStore
Gallery GalleryReaderWriter
Image ImageReaderWriter
Movie MovieReaderWriter
Performer PerformerReaderWriter
Scene SceneReaderWriter
SceneMarker SceneMarkerReaderWriter
ScrapedItem ScrapedItemReaderWriter
Studio StudioReaderWriter
Tag TagReaderWriter
SavedFilter SavedFilterReaderWriter
File file.Store
Folder file.FolderStore
Gallery GalleryReaderWriter
GalleryChapter GalleryChapterReaderWriter
Image ImageReaderWriter
Movie MovieReaderWriter
Performer PerformerReaderWriter
Scene SceneReaderWriter
SceneMarker SceneMarkerReaderWriter
ScrapedItem ScrapedItemReaderWriter
Studio StudioReaderWriter
Tag TagReaderWriter
SavedFilter SavedFilterReaderWriter
}
+12
View File
@@ -34,6 +34,10 @@ const (
GalleryUpdatePost HookTriggerEnum = "Gallery.Update.Post"
GalleryDestroyPost HookTriggerEnum = "Gallery.Destroy.Post"
GalleryChapterCreatePost HookTriggerEnum = "GalleryChapter.Create.Post"
GalleryChapterUpdatePost HookTriggerEnum = "GalleryChapter.Update.Post"
GalleryChapterDestroyPost HookTriggerEnum = "GalleryChapter.Destroy.Post"
MovieCreatePost HookTriggerEnum = "Movie.Create.Post"
MovieUpdatePost HookTriggerEnum = "Movie.Update.Post"
MovieDestroyPost HookTriggerEnum = "Movie.Destroy.Post"
@@ -69,6 +73,10 @@ var AllHookTriggerEnum = []HookTriggerEnum{
GalleryUpdatePost,
GalleryDestroyPost,
GalleryChapterCreatePost,
GalleryChapterUpdatePost,
GalleryChapterDestroyPost,
MovieCreatePost,
MovieUpdatePost,
MovieDestroyPost,
@@ -106,6 +114,10 @@ func (e HookTriggerEnum) IsValid() bool {
GalleryUpdatePost,
GalleryDestroyPost,
GalleryChapterCreatePost,
GalleryChapterUpdatePost,
GalleryChapterDestroyPost,
MovieCreatePost,
MovieUpdatePost,
MovieDestroyPost,
+1 -1
View File
@@ -32,7 +32,7 @@ const (
dbConnTimeout = 30
)
var appSchemaVersion uint = 43
var appSchemaVersion uint = 44
//go:embed migrations/*.sql
var migrationsBox embed.FS
+20 -1
View File
@@ -26,6 +26,7 @@ const (
galleriesTagsTable = "galleries_tags"
galleriesImagesTable = "galleries_images"
galleriesScenesTable = "scenes_galleries"
galleriesChaptersTable = "galleries_chapters"
galleryIDColumn = "gallery_id"
)
@@ -668,6 +669,7 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
query.handleCriterion(ctx, galleryTagCountCriterionHandler(qb, galleryFilter.TagCount))
query.handleCriterion(ctx, galleryPerformersCriterionHandler(qb, galleryFilter.Performers))
query.handleCriterion(ctx, galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount))
query.handleCriterion(ctx, hasChaptersCriterionHandler(galleryFilter.HasChapters))
query.handleCriterion(ctx, galleryStudioCriterionHandler(qb, galleryFilter.Studios))
query.handleCriterion(ctx, galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags))
query.handleCriterion(ctx, galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
@@ -729,11 +731,15 @@ func (qb *GalleryStore) makeQuery(ctx context.Context, galleryFilter *models.Gal
as: "gallery_folder",
onClause: "galleries.folder_id = gallery_folder.id",
},
join{
table: galleriesChaptersTable,
onClause: "galleries_chapters.gallery_id = galleries.id",
},
)
// add joins for files and checksum
filepathColumn := "folders.path || '" + string(filepath.Separator) + "' || files.basename"
searchColumns := []string{"galleries.title", "gallery_folder.path", filepathColumn, "files_fingerprints.fingerprint"}
searchColumns := []string{"galleries.title", "gallery_folder.path", filepathColumn, "files_fingerprints.fingerprint", "galleries_chapters.title"}
query.parseQueryString(searchColumns, *q)
}
@@ -949,6 +955,19 @@ func galleryImageCountCriterionHandler(qb *GalleryStore, imageCount *models.IntC
return h.handler(imageCount)
}
func hasChaptersCriterionHandler(hasChapters *string) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if hasChapters != nil {
f.addLeftJoin("galleries_chapters", "", "galleries_chapters.gallery_id = galleries.id")
if *hasChapters == "true" {
f.addHaving("count(galleries_chapters.gallery_id) > 0")
} else {
f.addWhere("galleries_chapters.id IS NULL")
}
}
}
}
func galleryStudioCriterionHandler(qb *GalleryStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
h := hierarchicalMultiCriterionHandlerBuilder{
tx: qb.tx,
+94
View File
@@ -0,0 +1,94 @@
package sqlite
import (
"context"
"fmt"
"github.com/stashapp/stash/pkg/models"
)
type galleryChapterQueryBuilder struct {
repository
}
var GalleryChapterReaderWriter = &galleryChapterQueryBuilder{
repository{
tableName: galleriesChaptersTable,
idColumn: idColumn,
},
}
func (qb *galleryChapterQueryBuilder) Create(ctx context.Context, newObject models.GalleryChapter) (*models.GalleryChapter, error) {
var ret models.GalleryChapter
if err := qb.insertObject(ctx, newObject, &ret); err != nil {
return nil, err
}
return &ret, nil
}
func (qb *galleryChapterQueryBuilder) Update(ctx context.Context, updatedObject models.GalleryChapter) (*models.GalleryChapter, error) {
const partial = false
if err := qb.update(ctx, updatedObject.ID, updatedObject, partial); err != nil {
return nil, err
}
var ret models.GalleryChapter
if err := qb.getByID(ctx, updatedObject.ID, &ret); err != nil {
return nil, err
}
return &ret, nil
}
func (qb *galleryChapterQueryBuilder) Destroy(ctx context.Context, id int) error {
return qb.destroyExisting(ctx, []int{id})
}
func (qb *galleryChapterQueryBuilder) Find(ctx context.Context, id int) (*models.GalleryChapter, error) {
query := "SELECT * FROM galleries_chapters WHERE id = ? LIMIT 1"
args := []interface{}{id}
results, err := qb.queryGalleryChapters(ctx, query, args)
if err != nil || len(results) < 1 {
return nil, err
}
return results[0], nil
}
func (qb *galleryChapterQueryBuilder) FindMany(ctx context.Context, ids []int) ([]*models.GalleryChapter, error) {
var markers []*models.GalleryChapter
for _, id := range ids {
marker, err := qb.Find(ctx, id)
if err != nil {
return nil, err
}
if marker == nil {
return nil, fmt.Errorf("gallery chapter with id %d not found", id)
}
markers = append(markers, marker)
}
return markers, nil
}
func (qb *galleryChapterQueryBuilder) FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error) {
query := `
SELECT galleries_chapters.* FROM galleries_chapters
WHERE galleries_chapters.gallery_id = ?
GROUP BY galleries_chapters.id
ORDER BY galleries_chapters.image_index ASC
`
args := []interface{}{galleryID}
return qb.queryGalleryChapters(ctx, query, args)
}
func (qb *galleryChapterQueryBuilder) queryGalleryChapters(ctx context.Context, query string, args []interface{}) ([]*models.GalleryChapter, error) {
var ret models.GalleryChapters
if err := qb.query(ctx, query, args, &ret); err != nil {
return nil, err
}
return []*models.GalleryChapter(ret), nil
}
+44
View File
@@ -0,0 +1,44 @@
//go:build integration
// +build integration
package sqlite_test
import (
"context"
"testing"
"github.com/stashapp/stash/pkg/sqlite"
"github.com/stretchr/testify/assert"
)
func TestChapterFindByGalleryID(t *testing.T) {
withTxn(func(ctx context.Context) error {
mqb := sqlite.GalleryChapterReaderWriter
galleryID := galleryIDs[galleryIdxWithChapters]
chapters, err := mqb.FindByGalleryID(ctx, galleryID)
if err != nil {
t.Errorf("Error finding chapters: %s", err.Error())
}
assert.Greater(t, len(chapters), 0)
for _, chapter := range chapters {
assert.Equal(t, galleryIDs[galleryIdxWithChapters], int(chapter.GalleryID.Int64))
}
chapters, err = mqb.FindByGalleryID(ctx, 0)
if err != nil {
t.Errorf("Error finding chapter: %s", err.Error())
}
assert.Len(t, chapters, 0)
return nil
})
}
// TODO Update
// TODO Destroy
// TODO Find
+31
View File
@@ -2616,6 +2616,37 @@ func TestGalleryStore_RemoveImages(t *testing.T) {
}
}
func TestGalleryQueryHasChapters(t *testing.T) {
withTxn(func(ctx context.Context) error {
sqb := db.Gallery
hasChapters := "true"
galleryFilter := models.GalleryFilterType{
HasChapters: &hasChapters,
}
q := getGalleryStringValue(galleryIdxWithChapters, titleField)
findFilter := models.FindFilterType{
Q: &q,
}
galleries := queryGallery(ctx, t, sqb, &galleryFilter, &findFilter)
assert.Len(t, galleries, 1)
assert.Equal(t, galleryIDs[galleryIdxWithChapters], galleries[0].ID)
hasChapters = "false"
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter)
assert.Len(t, galleries, 0)
findFilter.Q = nil
galleries = queryGallery(ctx, t, sqb, &galleryFilter, &findFilter)
assert.NotEqual(t, 0, len(galleries))
return nil
})
}
// TODO Count
// TODO All
// TODO Query
@@ -0,0 +1,10 @@
CREATE TABLE `galleries_chapters` (
`id` integer not null primary key autoincrement,
`title` varchar(255) not null,
`image_index` integer not null,
`gallery_id` integer not null,
`created_at` datetime not null,
`updated_at` datetime not null,
foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE
);
CREATE INDEX `index_galleries_chapters_on_gallery_id` on `galleries_chapters` (`gallery_id`);
+43
View File
@@ -146,6 +146,7 @@ const (
const (
galleryIdxWithScene = iota
galleryIdxWithChapters
galleryIdxWithImage
galleryIdx1WithImage
galleryIdx2WithImage
@@ -236,6 +237,11 @@ const (
totalMarkers
)
const (
chapterIdxWithGallery = iota
totalChapters
)
const (
savedFilterIdxDefaultScene = iota
savedFilterIdxDefaultImage
@@ -261,6 +267,7 @@ var (
sceneFileIDs []file.ID
imageFileIDs []file.ID
galleryFileIDs []file.ID
chapterIDs []int
sceneIDs []int
imageIDs []int
@@ -372,6 +379,19 @@ var (
}
)
type chapterSpec struct {
galleryIdx int
title string
imageIndex int
}
var (
// indexed by chapter
chapterSpecs = []chapterSpec{
{galleryIdxWithChapters, "Test1", 10},
}
)
var (
imageGalleries = linkMap{
imageIdxWithGallery: {galleryIdxWithImage},
@@ -599,6 +619,11 @@ func populateDB() error {
return fmt.Errorf("error creating scene marker: %s", err.Error())
}
}
for _, cs := range chapterSpecs {
if err := createChapter(ctx, sqlite.GalleryChapterReaderWriter, cs); err != nil {
return fmt.Errorf("error creating gallery chapter: %s", err.Error())
}
}
return nil
}); err != nil {
@@ -1580,6 +1605,24 @@ func createMarker(ctx context.Context, mqb models.SceneMarkerReaderWriter, marke
return nil
}
func createChapter(ctx context.Context, mqb models.GalleryChapterReaderWriter, chapterSpec chapterSpec) error {
chapter := models.GalleryChapter{
GalleryID: sql.NullInt64{Int64: int64(sceneIDs[chapterSpec.galleryIdx]), Valid: true},
Title: chapterSpec.title,
ImageIndex: chapterSpec.imageIndex,
}
created, err := mqb.Create(ctx, chapter)
if err != nil {
return fmt.Errorf("error creating chapter %v+: %w", chapter, err)
}
chapterIDs = append(chapterIDs, created.ID)
return nil
}
func getSavedFilterMode(index int) models.FilterMode {
switch index {
case savedFilterIdxScene, savedFilterIdxDefaultScene:
+14 -13
View File
@@ -125,18 +125,19 @@ func (db *Database) IsLocked(err error) bool {
func (db *Database) TxnRepository() models.Repository {
return models.Repository{
TxnManager: db,
File: db.File,
Folder: db.Folder,
Gallery: db.Gallery,
Image: db.Image,
Movie: MovieReaderWriter,
Performer: db.Performer,
Scene: db.Scene,
SceneMarker: SceneMarkerReaderWriter,
ScrapedItem: ScrapedItemReaderWriter,
Studio: StudioReaderWriter,
Tag: TagReaderWriter,
SavedFilter: SavedFilterReaderWriter,
TxnManager: db,
File: db.File,
Folder: db.Folder,
Gallery: db.Gallery,
GalleryChapter: GalleryChapterReaderWriter,
Image: db.Image,
Movie: MovieReaderWriter,
Performer: db.Performer,
Scene: db.Scene,
SceneMarker: SceneMarkerReaderWriter,
ScrapedItem: ScrapedItemReaderWriter,
Studio: StudioReaderWriter,
Tag: TagReaderWriter,
SavedFilter: SavedFilterReaderWriter,
}
}