Gallery URLs (#4114)

* Initial backend changes
* Fix unit tests
* UI changes
* Fix missing URL filters
This commit is contained in:
WithoutPants
2023-09-25 12:27:20 +10:00
committed by GitHub
parent a369e395e7
commit 9577600804
29 changed files with 361 additions and 117 deletions

View File

@@ -33,7 +33,7 @@ const (
dbConnTimeout = 30
)
var appSchemaVersion uint = 50
var appSchemaVersion uint = 51
//go:embed migrations/*.sql
var migrationsBox embed.FS

View File

@@ -26,12 +26,13 @@ const (
galleriesImagesTable = "galleries_images"
galleriesScenesTable = "scenes_galleries"
galleryIDColumn = "gallery_id"
galleriesURLsTable = "gallery_urls"
galleriesURLColumn = "url"
)
type galleryRow struct {
ID int `db:"id" goqu:"skipinsert"`
Title zero.String `db:"title"`
URL zero.String `db:"url"`
Date NullDate `db:"date"`
Details zero.String `db:"details"`
// expressed as 1-100
@@ -46,7 +47,6 @@ type galleryRow struct {
func (r *galleryRow) fromGallery(o models.Gallery) {
r.ID = o.ID
r.Title = zero.StringFrom(o.Title)
r.URL = zero.StringFrom(o.URL)
r.Date = NullDateFromDatePtr(o.Date)
r.Details = zero.StringFrom(o.Details)
r.Rating = intFromPtr(o.Rating)
@@ -70,7 +70,6 @@ func (r *galleryQueryRow) resolve() *models.Gallery {
ret := &models.Gallery{
ID: r.ID,
Title: r.Title.String,
URL: r.URL.String,
Date: r.Date.DatePtr(),
Details: r.Details.String,
Rating: nullIntPtr(r.Rating),
@@ -97,7 +96,6 @@ type galleryRowRecord struct {
func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) {
r.setNullString("title", o.Title)
r.setNullString("url", o.URL)
r.setNullDate("date", o.Date)
r.setNullString("details", o.Details)
r.setNullInt("rating", o.Rating)
@@ -178,6 +176,12 @@ func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, f
}
}
if newObject.URLs.Loaded() {
const startPos = 0
if err := galleriesURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
return err
}
}
if newObject.PerformerIDs.Loaded() {
if err := galleriesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs.List()); err != nil {
return err
@@ -212,6 +216,11 @@ func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Galler
return err
}
if updatedObject.URLs.Loaded() {
if err := galleriesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
return err
}
}
if updatedObject.PerformerIDs.Loaded() {
if err := galleriesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil {
return err
@@ -257,6 +266,11 @@ func (qb *GalleryStore) UpdatePartial(ctx context.Context, id int, partial model
}
}
if partial.URLs != nil {
if err := galleriesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
return nil, err
}
}
if partial.PerformerIDs != nil {
if err := galleriesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
return nil, err
@@ -669,7 +683,7 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating100, "galleries.rating", nil))
// legacy rating handler
query.handleCriterion(ctx, rating5CriterionHandler(galleryFilter.Rating, "galleries.rating", nil))
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.URL, "galleries.url"))
query.handleCriterion(ctx, galleryURLsCriterionHandler(galleryFilter.URL))
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags))
@@ -793,6 +807,18 @@ func (qb *GalleryStore) QueryCount(ctx context.Context, galleryFilter *models.Ga
return query.executeCount(ctx)
}
func galleryURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: galleriesURLsTable,
stringColumn: galleriesURLColumn,
addJoinTable: func(f *filterBuilder) {
galleriesURLsTableMgr.join(f, "", "galleries.id")
},
}
return h.handler(url)
}
func (qb *GalleryStore) galleryPathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if c != nil {
@@ -874,6 +900,9 @@ func galleryIsMissingCriterionHandler(qb *GalleryStore, isMissing *string) crite
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "url":
galleriesURLsTableMgr.join(f, "", "galleries.id")
f.addWhere("gallery_urls.url IS NULL")
case "scenes":
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
f.addWhere("scenes_join.gallery_id IS NULL")
@@ -1107,6 +1136,10 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F
query.sortAndPagination += ", COALESCE(galleries.title, galleries.id) COLLATE NATURAL_CI ASC"
}
func (qb *GalleryStore) GetURLs(ctx context.Context, galleryID int) ([]string, error) {
return galleriesURLsTableMgr.get(ctx, galleryID)
}
func (qb *GalleryStore) filesRepository() *filesRepository {
return &filesRepository{
repository: repository{

View File

@@ -17,6 +17,11 @@ import (
var invalidID = -1
func loadGalleryRelationships(ctx context.Context, expected models.Gallery, actual *models.Gallery) error {
if expected.URLs.Loaded() {
if err := actual.LoadURLs(ctx, db.Gallery); err != nil {
return err
}
}
if expected.SceneIDs.Loaded() {
if err := actual.LoadSceneIDs(ctx, db.Gallery); err != nil {
return err
@@ -72,7 +77,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
"full",
models.Gallery{
Title: title,
URL: url,
URLs: models.NewRelatedStrings([]string{url}),
Date: &date,
Details: details,
Rating: &rating,
@@ -90,7 +95,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
"with file",
models.Gallery{
Title: title,
URL: url,
URLs: models.NewRelatedStrings([]string{url}),
Date: &date,
Details: details,
Rating: &rating,
@@ -222,7 +227,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
&models.Gallery{
ID: galleryIDs[galleryIdxWithScene],
Title: title,
URL: url,
URLs: models.NewRelatedStrings([]string{url}),
Date: &date,
Details: details,
Rating: &rating,
@@ -243,6 +248,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
"clear nullables",
&models.Gallery{
ID: galleryIDs[galleryIdxWithImage],
URLs: models.NewRelatedStrings([]string{}),
SceneIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
@@ -384,7 +390,7 @@ func clearGalleryPartial() models.GalleryPartial {
return models.GalleryPartial{
Title: models.OptionalString{Set: true, Null: true},
Details: models.OptionalString{Set: true, Null: true},
URL: models.OptionalString{Set: true, Null: true},
URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet},
Date: models.OptionalDate{Set: true, Null: true},
Rating: models.OptionalInt{Set: true, Null: true},
StudioID: models.OptionalInt{Set: true, Null: true},
@@ -416,9 +422,12 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
"full",
galleryIDs[galleryIdxWithImage],
models.GalleryPartial{
Title: models.NewOptionalString(title),
Details: models.NewOptionalString(details),
URL: models.NewOptionalString(url),
Title: models.NewOptionalString(title),
Details: models.NewOptionalString(details),
URLs: &models.UpdateStrings{
Values: []string{url},
Mode: models.RelationshipUpdateModeSet,
},
Date: models.NewOptionalDate(date),
Rating: models.NewOptionalInt(rating),
Organized: models.NewOptionalBool(true),
@@ -443,7 +452,7 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
ID: galleryIDs[galleryIdxWithImage],
Title: title,
Details: details,
URL: url,
URLs: models.NewRelatedStrings([]string{url}),
Date: &date,
Rating: &rating,
Organized: true,
@@ -1653,7 +1662,13 @@ func TestGalleryQueryURL(t *testing.T) {
verifyFn := func(g *models.Gallery) {
t.Helper()
verifyString(t, g.URL, urlCriterion)
urls := g.URLs.List()
var url string
if len(urls) > 0 {
url = urls[0]
}
verifyString(t, url, urlCriterion)
}
verifyGalleryQuery(t, filter, verifyFn)
@@ -1683,6 +1698,12 @@ func verifyGalleryQuery(t *testing.T, filter models.GalleryFilterType, verifyFn
galleries := queryGallery(ctx, t, sqb, &filter, nil)
for _, g := range galleries {
if err := g.LoadURLs(ctx, sqb); err != nil {
t.Errorf("Error loading gallery URLs: %v", err)
}
}
// assume it should find at least one
assert.Greater(t, len(galleries), 0)

View File

@@ -0,0 +1,76 @@
PRAGMA foreign_keys=OFF;
CREATE TABLE `gallery_urls` (
`gallery_id` integer NOT NULL,
`position` integer NOT NULL,
`url` varchar(255) NOT NULL,
foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE,
PRIMARY KEY(`gallery_id`, `position`, `url`)
);
CREATE INDEX `gallery_urls_url` on `gallery_urls` (`url`);
-- drop url
CREATE TABLE `galleries_new` (
`id` integer not null primary key autoincrement,
`folder_id` integer,
`title` varchar(255),
`date` date,
`details` text,
`studio_id` integer,
`rating` tinyint,
`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,
foreign key(`folder_id`) references `folders`(`id`) on delete SET NULL
);
INSERT INTO `galleries_new`
(
`id`,
`folder_id`,
`title`,
`date`,
`details`,
`studio_id`,
`rating`,
`organized`,
`created_at`,
`updated_at`
)
SELECT
`id`,
`folder_id`,
`title`,
`date`,
`details`,
`studio_id`,
`rating`,
`organized`,
`created_at`,
`updated_at`
FROM `galleries`;
INSERT INTO `gallery_urls`
(
`gallery_id`,
`position`,
`url`
)
SELECT
`id`,
'0',
`url`
FROM `galleries`
WHERE `galleries`.`url` IS NOT NULL AND `galleries`.`url` != '';
DROP INDEX `index_galleries_on_studio_id`;
DROP INDEX `index_galleries_on_folder_id_unique`;
DROP TABLE `galleries`;
ALTER TABLE `galleries_new` rename to `galleries`;
CREATE INDEX `index_galleries_on_studio_id` on `galleries` (`studio_id`);
CREATE UNIQUE INDEX `index_galleries_on_folder_id_unique` on `galleries` (`folder_id`);
PRAGMA foreign_keys=ON;

View File

@@ -1291,6 +1291,9 @@ func sceneIsMissingCriterionHandler(qb *SceneStore, isMissing *string) criterion
return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
switch *isMissing {
case "url":
scenesURLsTableMgr.join(f, "", "scenes.id")
f.addWhere("scene_urls.url IS NULL")
case "galleries":
qb.galleriesRepository().join(f, "galleries_join", "scenes.id")
f.addWhere("galleries_join.scene_id IS NULL")

View File

@@ -1213,7 +1213,16 @@ func getGalleryNullStringValue(index int, field string) sql.NullString {
}
func getGalleryNullStringPtr(index int, field string) *string {
return getStringPtr(getPrefixedStringValue("gallery", index, field))
return getStringPtrFromNullString(getPrefixedNullStringValue("gallery", index, field))
}
func getGalleryEmptyString(index int, field string) string {
v := getGalleryNullStringPtr(index, field)
if v == nil {
return ""
}
return *v
}
func getGalleryBasename(index int) string {
@@ -1245,8 +1254,10 @@ func makeGallery(i int, includeScenes bool) *models.Gallery {
tids := indexesToIDs(tagIDs, galleryTags[i])
ret := &models.Gallery{
Title: getGalleryStringValue(i, titleField),
URL: getGalleryNullStringValue(i, urlField).String,
Title: getGalleryStringValue(i, titleField),
URLs: models.NewRelatedStrings([]string{
getGalleryEmptyString(i, urlField),
}),
Rating: getIntPtr(getRating(i)),
Date: getObjectDate(i),
StudioID: studioID,

View File

@@ -19,6 +19,7 @@ var (
galleriesTagsJoinTable = goqu.T(galleriesTagsTable)
performersGalleriesJoinTable = goqu.T(performersGalleriesTable)
galleriesScenesJoinTable = goqu.T(galleriesScenesTable)
galleriesURLsJoinTable = goqu.T(galleriesURLsTable)
scenesFilesJoinTable = goqu.T(scenesFilesTable)
scenesTagsJoinTable = goqu.T(scenesTagsTable)
@@ -122,6 +123,14 @@ var (
table: goqu.T(galleriesChaptersTable),
idColumn: goqu.T(galleriesChaptersTable).Col(idColumn),
}
galleriesURLsTableMgr = &orderedValueTable[string]{
table: table{
table: galleriesURLsJoinTable,
idColumn: galleriesURLsJoinTable.Col(galleryIDColumn),
},
valueColumn: galleriesURLsJoinTable.Col(galleriesURLColumn),
}
)
var (