mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Scene Marker duration filter and sort (#5472)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -193,6 +193,8 @@ input SceneMarkerFilterType {
|
|||||||
performers: MultiCriterionInput
|
performers: MultiCriterionInput
|
||||||
"Filter to only include scene markers from these scenes"
|
"Filter to only include scene markers from these scenes"
|
||||||
scenes: MultiCriterionInput
|
scenes: MultiCriterionInput
|
||||||
|
"Filter by duration (in seconds)"
|
||||||
|
duration: FloatCriterionInput
|
||||||
"Filter by creation time"
|
"Filter by creation time"
|
||||||
created_at: TimestampCriterionInput
|
created_at: TimestampCriterionInput
|
||||||
"Filter by last update time"
|
"Filter by last update time"
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ type SceneMarkerFilterType struct {
|
|||||||
Performers *MultiCriterionInput `json:"performers"`
|
Performers *MultiCriterionInput `json:"performers"`
|
||||||
// Filter to only include scene markers from these scenes
|
// Filter to only include scene markers from these scenes
|
||||||
Scenes *MultiCriterionInput `json:"scenes"`
|
Scenes *MultiCriterionInput `json:"scenes"`
|
||||||
|
// Filter by duration (in seconds)
|
||||||
|
Duration *FloatCriterionInput `json:"duration"`
|
||||||
// Filter by created at
|
// Filter by created at
|
||||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||||
// Filter by updated at
|
// Filter by updated at
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ var sceneMarkerSortOptions = sortOptions{
|
|||||||
"scenes_updated_at",
|
"scenes_updated_at",
|
||||||
"seconds",
|
"seconds",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"duration",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter *models.FindFilterType) error {
|
func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter *models.FindFilterType) error {
|
||||||
@@ -386,6 +387,9 @@ func (qb *SceneMarkerStore) setSceneMarkerSort(query *queryBuilder, findFilter *
|
|||||||
case "title":
|
case "title":
|
||||||
query.join(tagTable, "", "scene_markers.primary_tag_id = tags.id")
|
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
|
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, sceneMarkerTable)
|
||||||
default:
|
default:
|
||||||
query.sortAndPagination += getSort(sort, direction, sceneMarkerTable)
|
query.sortAndPagination += getSort(sort, direction, sceneMarkerTable)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ func (qb *sceneMarkerFilterHandler) criterionHandler() criterionHandler {
|
|||||||
qb.sceneTagsCriterionHandler(sceneMarkerFilter.SceneTags),
|
qb.sceneTagsCriterionHandler(sceneMarkerFilter.SceneTags),
|
||||||
qb.performersCriterionHandler(sceneMarkerFilter.Performers),
|
qb.performersCriterionHandler(sceneMarkerFilter.Performers),
|
||||||
qb.scenesCriterionHandler(sceneMarkerFilter.Scenes),
|
qb.scenesCriterionHandler(sceneMarkerFilter.Scenes),
|
||||||
|
floatCriterionHandler(sceneMarkerFilter.Duration, "COALESCE(scene_markers.end_seconds - scene_markers.seconds, NULL)", nil),
|
||||||
×tampCriterionHandler{sceneMarkerFilter.CreatedAt, "scene_markers.created_at", nil},
|
×tampCriterionHandler{sceneMarkerFilter.CreatedAt, "scene_markers.created_at", nil},
|
||||||
×tampCriterionHandler{sceneMarkerFilter.UpdatedAt, "scene_markers.updated_at", nil},
|
×tampCriterionHandler{sceneMarkerFilter.UpdatedAt, "scene_markers.updated_at", nil},
|
||||||
&dateCriterionHandler{sceneMarkerFilter.SceneDate, "scenes.date", qb.joinScenes},
|
&dateCriterionHandler{sceneMarkerFilter.SceneDate, "scenes.date", qb.joinScenes},
|
||||||
|
|||||||
@@ -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 {
|
func queryMarkers(ctx context.Context, t *testing.T, sqb models.SceneMarkerReader, markerFilter *models.SceneMarkerFilterType, findFilter *models.FindFilterType) []*models.SceneMarker {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
result, _, err := sqb.Query(ctx, markerFilter, findFilter)
|
result, _, err := sqb.Query(ctx, markerFilter, findFilter)
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ const (
|
|||||||
markerIdxWithScene = iota
|
markerIdxWithScene = iota
|
||||||
markerIdxWithTag
|
markerIdxWithTag
|
||||||
markerIdxWithSceneTag
|
markerIdxWithSceneTag
|
||||||
|
markerIdxWithDuration
|
||||||
|
markerIdx2WithDuration
|
||||||
totalMarkers
|
totalMarkers
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1754,10 +1756,20 @@ func createStudios(ctx context.Context, n int, o int) error {
|
|||||||
return nil
|
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 {
|
func createMarker(ctx context.Context, mqb models.SceneMarkerReaderWriter, markerSpec markerSpec) error {
|
||||||
|
markerIdx := len(markerIDs)
|
||||||
marker := models.SceneMarker{
|
marker := models.SceneMarker{
|
||||||
SceneID: sceneIDs[markerSpec.sceneIdx],
|
SceneID: sceneIDs[markerSpec.sceneIdx],
|
||||||
PrimaryTagID: tagIDs[markerSpec.primaryTagIdx],
|
PrimaryTagID: tagIDs[markerSpec.primaryTagIdx],
|
||||||
|
EndSeconds: getMarkerEndSeconds(markerIdx),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mqb.Create(ctx, &marker)
|
err := mqb.Create(ctx, &marker)
|
||||||
|
|||||||
@@ -637,7 +637,11 @@ export function createNumberCriterionOption(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NullNumberCriterionOption extends CriterionOption {
|
export class NullNumberCriterionOption extends CriterionOption {
|
||||||
constructor(messageID: string, value: CriterionType) {
|
constructor(
|
||||||
|
messageID: string,
|
||||||
|
value: CriterionType,
|
||||||
|
makeCriterion?: () => Criterion<CriterionValue>
|
||||||
|
) {
|
||||||
super({
|
super({
|
||||||
messageID,
|
messageID,
|
||||||
type: value,
|
type: value,
|
||||||
@@ -653,7 +657,9 @@ export class NullNumberCriterionOption extends CriterionOption {
|
|||||||
],
|
],
|
||||||
defaultModifier: CriterionModifier.Equals,
|
defaultModifier: CriterionModifier.Equals,
|
||||||
inputType: "number",
|
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);
|
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<INumberValue> {
|
export class DurationCriterion extends Criterion<INumberValue> {
|
||||||
constructor(type: CriterionOption) {
|
constructor(type: CriterionOption) {
|
||||||
super(type, { value: undefined, value2: undefined });
|
super(type, { value: undefined, value2: undefined });
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import { DisplayMode } from "./types";
|
|||||||
import {
|
import {
|
||||||
createDateCriterionOption,
|
createDateCriterionOption,
|
||||||
createMandatoryTimestampCriterionOption,
|
createMandatoryTimestampCriterionOption,
|
||||||
|
createNullDurationCriterionOption,
|
||||||
} from "./criteria/criterion";
|
} from "./criteria/criterion";
|
||||||
|
|
||||||
const defaultSortBy = "title";
|
const defaultSortBy = "title";
|
||||||
const sortByOptions = [
|
const sortByOptions = [
|
||||||
|
"duration",
|
||||||
"title",
|
"title",
|
||||||
"seconds",
|
"seconds",
|
||||||
"scene_id",
|
"scene_id",
|
||||||
@@ -22,6 +24,7 @@ const criterionOptions = [
|
|||||||
MarkersScenesCriterionOption,
|
MarkersScenesCriterionOption,
|
||||||
SceneTagsCriterionOption,
|
SceneTagsCriterionOption,
|
||||||
PerformersCriterionOption,
|
PerformersCriterionOption,
|
||||||
|
createNullDurationCriterionOption("duration"),
|
||||||
createMandatoryTimestampCriterionOption("created_at"),
|
createMandatoryTimestampCriterionOption("created_at"),
|
||||||
createMandatoryTimestampCriterionOption("updated_at"),
|
createMandatoryTimestampCriterionOption("updated_at"),
|
||||||
createDateCriterionOption("scene_date"),
|
createDateCriterionOption("scene_date"),
|
||||||
|
|||||||
Reference in New Issue
Block a user