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

@@ -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 {