mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Scene Marker duration filter and sort (#5472)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
e097f2b3f4
commit
6ad0951878
8 changed files with 155 additions and 2 deletions
|
|
@ -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: FloatCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
|
|
|
|||
|
|
@ -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 *FloatCriterionInput `json:"duration"`
|
||||
// Filter by created at
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
|
|
|
|||
|
|
@ -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, sceneMarkerTable)
|
||||
default:
|
||||
query.sortAndPagination += getSort(sort, direction, sceneMarkerTable)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ func (qb *sceneMarkerFilterHandler) criterionHandler() criterionHandler {
|
|||
qb.sceneTagsCriterionHandler(sceneMarkerFilter.SceneTags),
|
||||
qb.performersCriterionHandler(sceneMarkerFilter.Performers),
|
||||
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.UpdatedAt, "scene_markers.updated_at", nil},
|
||||
&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 {
|
||||
t.Helper()
|
||||
result, _, err := sqb.Query(ctx, markerFilter, findFilter)
|
||||
|
|
|
|||
|
|
@ -276,6 +276,8 @@ const (
|
|||
markerIdxWithScene = iota
|
||||
markerIdxWithTag
|
||||
markerIdxWithSceneTag
|
||||
markerIdxWithDuration
|
||||
markerIdx2WithDuration
|
||||
totalMarkers
|
||||
)
|
||||
|
||||
|
|
@ -1754,10 +1756,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)
|
||||
|
|
|
|||
|
|
@ -637,7 +637,11 @@ export function createNumberCriterionOption(
|
|||
}
|
||||
|
||||
export class NullNumberCriterionOption extends CriterionOption {
|
||||
constructor(messageID: string, value: CriterionType) {
|
||||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
makeCriterion?: () => Criterion<CriterionValue>
|
||||
) {
|
||||
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<INumberValue> {
|
||||
constructor(type: CriterionOption) {
|
||||
super(type, { value: undefined, value2: undefined });
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import { DisplayMode } from "./types";
|
|||
import {
|
||||
createDateCriterionOption,
|
||||
createMandatoryTimestampCriterionOption,
|
||||
createNullDurationCriterionOption,
|
||||
} from "./criteria/criterion";
|
||||
|
||||
const defaultSortBy = "title";
|
||||
const sortByOptions = [
|
||||
"duration",
|
||||
"title",
|
||||
"seconds",
|
||||
"scene_id",
|
||||
|
|
@ -22,6 +24,7 @@ const criterionOptions = [
|
|||
MarkersScenesCriterionOption,
|
||||
SceneTagsCriterionOption,
|
||||
PerformersCriterionOption,
|
||||
createNullDurationCriterionOption("duration"),
|
||||
createMandatoryTimestampCriterionOption("created_at"),
|
||||
createMandatoryTimestampCriterionOption("updated_at"),
|
||||
createDateCriterionOption("scene_date"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue