Scene play and o-counter history view and editing (#4532)

Co-authored-by: randemgame <61895715+randemgame@users.noreply.github.com>
This commit is contained in:
WithoutPants
2024-02-22 11:28:18 +11:00
committed by GitHub
parent 0c2a2190e5
commit a303446bb7
51 changed files with 3581 additions and 564 deletions

View File

@@ -46,25 +46,37 @@ type Scene struct {
// deprecated - for import only
URL string `json:"url,omitempty"`
URLs []string `json:"urls,omitempty"`
Date string `json:"date,omitempty"`
Rating int `json:"rating,omitempty"`
Organized bool `json:"organized,omitempty"`
OCounter int `json:"o_counter,omitempty"`
Details string `json:"details,omitempty"`
Director string `json:"director,omitempty"`
Galleries []GalleryRef `json:"galleries,omitempty"`
Performers []string `json:"performers,omitempty"`
Movies []SceneMovie `json:"movies,omitempty"`
Tags []string `json:"tags,omitempty"`
Markers []SceneMarker `json:"markers,omitempty"`
Files []string `json:"files,omitempty"`
Cover string `json:"cover,omitempty"`
CreatedAt json.JSONTime `json:"created_at,omitempty"`
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
LastPlayedAt json.JSONTime `json:"last_played_at,omitempty"`
ResumeTime float64 `json:"resume_time,omitempty"`
PlayCount int `json:"play_count,omitempty"`
URLs []string `json:"urls,omitempty"`
Date string `json:"date,omitempty"`
Rating int `json:"rating,omitempty"`
Organized bool `json:"organized,omitempty"`
// deprecated - for import only
OCounter int `json:"o_counter,omitempty"`
Details string `json:"details,omitempty"`
Director string `json:"director,omitempty"`
Galleries []GalleryRef `json:"galleries,omitempty"`
Performers []string `json:"performers,omitempty"`
Movies []SceneMovie `json:"movies,omitempty"`
Tags []string `json:"tags,omitempty"`
Markers []SceneMarker `json:"markers,omitempty"`
Files []string `json:"files,omitempty"`
Cover string `json:"cover,omitempty"`
CreatedAt json.JSONTime `json:"created_at,omitempty"`
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
// deprecated - for import only
LastPlayedAt json.JSONTime `json:"last_played_at,omitempty"`
ResumeTime float64 `json:"resume_time,omitempty"`
// deprecated - for import only
PlayCount int `json:"play_count,omitempty"`
PlayHistory []json.JSONTime `json:"play_history,omitempty"`
OHistory []json.JSONTime `json:"o_history,omitempty"`
PlayDuration float64 `json:"play_duration,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"`
}

View File

@@ -7,6 +7,8 @@ import (
models "github.com/stashapp/stash/pkg/models"
mock "github.com/stretchr/testify/mock"
time "time"
)
// SceneReaderWriter is an autogenerated mock type for the SceneReaderWriter type
@@ -42,6 +44,52 @@ func (_m *SceneReaderWriter) AddGalleryIDs(ctx context.Context, sceneID int, gal
return r0
}
// AddO provides a mock function with given fields: ctx, id, dates
func (_m *SceneReaderWriter) AddO(ctx context.Context, id int, dates []time.Time) ([]time.Time, error) {
ret := _m.Called(ctx, id, dates)
var r0 []time.Time
if rf, ok := ret.Get(0).(func(context.Context, int, []time.Time) []time.Time); ok {
r0 = rf(ctx, id, dates)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]time.Time)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int, []time.Time) error); ok {
r1 = rf(ctx, id, dates)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AddViews provides a mock function with given fields: ctx, sceneID, dates
func (_m *SceneReaderWriter) AddViews(ctx context.Context, sceneID int, dates []time.Time) ([]time.Time, error) {
ret := _m.Called(ctx, sceneID, dates)
var r0 []time.Time
if rf, ok := ret.Get(0).(func(context.Context, int, []time.Time) []time.Time); ok {
r0 = rf(ctx, sceneID, dates)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]time.Time)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int, []time.Time) error); ok {
r1 = rf(ctx, sceneID, dates)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// All provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) All(ctx context.Context) ([]*models.Scene, error) {
ret := _m.Called(ctx)
@@ -100,6 +148,27 @@ func (_m *SceneReaderWriter) Count(ctx context.Context) (int, error) {
return r0, r1
}
// CountAllViews provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) CountAllViews(ctx context.Context) (int, error) {
ret := _m.Called(ctx)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context) int); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountByFileID provides a mock function with given fields: ctx, fileID
func (_m *SceneReaderWriter) CountByFileID(ctx context.Context, fileID models.FileID) (int, error) {
ret := _m.Called(ctx, fileID)
@@ -247,6 +316,48 @@ func (_m *SceneReaderWriter) CountMissingOSHash(ctx context.Context) (int, error
return r0, r1
}
// CountUniqueViews provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) CountUniqueViews(ctx context.Context) (int, error) {
ret := _m.Called(ctx)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context) int); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountViews provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) CountViews(ctx context.Context, id int) (int, error) {
ret := _m.Called(ctx, id)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Create provides a mock function with given fields: ctx, newScene, fileIDs
func (_m *SceneReaderWriter) Create(ctx context.Context, newScene *models.Scene, fileIDs []models.FileID) error {
ret := _m.Called(ctx, newScene, fileIDs)
@@ -261,8 +372,8 @@ func (_m *SceneReaderWriter) Create(ctx context.Context, newScene *models.Scene,
return r0
}
// DecrementOCounter provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) DecrementOCounter(ctx context.Context, id int) (int, error) {
// DeleteAllViews provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) DeleteAllViews(ctx context.Context, id int) (int, error) {
ret := _m.Called(ctx, id)
var r0 int
@@ -282,6 +393,52 @@ func (_m *SceneReaderWriter) DecrementOCounter(ctx context.Context, id int) (int
return r0, r1
}
// DeleteO provides a mock function with given fields: ctx, id, dates
func (_m *SceneReaderWriter) DeleteO(ctx context.Context, id int, dates []time.Time) ([]time.Time, error) {
ret := _m.Called(ctx, id, dates)
var r0 []time.Time
if rf, ok := ret.Get(0).(func(context.Context, int, []time.Time) []time.Time); ok {
r0 = rf(ctx, id, dates)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]time.Time)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int, []time.Time) error); ok {
r1 = rf(ctx, id, dates)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteViews provides a mock function with given fields: ctx, id, dates
func (_m *SceneReaderWriter) DeleteViews(ctx context.Context, id int, dates []time.Time) ([]time.Time, error) {
ret := _m.Called(ctx, id, dates)
var r0 []time.Time
if rf, ok := ret.Get(0).(func(context.Context, int, []time.Time) []time.Time); ok {
r0 = rf(ctx, id, dates)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]time.Time)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int, []time.Time) error); ok {
r1 = rf(ctx, id, dates)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Destroy provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) Destroy(ctx context.Context, id int) error {
ret := _m.Called(ctx, id)
@@ -593,6 +750,27 @@ func (_m *SceneReaderWriter) FindMany(ctx context.Context, ids []int) ([]*models
return r0, r1
}
// GetAllOCount provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) GetAllOCount(ctx context.Context) (int, error) {
ret := _m.Called(ctx)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context) int); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCover provides a mock function with given fields: ctx, sceneID
func (_m *SceneReaderWriter) GetCover(ctx context.Context, sceneID int) ([]byte, error) {
ret := _m.Called(ctx, sceneID)
@@ -685,6 +863,121 @@ func (_m *SceneReaderWriter) GetManyFileIDs(ctx context.Context, ids []int) ([][
return r0, r1
}
// GetManyLastViewed provides a mock function with given fields: ctx, ids
func (_m *SceneReaderWriter) GetManyLastViewed(ctx context.Context, ids []int) ([]*time.Time, error) {
ret := _m.Called(ctx, ids)
var r0 []*time.Time
if rf, ok := ret.Get(0).(func(context.Context, []int) []*time.Time); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*time.Time)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
r1 = rf(ctx, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetManyOCount provides a mock function with given fields: ctx, ids
func (_m *SceneReaderWriter) GetManyOCount(ctx context.Context, ids []int) ([]int, error) {
ret := _m.Called(ctx, ids)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, []int) []int); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
r1 = rf(ctx, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetManyODates provides a mock function with given fields: ctx, ids
func (_m *SceneReaderWriter) GetManyODates(ctx context.Context, ids []int) ([][]time.Time, error) {
ret := _m.Called(ctx, ids)
var r0 [][]time.Time
if rf, ok := ret.Get(0).(func(context.Context, []int) [][]time.Time); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([][]time.Time)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
r1 = rf(ctx, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetManyViewCount provides a mock function with given fields: ctx, ids
func (_m *SceneReaderWriter) GetManyViewCount(ctx context.Context, ids []int) ([]int, error) {
ret := _m.Called(ctx, ids)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, []int) []int); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
r1 = rf(ctx, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetManyViewDates provides a mock function with given fields: ctx, ids
func (_m *SceneReaderWriter) GetManyViewDates(ctx context.Context, ids []int) ([][]time.Time, error) {
ret := _m.Called(ctx, ids)
var r0 [][]time.Time
if rf, ok := ret.Get(0).(func(context.Context, []int) [][]time.Time); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([][]time.Time)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
r1 = rf(ctx, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMovies provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) GetMovies(ctx context.Context, id int) ([]models.MoviesScenes, error) {
ret := _m.Called(ctx, id)
@@ -708,6 +1001,50 @@ func (_m *SceneReaderWriter) GetMovies(ctx context.Context, id int) ([]models.Mo
return r0, r1
}
// GetOCount provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) GetOCount(ctx context.Context, id int) (int, error) {
ret := _m.Called(ctx, id)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetODates provides a mock function with given fields: ctx, relatedID
func (_m *SceneReaderWriter) GetODates(ctx context.Context, relatedID int) ([]time.Time, error) {
ret := _m.Called(ctx, relatedID)
var r0 []time.Time
if rf, ok := ret.Get(0).(func(context.Context, int) []time.Time); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]time.Time)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPerformerIDs provides a mock function with given fields: ctx, relatedID
func (_m *SceneReaderWriter) GetPerformerIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
@@ -800,6 +1137,29 @@ func (_m *SceneReaderWriter) GetURLs(ctx context.Context, relatedID int) ([]stri
return r0, r1
}
// GetViewDates provides a mock function with given fields: ctx, relatedID
func (_m *SceneReaderWriter) GetViewDates(ctx context.Context, relatedID int) ([]time.Time, error) {
ret := _m.Called(ctx, relatedID)
var r0 []time.Time
if rf, ok := ret.Get(0).(func(context.Context, int) []time.Time); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]time.Time)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasCover provides a mock function with given fields: ctx, sceneID
func (_m *SceneReaderWriter) HasCover(ctx context.Context, sceneID int) (bool, error) {
ret := _m.Called(ctx, sceneID)
@@ -821,69 +1181,6 @@ func (_m *SceneReaderWriter) HasCover(ctx context.Context, sceneID int) (bool, e
return r0, r1
}
// IncrementOCounter provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) IncrementOCounter(ctx context.Context, id int) (int, error) {
ret := _m.Called(ctx, id)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IncrementWatchCount provides a mock function with given fields: ctx, sceneID
func (_m *SceneReaderWriter) IncrementWatchCount(ctx context.Context, sceneID int) (int, error) {
ret := _m.Called(ctx, sceneID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, sceneID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, sceneID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OCount provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) OCount(ctx context.Context) (int, error) {
ret := _m.Called(ctx)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context) int); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OCountByPerformerID provides a mock function with given fields: ctx, performerID
func (_m *SceneReaderWriter) OCountByPerformerID(ctx context.Context, performerID int) (int, error) {
ret := _m.Called(ctx, performerID)
@@ -905,27 +1202,6 @@ func (_m *SceneReaderWriter) OCountByPerformerID(ctx context.Context, performerI
return r0, r1
}
// PlayCount provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) PlayCount(ctx context.Context) (int, error) {
ret := _m.Called(ctx)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context) int); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PlayDuration provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) PlayDuration(ctx context.Context) (float64, error) {
ret := _m.Called(ctx)
@@ -991,8 +1267,8 @@ func (_m *SceneReaderWriter) QueryCount(ctx context.Context, sceneFilter *models
return r0, r1
}
// ResetOCounter provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) ResetOCounter(ctx context.Context, id int) (int, error) {
// ResetO provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) ResetO(ctx context.Context, id int) (int, error) {
ret := _m.Called(ctx, id)
var r0 int
@@ -1054,27 +1330,6 @@ func (_m *SceneReaderWriter) Size(ctx context.Context) (float64, error) {
return r0, r1
}
// UniqueScenePlayCount provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) UniqueScenePlayCount(ctx context.Context) (int, error) {
ret := _m.Called(ctx)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context) int); ok {
r0 = rf(ctx)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Update provides a mock function with given fields: ctx, updatedScene
func (_m *SceneReaderWriter) Update(ctx context.Context, updatedScene *models.Scene) error {
ret := _m.Called(ctx, updatedScene)

View File

@@ -19,7 +19,6 @@ type Scene struct {
// Rating expressed in 1-100 scale
Rating *int `json:"rating"`
Organized bool `json:"organized"`
OCounter int `json:"o_counter"`
StudioID *int `json:"studio_id"`
// transient - not persisted
@@ -35,10 +34,8 @@ type Scene struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastPlayedAt *time.Time `json:"last_played_at"`
ResumeTime float64 `json:"resume_time"`
PlayDuration float64 `json:"play_duration"`
PlayCount int `json:"play_count"`
ResumeTime float64 `json:"resume_time"`
PlayDuration float64 `json:"play_duration"`
URLs RelatedStrings `json:"urls"`
GalleryIDs RelatedIDs `json:"gallery_ids"`
@@ -67,14 +64,11 @@ type ScenePartial struct {
// Rating expressed in 1-100 scale
Rating OptionalInt
Organized OptionalBool
OCounter OptionalInt
StudioID OptionalInt
CreatedAt OptionalTime
UpdatedAt OptionalTime
ResumeTime OptionalFloat64
PlayDuration OptionalFloat64
PlayCount OptionalInt
LastPlayedAt OptionalTime
URLs *UpdateStrings
GalleryIDs *UpdateIDs

View File

@@ -1,6 +1,8 @@
package models
import "context"
import (
"context"
)
type SceneIDLoader interface {
GetSceneIDs(ctx context.Context, relatedID int) ([]int, error)

View File

@@ -1,6 +1,9 @@
package models
import "context"
import (
"context"
"time"
)
// SceneGetter provides methods to get scenes by ID.
type SceneGetter interface {
@@ -40,10 +43,7 @@ type SceneCounter interface {
CountByTagID(ctx context.Context, tagID int) (int, error)
CountMissingChecksum(ctx context.Context) (int, error)
CountMissingOSHash(ctx context.Context) (int, error)
OCount(ctx context.Context) (int, error)
OCountByPerformerID(ctx context.Context, performerID int) (int, error)
PlayCount(ctx context.Context) (int, error)
UniqueScenePlayCount(ctx context.Context) (int, error)
}
// SceneCreator provides methods to create scenes.
@@ -68,6 +68,24 @@ type SceneCreatorUpdater interface {
SceneUpdater
}
type ViewDateReader interface {
CountViews(ctx context.Context, id int) (int, error)
CountAllViews(ctx context.Context) (int, error)
CountUniqueViews(ctx context.Context) (int, error)
GetManyViewCount(ctx context.Context, ids []int) ([]int, error)
GetViewDates(ctx context.Context, relatedID int) ([]time.Time, error)
GetManyViewDates(ctx context.Context, ids []int) ([][]time.Time, error)
GetManyLastViewed(ctx context.Context, ids []int) ([]*time.Time, error)
}
type ODateReader interface {
GetOCount(ctx context.Context, id int) (int, error)
GetManyOCount(ctx context.Context, ids []int) ([]int, error)
GetAllOCount(ctx context.Context) (int, error)
GetODates(ctx context.Context, relatedID int) ([]time.Time, error)
GetManyODates(ctx context.Context, ids []int) ([][]time.Time, error)
}
// SceneReader provides all methods to read scenes.
type SceneReader interface {
SceneFinder
@@ -75,6 +93,8 @@ type SceneReader interface {
SceneCounter
URLLoader
ViewDateReader
ODateReader
FileIDLoader
GalleryIDLoader
PerformerIDLoader
@@ -92,6 +112,18 @@ type SceneReader interface {
HasCover(ctx context.Context, sceneID int) (bool, error)
}
type OHistoryWriter interface {
AddO(ctx context.Context, id int, dates []time.Time) ([]time.Time, error)
DeleteO(ctx context.Context, id int, dates []time.Time) ([]time.Time, error)
ResetO(ctx context.Context, id int) (int, error)
}
type ViewHistoryWriter interface {
AddViews(ctx context.Context, sceneID int, dates []time.Time) ([]time.Time, error)
DeleteViews(ctx context.Context, id int, dates []time.Time) ([]time.Time, error)
DeleteAllViews(ctx context.Context, id int) (int, error)
}
// SceneWriter provides all methods to modify scenes.
type SceneWriter interface {
SceneCreator
@@ -101,11 +133,10 @@ type SceneWriter interface {
AddFileID(ctx context.Context, id int, fileID FileID) error
AddGalleryIDs(ctx context.Context, sceneID int, galleryIDs []int) error
AssignFiles(ctx context.Context, sceneID int, fileID []FileID) error
IncrementOCounter(ctx context.Context, id int) (int, error)
DecrementOCounter(ctx context.Context, id int) (int, error)
ResetOCounter(ctx context.Context, id int) (int, error)
OHistoryWriter
ViewHistoryWriter
SaveActivity(ctx context.Context, sceneID int, resumeTime *float64, playDuration *float64) (bool, error)
IncrementWatchCount(ctx context.Context, sceneID int) (int, error)
}
// SceneReaderWriter provides all scene methods.

View File

@@ -14,7 +14,9 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
type CoverGetter interface {
type ExportGetter interface {
models.ViewDateReader
models.ODateReader
GetCover(ctx context.Context, sceneID int) ([]byte, error)
}
@@ -27,7 +29,7 @@ type TagFinder interface {
// ToBasicJSON converts a scene object into its JSON object equivalent. It
// does not convert the relationships to other objects, with the exception
// of cover image.
func ToBasicJSON(ctx context.Context, reader CoverGetter, scene *models.Scene) (*jsonschema.Scene, error) {
func ToBasicJSON(ctx context.Context, reader ExportGetter, scene *models.Scene) (*jsonschema.Scene, error) {
newSceneJSON := jsonschema.Scene{
Title: scene.Title,
Code: scene.Code,
@@ -47,7 +49,6 @@ func ToBasicJSON(ctx context.Context, reader CoverGetter, scene *models.Scene) (
}
newSceneJSON.Organized = scene.Organized
newSceneJSON.OCounter = scene.OCounter
for _, f := range scene.Files.List() {
newSceneJSON.Files = append(newSceneJSON.Files, f.Base().Path)
@@ -73,6 +74,24 @@ func ToBasicJSON(ctx context.Context, reader CoverGetter, scene *models.Scene) (
newSceneJSON.StashIDs = ret
dates, err := reader.GetViewDates(ctx, scene.ID)
if err != nil {
return nil, fmt.Errorf("error getting view dates: %v", err)
}
for _, date := range dates {
newSceneJSON.PlayHistory = append(newSceneJSON.PlayHistory, json.JSONTime{Time: date})
}
odates, err := reader.GetODates(ctx, scene.ID)
if err != nil {
return nil, fmt.Errorf("error getting o dates: %v", err)
}
for _, date := range odates {
newSceneJSON.OHistory = append(newSceneJSON.OHistory, json.JSONTime{Time: date})
}
return &newSceneJSON, nil
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/stashapp/stash/pkg/models/jsonschema"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"
"time"
@@ -40,7 +41,6 @@ var (
date = "2001-01-01"
dateObj, _ = models.ParseDate(date)
rating = 5
ocounter = 2
organized = true
details = "details"
)
@@ -88,7 +88,6 @@ func createFullScene(id int) models.Scene {
Title: title,
Date: &dateObj,
Details: details,
OCounter: ocounter,
Rating: &rating,
Organized: organized,
URLs: models.NewRelatedStrings([]string{url}),
@@ -130,7 +129,6 @@ func createFullJSONScene(image string) *jsonschema.Scene {
Files: []string{path},
Date: date,
Details: details,
OCounter: ocounter,
Rating: rating,
Organized: organized,
URLs: []string{url},
@@ -193,6 +191,8 @@ func TestToJSON(t *testing.T) {
db.Scene.On("GetCover", testCtx, sceneID).Return(imageBytes, nil).Once()
db.Scene.On("GetCover", testCtx, noImageID).Return(nil, nil).Once()
db.Scene.On("GetCover", testCtx, errImageID).Return(nil, imageErr).Once()
db.Scene.On("GetViewDates", testCtx, mock.Anything).Return(nil, nil)
db.Scene.On("GetODates", testCtx, mock.Anything).Return(nil, nil)
for i, s := range scenarios {
scene := s.input

View File

@@ -4,8 +4,10 @@ import (
"context"
"fmt"
"strings"
"time"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/json"
"github.com/stashapp/stash/pkg/models/jsonschema"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/utils"
@@ -13,6 +15,8 @@ import (
type ImporterReaderWriter interface {
models.SceneCreatorUpdater
models.ViewHistoryWriter
models.OHistoryWriter
FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Scene, error)
}
@@ -31,6 +35,8 @@ type Importer struct {
ID int
scene models.Scene
coverImageData []byte
viewHistory []time.Time
oHistory []time.Time
}
func (i *Importer) PreImport(ctx context.Context) error {
@@ -68,6 +74,9 @@ func (i *Importer) PreImport(ctx context.Context) error {
}
}
i.populateViewHistory()
i.populateOHistory()
return nil
}
@@ -101,20 +110,54 @@ func (i *Importer) sceneJSONToScene(sceneJSON jsonschema.Scene) models.Scene {
}
newScene.Organized = sceneJSON.Organized
newScene.OCounter = sceneJSON.OCounter
newScene.CreatedAt = sceneJSON.CreatedAt.GetTime()
newScene.UpdatedAt = sceneJSON.UpdatedAt.GetTime()
if !sceneJSON.LastPlayedAt.IsZero() {
t := sceneJSON.LastPlayedAt.GetTime()
newScene.LastPlayedAt = &t
}
newScene.ResumeTime = sceneJSON.ResumeTime
newScene.PlayDuration = sceneJSON.PlayDuration
newScene.PlayCount = sceneJSON.PlayCount
return newScene
}
func getHistory(historyJSON []json.JSONTime, count int, last json.JSONTime, createdAt json.JSONTime) []time.Time {
var ret []time.Time
if len(historyJSON) > 0 {
for _, d := range historyJSON {
ret = append(ret, d.GetTime())
}
} else if count > 0 {
createdAt := createdAt.GetTime()
for j := 0; j < count; j++ {
t := createdAt
if j+1 == count && !last.IsZero() {
// last one, use last play date
t = last.GetTime()
}
ret = append(ret, t)
}
}
return ret
}
func (i *Importer) populateViewHistory() {
i.viewHistory = getHistory(
i.Input.PlayHistory,
i.Input.PlayCount,
i.Input.LastPlayedAt,
i.Input.CreatedAt,
)
}
func (i *Importer) populateOHistory() {
i.viewHistory = getHistory(
i.Input.OHistory,
i.Input.OCounter,
i.Input.CreatedAt, // no last o count date
i.Input.CreatedAt,
)
}
func (i *Importer) populateFiles(ctx context.Context) error {
files := make([]*models.VideoFile, 0)
@@ -365,6 +408,28 @@ func (i *Importer) populateTags(ctx context.Context) error {
return nil
}
func (i *Importer) addViewHistory(ctx context.Context) error {
if len(i.viewHistory) > 0 {
_, err := i.ReaderWriter.AddViews(ctx, i.ID, i.viewHistory)
if err != nil {
return fmt.Errorf("error adding view date: %v", err)
}
}
return nil
}
func (i *Importer) addOHistory(ctx context.Context) error {
if len(i.oHistory) > 0 {
_, err := i.ReaderWriter.AddO(ctx, i.ID, i.oHistory)
if err != nil {
return fmt.Errorf("error adding o date: %v", err)
}
}
return nil
}
func (i *Importer) PostImport(ctx context.Context, id int) error {
if len(i.coverImageData) > 0 {
if err := i.ReaderWriter.UpdateCover(ctx, id, i.coverImageData); err != nil {
@@ -372,6 +437,15 @@ func (i *Importer) PostImport(ctx context.Context, id int) error {
}
}
// add histories
if err := i.addViewHistory(ctx); err != nil {
return err
}
if err := i.addOHistory(ctx); err != nil {
return err
}
return nil
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
@@ -14,13 +15,15 @@ import (
"github.com/stashapp/stash/pkg/txn"
)
func (s *Service) Merge(
ctx context.Context,
sourceIDs []int,
destinationID int,
scenePartial models.ScenePartial,
fileDeleter *FileDeleter,
) error {
type MergeOptions struct {
ScenePartial models.ScenePartial
IncludePlayHistory bool
IncludeOHistory bool
}
func (s *Service) Merge(ctx context.Context, sourceIDs []int, destinationID int, fileDeleter *FileDeleter, options MergeOptions) error {
scenePartial := options.ScenePartial
// ensure source ids are unique
sourceIDs = sliceutil.AppendUniques(nil, sourceIDs)
@@ -74,6 +77,44 @@ func (s *Service) Merge(
return fmt.Errorf("updating scene: %w", err)
}
// merge play history
if options.IncludePlayHistory {
var allDates []time.Time
for _, src := range sources {
thisDates, err := s.Repository.GetViewDates(ctx, src.ID)
if err != nil {
return fmt.Errorf("getting view dates for scene %d: %w", src.ID, err)
}
allDates = append(allDates, thisDates...)
}
if len(allDates) > 0 {
if _, err := s.Repository.AddViews(ctx, destinationID, allDates); err != nil {
return fmt.Errorf("adding view dates to scene %d: %w", destinationID, err)
}
}
}
// merge o history
if options.IncludeOHistory {
var allDates []time.Time
for _, src := range sources {
thisDates, err := s.Repository.GetODates(ctx, src.ID)
if err != nil {
return fmt.Errorf("getting o dates for scene %d: %w", src.ID, err)
}
allDates = append(allDates, thisDates...)
}
if len(allDates) > 0 {
if _, err := s.Repository.AddO(ctx, destinationID, allDates); err != nil {
return fmt.Errorf("adding o dates to scene %d: %w", destinationID, err)
}
}
}
// delete old scenes
for _, src := range sources {
const deleteGenerated = true

View File

@@ -153,3 +153,20 @@ func Map[T any, V any](vs []T, f func(T) V) []V {
}
return ret
}
func PtrsToValues[T any](vs []*T) []T {
ret := make([]T, len(vs))
for i, v := range vs {
ret[i] = *v
}
return ret
}
func ValuesToPtrs[T any](vs []T) []*T {
ret := make([]*T, len(vs))
for i, v := range vs {
vv := v
ret[i] = &vv
}
return ret
}

View File

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

View File

@@ -777,28 +777,6 @@ func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInp
}
}
type joinedMultiSumCriterionHandlerBuilder struct {
primaryTable string
foreignTable1 string
joinTable1 string
foreignTable2 string
joinTable2 string
primaryFK string
foreignFK1 string
foreignFK2 string
sum string
}
func (m *joinedMultiSumCriterionHandlerBuilder) handler(criterion *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if criterion != nil {
clause, args := getJoinedMultiSumCriterionClause(m.primaryTable, m.foreignTable1, m.joinTable1, m.foreignTable2, m.joinTable2, m.primaryFK, m.foreignFK1, m.foreignFK2, m.sum, *criterion)
f.addWhere(clause, args...)
}
}
}
// handler for StringCriterion for string list fields
type stringListCriterionHandlerBuilder struct {
// table joining primary and foreign objects

95
pkg/sqlite/history.go Normal file
View File

@@ -0,0 +1,95 @@
package sqlite
import (
"context"
"time"
)
type viewDateManager struct {
tableMgr *viewHistoryTable
}
func (qb *viewDateManager) GetViewDates(ctx context.Context, id int) ([]time.Time, error) {
return qb.tableMgr.getDates(ctx, id)
}
func (qb *viewDateManager) GetManyViewDates(ctx context.Context, ids []int) ([][]time.Time, error) {
return qb.tableMgr.getManyDates(ctx, ids)
}
func (qb *viewDateManager) CountViews(ctx context.Context, id int) (int, error) {
return qb.tableMgr.getCount(ctx, id)
}
func (qb *viewDateManager) GetManyViewCount(ctx context.Context, ids []int) ([]int, error) {
return qb.tableMgr.getManyCount(ctx, ids)
}
func (qb *viewDateManager) CountAllViews(ctx context.Context) (int, error) {
return qb.tableMgr.getAllCount(ctx)
}
func (qb *viewDateManager) CountUniqueViews(ctx context.Context) (int, error) {
return qb.tableMgr.getUniqueCount(ctx)
}
func (qb *viewDateManager) LastView(ctx context.Context, id int) (*time.Time, error) {
return qb.tableMgr.getLastDate(ctx, id)
}
func (qb *viewDateManager) GetManyLastViewed(ctx context.Context, ids []int) ([]*time.Time, error) {
return qb.tableMgr.getManyLastDate(ctx, ids)
}
func (qb *viewDateManager) AddViews(ctx context.Context, id int, dates []time.Time) ([]time.Time, error) {
return qb.tableMgr.addDates(ctx, id, dates)
}
func (qb *viewDateManager) DeleteViews(ctx context.Context, id int, dates []time.Time) ([]time.Time, error) {
return qb.tableMgr.deleteDates(ctx, id, dates)
}
func (qb *viewDateManager) DeleteAllViews(ctx context.Context, id int) (int, error) {
return qb.tableMgr.deleteAllDates(ctx, id)
}
type oDateManager struct {
tableMgr *viewHistoryTable
}
func (qb *oDateManager) GetODates(ctx context.Context, id int) ([]time.Time, error) {
return qb.tableMgr.getDates(ctx, id)
}
func (qb *oDateManager) GetManyODates(ctx context.Context, ids []int) ([][]time.Time, error) {
return qb.tableMgr.getManyDates(ctx, ids)
}
func (qb *oDateManager) GetOCount(ctx context.Context, id int) (int, error) {
return qb.tableMgr.getCount(ctx, id)
}
func (qb *oDateManager) GetManyOCount(ctx context.Context, ids []int) ([]int, error) {
return qb.tableMgr.getManyCount(ctx, ids)
}
func (qb *oDateManager) GetAllOCount(ctx context.Context) (int, error) {
return qb.tableMgr.getAllCount(ctx)
}
func (qb *oDateManager) GetUniqueOCount(ctx context.Context) (int, error) {
return qb.tableMgr.getUniqueCount(ctx)
}
func (qb *oDateManager) AddO(ctx context.Context, id int, dates []time.Time) ([]time.Time, error) {
return qb.tableMgr.addDates(ctx, id, dates)
}
func (qb *oDateManager) DeleteO(ctx context.Context, id int, dates []time.Time) ([]time.Time, error) {
return qb.tableMgr.deleteDates(ctx, id, dates)
}
func (qb *oDateManager) ResetO(ctx context.Context, id int) (int, error) {
return qb.tableMgr.deleteAllDates(ctx, id)
}

View File

@@ -17,7 +17,7 @@ import (
"github.com/doug-martin/goqu/v9/exp"
)
var imageTable = "images"
const imageTable = "images"
const (
imageIDColumn = "image_id"

View File

@@ -0,0 +1,111 @@
PRAGMA foreign_keys=OFF;
CREATE TABLE `scenes_view_dates` (
`scene_id` integer,
`view_date` datetime not null,
foreign key(`scene_id`) references `scenes`(`id`) on delete CASCADE
);
CREATE TABLE `scenes_o_dates` (
`scene_id` integer,
`o_date` datetime not null,
foreign key(`scene_id`) references `scenes`(`id`) on delete CASCADE
);
-- drop o_counter, play_count and last_played_at
CREATE TABLE "scenes_new" (
`id` integer not null primary key autoincrement,
`title` varchar(255),
`details` text,
`date` date,
`rating` tinyint,
`studio_id` integer,
`organized` boolean not null default '0',
`created_at` datetime not null,
`updated_at` datetime not null,
`code` text,
`director` text,
`resume_time` float not null default 0,
`play_duration` float not null default 0,
`cover_blob` varchar(255) REFERENCES `blobs`(`checksum`),
foreign key(`studio_id`) references `studios`(`id`) on delete SET NULL
);
INSERT INTO `scenes_new`
(
`id`,
`title`,
`details`,
`date`,
`rating`,
`studio_id`,
`organized`,
`created_at`,
`updated_at`,
`code`,
`director`,
`resume_time`,
`play_duration`,
`cover_blob`
)
SELECT
`id`,
`title`,
`details`,
`date`,
`rating`,
`studio_id`,
`organized`,
`created_at`,
`updated_at`,
`code`,
`director`,
`resume_time`,
`play_duration`,
`cover_blob`
FROM `scenes`;
WITH max_view_count AS (
SELECT MAX(play_count) AS max_count
FROM scenes
), numbers AS (
SELECT 1 AS n
FROM max_view_count
UNION ALL
SELECT n + 1
FROM numbers
WHERE n < (SELECT max_count FROM max_view_count)
)
INSERT INTO scenes_view_dates (scene_id, view_date)
SELECT scenes.id,
CASE
WHEN numbers.n = scenes.play_count THEN COALESCE(scenes.last_played_at, scenes.created_at)
ELSE scenes.created_at
END AS view_date
FROM scenes
JOIN numbers
WHERE numbers.n <= scenes.play_count;
WITH numbers AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1
FROM numbers
WHERE n < (SELECT MAX(o_counter) FROM scenes)
)
INSERT INTO scenes_o_dates (scene_id, o_date)
SELECT scenes.id,
CASE
WHEN numbers.n <= scenes.o_counter THEN scenes.created_at
END AS o_date
FROM scenes
CROSS JOIN numbers
WHERE numbers.n <= scenes.o_counter;
DROP INDEX `index_scenes_on_studio_id`;
DROP TABLE `scenes`;
ALTER TABLE `scenes_new` rename to `scenes`;
CREATE INDEX `index_scenes_on_studio_id` on `scenes` (`studio_id`);
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,71 @@
package migrations
import (
"context"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/sqlite"
)
type schema55Migrator struct {
migrator
}
func post55(ctx context.Context, db *sqlx.DB) error {
logger.Info("Running post-migration for schema version 55")
m := schema55Migrator{
migrator: migrator{
db: db,
},
}
return m.migrate(ctx)
}
func (m *schema55Migrator) migrate(ctx context.Context) error {
// the last_played_at column was storing in a different format than the rest of the timestamps
// convert the play history date to the correct format
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
query := "SELECT DISTINCT `scene_id`, `view_date` FROM `scenes_view_dates`"
rows, err := m.db.Query(query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id int
viewDate sqlite.Timestamp
)
err := rows.Scan(&id, &viewDate)
if err != nil {
return err
}
utcTimestamp := sqlite.UTCTimestamp{
Timestamp: viewDate,
}
// convert the timestamp to the correct format
if _, err := m.db.Exec("UPDATE scenes_view_dates SET view_date = ? WHERE view_date = ?", utcTimestamp, viewDate.Timestamp); err != nil {
return fmt.Errorf("error correcting view date %s to %s: %w", viewDate.Timestamp, viewDate, err)
}
}
return rows.Err()
}); err != nil {
return err
}
return nil
}
func init() {
sqlite.RegisterPostMigration(55, post55)
}

View File

@@ -847,20 +847,44 @@ func performerGalleryCountCriterionHandler(qb *PerformerStore, count *models.Int
return h.handler(count)
}
func performerOCounterCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
h := joinedMultiSumCriterionHandlerBuilder{
primaryTable: performerTable,
foreignTable1: sceneTable,
joinTable1: performersScenesTable,
foreignTable2: imageTable,
joinTable2: performersImagesTable,
primaryFK: performerIDColumn,
foreignFK1: sceneIDColumn,
foreignFK2: imageIDColumn,
sum: "o_counter",
}
// used for sorting and filtering on performer o-count
var selectPerformerOCountSQL = utils.StrFormat(
"SELECT SUM(o_counter) "+
"FROM ("+
"SELECT SUM(o_counter) as o_counter from {performers_images} s "+
"LEFT JOIN {images} ON {images}.id = s.{images_id} "+
"WHERE s.{performer_id} = {performers}.id "+
"UNION ALL "+
"SELECT COUNT({scenes_o_dates}.{o_date}) as o_counter from {performers_scenes} s "+
"LEFT JOIN {scenes} ON {scenes}.id = s.{scene_id} "+
"LEFT JOIN {scenes_o_dates} ON {scenes_o_dates}.{scene_id} = {scenes}.id "+
"WHERE s.{performer_id} = {performers}.id "+
")",
map[string]interface{}{
"performers_images": performersImagesTable,
"images": imageTable,
"performer_id": performerIDColumn,
"images_id": imageIDColumn,
"performers": performerTable,
"performers_scenes": performersScenesTable,
"scenes": sceneTable,
"scene_id": sceneIDColumn,
"scenes_o_dates": scenesODatesTable,
"o_date": sceneODateColumn,
},
)
return h.handler(count)
func performerOCounterCriterionHandler(qb *PerformerStore, count *models.IntCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if count == nil {
return
}
lhs := "(" + selectPerformerOCountSQL + ")"
clause, args := getIntCriterionWhereClause(lhs, *count)
f.addWhere(clause, args...)
}
}
func performerStudiosCriterionHandler(qb *PerformerStore, studios *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
@@ -998,6 +1022,11 @@ func performerAppearsWithCriterionHandler(qb *PerformerStore, performers *models
}
}
func (qb *PerformerStore) sortByOCounter(direction string) string {
// need to sum the o_counter from scenes and images
return " ORDER BY (" + selectPerformerOCountSQL + ") " + direction
}
func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) string {
var sort string
var direction string
@@ -1019,12 +1048,11 @@ func (qb *PerformerStore) getPerformerSort(findFilter *models.FindFilterType) st
sortQuery += getCountSort(performerTable, performersImagesTable, performerIDColumn, direction)
case "galleries_count":
sortQuery += getCountSort(performerTable, performersGalleriesTable, performerIDColumn, direction)
case "o_counter":
sortQuery += qb.sortByOCounter(direction)
default:
sortQuery += getSort(sort, direction, "performers")
}
if sort == "o_counter" {
return getMultiSumSort("o_counter", performerTable, sceneTable, performersScenesTable, imageTable, performersImagesTable, performerIDColumn, sceneIDColumn, imageIDColumn, direction)
}
// Whatever the sorting, always use name/id as a final sort
sortQuery += ", COALESCE(performers.name, performers.id) COLLATE NATURAL_CI ASC"

View File

@@ -93,6 +93,7 @@ func (r *updateRecord) setTimestamp(destField string, v models.OptionalTime) {
}
}
//nolint:golint,unused
func (r *updateRecord) setNullTimestamp(destField string, v models.OptionalTime) {
if v.Set {
r.set(destField, NullTimestampFromTimePtr(v.Ptr()))

View File

@@ -447,6 +447,14 @@ type relatedFileRow struct {
Primary bool `db:"primary"`
}
func idToIndexMap(ids []int) map[int]int {
ret := make(map[int]int)
for i, id := range ids {
ret[id] = i
}
return ret
}
func (r *filesRepository) getMany(ctx context.Context, ids []int, primaryOnly bool) ([][]models.FileID, error) {
var primaryClause string
if primaryOnly {
@@ -476,10 +484,7 @@ func (r *filesRepository) getMany(ctx context.Context, ids []int, primaryOnly bo
}
ret := make([][]models.FileID, len(ids))
idToIndex := make(map[int]int)
for i, id := range ids {
idToIndex[id] = i
}
idToIndex := idToIndexMap(ids)
for _, row := range fileRows {
id := row.ID

View File

@@ -9,7 +9,6 @@ import (
"sort"
"strconv"
"strings"
"time"
"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
@@ -32,6 +31,10 @@ const (
moviesScenesTable = "movies_scenes"
scenesURLsTable = "scene_urls"
sceneURLColumn = "url"
scenesViewDatesTable = "scenes_view_dates"
sceneViewDateColumn = "view_date"
scenesODatesTable = "scenes_o_dates"
sceneODateColumn = "o_date"
sceneCoverBlobColumn = "cover_blob"
)
@@ -79,16 +82,13 @@ type sceneRow struct {
Director zero.String `db:"director"`
Date NullDate `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 Timestamp `db:"created_at"`
UpdatedAt Timestamp `db:"updated_at"`
LastPlayedAt NullTimestamp `db:"last_played_at"`
ResumeTime float64 `db:"resume_time"`
PlayDuration float64 `db:"play_duration"`
PlayCount int `db:"play_count"`
Rating null.Int `db:"rating"`
Organized bool `db:"organized"`
StudioID null.Int `db:"studio_id,omitempty"`
CreatedAt Timestamp `db:"created_at"`
UpdatedAt Timestamp `db:"updated_at"`
ResumeTime float64 `db:"resume_time"`
PlayDuration float64 `db:"play_duration"`
// not used in resolutions or updates
CoverBlob zero.String `db:"cover_blob"`
@@ -103,14 +103,11 @@ func (r *sceneRow) fromScene(o models.Scene) {
r.Date = NullDateFromDatePtr(o.Date)
r.Rating = intFromPtr(o.Rating)
r.Organized = o.Organized
r.OCounter = o.OCounter
r.StudioID = intFromPtr(o.StudioID)
r.CreatedAt = Timestamp{Timestamp: o.CreatedAt}
r.UpdatedAt = Timestamp{Timestamp: o.UpdatedAt}
r.LastPlayedAt = NullTimestampFromTimePtr(o.LastPlayedAt)
r.ResumeTime = o.ResumeTime
r.PlayDuration = o.PlayDuration
r.PlayCount = o.PlayCount
}
type sceneQueryRow struct {
@@ -132,7 +129,6 @@ func (r *sceneQueryRow) resolve() *models.Scene {
Date: r.Date.DatePtr(),
Rating: nullIntPtr(r.Rating),
Organized: r.Organized,
OCounter: r.OCounter,
StudioID: nullIntPtr(r.StudioID),
PrimaryFileID: nullIntFileIDPtr(r.PrimaryFileID),
@@ -142,10 +138,8 @@ func (r *sceneQueryRow) resolve() *models.Scene {
CreatedAt: r.CreatedAt.Timestamp,
UpdatedAt: r.UpdatedAt.Timestamp,
LastPlayedAt: r.LastPlayedAt.TimePtr(),
ResumeTime: r.ResumeTime,
PlayDuration: r.PlayDuration,
PlayCount: r.PlayCount,
}
if r.PrimaryFileFolderPath.Valid && r.PrimaryFileBasename.Valid {
@@ -167,14 +161,11 @@ func (r *sceneRowRecord) fromPartial(o models.ScenePartial) {
r.setNullDate("date", o.Date)
r.setNullInt("rating", o.Rating)
r.setBool("organized", o.Organized)
r.setInt("o_counter", o.OCounter)
r.setNullInt("studio_id", o.StudioID)
r.setTimestamp("created_at", o.CreatedAt)
r.setTimestamp("updated_at", o.UpdatedAt)
r.setNullTimestamp("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 {
@@ -182,7 +173,8 @@ type SceneStore struct {
blobJoinQueryBuilder
tableMgr *table
oCounterManager
oDateManager
viewDateManager
fileStore *FileStore
}
@@ -199,7 +191,8 @@ func NewSceneStore(fileStore *FileStore, blobStore *BlobStore) *SceneStore {
},
tableMgr: sceneTableMgr,
oCounterManager: oCounterManager{sceneTableMgr},
viewDateManager: viewDateManager{scenesViewTableMgr},
oDateManager: oDateManager{scenesOTableMgr},
fileStore: fileStore,
}
}
@@ -710,20 +703,18 @@ func (qb *SceneStore) CountByPerformerID(ctx context.Context, performerID int) (
func (qb *SceneStore) OCountByPerformerID(ctx context.Context, performerID int) (int, error) {
table := qb.table()
joinTable := scenesPerformersJoinTable
oHistoryTable := goqu.T(scenesODatesTable)
q := dialect.Select(goqu.COALESCE(goqu.SUM("o_counter"), 0)).From(table).InnerJoin(joinTable, goqu.On(table.Col(idColumn).Eq(joinTable.Col(sceneIDColumn)))).Where(joinTable.Col(performerIDColumn).Eq(performerID))
var ret int
if err := querySimple(ctx, q, &ret); err != nil {
return 0, err
}
q := dialect.Select(goqu.COUNT("*")).From(table).InnerJoin(
oHistoryTable,
goqu.On(table.Col(idColumn).Eq(oHistoryTable.Col(sceneIDColumn))),
).InnerJoin(
joinTable,
goqu.On(
table.Col(idColumn).Eq(joinTable.Col(sceneIDColumn)),
),
).Where(joinTable.Col(performerIDColumn).Eq(performerID))
return ret, nil
}
func (qb *SceneStore) OCount(ctx context.Context) (int, error) {
table := qb.table()
q := dialect.Select(goqu.COALESCE(goqu.SUM("o_counter"), 0)).From(table)
var ret int
if err := querySimple(ctx, q, &ret); err != nil {
return 0, err
@@ -757,24 +748,6 @@ func (qb *SceneStore) Count(ctx context.Context) (int, error) {
return count(ctx, q)
}
func (qb *SceneStore) PlayCount(ctx context.Context) (int, error) {
q := dialect.Select(goqu.COALESCE(goqu.SUM("play_count"), 0)).From(qb.table())
var ret int
if err := querySimple(ctx, q, &ret); err != nil {
return 0, err
}
return ret, nil
}
func (qb *SceneStore) UniqueScenePlayCount(ctx context.Context) (int, error) {
table := qb.table()
q := dialect.Select(goqu.COUNT("*")).From(table).Where(table.Col("play_count").Gt(0))
return count(ctx, q)
}
func (qb *SceneStore) Size(ctx context.Context) (float64, error) {
table := qb.table()
fileTable := fileTableMgr.table
@@ -977,7 +950,7 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
query.handleCriterion(ctx, scenePhashDistanceCriterionHandler(qb, sceneFilter.PhashDistance))
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating100, "scenes.rating", nil))
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter", nil))
query.handleCriterion(ctx, sceneOCountCriterionHandler(sceneFilter.OCounter))
query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil))
query.handleCriterion(ctx, floatIntCriterionHandler(sceneFilter.Duration, "video_files.duration", qb.addVideoFilesTable))
@@ -1011,7 +984,7 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
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, scenePlayCountCriterionHandler(sceneFilter.PlayCount))
query.handleCriterion(ctx, sceneTagsCriterionHandler(qb, sceneFilter.Tags))
query.handleCriterion(ctx, sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
@@ -1194,6 +1167,26 @@ func (qb *SceneStore) QueryCount(ctx context.Context, sceneFilter *models.SceneF
return query.executeCount(ctx)
}
func scenePlayCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesViewDatesTable,
primaryFK: sceneIDColumn,
}
return h.handler(count)
}
func sceneOCountCriterionHandler(count *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
joinTable: scenesODatesTable,
primaryFK: sceneIDColumn,
}
return h.handler(count)
}
func sceneFileCountCriterionHandler(qb *SceneStore, fileCount *models.IntCriterionInput) criterionHandlerFunc {
h := countCriterionHandlerBuilder{
primaryTable: sceneTable,
@@ -1600,8 +1593,11 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
addFolderTable()
query.sortAndPagination += " ORDER BY COALESCE(scenes.title, files.basename) COLLATE NATURAL_CI " + direction + ", folders.path COLLATE NATURAL_CI " + direction
case "play_count":
// handle here since getSort has special handling for _count suffix
query.sortAndPagination += " ORDER BY scenes.play_count " + direction
query.sortAndPagination += getCountSort(sceneTable, scenesViewDatesTable, sceneIDColumn, direction)
case "last_played_at":
query.sortAndPagination += fmt.Sprintf(" ORDER BY (SELECT MAX(view_date) FROM %s AS sort WHERE sort.%s = %s.id) %s", scenesViewDatesTable, sceneIDColumn, sceneTable, getSortDirection(direction))
case "o_counter":
query.sortAndPagination += getCountSort(sceneTable, scenesODatesTable, sceneIDColumn, direction)
default:
query.sortAndPagination += getSort(sort, direction, "scenes")
}
@@ -1610,23 +1606,6 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
query.sortAndPagination += ", COALESCE(scenes.title, scenes.id) COLLATE NATURAL_CI ASC"
}
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
@@ -1651,21 +1630,6 @@ func (qb *SceneStore) SaveActivity(ctx context.Context, id int, resumeTime *floa
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) GetURLs(ctx context.Context, sceneID int) ([]string, error) {
return scenesURLsTableMgr.get(ctx, sceneID)
}

View File

@@ -82,10 +82,7 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
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)
@@ -117,7 +114,6 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
Date: &date,
Rating: &rating,
Organized: true,
OCounter: ocounter,
StudioID: &studioIDs[studioIdxWithScene],
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -144,9 +140,7 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
Endpoint: endpoint2,
},
}),
LastPlayedAt: &lastPlayedAt,
ResumeTime: float64(resumeTime),
PlayCount: playCount,
PlayDuration: playDuration,
},
false,
@@ -162,7 +156,6 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
Date: &date,
Rating: &rating,
Organized: true,
OCounter: ocounter,
StudioID: &studioIDs[studioIdxWithScene],
Files: models.NewRelatedVideoFiles([]*models.VideoFile{
videoFile.(*models.VideoFile),
@@ -192,9 +185,7 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
Endpoint: endpoint2,
},
}),
LastPlayedAt: &lastPlayedAt,
ResumeTime: resumeTime,
PlayCount: playCount,
PlayDuration: playDuration,
},
false,
@@ -321,10 +312,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
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)
@@ -355,7 +343,6 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Date: &date,
Rating: &rating,
Organized: true,
OCounter: ocounter,
StudioID: &studioIDs[studioIdxWithScene],
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -382,9 +369,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Endpoint: endpoint2,
},
}),
LastPlayedAt: &lastPlayedAt,
ResumeTime: resumeTime,
PlayCount: playCount,
PlayDuration: playDuration,
},
false,
@@ -537,10 +522,7 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
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)
@@ -576,7 +558,6 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
Date: models.NewOptionalDate(date),
Rating: models.NewOptionalInt(rating),
Organized: models.NewOptionalBool(true),
OCounter: models.NewOptionalInt(ocounter),
StudioID: models.NewOptionalInt(studioIDs[studioIdxWithScene]),
CreatedAt: models.NewOptionalTime(createdAt),
UpdatedAt: models.NewOptionalTime(updatedAt),
@@ -618,9 +599,7 @@ 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{
@@ -636,7 +615,6 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
Date: &date,
Rating: &rating,
Organized: true,
OCounter: ocounter,
StudioID: &studioIDs[studioIdxWithScene],
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -663,9 +641,7 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
Endpoint: endpoint2,
},
}),
LastPlayedAt: &lastPlayedAt,
ResumeTime: resumeTime,
PlayCount: playCount,
PlayDuration: playDuration,
},
false,
@@ -675,8 +651,7 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
sceneIDs[sceneIdxWithSpacedName],
clearScenePartial(),
models.Scene{
ID: sceneIDs[sceneIdxWithSpacedName],
OCounter: getOCounter(sceneIdxWithSpacedName),
ID: sceneIDs[sceneIdxWithSpacedName],
Files: models.NewRelatedVideoFiles([]*models.VideoFile{
makeSceneFile(sceneIdxWithSpacedName),
}),
@@ -685,9 +660,7 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
PerformerIDs: models.NewRelatedIDs([]int{}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
PlayCount: getScenePlayCount(sceneIdxWithSpacedName),
PlayDuration: getScenePlayDuration(sceneIdxWithSpacedName),
LastPlayedAt: getSceneLastPlayed(sceneIdxWithSpacedName),
ResumeTime: getSceneResumeTime(sceneIdxWithSpacedName),
},
false,
@@ -1296,7 +1269,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
}
}
func Test_sceneQueryBuilder_IncrementOCounter(t *testing.T) {
func Test_sceneQueryBuilder_AddO(t *testing.T) {
tests := []struct {
name string
id int
@@ -1306,52 +1279,9 @@ func Test_sceneQueryBuilder_IncrementOCounter(t *testing.T) {
{
"increment",
sceneIDs[1],
2,
false,
},
{
"invalid",
invalidID,
0,
true,
},
}
qb := db.Scene
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
got, err := qb.IncrementOCounter(ctx, tt.id)
if (err != nil) != tt.wantErr {
t.Errorf("sceneQueryBuilder.IncrementOCounter() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("sceneQueryBuilder.IncrementOCounter() = %v, want %v", got, tt.want)
}
})
}
}
func Test_sceneQueryBuilder_DecrementOCounter(t *testing.T) {
tests := []struct {
name string
id int
want int
wantErr bool
}{
{
"decrement",
sceneIDs[2],
1,
false,
},
{
"zero",
sceneIDs[0],
0,
false,
},
{
"invalid",
invalidID,
@@ -1364,19 +1294,19 @@ func Test_sceneQueryBuilder_DecrementOCounter(t *testing.T) {
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
got, err := qb.DecrementOCounter(ctx, tt.id)
got, err := qb.AddO(ctx, tt.id, nil)
if (err != nil) != tt.wantErr {
t.Errorf("sceneQueryBuilder.DecrementOCounter() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("sceneQueryBuilder.AddO() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("sceneQueryBuilder.DecrementOCounter() = %v, want %v", got, tt.want)
if len(got) != tt.want {
t.Errorf("sceneQueryBuilder.AddO() = %v, want %v", got, tt.want)
}
})
}
}
func Test_sceneQueryBuilder_ResetOCounter(t *testing.T) {
func Test_sceneQueryBuilder_DeleteO(t *testing.T) {
tests := []struct {
name string
id int
@@ -1395,11 +1325,42 @@ func Test_sceneQueryBuilder_ResetOCounter(t *testing.T) {
0,
false,
},
}
qb := db.Scene
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
got, err := qb.DeleteO(ctx, tt.id, nil)
if (err != nil) != tt.wantErr {
t.Errorf("sceneQueryBuilder.DeleteO() error = %v, wantErr %v", err, tt.wantErr)
return
}
if len(got) != tt.want {
t.Errorf("sceneQueryBuilder.DeleteO() = %v, want %v", got, tt.want)
}
})
}
}
func Test_sceneQueryBuilder_ResetO(t *testing.T) {
tests := []struct {
name string
id int
want int
wantErr bool
}{
{
"invalid",
invalidID,
"decrement",
sceneIDs[2],
0,
true,
false,
},
{
"zero",
sceneIDs[0],
0,
false,
},
}
@@ -1407,9 +1368,9 @@ func Test_sceneQueryBuilder_ResetOCounter(t *testing.T) {
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
got, err := qb.ResetOCounter(ctx, tt.id)
got, err := qb.ResetO(ctx, tt.id)
if (err != nil) != tt.wantErr {
t.Errorf("sceneQueryBuilder.ResetOCounter() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("sceneQueryBuilder.ResetO() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
@@ -1419,6 +1380,10 @@ func Test_sceneQueryBuilder_ResetOCounter(t *testing.T) {
}
}
func Test_sceneQueryBuilder_ResetWatchCount(t *testing.T) {
return
}
func Test_sceneQueryBuilder_Destroy(t *testing.T) {
tests := []struct {
name string
@@ -2158,19 +2123,19 @@ func TestSceneQuery(t *testing.T) {
[]int{sceneIdxWithMovie},
false,
},
{
"specific play count",
nil,
&models.SceneFilterType{
PlayCount: &models.IntCriterionInput{
Modifier: models.CriterionModifierEquals,
Value: getScenePlayCount(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,
@@ -2767,7 +2732,11 @@ func verifyScenesOCounter(t *testing.T, oCounterCriterion models.IntCriterionInp
scenes := queryScene(ctx, t, sqb, &sceneFilter, nil)
for _, scene := range scenes {
verifyInt(t, scene.OCounter, oCounterCriterion)
count, err := sqb.GetOCount(ctx, scene.ID)
if err != nil {
t.Errorf("Error getting ocounter: %v", err)
}
verifyInt(t, count, oCounterCriterion)
}
return nil
@@ -4023,14 +3992,14 @@ func TestSceneQuerySorting(t *testing.T) {
"play_count",
"play_count",
models.SortDirectionEnumDesc,
sceneIDs[sceneIdx1WithPerformer],
-1,
-1,
},
{
"last_played_at",
"last_played_at",
models.SortDirectionEnumDesc,
sceneIDs[sceneIdx1WithPerformer],
-1,
-1,
},
{
@@ -4551,7 +4520,7 @@ func TestSceneStore_AssignFiles(t *testing.T) {
}
}
func TestSceneStore_IncrementWatchCount(t *testing.T) {
func TestSceneStore_AddView(t *testing.T) {
tests := []struct {
name string
sceneID int
@@ -4561,7 +4530,7 @@ func TestSceneStore_IncrementWatchCount(t *testing.T) {
{
"valid",
sceneIDs[sceneIdx1WithPerformer],
getScenePlayCount(sceneIdx1WithPerformer) + 1,
1, //getScenePlayCount(sceneIdx1WithPerformer) + 1,
false,
},
{
@@ -4577,9 +4546,9 @@ func TestSceneStore_IncrementWatchCount(t *testing.T) {
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)
views, err := qb.AddViews(ctx, tt.sceneID, nil)
if (err != nil) != tt.wantErr {
t.Errorf("SceneStore.IncrementWatchCount() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("SceneStore.AddView() error = %v, wantErr %v", err, tt.wantErr)
}
if err != nil {
@@ -4587,16 +4556,21 @@ func TestSceneStore_IncrementWatchCount(t *testing.T) {
}
assert := assert.New(t)
assert.Equal(tt.expectedCount, newVal)
assert.Equal(tt.expectedCount, len(views))
// find the scene and check the count
scene, err := qb.Find(ctx, tt.sceneID)
count, err := qb.CountViews(ctx, tt.sceneID)
if err != nil {
t.Errorf("SceneStore.Find() error = %v", err)
t.Errorf("SceneStore.CountViews() error = %v", err)
}
assert.Equal(tt.expectedCount, scene.PlayCount)
assert.True(scene.LastPlayedAt.After(time.Now().Add(-1 * time.Minute)))
lastView, err := qb.LastView(ctx, tt.sceneID)
if err != nil {
t.Errorf("SceneStore.LastView() error = %v", err)
}
assert.Equal(tt.expectedCount, count)
assert.True(lastView.After(time.Now().Add(-1 * time.Minute)))
return nil
})
@@ -4604,6 +4578,10 @@ func TestSceneStore_IncrementWatchCount(t *testing.T) {
}
}
func TestSceneStore_DecrementWatchCount(t *testing.T) {
return
}
func TestSceneStore_SaveActivity(t *testing.T) {
var (
resumeTime = 111.2
@@ -4702,3 +4680,77 @@ func TestSceneStore_SaveActivity(t *testing.T) {
// TODO Count
// TODO SizeCount
// TODO - this should be in history_test and generalised
func TestSceneStore_CountAllViews(t *testing.T) {
withRollbackTxn(func(ctx context.Context) error {
qb := db.Scene
sceneID := sceneIDs[sceneIdx1WithPerformer]
// get the current play count
currentCount, err := qb.CountAllViews(ctx)
if err != nil {
t.Errorf("SceneStore.CountAllViews() error = %v", err)
return nil
}
// add a view
_, err = qb.AddViews(ctx, sceneID, nil)
if err != nil {
t.Errorf("SceneStore.AddViews() error = %v", err)
return nil
}
// get the new play count
newCount, err := qb.CountAllViews(ctx)
if err != nil {
t.Errorf("SceneStore.CountAllViews() error = %v", err)
return nil
}
assert.Equal(t, currentCount+1, newCount)
return nil
})
}
func TestSceneStore_CountUniqueViews(t *testing.T) {
withRollbackTxn(func(ctx context.Context) error {
qb := db.Scene
sceneID := sceneIDs[sceneIdx1WithPerformer]
// get the current play count
currentCount, err := qb.CountUniqueViews(ctx)
if err != nil {
t.Errorf("SceneStore.CountUniqueViews() error = %v", err)
return nil
}
// add a view
_, err = qb.AddViews(ctx, sceneID, nil)
if err != nil {
t.Errorf("SceneStore.AddViews() error = %v", err)
return nil
}
// add a second view
_, err = qb.AddViews(ctx, sceneID, nil)
if err != nil {
t.Errorf("SceneStore.AddViews() error = %v", err)
return nil
}
// get the new play count
newCount, err := qb.CountUniqueViews(ctx)
if err != nil {
t.Errorf("SceneStore.CountUniqueViews() error = %v", err)
return nil
}
assert.Equal(t, currentCount+1, newCount)
return nil
})
}

View File

@@ -1012,10 +1012,6 @@ func makeSceneFile(i int) *models.VideoFile {
}
}
func getScenePlayCount(index int) int {
return index % 5
}
func getScenePlayDuration(index int) float64 {
if index%5 == 0 {
return 0
@@ -1032,15 +1028,6 @@ func getSceneResumeTime(index int) float64 {
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")
@@ -1073,7 +1060,6 @@ func makeScene(i int) *models.Scene {
getSceneEmptyString(i, urlField),
}),
Rating: getIntPtr(rating),
OCounter: getOCounter(i),
Date: getObjectDate(i),
StudioID: studioID,
GalleryIDs: models.NewRelatedIDs(gids),
@@ -1083,9 +1069,7 @@ 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),
}
}

View File

@@ -110,27 +110,6 @@ func getCountSort(primaryTable, joinTable, primaryFK, direction string) string {
return fmt.Sprintf(" ORDER BY (SELECT COUNT(*) FROM %s AS sort WHERE sort.%s = %s.id) %s", joinTable, primaryFK, primaryTable, getSortDirection(direction))
}
func getMultiSumSort(sum string, primaryTable, foreignTable1, joinTable1, foreignTable2, joinTable2, primaryFK, foreignFK1, foreignFK2, direction string) string {
return fmt.Sprintf(" ORDER BY (SELECT SUM(%s) "+
"FROM ("+
"SELECT SUM(%s) as %s from %s s "+
"LEFT JOIN %s ON %s.id = s.%s "+
"WHERE s.%s = %s.id "+
"UNION ALL "+
"SELECT SUM(%s) as %s from %s s "+
"LEFT JOIN %s ON %s.id = s.%s "+
"WHERE s.%s = %s.id "+
")) %s",
sum,
sum, sum, joinTable1,
foreignTable1, foreignTable1, foreignFK1,
primaryFK, primaryTable,
sum, sum, joinTable2,
foreignTable2, foreignTable2, foreignFK2,
primaryFK, primaryTable,
getSortDirection(direction))
}
func getStringSearchClause(columns []string, q string, not bool) sqlClause {
var likeClauses []string
var args []interface{}
@@ -349,28 +328,6 @@ func getCountCriterionClause(primaryTable, joinTable, primaryFK string, criterio
return getIntCriterionWhereClause(lhs, criterion)
}
func getJoinedMultiSumCriterionClause(primaryTable, foreignTable1, joinTable1, foreignTable2, joinTable2, primaryFK string, foreignFK1 string, foreignFK2 string, sum string, criterion models.IntCriterionInput) (string, []interface{}) {
lhs := fmt.Sprintf("(SELECT SUM(%s) "+
"FROM ("+
"SELECT SUM(%s) as %s from %s s "+
"LEFT JOIN %s ON %s.id = s.%s "+
"WHERE s.%s = %s.id "+
"UNION ALL "+
"SELECT SUM(%s) as %s from %s s "+
"LEFT JOIN %s ON %s.id = s.%s "+
"WHERE s.%s = %s.id "+
"))",
sum,
sum, sum, joinTable1,
foreignTable1, foreignTable1, foreignFK1,
primaryFK, primaryTable,
sum, sum, joinTable2,
foreignTable2, foreignTable2, foreignFK2,
primaryFK, primaryTable,
)
return getIntCriterionWhereClause(lhs, criterion)
}
func coalesce(column string) string {
return fmt.Sprintf("COALESCE(%s, '')", column)
}

View File

@@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"fmt"
"time"
"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
@@ -773,6 +774,270 @@ func (t *relatedFilesTable) setPrimary(ctx context.Context, id int, fileID model
return nil
}
type viewHistoryTable struct {
table
dateColumn exp.IdentifierExpression
}
func (t *viewHistoryTable) getDates(ctx context.Context, id int) ([]time.Time, error) {
table := t.table.table
q := dialect.Select(
t.dateColumn,
).From(table).Where(
t.idColumn.Eq(id),
).Order(t.dateColumn.Desc())
const single = false
var ret []time.Time
if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error {
var date Timestamp
if err := rows.Scan(&date); err != nil {
return err
}
ret = append(ret, date.Timestamp)
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (t *viewHistoryTable) getManyDates(ctx context.Context, ids []int) ([][]time.Time, error) {
table := t.table.table
q := dialect.Select(
t.idColumn,
t.dateColumn,
).From(table).Where(
t.idColumn.In(ids),
).Order(t.dateColumn.Desc())
ret := make([][]time.Time, len(ids))
idToIndex := idToIndexMap(ids)
const single = false
if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error {
var id int
var date Timestamp
if err := rows.Scan(&id, &date); err != nil {
return err
}
idx := idToIndex[id]
ret[idx] = append(ret[idx], date.Timestamp)
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (t *viewHistoryTable) getLastDate(ctx context.Context, id int) (*time.Time, error) {
table := t.table.table
q := dialect.Select(t.dateColumn).From(table).Where(
t.idColumn.Eq(id),
).Order(t.dateColumn.Desc()).Limit(1)
var date NullTimestamp
if err := querySimple(ctx, q, &date); err != nil {
return nil, err
}
return date.TimePtr(), nil
}
func (t *viewHistoryTable) getManyLastDate(ctx context.Context, ids []int) ([]*time.Time, error) {
table := t.table.table
q := dialect.Select(
t.idColumn,
goqu.MAX(t.dateColumn),
).From(table).Where(
t.idColumn.In(ids),
).GroupBy(t.idColumn)
ret := make([]*time.Time, len(ids))
idToIndex := idToIndexMap(ids)
const single = false
if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error {
var id int
// MAX appears to return a string, so handle it manually
var dateString string
if err := rows.Scan(&id, &dateString); err != nil {
return err
}
t, err := time.Parse(TimestampFormat, dateString)
if err != nil {
return fmt.Errorf("parsing date %v: %w", dateString, err)
}
idx := idToIndex[id]
ret[idx] = &t
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (t *viewHistoryTable) getCount(ctx context.Context, id int) (int, error) {
table := t.table.table
q := dialect.Select(goqu.COUNT("*")).From(table).Where(t.idColumn.Eq(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 (t *viewHistoryTable) getManyCount(ctx context.Context, ids []int) ([]int, error) {
table := t.table.table
q := dialect.Select(
t.idColumn,
goqu.COUNT(t.dateColumn),
).From(table).Where(
t.idColumn.In(ids),
).GroupBy(t.idColumn)
ret := make([]int, len(ids))
idToIndex := idToIndexMap(ids)
const single = false
if err := queryFunc(ctx, q, single, func(rows *sqlx.Rows) error {
var id int
var count int
if err := rows.Scan(&id, &count); err != nil {
return err
}
idx := idToIndex[id]
ret[idx] = count
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (t *viewHistoryTable) getAllCount(ctx context.Context) (int, error) {
table := t.table.table
q := dialect.Select(goqu.COUNT("*")).From(table)
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 (t *viewHistoryTable) getUniqueCount(ctx context.Context) (int, error) {
table := t.table.table
q := dialect.Select(goqu.COUNT(goqu.DISTINCT(t.idColumn))).From(table)
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 (t *viewHistoryTable) addDates(ctx context.Context, id int, dates []time.Time) ([]time.Time, error) {
table := t.table.table
if len(dates) == 0 {
dates = []time.Time{time.Now()}
}
for _, d := range dates {
q := dialect.Insert(table).Cols(t.idColumn.GetCol(), t.dateColumn.GetCol()).Vals(
// convert all dates to UTC
goqu.Vals{id, UTCTimestamp{Timestamp{d}}},
)
if _, err := exec(ctx, q); err != nil {
return nil, fmt.Errorf("inserting into %s: %w", table.GetTable(), err)
}
}
return t.getDates(ctx, id)
}
func (t *viewHistoryTable) deleteDates(ctx context.Context, id int, dates []time.Time) ([]time.Time, error) {
table := t.table.table
mostRecent := false
if len(dates) == 0 {
mostRecent = true
dates = []time.Time{time.Now()}
}
for _, date := range dates {
var subquery *goqu.SelectDataset
if mostRecent {
// delete the most recent
subquery = dialect.Select("rowid").From(table).Where(
t.idColumn.Eq(id),
).Order(t.dateColumn.Desc()).Limit(1)
} else {
subquery = dialect.Select("rowid").From(table).Where(
t.idColumn.Eq(id),
t.dateColumn.Eq(UTCTimestamp{Timestamp{date}}),
).Limit(1)
}
q := dialect.Delete(table).Where(goqu.I("rowid").Eq(subquery))
if _, err := exec(ctx, q); err != nil {
return nil, fmt.Errorf("deleting from %s: %w", table.GetTable(), err)
}
}
return t.getDates(ctx, id)
}
func (t *viewHistoryTable) deleteAllDates(ctx context.Context, id int) (int, error) {
table := t.table.table
q := dialect.Delete(table).Where(t.idColumn.Eq(id))
if _, err := exec(ctx, q); err != nil {
return 0, fmt.Errorf("resetting dates for id %v: %w", id, err)
}
return t.getCount(ctx, id)
}
type sqler interface {
ToSQL() (sql string, params []interface{}, err error)
}

View File

@@ -190,6 +190,22 @@ var (
},
valueColumn: scenesURLsJoinTable.Col(sceneURLColumn),
}
scenesViewTableMgr = &viewHistoryTable{
table: table{
table: goqu.T(scenesViewDatesTable),
idColumn: goqu.T(scenesViewDatesTable).Col(sceneIDColumn),
},
dateColumn: goqu.T(scenesViewDatesTable).Col(sceneViewDateColumn),
}
scenesOTableMgr = &viewHistoryTable{
table: table{
table: goqu.T(scenesODatesTable),
idColumn: goqu.T(scenesODatesTable).Col(sceneIDColumn),
},
dateColumn: goqu.T(scenesODatesTable).Col(sceneODateColumn),
}
)
var (

View File

@@ -5,6 +5,8 @@ import (
"time"
)
const TimestampFormat = time.RFC3339
// Timestamp represents a time stored in RFC3339 format.
type Timestamp struct {
Timestamp time.Time
@@ -18,7 +20,18 @@ func (t *Timestamp) Scan(value interface{}) error {
// Value implements the driver Valuer interface.
func (t Timestamp) Value() (driver.Value, error) {
return t.Timestamp.Format(time.RFC3339), nil
return t.Timestamp.Format(TimestampFormat), nil
}
// UTCTimestamp stores a time in UTC.
// TODO - Timestamp should use UTC by default
type UTCTimestamp struct {
Timestamp
}
// Value implements the driver Valuer interface.
func (t UTCTimestamp) Value() (driver.Value, error) {
return t.Timestamp.Timestamp.UTC().Format(TimestampFormat), nil
}
// NullTimestamp represents a nullable time stored in RFC3339 format.
@@ -47,7 +60,7 @@ func (t NullTimestamp) Value() (driver.Value, error) {
return nil, nil
}
return t.Timestamp.Format(time.RFC3339), nil
return t.Timestamp.Format(TimestampFormat), nil
}
func (t NullTimestamp) TimePtr() *time.Time {