mirror of
https://github.com/stashapp/stash-box.git
synced 2026-02-07 03:09:55 -06:00
977 lines
28 KiB
Go
977 lines
28 KiB
Go
package sqlx
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stashapp/stash-box/pkg/edit"
|
|
"github.com/stashapp/stash-box/pkg/manager/config"
|
|
"github.com/stashapp/stash-box/pkg/models"
|
|
"github.com/stashapp/stash-box/pkg/utils"
|
|
)
|
|
|
|
const (
|
|
sceneTable = "scenes"
|
|
sceneJoinKey = "scene_id"
|
|
)
|
|
|
|
var (
|
|
sceneDBTable = newTable(sceneTable, func() interface{} {
|
|
return &models.Scene{}
|
|
})
|
|
|
|
sceneFingerprintTable = newTableJoin(sceneTable, "scene_fingerprints", sceneJoinKey, func() interface{} {
|
|
return &models.SceneFingerprint{}
|
|
})
|
|
|
|
sceneURLTable = newTableJoin(sceneTable, "scene_urls", sceneJoinKey, func() interface{} {
|
|
return &models.SceneURL{}
|
|
})
|
|
|
|
sceneRedirectTable = newTableJoin(sceneTable, "scene_redirects", "source_id", func() interface{} {
|
|
return &models.Redirect{}
|
|
})
|
|
)
|
|
|
|
type sceneQueryBuilder struct {
|
|
dbi *dbi
|
|
}
|
|
|
|
func newSceneQueryBuilder(txn *txnState) models.SceneRepo {
|
|
return &sceneQueryBuilder{
|
|
dbi: newDBI(txn),
|
|
}
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) toModel(ro interface{}) *models.Scene {
|
|
if ro != nil {
|
|
return ro.(*models.Scene)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) Create(newScene models.Scene) (*models.Scene, error) {
|
|
ret, err := qb.dbi.Insert(sceneDBTable, newScene)
|
|
return qb.toModel(ret), err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) Update(updatedScene models.Scene) (*models.Scene, error) {
|
|
ret, err := qb.dbi.Update(sceneDBTable, updatedScene, true)
|
|
return qb.toModel(ret), err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) Destroy(id uuid.UUID) error {
|
|
return qb.dbi.Delete(id, sceneDBTable)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) CreateURLs(newJoins models.SceneURLs) error {
|
|
return qb.dbi.InsertJoins(sceneURLTable, &newJoins)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) UpdateURLs(scene uuid.UUID, updatedJoins models.SceneURLs) error {
|
|
return qb.dbi.ReplaceJoins(sceneURLTable, scene, &updatedJoins)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) CreateFingerprints(newJoins models.SceneFingerprints) error {
|
|
conflictHandling := `
|
|
ON CONFLICT ON CONSTRAINT scene_hash_unique
|
|
DO NOTHING
|
|
`
|
|
|
|
return qb.dbi.InsertJoinsWithConflictHandling(sceneFingerprintTable, &newJoins, conflictHandling)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) UpdateFingerprints(sceneID uuid.UUID, updatedJoins models.SceneFingerprints) error {
|
|
return qb.dbi.ReplaceJoins(sceneFingerprintTable, sceneID, &updatedJoins)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) DestroyFingerprints(sceneID uuid.UUID, toDestroy models.SceneFingerprints) error {
|
|
for _, fp := range toDestroy {
|
|
query := qb.dbi.db().Rebind(`DELETE FROM ` + sceneFingerprintTable.name + ` WHERE algorithm = ? AND HASH = ? AND user_id = ?`)
|
|
if _, err := qb.dbi.db().Exec(query, fp.Algorithm, fp.Hash, fp.UserID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) Find(id uuid.UUID) (*models.Scene, error) {
|
|
ret, err := qb.dbi.Find(id, sceneDBTable)
|
|
return qb.toModel(ret), err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) FindByFingerprint(algorithm models.FingerprintAlgorithm, hash string) ([]*models.Scene, error) {
|
|
query := `
|
|
SELECT scenes.* FROM scenes
|
|
LEFT JOIN scene_fingerprints as scenes_join on scenes_join.scene_id = scenes.id
|
|
WHERE scenes_join.algorithm = ? AND scenes_join.hash = ?`
|
|
var args []interface{}
|
|
args = append(args, algorithm.String())
|
|
args = append(args, hash)
|
|
return qb.queryScenes(query, args)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) FindByFingerprints(fingerprints []string) ([]*models.Scene, error) {
|
|
query := `
|
|
SELECT scenes.* FROM scenes
|
|
WHERE id IN (
|
|
SELECT scene_id id FROM scene_fingerprints
|
|
WHERE hash IN (?)
|
|
GROUP BY id
|
|
)`
|
|
query, args, err := sqlx.In(query, fingerprints)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return qb.queryScenes(query, args)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) FindByFullFingerprints(fingerprints []*models.FingerprintQueryInput) ([]*models.Scene, error) {
|
|
hashClause := `
|
|
SELECT scene_id id FROM scene_fingerprints
|
|
WHERE hash IN (:hashes)
|
|
GROUP BY id
|
|
`
|
|
phashClause := `
|
|
SELECT scene_id as id
|
|
FROM UNNEST(ARRAY[:phashes]) phash
|
|
JOIN scene_fingerprints ON ('x' || hash)::::bit(64)::::bigint <@ (phash::::BIGINT, :distance)
|
|
AND algorithm = 'PHASH'
|
|
`
|
|
|
|
var phashes []int64
|
|
var hashes []string
|
|
for _, fp := range fingerprints {
|
|
if fp.Algorithm == models.FingerprintAlgorithmPhash {
|
|
// Postgres only supports signed integers, so we parse
|
|
// as uint64 and cast to int64 to ensure values are the same.
|
|
value, err := strconv.ParseUint(fp.Hash, 16, 64)
|
|
if err == nil {
|
|
phashes = append(phashes, int64(value))
|
|
}
|
|
} else {
|
|
hashes = append(hashes, fp.Hash)
|
|
}
|
|
}
|
|
|
|
var clauses []string
|
|
if len(phashes) > 0 {
|
|
clauses = append(clauses, phashClause)
|
|
}
|
|
if len(hashes) > 0 {
|
|
clauses = append(clauses, hashClause)
|
|
}
|
|
if len(clauses) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
arg := map[string]interface{}{
|
|
"phashes": phashes,
|
|
"hashes": hashes,
|
|
"distance": config.GetPHashDistance(),
|
|
}
|
|
|
|
query := `
|
|
SELECT scenes.* FROM scenes
|
|
WHERE id IN (` + strings.Join(clauses, " UNION ") + ")"
|
|
query, args, err := sqlx.Named(query, arg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query, args, err = sqlx.In(query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return qb.queryScenes(query, args)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) FindByIds(ids []uuid.UUID) ([]*models.Scene, error) {
|
|
query := `
|
|
SELECT scenes.* FROM scenes
|
|
WHERE id IN (?)
|
|
`
|
|
query, args, _ := sqlx.In(query, ids)
|
|
scenes, err := qb.queryScenes(query, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m := make(map[uuid.UUID]*models.Scene)
|
|
for _, scene := range scenes {
|
|
m[scene.ID] = scene
|
|
}
|
|
|
|
result := make([]*models.Scene, len(ids))
|
|
for i, id := range ids {
|
|
result[i] = m[id]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) FindIdsBySceneFingerprints(fingerprints []*models.FingerprintQueryInput) (map[string][]uuid.UUID, error) {
|
|
hashClause := `
|
|
SELECT scene_id, hash
|
|
FROM scene_fingerprints
|
|
WHERE hash IN (:hashes)
|
|
GROUP BY scene_id, hash
|
|
`
|
|
phashClause := `
|
|
SELECT scene_id, phash as hash
|
|
FROM UNNEST(ARRAY[:phashes]) phash
|
|
JOIN scene_fingerprints ON ('x' || hash)::::bit(64)::::bigint <@ (phash::::BIGINT, :distance)
|
|
AND algorithm = 'PHASH'
|
|
GROUP BY scene_id, phash
|
|
`
|
|
|
|
var phashes []int64
|
|
var hashes []string
|
|
for _, fp := range fingerprints {
|
|
if fp.Algorithm == models.FingerprintAlgorithmPhash {
|
|
// Postgres only supports signed integers, so we parse
|
|
// as uint64 and cast to int64 to ensure values are the same.
|
|
value, err := strconv.ParseUint(fp.Hash, 16, 64)
|
|
if err == nil {
|
|
phashes = append(phashes, int64(value))
|
|
}
|
|
} else {
|
|
hashes = append(hashes, fp.Hash)
|
|
}
|
|
}
|
|
|
|
var clauses []string
|
|
if len(phashes) > 0 {
|
|
clauses = append(clauses, phashClause)
|
|
}
|
|
if len(hashes) > 0 {
|
|
clauses = append(clauses, hashClause)
|
|
}
|
|
if len(clauses) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
arg := map[string]interface{}{
|
|
"phashes": phashes,
|
|
"hashes": hashes,
|
|
"distance": config.GetPHashDistance(),
|
|
}
|
|
|
|
query := strings.Join(clauses, " UNION ")
|
|
query, args, err := sqlx.Named(query, arg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query, args, err = sqlx.In(query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
output := models.SceneFingerprints{}
|
|
if err := qb.dbi.RawQuery(sceneFingerprintTable.table, query, args, &output); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := make(map[string][]uuid.UUID)
|
|
output.Each(func(row interface{}) {
|
|
fp := row.(models.SceneFingerprint)
|
|
res[fp.Hash] = append(res[fp.Hash], fp.SceneID)
|
|
})
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// func (qb *SceneQueryBuilder) FindByStudioID(sceneID int) ([]*models.Scene, error) {
|
|
// query := `
|
|
// SELECT scenes.* FROM scenes
|
|
// LEFT JOIN scenes_scenes as scenes_join on scenes_join.scene_id = scenes.id
|
|
// LEFT JOIN scenes on scenes_join.scene_id = scenes.id
|
|
// WHERE scenes.id = ?
|
|
// GROUP BY scenes.id
|
|
// `
|
|
// args := []interface{}{sceneID}
|
|
// return qb.queryScenes(query, args)
|
|
// }
|
|
|
|
// func (qb *SceneQueryBuilder) FindByChecksum(checksum string) (*models.Scene, error) {
|
|
// query := `SELECT scenes.* FROM scenes
|
|
// left join scene_checksums on scenes.id = scene_checksums.scene_id
|
|
// WHERE scene_checksums.checksum = ?`
|
|
|
|
// var args []interface{}
|
|
// args = append(args, checksum)
|
|
|
|
// results, err := qb.queryScenes(query, args)
|
|
// if err != nil || len(results) < 1 {
|
|
// return nil, err
|
|
// }
|
|
// return results[0], nil
|
|
// }
|
|
|
|
// func (qb *SceneQueryBuilder) FindByChecksums(checksums []string) ([]*models.Scene, error) {
|
|
// query := `SELECT scenes.* FROM scenes
|
|
// left join scene_checksums on scenes.id = scene_checksums.scene_id
|
|
// WHERE scene_checksums.checksum IN ` + getInBinding(len(checksums))
|
|
|
|
// var args []interface{}
|
|
// for _, name := range checksums {
|
|
// args = append(args, name)
|
|
// }
|
|
// return qb.queryScenes(query, args)
|
|
// }
|
|
|
|
func (qb *sceneQueryBuilder) FindByTitle(name string) ([]*models.Scene, error) {
|
|
query := "SELECT * FROM scenes WHERE upper(title) = upper(?)"
|
|
var args []interface{}
|
|
args = append(args, name)
|
|
return qb.queryScenes(query, args)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) Count() (int, error) {
|
|
return runCountQuery(qb.dbi.db(), buildCountQuery("SELECT scenes.id FROM scenes"), nil)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) buildQuery(filter models.SceneQueryInput, isCount bool) (*queryBuilder, error) {
|
|
query := newQueryBuilder(sceneDBTable)
|
|
|
|
if q := filter.Text; q != nil && *q != "" {
|
|
searchColumns := []string{"scenes.title", "scenes.details"}
|
|
clause, thisArgs := getSearchBinding(searchColumns, *q, false, false)
|
|
query.AddWhere(clause)
|
|
query.AddArg(thisArgs...)
|
|
}
|
|
|
|
if q := filter.Title; q != nil && *q != "" {
|
|
searchColumns := []string{"scenes.title"}
|
|
clause, thisArgs := getSearchBinding(searchColumns, *q, false, false)
|
|
query.AddWhere(clause)
|
|
query.AddArg(thisArgs...)
|
|
}
|
|
|
|
if q := filter.URL; q != nil && *q != "" {
|
|
searchColumns := []string{"scenes.url"}
|
|
clause, thisArgs := getSearchBinding(searchColumns, *q, false, true)
|
|
query.AddWhere(clause)
|
|
query.AddArg(thisArgs...)
|
|
}
|
|
|
|
if q := filter.Studios; q != nil && len(q.Value) > 0 {
|
|
column := "scenes.studio_id"
|
|
|
|
switch q.Modifier {
|
|
case models.CriterionModifierEquals:
|
|
query.Eq(column, q.Value[0])
|
|
case models.CriterionModifierNotEquals:
|
|
query.NotEq(column, q.Value[0])
|
|
case models.CriterionModifierIsNull:
|
|
query.IsNull(column)
|
|
case models.CriterionModifierNotNull:
|
|
query.IsNotNull(column)
|
|
case models.CriterionModifierIncludes:
|
|
query.AddWhere(column + " IN " + getInBinding(len(q.Value)))
|
|
for _, studioID := range q.Value {
|
|
query.AddArg(studioID)
|
|
}
|
|
case models.CriterionModifierExcludes:
|
|
query.AddWhere(column + " NOT IN " + getInBinding(len(q.Value)))
|
|
for _, studioID := range q.Value {
|
|
query.AddArg(studioID)
|
|
}
|
|
case models.CriterionModifierGreaterThan:
|
|
fallthrough
|
|
case models.CriterionModifierIncludesAll:
|
|
fallthrough
|
|
case models.CriterionModifierLessThan:
|
|
return nil, fmt.Errorf("unsupported modifier %s for scenes.studio_id", q.Modifier)
|
|
}
|
|
}
|
|
|
|
if filter.ParentStudio != nil {
|
|
query.Body += "LEFT JOIN studios ON scenes.studio_id = studios.id"
|
|
query.AddWhere("(studios.parent_studio_id = ? OR studios.id = ?)")
|
|
query.AddArg(*filter.ParentStudio, *filter.ParentStudio)
|
|
}
|
|
|
|
if q := filter.Performers; q != nil && len(q.Value) > 0 {
|
|
query.AddJoin(scenePerformerTable.table, scenePerformerTable.Name()+".scene_id = scenes.id", len(q.Value) > 1)
|
|
whereClause, havingClause, err := getMultiCriterionClause(scenePerformerTable, performerJoinKey, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.AddWhere(whereClause)
|
|
query.AddHaving(havingClause)
|
|
|
|
for _, performerID := range q.Value {
|
|
query.AddArg(performerID)
|
|
}
|
|
}
|
|
|
|
if q := filter.Tags; q != nil && len(q.Value) > 0 {
|
|
query.AddJoin(sceneTagTable.table, sceneTagTable.Name()+".scene_id = scenes.id", len(q.Value) > 1)
|
|
whereClause, havingClause, err := getMultiCriterionClause(sceneTagTable, tagJoinKey, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.AddWhere(whereClause)
|
|
query.AddHaving(havingClause)
|
|
|
|
for _, tagID := range q.Value {
|
|
query.AddArg(tagID)
|
|
}
|
|
}
|
|
|
|
if q := filter.Fingerprints; q != nil && len(q.Value) > 0 {
|
|
query.AddJoin(sceneFingerprintTable.table, sceneFingerprintTable.Name()+".scene_id = scenes.id", true)
|
|
whereClause, havingClause, err := getMultiCriterionClause(sceneFingerprintTable, "hash", q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
query.AddWhere(whereClause)
|
|
query.AddHaving(havingClause)
|
|
|
|
for _, fingerprint := range q.Value {
|
|
query.AddArg(fingerprint)
|
|
}
|
|
}
|
|
|
|
if filter.Sort == models.SceneSortEnumTrending {
|
|
limit := ""
|
|
if len(query.whereClauses) == 0 && !isCount {
|
|
// If no other filters are applied we can optimize query
|
|
// by sorting and limiting fingerprint count directly
|
|
limit = "ORDER BY count DESC " + getPagination(filter.Page, filter.PerPage)
|
|
} else {
|
|
query.Pagination = getPagination(filter.Page, filter.PerPage)
|
|
}
|
|
|
|
query.Body += `
|
|
JOIN (
|
|
SELECT scene_id, COUNT(*) AS count
|
|
FROM scene_fingerprints
|
|
WHERE created_at >= (now()::DATE - 7)
|
|
GROUP BY scene_id
|
|
` + limit + `
|
|
) T ON scenes.id = T.scene_id
|
|
`
|
|
query.Sort = " ORDER BY T.count DESC, T.scene_id DESC "
|
|
} else {
|
|
query.Sort = qb.getSceneSort(filter)
|
|
query.Pagination = getPagination(filter.Page, filter.PerPage)
|
|
}
|
|
|
|
query.Eq("scenes.deleted", false)
|
|
|
|
return query, nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) QueryScenes(filter models.SceneQueryInput) ([]*models.Scene, error) {
|
|
query, err := qb.buildQuery(filter, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var scenes models.Scenes
|
|
err = qb.dbi.QueryOnly(*query, &scenes)
|
|
|
|
return scenes, err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) QueryCount(filter models.SceneQueryInput) (int, error) {
|
|
query, err := qb.buildQuery(filter, true)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return qb.dbi.CountOnly(*query)
|
|
}
|
|
|
|
func getMultiCriterionClause(joinTable tableJoin, joinTableField string, criterion models.MultiCriterionInput) (string, string, error) {
|
|
joinTableName := joinTable.Name()
|
|
whereClause := ""
|
|
havingClause := ""
|
|
|
|
switch criterion.GetModifier() {
|
|
case models.CriterionModifierIncludes:
|
|
// includes any of the provided ids
|
|
whereClause = joinTableName + "." + joinTableField + " IN " + getInBinding(criterion.Count())
|
|
case models.CriterionModifierIncludesAll:
|
|
// includes all of the provided ids
|
|
whereClause = joinTableName + "." + joinTableField + " IN " + getInBinding(criterion.Count())
|
|
havingClause = "count(distinct " + joinTableName + "." + joinTableField + ") = " + strconv.Itoa(criterion.Count())
|
|
case models.CriterionModifierExcludes:
|
|
// excludes all of the provided ids
|
|
whereClause = "not exists (select " + joinTableName + ".scene_id from " + joinTableName + " where " + joinTableName + ".scene_id = scenes.id and " + joinTableName + "." + joinTableField + " in " + getInBinding(criterion.Count()) + ")"
|
|
default:
|
|
return "", "", fmt.Errorf("unsupported modifier %s for %s.%s", criterion.GetModifier(), joinTableName, joinTableField)
|
|
}
|
|
|
|
return whereClause, havingClause, nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) getSceneSort(filter models.SceneQueryInput) string {
|
|
secondary := "title"
|
|
if filter.Sort != models.SceneSortEnumTitle {
|
|
secondary = "id"
|
|
}
|
|
return getSort(filter.Sort.String(), filter.Direction.String(), "scenes", &secondary)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) queryScenes(query string, args []interface{}) (models.Scenes, error) {
|
|
output := models.Scenes{}
|
|
err := qb.dbi.RawQuery(sceneDBTable, query, args, &output)
|
|
return output, err
|
|
}
|
|
|
|
type sceneFingerprintGroup struct {
|
|
SceneID uuid.UUID `db:"scene_id"`
|
|
Hash string `db:"hash"`
|
|
Algorithm models.FingerprintAlgorithm `db:"algorithm"`
|
|
Duration float64 `db:"duration"`
|
|
Submissions int `db:"submissions"`
|
|
UserSubmitted bool `db:"user_submitted"`
|
|
CreatedAt time.Time `db:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at"`
|
|
}
|
|
|
|
func fingerprintGroupToFingerprint(fpg sceneFingerprintGroup) *models.Fingerprint {
|
|
return &models.Fingerprint{
|
|
Hash: fpg.Hash,
|
|
Algorithm: fpg.Algorithm,
|
|
Duration: int(fpg.Duration),
|
|
Submissions: fpg.Submissions,
|
|
UserSubmitted: fpg.UserSubmitted,
|
|
Created: fpg.CreatedAt,
|
|
Updated: fpg.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetFingerprints(id uuid.UUID) (models.SceneFingerprints, error) {
|
|
joins := models.SceneFingerprints{}
|
|
err := qb.dbi.FindJoins(sceneFingerprintTable, id, &joins)
|
|
return joins, err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetAllFingerprints(currentUserID uuid.UUID, ids []uuid.UUID) ([][]*models.Fingerprint, []error) {
|
|
query := `
|
|
SELECT
|
|
f.scene_id,
|
|
f.hash,
|
|
f.algorithm,
|
|
mode() WITHIN GROUP (ORDER BY f.duration) as duration,
|
|
COUNT(f.hash) as submissions,
|
|
MIN(created_at) as created_at,
|
|
MAX(created_at) as updated_at,
|
|
bool_or(f.user_id = :userid) as user_submitted
|
|
FROM scene_fingerprints f
|
|
WHERE f.scene_id IN (:sceneids)
|
|
GROUP BY f.scene_id, f.algorithm, f.hash
|
|
ORDER BY submissions DESC`
|
|
|
|
m := make(map[uuid.UUID][]*models.Fingerprint)
|
|
|
|
arg := map[string]interface{}{
|
|
"userid": currentUserID,
|
|
"sceneids": ids,
|
|
}
|
|
query, args, err := sqlx.Named(query, arg)
|
|
if err != nil {
|
|
return nil, utils.DuplicateError(err, len(ids))
|
|
}
|
|
|
|
query, args, err = sqlx.In(query, args...)
|
|
if err != nil {
|
|
return nil, utils.DuplicateError(err, len(ids))
|
|
}
|
|
|
|
if err := qb.dbi.queryFunc(query, args, func(rows *sqlx.Rows) error {
|
|
var fg sceneFingerprintGroup
|
|
|
|
if err := rows.StructScan(&fg); err != nil {
|
|
return err
|
|
}
|
|
|
|
fp := fingerprintGroupToFingerprint(fg)
|
|
|
|
m[fg.SceneID] = append(m[fg.SceneID], fp)
|
|
return nil
|
|
}); err != nil {
|
|
return nil, utils.DuplicateError(err, len(ids))
|
|
}
|
|
|
|
result := make([][]*models.Fingerprint, len(ids))
|
|
for i, id := range ids {
|
|
result[i] = m[id]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetPerformers(id uuid.UUID) (models.PerformersScenes, error) {
|
|
joins := models.PerformersScenes{}
|
|
err := qb.dbi.FindJoins(scenePerformerTable, id, &joins)
|
|
|
|
return joins, err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetAllAppearances(ids []uuid.UUID) ([]models.PerformersScenes, []error) {
|
|
joins := models.PerformersScenes{}
|
|
err := qb.dbi.FindAllJoins(scenePerformerTable, ids, &joins)
|
|
if err != nil {
|
|
return nil, utils.DuplicateError(err, len(ids))
|
|
}
|
|
|
|
m := make(map[uuid.UUID]models.PerformersScenes)
|
|
for _, join := range joins {
|
|
m[join.SceneID] = append(m[join.SceneID], join)
|
|
}
|
|
|
|
result := make([]models.PerformersScenes, len(ids))
|
|
for i, id := range ids {
|
|
result[i] = m[id]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetImages(id uuid.UUID) (models.ScenesImages, error) {
|
|
joins := models.ScenesImages{}
|
|
err := qb.dbi.FindJoins(sceneImageTable, id, &joins)
|
|
|
|
return joins, err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetTags(id uuid.UUID) (models.ScenesTags, error) {
|
|
joins := models.ScenesTags{}
|
|
err := qb.dbi.FindJoins(sceneTagTable, id, &joins)
|
|
|
|
return joins, err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) getSceneURLs(id uuid.UUID) (models.SceneURLs, error) {
|
|
joins := models.SceneURLs{}
|
|
err := qb.dbi.FindJoins(sceneURLTable, id, &joins)
|
|
return joins, err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetURLs(id uuid.UUID) ([]*models.URL, error) {
|
|
joins, err := qb.getSceneURLs(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
urls := make([]*models.URL, len(joins))
|
|
for i, u := range joins {
|
|
url := models.URL{
|
|
URL: u.URL,
|
|
SiteID: u.SiteID,
|
|
}
|
|
urls[i] = &url
|
|
}
|
|
|
|
return urls, nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetAllURLs(ids []uuid.UUID) ([][]*models.URL, []error) {
|
|
joins := models.SceneURLs{}
|
|
err := qb.dbi.FindAllJoins(sceneURLTable, ids, &joins)
|
|
if err != nil {
|
|
return nil, utils.DuplicateError(err, len(ids))
|
|
}
|
|
|
|
m := make(map[uuid.UUID][]*models.URL)
|
|
for _, join := range joins {
|
|
url := models.URL{
|
|
URL: join.URL,
|
|
SiteID: join.SiteID,
|
|
}
|
|
m[join.SceneID] = append(m[join.SceneID], &url)
|
|
}
|
|
|
|
result := make([][]*models.URL, len(ids))
|
|
for i, id := range ids {
|
|
result[i] = m[id]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) SearchScenes(term string, limit int) ([]*models.Scene, error) {
|
|
query := `
|
|
SELECT S.* FROM scenes S
|
|
LEFT JOIN scene_search SS ON SS.scene_id = S.id
|
|
WHERE (
|
|
to_tsvector('simple', COALESCE(scene_date, '')) ||
|
|
to_tsvector('english', studio_name) ||
|
|
to_tsvector('english', COALESCE(performer_names, '')) ||
|
|
to_tsvector('english', scene_title) ||
|
|
to_tsvector('simple', COALESCE(scene_code, ''))
|
|
) @@ plainto_tsquery(?)
|
|
AND S.deleted = FALSE
|
|
LIMIT ?`
|
|
var args []interface{}
|
|
args = append(args, term, limit)
|
|
return qb.queryScenes(query, args)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) CountByPerformer(id uuid.UUID) (int, error) {
|
|
var args []interface{}
|
|
args = append(args, id)
|
|
return runCountQuery(qb.dbi.db(), buildCountQuery("SELECT scene_id FROM scene_performers WHERE performer_id = ?"), args)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) SoftDelete(scene models.Scene) (*models.Scene, error) {
|
|
// Delete joins
|
|
if err := qb.dbi.DeleteJoins(sceneFingerprintTable, scene.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := qb.dbi.DeleteJoins(sceneImageTable, scene.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := qb.dbi.DeleteJoins(sceneURLTable, scene.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := qb.dbi.DeleteJoins(scenePerformerTable, scene.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := qb.dbi.DeleteJoins(sceneTagTable, scene.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret, err := qb.dbi.SoftDelete(sceneDBTable, scene)
|
|
return qb.toModel(ret), err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) CreateRedirect(newJoin models.Redirect) error {
|
|
return qb.dbi.InsertJoin(sceneRedirectTable, newJoin, nil)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) UpdateRedirects(oldTargetID uuid.UUID, newTargetID uuid.UUID) error {
|
|
query := "UPDATE " + sceneRedirectTable.table.Name() + " SET target_id = ? WHERE target_id = ?"
|
|
args := []interface{}{newTargetID, oldTargetID}
|
|
return qb.dbi.RawQuery(sceneRedirectTable.table, query, args, nil)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) UpdateImages(sceneID uuid.UUID, updatedJoins models.ScenesImages) error {
|
|
return qb.dbi.ReplaceJoins(sceneImageTable, sceneID, &updatedJoins)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) UpdateTags(sceneID uuid.UUID, updatedJoins models.ScenesTags) error {
|
|
return qb.dbi.ReplaceJoins(sceneTagTable, sceneID, &updatedJoins)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) UpdatePerformers(sceneID uuid.UUID, updatedJoins models.PerformersScenes) error {
|
|
return qb.dbi.ReplaceJoins(scenePerformerTable, sceneID, &updatedJoins)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) ApplyEdit(scene *models.Scene, create bool, data *models.SceneEditData, userID *uuid.UUID) (*models.Scene, error) {
|
|
old := data.Old
|
|
if old == nil {
|
|
old = &models.SceneEdit{}
|
|
}
|
|
scene.CopyFromSceneEdit(*data.New, old)
|
|
|
|
var updatedScene *models.Scene
|
|
var err error
|
|
if create {
|
|
updatedScene, err = qb.Create(*scene)
|
|
} else {
|
|
updatedScene, err = qb.Update(*scene)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := qb.updateURLsFromEdit(scene, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := qb.updateImagesFromEdit(scene, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := qb.updateTagsFromEdit(scene, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := qb.updatePerformersFromEdit(scene, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if create && len(data.New.AddedFingerprints) > 0 && userID != nil {
|
|
if err := qb.addFingerprintsFromEdit(scene, data, *userID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return updatedScene, err
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetEditURLs(id *uuid.UUID, data *models.SceneEdit) ([]*models.URL, error) {
|
|
var urls []*models.URL
|
|
if id != nil {
|
|
currentURLs, err := qb.GetURLs(*id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
urls = currentURLs
|
|
}
|
|
return edit.MergeURLs(urls, data.AddedUrls, data.RemovedUrls), nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) updateURLsFromEdit(scene *models.Scene, data *models.SceneEditData) error {
|
|
urls, err := qb.GetEditURLs(&scene.ID, data.New)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newURLs := models.CreateSceneURLs(scene.ID, urls)
|
|
return qb.UpdateURLs(scene.ID, newURLs)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetEditImages(id *uuid.UUID, data *models.SceneEdit) ([]uuid.UUID, error) {
|
|
var imageIds []uuid.UUID
|
|
if id != nil {
|
|
currentImages, err := qb.GetImages(*id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, v := range currentImages {
|
|
imageIds = append(imageIds, v.ImageID)
|
|
}
|
|
}
|
|
return utils.ProcessSlice(imageIds, data.AddedImages, data.RemovedImages), nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) updateImagesFromEdit(scene *models.Scene, data *models.SceneEditData) error {
|
|
ids, err := qb.GetEditImages(&scene.ID, data.New)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
images := models.CreateSceneImages(scene.ID, ids)
|
|
return qb.UpdateImages(scene.ID, images)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetEditTags(id *uuid.UUID, data *models.SceneEdit) ([]uuid.UUID, error) {
|
|
var tagIds []uuid.UUID
|
|
if id != nil {
|
|
currentTags, err := qb.GetTags(*id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, tag := range currentTags {
|
|
tagIds = append(tagIds, tag.TagID)
|
|
}
|
|
}
|
|
|
|
return utils.ProcessSlice(tagIds, data.AddedTags, data.RemovedTags), nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) updateTagsFromEdit(scene *models.Scene, data *models.SceneEditData) error {
|
|
tags, err := qb.GetEditTags(&scene.ID, data.New)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newTags := models.CreateSceneTags(scene.ID, tags)
|
|
|
|
return qb.UpdateTags(scene.ID, newTags)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) GetEditPerformers(id *uuid.UUID, obj *models.SceneEdit) ([]*models.PerformerAppearanceInput, error) {
|
|
// Pointers aren't compared by value, so we have to use a temporary struct
|
|
type appearance struct {
|
|
ID uuid.UUID
|
|
As string
|
|
}
|
|
|
|
var appearances []appearance
|
|
if id != nil {
|
|
currentPerformers, err := qb.GetPerformers(*id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, a := range currentPerformers {
|
|
appearances = append(appearances, appearance{
|
|
As: a.As.String,
|
|
ID: a.PerformerID,
|
|
})
|
|
}
|
|
}
|
|
|
|
var added []appearance
|
|
for _, a := range obj.AddedPerformers {
|
|
added = append(added, appearance{
|
|
As: utils.StrPtrToString(a.As),
|
|
ID: a.PerformerID,
|
|
})
|
|
}
|
|
|
|
var removed []appearance
|
|
for _, a := range obj.RemovedPerformers {
|
|
removed = append(removed, appearance{
|
|
As: utils.StrPtrToString(a.As),
|
|
ID: a.PerformerID,
|
|
})
|
|
}
|
|
|
|
appearances = utils.ProcessSlice(appearances, added, removed)
|
|
|
|
var ret []*models.PerformerAppearanceInput
|
|
for i := range appearances {
|
|
ret = append(ret, &models.PerformerAppearanceInput{
|
|
As: utils.StringToStrPtr(appearances[i].As),
|
|
PerformerID: appearances[i].ID,
|
|
})
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) updatePerformersFromEdit(scene *models.Scene, data *models.SceneEditData) error {
|
|
appearances, err := qb.GetEditPerformers(&scene.ID, data.New)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newPerformers := models.CreateScenePerformers(scene.ID, appearances)
|
|
return qb.UpdatePerformers(scene.ID, newPerformers)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) addFingerprintsFromEdit(scene *models.Scene, data *models.SceneEditData, userID uuid.UUID) error {
|
|
var newFingerprints models.SceneFingerprints
|
|
for _, fingerprint := range data.New.AddedFingerprints {
|
|
if fingerprint.Duration > 0 {
|
|
newFingerprints = append(newFingerprints, &models.SceneFingerprint{
|
|
SceneID: scene.ID,
|
|
UserID: userID,
|
|
Hash: fingerprint.Hash,
|
|
Algorithm: fingerprint.Algorithm.String(),
|
|
Duration: fingerprint.Duration,
|
|
CreatedAt: time.Now(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return qb.UpdateFingerprints(scene.ID, newFingerprints)
|
|
}
|
|
|
|
func (qb *sceneQueryBuilder) MergeInto(source *models.Scene, target *models.Scene) error {
|
|
if source.Deleted {
|
|
return fmt.Errorf("merge source scene is deleted: %s", source.ID.String())
|
|
}
|
|
if target.Deleted {
|
|
return fmt.Errorf("merge target scene is deleted: %s", target.ID.String())
|
|
}
|
|
|
|
if _, err := qb.SoftDelete(*source); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := qb.UpdateRedirects(source.ID, target.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
redirect := models.Redirect{SourceID: source.ID, TargetID: target.ID}
|
|
return qb.CreateRedirect(redirect)
|
|
}
|