mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Track watch activity for scenes. (#3055)
* track watchtime and view time * add view count sorting, added continue position filter * display metrics in file info * add toggle for tracking activity * save activity every 10 seconds * reset resume when video is nearly complete * start from beginning when playing scene in queue Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 40
|
||||
var appSchemaVersion uint = 41
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
||||
4
pkg/sqlite/migrations/41_scene_activity.up.sql
Normal file
4
pkg/sqlite/migrations/41_scene_activity.up.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `scenes` ADD COLUMN `resume_time` float not null default 0;
|
||||
ALTER TABLE `scenes` ADD COLUMN `last_played_at` datetime default null;
|
||||
ALTER TABLE `scenes` ADD COLUMN `play_count` tinyint not null default 0;
|
||||
ALTER TABLE `scenes` ADD COLUMN `play_duration` float not null default 0;
|
||||
@@ -150,7 +150,7 @@ func (qb *movieQueryBuilder) makeFilter(ctx context.Context, movieFilter *models
|
||||
query.handleCriterion(ctx, intCriterionHandler(movieFilter.Rating100, "movies.rating", nil))
|
||||
// legacy rating handler
|
||||
query.handleCriterion(ctx, rating5CriterionHandler(movieFilter.Rating, "movies.rating", nil))
|
||||
query.handleCriterion(ctx, durationCriterionHandler(movieFilter.Duration, "movies.duration", nil))
|
||||
query.handleCriterion(ctx, floatIntCriterionHandler(movieFilter.Duration, "movies.duration", nil))
|
||||
query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
|
||||
query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url"))
|
||||
query.handleCriterion(ctx, movieStudioCriterionHandler(qb, movieFilter.Studios))
|
||||
|
||||
@@ -68,14 +68,14 @@ func (r *updateRecord) setNullInt(destField string, v models.OptionalInt) {
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (r *updateRecord) setFloat64(destField string, v models.OptionalFloat64) {
|
||||
// if v.Set {
|
||||
// if v.Null {
|
||||
// panic("null value not allowed in optional float64")
|
||||
// }
|
||||
// r.set(destField, v.Value)
|
||||
// }
|
||||
// }
|
||||
func (r *updateRecord) setFloat64(destField string, v models.OptionalFloat64) {
|
||||
if v.Set {
|
||||
if v.Null {
|
||||
panic("null value not allowed in optional float64")
|
||||
}
|
||||
r.set(destField, v.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// func (r *updateRecord) setNullFloat64(destField string, v models.OptionalFloat64) {
|
||||
// if v.Set {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/doug-martin/goqu/v9/exp"
|
||||
@@ -60,12 +61,16 @@ type sceneRow struct {
|
||||
URL zero.String `db:"url"`
|
||||
Date models.SQLiteDate `db:"date"`
|
||||
// expressed as 1-100
|
||||
Rating null.Int `db:"rating"`
|
||||
Organized bool `db:"organized"`
|
||||
OCounter int `db:"o_counter"`
|
||||
StudioID null.Int `db:"studio_id,omitempty"`
|
||||
CreatedAt models.SQLiteTimestamp `db:"created_at"`
|
||||
UpdatedAt models.SQLiteTimestamp `db:"updated_at"`
|
||||
Rating null.Int `db:"rating"`
|
||||
Organized bool `db:"organized"`
|
||||
OCounter int `db:"o_counter"`
|
||||
StudioID null.Int `db:"studio_id,omitempty"`
|
||||
CreatedAt models.SQLiteTimestamp `db:"created_at"`
|
||||
UpdatedAt models.SQLiteTimestamp `db:"updated_at"`
|
||||
LastPlayedAt models.NullSQLiteTimestamp `db:"last_played_at"`
|
||||
ResumeTime float64 `db:"resume_time"`
|
||||
PlayDuration float64 `db:"play_duration"`
|
||||
PlayCount int `db:"play_count"`
|
||||
}
|
||||
|
||||
func (r *sceneRow) fromScene(o models.Scene) {
|
||||
@@ -84,6 +89,15 @@ func (r *sceneRow) fromScene(o models.Scene) {
|
||||
r.StudioID = intFromPtr(o.StudioID)
|
||||
r.CreatedAt = models.SQLiteTimestamp{Timestamp: o.CreatedAt}
|
||||
r.UpdatedAt = models.SQLiteTimestamp{Timestamp: o.UpdatedAt}
|
||||
if o.LastPlayedAt != nil {
|
||||
r.LastPlayedAt = models.NullSQLiteTimestamp{
|
||||
Timestamp: *o.LastPlayedAt,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
r.ResumeTime = o.ResumeTime
|
||||
r.PlayDuration = o.PlayDuration
|
||||
r.PlayCount = o.PlayCount
|
||||
}
|
||||
|
||||
type sceneQueryRow struct {
|
||||
@@ -115,12 +129,20 @@ func (r *sceneQueryRow) resolve() *models.Scene {
|
||||
|
||||
CreatedAt: r.CreatedAt.Timestamp,
|
||||
UpdatedAt: r.UpdatedAt.Timestamp,
|
||||
|
||||
ResumeTime: r.ResumeTime,
|
||||
PlayDuration: r.PlayDuration,
|
||||
PlayCount: r.PlayCount,
|
||||
}
|
||||
|
||||
if r.PrimaryFileFolderPath.Valid && r.PrimaryFileBasename.Valid {
|
||||
ret.Path = filepath.Join(r.PrimaryFileFolderPath.String, r.PrimaryFileBasename.String)
|
||||
}
|
||||
|
||||
if r.LastPlayedAt.Valid {
|
||||
ret.LastPlayedAt = &r.LastPlayedAt.Timestamp
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -141,6 +163,10 @@ func (r *sceneRowRecord) fromPartial(o models.ScenePartial) {
|
||||
r.setNullInt("studio_id", o.StudioID)
|
||||
r.setSQLiteTimestamp("created_at", o.CreatedAt)
|
||||
r.setSQLiteTimestamp("updated_at", o.UpdatedAt)
|
||||
r.setSQLiteTimestamp("last_played_at", o.LastPlayedAt)
|
||||
r.setFloat64("resume_time", o.ResumeTime)
|
||||
r.setFloat64("play_duration", o.PlayDuration)
|
||||
r.setInt("play_count", o.PlayCount)
|
||||
}
|
||||
|
||||
type SceneStore struct {
|
||||
@@ -851,7 +877,7 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
|
||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter", nil))
|
||||
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil))
|
||||
|
||||
query.handleCriterion(ctx, durationCriterionHandler(sceneFilter.Duration, "video_files.duration", qb.addVideoFilesTable))
|
||||
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.Duration, "video_files.duration", qb.addVideoFilesTable))
|
||||
query.handleCriterion(ctx, resolutionCriterionHandler(sceneFilter.Resolution, "video_files.height", "video_files.width", qb.addVideoFilesTable))
|
||||
|
||||
query.handleCriterion(ctx, hasMarkersCriterionHandler(sceneFilter.HasMarkers))
|
||||
@@ -876,6 +902,10 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
|
||||
|
||||
query.handleCriterion(ctx, sceneCaptionCriterionHandler(qb, sceneFilter.Captions))
|
||||
|
||||
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.ResumeTime, "scenes.resume_time", nil))
|
||||
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.PlayDuration, "scenes.play_duration", nil))
|
||||
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.PlayCount, "scenes.play_count", nil))
|
||||
|
||||
query.handleCriterion(ctx, sceneTagsCriterionHandler(qb, sceneFilter.Tags))
|
||||
query.handleCriterion(ctx, sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
|
||||
query.handleCriterion(ctx, scenePerformersCriterionHandler(qb, sceneFilter.Performers))
|
||||
@@ -1070,7 +1100,7 @@ func scenePhashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicat
|
||||
}
|
||||
}
|
||||
|
||||
func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
func floatIntCriterionHandler(durationFilter *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if durationFilter != nil {
|
||||
if addJoinFn != nil {
|
||||
@@ -1417,6 +1447,9 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
|
||||
addFileTable()
|
||||
addFolderTable()
|
||||
query.sortAndPagination += " ORDER BY scenes.title COLLATE NATURAL_CS " + direction + ", folders.path " + direction + ", files.basename COLLATE NATURAL_CS " + direction
|
||||
case "play_count":
|
||||
// handle here since getSort has special handling for _count suffix
|
||||
query.sortAndPagination += " ORDER BY scenes.play_count " + direction
|
||||
default:
|
||||
query.sortAndPagination += getSort(sort, direction, "scenes")
|
||||
}
|
||||
@@ -1433,6 +1466,62 @@ func (qb *SceneStore) imageRepository() *imageRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *SceneStore) getPlayCount(ctx context.Context, id int) (int, error) {
|
||||
q := dialect.From(qb.tableMgr.table).Select("play_count").Where(goqu.Ex{"id": id})
|
||||
|
||||
const single = true
|
||||
var ret int
|
||||
if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error {
|
||||
if err := rows.Scan(&ret); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (qb *SceneStore) SaveActivity(ctx context.Context, id int, resumeTime *float64, playDuration *float64) (bool, error) {
|
||||
if err := qb.tableMgr.checkIDExists(ctx, id); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
record := goqu.Record{}
|
||||
|
||||
if resumeTime != nil {
|
||||
record["resume_time"] = resumeTime
|
||||
}
|
||||
|
||||
if playDuration != nil {
|
||||
record["play_duration"] = goqu.L("play_duration + ?", playDuration)
|
||||
}
|
||||
|
||||
if len(record) > 0 {
|
||||
if err := qb.tableMgr.updateByID(ctx, id, record); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (qb *SceneStore) IncrementWatchCount(ctx context.Context, id int) (int, error) {
|
||||
if err := qb.tableMgr.checkIDExists(ctx, id); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := qb.tableMgr.updateByID(ctx, id, goqu.Record{
|
||||
"play_count": goqu.L("play_count + 1"),
|
||||
"last_played_at": time.Now(),
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return qb.getPlayCount(ctx, id)
|
||||
}
|
||||
|
||||
func (qb *SceneStore) GetCover(ctx context.Context, sceneID int) ([]byte, error) {
|
||||
return qb.imageRepository().get(ctx, sceneID)
|
||||
}
|
||||
|
||||
@@ -72,21 +72,25 @@ func loadSceneRelationships(ctx context.Context, expected models.Scene, actual *
|
||||
|
||||
func Test_sceneQueryBuilder_Create(t *testing.T) {
|
||||
var (
|
||||
title = "title"
|
||||
code = "1337"
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
sceneIndex = 123
|
||||
sceneIndex2 = 234
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
stashID1 = "stashid1"
|
||||
stashID2 = "stashid2"
|
||||
title = "title"
|
||||
code = "1337"
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
lastPlayedAt = time.Date(2002, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
resumeTime = 10.0
|
||||
playCount = 3
|
||||
playDuration = 34.0
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
sceneIndex = 123
|
||||
sceneIndex2 = 234
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
stashID1 = "stashid1"
|
||||
stashID2 = "stashid2"
|
||||
|
||||
date = models.NewDate("2003-02-01")
|
||||
|
||||
@@ -136,6 +140,10 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
|
||||
Endpoint: endpoint2,
|
||||
},
|
||||
}),
|
||||
LastPlayedAt: &lastPlayedAt,
|
||||
ResumeTime: float64(resumeTime),
|
||||
PlayCount: playCount,
|
||||
PlayDuration: playDuration,
|
||||
},
|
||||
false,
|
||||
},
|
||||
@@ -180,6 +188,10 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
|
||||
Endpoint: endpoint2,
|
||||
},
|
||||
}),
|
||||
LastPlayedAt: &lastPlayedAt,
|
||||
ResumeTime: resumeTime,
|
||||
PlayCount: playCount,
|
||||
PlayDuration: playDuration,
|
||||
},
|
||||
false,
|
||||
},
|
||||
@@ -299,21 +311,25 @@ func makeSceneFileWithID(i int) *file.VideoFile {
|
||||
|
||||
func Test_sceneQueryBuilder_Update(t *testing.T) {
|
||||
var (
|
||||
title = "title"
|
||||
code = "1337"
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
sceneIndex = 123
|
||||
sceneIndex2 = 234
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
stashID1 = "stashid1"
|
||||
stashID2 = "stashid2"
|
||||
title = "title"
|
||||
code = "1337"
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
lastPlayedAt = time.Date(2002, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
resumeTime = 10.0
|
||||
playCount = 3
|
||||
playDuration = 34.0
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
sceneIndex = 123
|
||||
sceneIndex2 = 234
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
stashID1 = "stashid1"
|
||||
stashID2 = "stashid2"
|
||||
|
||||
date = models.NewDate("2003-02-01")
|
||||
)
|
||||
@@ -362,6 +378,10 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
|
||||
Endpoint: endpoint2,
|
||||
},
|
||||
}),
|
||||
LastPlayedAt: &lastPlayedAt,
|
||||
ResumeTime: resumeTime,
|
||||
PlayCount: playCount,
|
||||
PlayDuration: playDuration,
|
||||
},
|
||||
false,
|
||||
},
|
||||
@@ -507,21 +527,25 @@ func clearScenePartial() models.ScenePartial {
|
||||
|
||||
func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
|
||||
var (
|
||||
title = "title"
|
||||
code = "1337"
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
sceneIndex = 123
|
||||
sceneIndex2 = 234
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
stashID1 = "stashid1"
|
||||
stashID2 = "stashid2"
|
||||
title = "title"
|
||||
code = "1337"
|
||||
details = "details"
|
||||
director = "director"
|
||||
url = "url"
|
||||
rating = 60
|
||||
ocounter = 5
|
||||
lastPlayedAt = time.Date(2002, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
resumeTime = 10.0
|
||||
playCount = 3
|
||||
playDuration = 34.0
|
||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
sceneIndex = 123
|
||||
sceneIndex2 = 234
|
||||
endpoint1 = "endpoint1"
|
||||
endpoint2 = "endpoint2"
|
||||
stashID1 = "stashid1"
|
||||
stashID2 = "stashid2"
|
||||
|
||||
date = models.NewDate("2003-02-01")
|
||||
)
|
||||
@@ -587,6 +611,10 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
|
||||
},
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
},
|
||||
LastPlayedAt: models.NewOptionalTime(lastPlayedAt),
|
||||
ResumeTime: models.NewOptionalFloat64(resumeTime),
|
||||
PlayCount: models.NewOptionalInt(playCount),
|
||||
PlayDuration: models.NewOptionalFloat64(playDuration),
|
||||
},
|
||||
models.Scene{
|
||||
ID: sceneIDs[sceneIdxWithSpacedName],
|
||||
@@ -628,6 +656,10 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
|
||||
Endpoint: endpoint2,
|
||||
},
|
||||
}),
|
||||
LastPlayedAt: &lastPlayedAt,
|
||||
ResumeTime: resumeTime,
|
||||
PlayCount: playCount,
|
||||
PlayDuration: playDuration,
|
||||
},
|
||||
false,
|
||||
},
|
||||
@@ -2088,6 +2120,45 @@ func TestSceneQuery(t *testing.T) {
|
||||
excludeIdxs []int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"specific resume time",
|
||||
nil,
|
||||
&models.SceneFilterType{
|
||||
ResumeTime: &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
Value: int(getSceneResumeTime(sceneIdxWithGallery)),
|
||||
},
|
||||
},
|
||||
[]int{sceneIdxWithGallery},
|
||||
[]int{sceneIdxWithMovie},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"specific play duration",
|
||||
nil,
|
||||
&models.SceneFilterType{
|
||||
PlayDuration: &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
Value: int(getScenePlayDuration(sceneIdxWithGallery)),
|
||||
},
|
||||
},
|
||||
[]int{sceneIdxWithGallery},
|
||||
[]int{sceneIdxWithMovie},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"specific play count",
|
||||
nil,
|
||||
&models.SceneFilterType{
|
||||
PlayCount: &models.IntCriterionInput{
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
Value: getScenePlayCount(sceneIdxWithGallery),
|
||||
},
|
||||
},
|
||||
[]int{sceneIdxWithGallery},
|
||||
[]int{sceneIdxWithMovie},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"stash id with endpoint",
|
||||
nil,
|
||||
@@ -3697,6 +3768,34 @@ func TestSceneQuerySorting(t *testing.T) {
|
||||
-1,
|
||||
-1,
|
||||
},
|
||||
{
|
||||
"play_count",
|
||||
"play_count",
|
||||
models.SortDirectionEnumDesc,
|
||||
sceneIDs[sceneIdx1WithPerformer],
|
||||
-1,
|
||||
},
|
||||
{
|
||||
"last_played_at",
|
||||
"last_played_at",
|
||||
models.SortDirectionEnumDesc,
|
||||
sceneIDs[sceneIdx1WithPerformer],
|
||||
-1,
|
||||
},
|
||||
{
|
||||
"resume_time",
|
||||
"resume_time",
|
||||
models.SortDirectionEnumDesc,
|
||||
sceneIDs[sceneIdx1WithPerformer],
|
||||
-1,
|
||||
},
|
||||
{
|
||||
"play_duration",
|
||||
"play_duration",
|
||||
models.SortDirectionEnumDesc,
|
||||
sceneIDs[sceneIdx1WithPerformer],
|
||||
-1,
|
||||
},
|
||||
}
|
||||
|
||||
qb := db.Scene
|
||||
@@ -4245,5 +4344,154 @@ func TestSceneStore_AssignFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSceneStore_IncrementWatchCount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sceneID int
|
||||
expectedCount int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"valid",
|
||||
sceneIDs[sceneIdx1WithPerformer],
|
||||
getScenePlayCount(sceneIdx1WithPerformer) + 1,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid scene id",
|
||||
invalidID,
|
||||
0,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
qb := db.Scene
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
withRollbackTxn(func(ctx context.Context) error {
|
||||
newVal, err := qb.IncrementWatchCount(ctx, tt.sceneID)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SceneStore.IncrementWatchCount() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
assert := assert.New(t)
|
||||
assert.Equal(tt.expectedCount, newVal)
|
||||
|
||||
// find the scene and check the count
|
||||
scene, err := qb.Find(ctx, tt.sceneID)
|
||||
if err != nil {
|
||||
t.Errorf("SceneStore.Find() error = %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(tt.expectedCount, scene.PlayCount)
|
||||
assert.True(scene.LastPlayedAt.After(time.Now().Add(-1 * time.Minute)))
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSceneStore_SaveActivity(t *testing.T) {
|
||||
var (
|
||||
resumeTime = 111.2
|
||||
playDuration = 98.7
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sceneIdx int
|
||||
resumeTime *float64
|
||||
playDuration *float64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"both",
|
||||
sceneIdx1WithPerformer,
|
||||
&resumeTime,
|
||||
&playDuration,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"resumeTime only",
|
||||
sceneIdx1WithPerformer,
|
||||
&resumeTime,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"playDuration only",
|
||||
sceneIdx1WithPerformer,
|
||||
nil,
|
||||
&playDuration,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"none",
|
||||
sceneIdx1WithPerformer,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid scene id",
|
||||
-1,
|
||||
&resumeTime,
|
||||
&playDuration,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
qb := db.Scene
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
withRollbackTxn(func(ctx context.Context) error {
|
||||
id := -1
|
||||
if tt.sceneIdx != -1 {
|
||||
id = sceneIDs[tt.sceneIdx]
|
||||
}
|
||||
|
||||
_, err := qb.SaveActivity(ctx, id, tt.resumeTime, tt.playDuration)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SceneStore.SaveActivity() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
// find the scene and check the values
|
||||
scene, err := qb.Find(ctx, id)
|
||||
if err != nil {
|
||||
t.Errorf("SceneStore.Find() error = %v", err)
|
||||
}
|
||||
|
||||
expectedResumeTime := getSceneResumeTime(tt.sceneIdx)
|
||||
expectedPlayDuration := getScenePlayDuration(tt.sceneIdx)
|
||||
|
||||
if tt.resumeTime != nil {
|
||||
expectedResumeTime = *tt.resumeTime
|
||||
}
|
||||
if tt.playDuration != nil {
|
||||
expectedPlayDuration += *tt.playDuration
|
||||
}
|
||||
|
||||
assert.Equal(expectedResumeTime, scene.ResumeTime)
|
||||
assert.Equal(expectedPlayDuration, scene.PlayDuration)
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Count
|
||||
// TODO SizeCount
|
||||
|
||||
@@ -944,6 +944,35 @@ func makeSceneFile(i int) *file.VideoFile {
|
||||
}
|
||||
}
|
||||
|
||||
func getScenePlayCount(index int) int {
|
||||
return index % 5
|
||||
}
|
||||
|
||||
func getScenePlayDuration(index int) float64 {
|
||||
if index%5 == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(index%5) * 123.4
|
||||
}
|
||||
|
||||
func getSceneResumeTime(index int) float64 {
|
||||
if index%5 == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(index%5) * 1.2
|
||||
}
|
||||
|
||||
func getSceneLastPlayed(index int) *time.Time {
|
||||
if index%5 == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
t := time.Date(2020, 1, index%5, 1, 2, 3, 0, time.UTC)
|
||||
return &t
|
||||
}
|
||||
|
||||
func makeScene(i int) *models.Scene {
|
||||
title := getSceneTitle(i)
|
||||
details := getSceneStringValue(i, "Details")
|
||||
@@ -984,6 +1013,10 @@ func makeScene(i int) *models.Scene {
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{
|
||||
sceneStashID(i),
|
||||
}),
|
||||
PlayCount: getScenePlayCount(i),
|
||||
PlayDuration: getScenePlayDuration(i),
|
||||
LastPlayedAt: getSceneLastPlayed(i),
|
||||
ResumeTime: getSceneResumeTime(i),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user