From 89d236e454be1ec11053dd19a716450bacaf92a1 Mon Sep 17 00:00:00 2001 From: dogwithakeyboard Date: Sun, 10 Nov 2024 14:45:23 +0000 Subject: [PATCH 1/5] Add marker duration filter and sort --- graphql/schema/types/filters.graphql | 2 ++ pkg/models/scene_marker.go | 2 ++ pkg/sqlite/scene_marker.go | 4 ++++ pkg/sqlite/scene_marker_filter.go | 1 + ui/v2.5/src/models/list-filter/scene-markers.ts | 3 +++ 5 files changed, 12 insertions(+) diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index f0f84efda8c..df8a222e0b1 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -193,6 +193,8 @@ input SceneMarkerFilterType { performers: MultiCriterionInput "Filter to only include scene markers from these scenes" scenes: MultiCriterionInput + "Filter by duration (in seconds)" + duration: IntCriterionInput "Filter by creation time" created_at: TimestampCriterionInput "Filter by last update time" diff --git a/pkg/models/scene_marker.go b/pkg/models/scene_marker.go index 8c4598a6df4..3065f75ce7e 100644 --- a/pkg/models/scene_marker.go +++ b/pkg/models/scene_marker.go @@ -11,6 +11,8 @@ type SceneMarkerFilterType struct { Performers *MultiCriterionInput `json:"performers"` // Filter to only include scene markers from these scenes Scenes *MultiCriterionInput `json:"scenes"` + // Filter by duration (in seconds) + Duration *IntCriterionInput `json:"duration"` // Filter by created at CreatedAt *TimestampCriterionInput `json:"created_at"` // Filter by updated at diff --git a/pkg/sqlite/scene_marker.go b/pkg/sqlite/scene_marker.go index 8b2306eab4b..48b37bc773f 100644 --- a/pkg/sqlite/scene_marker.go +++ b/pkg/sqlite/scene_marker.go @@ -367,6 +367,7 @@ var sceneMarkerSortOptions = sortOptions{ "scenes_updated_at", "seconds", "updated_at", + "duration", } func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter *models.FindFilterType) error { @@ -386,6 +387,9 @@ func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter * case "title": query.join(tagTable, "", "scene_markers.primary_tag_id = tags.id") query.sortAndPagination += " ORDER BY COALESCE(NULLIF(scene_markers.title,''), tags.name) COLLATE NATURAL_CI " + direction + case "duration": + sort = "(scene_markers.end_seconds - scene_markers.seconds)" + query.sortAndPagination += getSort(sort, direction, sceneTable) default: query.sortAndPagination += getSort(sort, direction, sceneMarkerTable) } diff --git a/pkg/sqlite/scene_marker_filter.go b/pkg/sqlite/scene_marker_filter.go index d5e044e85a7..b77857b0e07 100644 --- a/pkg/sqlite/scene_marker_filter.go +++ b/pkg/sqlite/scene_marker_filter.go @@ -41,6 +41,7 @@ func (qb *sceneMarkerFilterHandler) criterionHandler() criterionHandler { qb.sceneTagsCriterionHandler(sceneMarkerFilter.SceneTags), qb.performersCriterionHandler(sceneMarkerFilter.Performers), qb.scenesCriterionHandler(sceneMarkerFilter.Scenes), + floatIntCriterionHandler(sceneMarkerFilter.Duration, "COALESCE(scene_markers.end_seconds - scene_markers.seconds, 0)", nil), ×tampCriterionHandler{sceneMarkerFilter.CreatedAt, "scene_markers.created_at", nil}, ×tampCriterionHandler{sceneMarkerFilter.UpdatedAt, "scene_markers.updated_at", nil}, &dateCriterionHandler{sceneMarkerFilter.SceneDate, "scenes.date", qb.joinScenes}, diff --git a/ui/v2.5/src/models/list-filter/scene-markers.ts b/ui/v2.5/src/models/list-filter/scene-markers.ts index 7f6e555ccf3..b1137c825c8 100644 --- a/ui/v2.5/src/models/list-filter/scene-markers.ts +++ b/ui/v2.5/src/models/list-filter/scene-markers.ts @@ -6,10 +6,12 @@ import { DisplayMode } from "./types"; import { createDateCriterionOption, createMandatoryTimestampCriterionOption, + createDurationCriterionOption, } from "./criteria/criterion"; const defaultSortBy = "title"; const sortByOptions = [ + "duration", "title", "seconds", "scene_id", @@ -22,6 +24,7 @@ const criterionOptions = [ MarkersScenesCriterionOption, SceneTagsCriterionOption, PerformersCriterionOption, + createDurationCriterionOption("duration"), createMandatoryTimestampCriterionOption("created_at"), createMandatoryTimestampCriterionOption("updated_at"), createDateCriterionOption("scene_date"), From 9a1d667861f56ec3c3ae6041ca52fafe42149c93 Mon Sep 17 00:00:00 2001 From: dogwithakeyboard Date: Mon, 11 Nov 2024 21:20:59 +0000 Subject: [PATCH 2/5] use correct table --- pkg/sqlite/scene_marker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sqlite/scene_marker.go b/pkg/sqlite/scene_marker.go index 48b37bc773f..ed98d0ef74a 100644 --- a/pkg/sqlite/scene_marker.go +++ b/pkg/sqlite/scene_marker.go @@ -389,7 +389,7 @@ func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter * query.sortAndPagination += " ORDER BY COALESCE(NULLIF(scene_markers.title,''), tags.name) COLLATE NATURAL_CI " + direction case "duration": sort = "(scene_markers.end_seconds - scene_markers.seconds)" - query.sortAndPagination += getSort(sort, direction, sceneTable) + query.sortAndPagination += getSort(sort, direction, sceneMarkerTable) default: query.sortAndPagination += getSort(sort, direction, sceneMarkerTable) } From 9f7ead097a4711ff7a6870f6d9a5681beeb9b87e Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:12:36 +1100 Subject: [PATCH 3/5] Make duration a nullable float filter --- graphql/schema/types/filters.graphql | 2 +- pkg/models/scene_marker.go | 2 +- pkg/sqlite/scene_marker_filter.go | 2 +- .../models/list-filter/criteria/criterion.ts | 23 +++++++++++++++++-- .../src/models/list-filter/scene-markers.ts | 4 ++-- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index df8a222e0b1..23396a98ffd 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -194,7 +194,7 @@ input SceneMarkerFilterType { "Filter to only include scene markers from these scenes" scenes: MultiCriterionInput "Filter by duration (in seconds)" - duration: IntCriterionInput + duration: FloatCriterionInput "Filter by creation time" created_at: TimestampCriterionInput "Filter by last update time" diff --git a/pkg/models/scene_marker.go b/pkg/models/scene_marker.go index 3065f75ce7e..82f9faa1918 100644 --- a/pkg/models/scene_marker.go +++ b/pkg/models/scene_marker.go @@ -12,7 +12,7 @@ type SceneMarkerFilterType struct { // Filter to only include scene markers from these scenes Scenes *MultiCriterionInput `json:"scenes"` // Filter by duration (in seconds) - Duration *IntCriterionInput `json:"duration"` + Duration *FloatCriterionInput `json:"duration"` // Filter by created at CreatedAt *TimestampCriterionInput `json:"created_at"` // Filter by updated at diff --git a/pkg/sqlite/scene_marker_filter.go b/pkg/sqlite/scene_marker_filter.go index b77857b0e07..34fa0f39b36 100644 --- a/pkg/sqlite/scene_marker_filter.go +++ b/pkg/sqlite/scene_marker_filter.go @@ -41,7 +41,7 @@ func (qb *sceneMarkerFilterHandler) criterionHandler() criterionHandler { qb.sceneTagsCriterionHandler(sceneMarkerFilter.SceneTags), qb.performersCriterionHandler(sceneMarkerFilter.Performers), qb.scenesCriterionHandler(sceneMarkerFilter.Scenes), - floatIntCriterionHandler(sceneMarkerFilter.Duration, "COALESCE(scene_markers.end_seconds - scene_markers.seconds, 0)", nil), + floatCriterionHandler(sceneMarkerFilter.Duration, "COALESCE(scene_markers.end_seconds - scene_markers.seconds, NULL)", nil), ×tampCriterionHandler{sceneMarkerFilter.CreatedAt, "scene_markers.created_at", nil}, ×tampCriterionHandler{sceneMarkerFilter.UpdatedAt, "scene_markers.updated_at", nil}, &dateCriterionHandler{sceneMarkerFilter.SceneDate, "scenes.date", qb.joinScenes}, diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index d950073be4c..4fbf7c03b68 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -637,7 +637,11 @@ export function createNumberCriterionOption( } export class NullNumberCriterionOption extends CriterionOption { - constructor(messageID: string, value: CriterionType) { + constructor( + messageID: string, + value: CriterionType, + makeCriterion?: () => Criterion + ) { super({ messageID, type: value, @@ -653,7 +657,9 @@ export class NullNumberCriterionOption extends CriterionOption { ], defaultModifier: CriterionModifier.Equals, inputType: "number", - makeCriterion: () => new NumberCriterion(this), + makeCriterion: makeCriterion + ? makeCriterion + : () => new NumberCriterion(this), }); } } @@ -780,6 +786,19 @@ export function createDurationCriterionOption( return new DurationCriterionOption(messageID ?? value, value); } +export class NullDurationCriterionOption extends NullNumberCriterionOption { + constructor(messageID: string, value: CriterionType) { + super(messageID, value, () => new DurationCriterion(this)); + } +} + +export function createNullDurationCriterionOption( + value: CriterionType, + messageID?: string +) { + return new NullDurationCriterionOption(messageID ?? value, value); +} + export class DurationCriterion extends Criterion { constructor(type: CriterionOption) { super(type, { value: undefined, value2: undefined }); diff --git a/ui/v2.5/src/models/list-filter/scene-markers.ts b/ui/v2.5/src/models/list-filter/scene-markers.ts index b1137c825c8..a70cd16291e 100644 --- a/ui/v2.5/src/models/list-filter/scene-markers.ts +++ b/ui/v2.5/src/models/list-filter/scene-markers.ts @@ -6,7 +6,7 @@ import { DisplayMode } from "./types"; import { createDateCriterionOption, createMandatoryTimestampCriterionOption, - createDurationCriterionOption, + createNullDurationCriterionOption, } from "./criteria/criterion"; const defaultSortBy = "title"; @@ -24,7 +24,7 @@ const criterionOptions = [ MarkersScenesCriterionOption, SceneTagsCriterionOption, PerformersCriterionOption, - createDurationCriterionOption("duration"), + createNullDurationCriterionOption("duration"), createMandatoryTimestampCriterionOption("created_at"), createMandatoryTimestampCriterionOption("updated_at"), createDateCriterionOption("scene_date"), From bb4ed07f9d8dd7e590464b0365a5a9cebd0fff34 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:19:38 +1100 Subject: [PATCH 4/5] Add unit tests --- pkg/sqlite/scene_marker_test.go | 110 ++++++++++++++++++++++++++++++++ pkg/sqlite/setup_test.go | 13 ++++ 2 files changed, 123 insertions(+) diff --git a/pkg/sqlite/scene_marker_test.go b/pkg/sqlite/scene_marker_test.go index ce8f4d3ad6b..64893b3a67f 100644 --- a/pkg/sqlite/scene_marker_test.go +++ b/pkg/sqlite/scene_marker_test.go @@ -391,6 +391,116 @@ func TestMarkerQuerySceneTags(t *testing.T) { }) } +func markersToIDs(i []*models.SceneMarker) []int { + ret := make([]int, len(i)) + for i, v := range i { + ret[i] = v.ID + } + + return ret +} + +func TestMarkerQueryDuration(t *testing.T) { + type test struct { + name string + markerFilter *models.SceneMarkerFilterType + include []int + exclude []int + } + + cases := []test{ + { + "is null", + &models.SceneMarkerFilterType{ + Duration: &models.FloatCriterionInput{ + Modifier: models.CriterionModifierIsNull, + }, + }, + []int{markerIdxWithScene}, + []int{markerIdxWithDuration}, + }, + { + "not null", + &models.SceneMarkerFilterType{ + Duration: &models.FloatCriterionInput{ + Modifier: models.CriterionModifierNotNull, + }, + }, + []int{markerIdxWithDuration}, + []int{markerIdxWithScene}, + }, + { + "equals", + &models.SceneMarkerFilterType{ + Duration: &models.FloatCriterionInput{ + Modifier: models.CriterionModifierEquals, + Value: markerIdxWithDuration, + }, + }, + []int{markerIdxWithDuration}, + []int{markerIdx2WithDuration, markerIdxWithScene}, + }, + { + "not equals", + &models.SceneMarkerFilterType{ + Duration: &models.FloatCriterionInput{ + Modifier: models.CriterionModifierNotEquals, + Value: markerIdx2WithDuration, + }, + }, + []int{markerIdxWithDuration}, + []int{markerIdx2WithDuration, markerIdxWithScene}, + }, + { + "greater than", + &models.SceneMarkerFilterType{ + Duration: &models.FloatCriterionInput{ + Modifier: models.CriterionModifierGreaterThan, + Value: markerIdxWithDuration, + }, + }, + []int{markerIdx2WithDuration}, + []int{markerIdxWithDuration, markerIdxWithScene}, + }, + { + "less than", + &models.SceneMarkerFilterType{ + Duration: &models.FloatCriterionInput{ + Modifier: models.CriterionModifierLessThan, + Value: markerIdx2WithDuration, + }, + }, + []int{markerIdxWithDuration}, + []int{markerIdx2WithDuration, markerIdxWithScene}, + }, + } + + qb := db.SceneMarker + + for _, tt := range cases { + runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) { + assert := assert.New(t) + got, _, err := qb.Query(ctx, tt.markerFilter, nil) + if err != nil { + t.Errorf("SceneMarkerStore.Query() error = %v", err) + return + } + + ids := markersToIDs(got) + include := indexesToIDs(markerIDs, tt.include) + exclude := indexesToIDs(markerIDs, tt.exclude) + + for _, i := range include { + assert.Contains(ids, i) + } + for _, e := range exclude { + assert.NotContains(ids, e) + } + }) + } + +} + func queryMarkers(ctx context.Context, t *testing.T, sqb models.SceneMarkerReader, markerFilter *models.SceneMarkerFilterType, findFilter *models.FindFilterType) []*models.SceneMarker { t.Helper() result, _, err := sqb.Query(ctx, markerFilter, findFilter) diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 1c3f914d3ba..0fc67d5afbb 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -276,6 +276,8 @@ const ( markerIdxWithScene = iota markerIdxWithTag markerIdxWithSceneTag + markerIdxWithDuration + markerIdx2WithDuration totalMarkers ) @@ -425,6 +427,7 @@ var ( {sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, []int{tagIdxWithMarkers, tagIdx2WithMarkers}}, {sceneIdxWithMarkerAndTag, tagIdxWithPrimaryMarkers, nil}, {sceneIdxWithMarkerTwoTags, tagIdxWithPrimaryMarkers, nil}, + {sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, nil}, } ) @@ -1754,10 +1757,20 @@ func createStudios(ctx context.Context, n int, o int) error { return nil } +func getMarkerEndSeconds(index int) *float64 { + if index != markerIdxWithDuration && index != markerIdx2WithDuration { + return nil + } + ret := float64(index) + return &ret +} + func createMarker(ctx context.Context, mqb models.SceneMarkerReaderWriter, markerSpec markerSpec) error { + markerIdx := len(markerIDs) marker := models.SceneMarker{ SceneID: sceneIDs[markerSpec.sceneIdx], PrimaryTagID: tagIDs[markerSpec.primaryTagIdx], + EndSeconds: getMarkerEndSeconds(markerIdx), } err := mqb.Create(ctx, &marker) From 9322cdbc89f0b807a7b3a0b8821f38301ec529bf Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:49:44 +1100 Subject: [PATCH 5/5] Fix unit test --- pkg/sqlite/setup_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 0fc67d5afbb..b63b6a04a2c 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -427,7 +427,6 @@ var ( {sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, []int{tagIdxWithMarkers, tagIdx2WithMarkers}}, {sceneIdxWithMarkerAndTag, tagIdxWithPrimaryMarkers, nil}, {sceneIdxWithMarkerTwoTags, tagIdxWithPrimaryMarkers, nil}, - {sceneIdxWithMarkers, tagIdxWithPrimaryMarkers, nil}, } )