2019-02-09 12:30:49 +00:00
package models
import (
"database/sql"
"strconv"
"strings"
2019-08-15 07:32:57 +00:00
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
2019-02-09 12:30:49 +00:00
)
const scenesForPerformerQuery = `
SELECT scenes . * FROM scenes
LEFT JOIN performers_scenes as performers_join on performers_join . scene_id = scenes . id
LEFT JOIN performers on performers_join . performer_id = performers . id
WHERE performers . id = ?
GROUP BY scenes . id
`
const scenesForStudioQuery = `
SELECT scenes . * FROM scenes
JOIN studios ON studios . id = scenes . studio_id
WHERE studios . id = ?
GROUP BY scenes . id
`
const scenesForTagQuery = `
SELECT scenes . * FROM scenes
LEFT JOIN scenes_tags as tags_join on tags_join . scene_id = scenes . id
LEFT JOIN tags on tags_join . tag_id = tags . id
WHERE tags . id = ?
GROUP BY scenes . id
`
2019-02-14 22:53:32 +00:00
type SceneQueryBuilder struct { }
2019-02-09 12:30:49 +00:00
2019-02-14 22:53:32 +00:00
func NewSceneQueryBuilder ( ) SceneQueryBuilder {
return SceneQueryBuilder { }
2019-02-09 12:30:49 +00:00
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) Create ( newScene Scene , tx * sqlx . Tx ) ( * Scene , error ) {
2019-02-09 12:30:49 +00:00
ensureTx ( tx )
result , err := tx . NamedExec (
` INSERT INTO scenes ( checksum , path , title , details , url , date , rating , size , duration , video_codec ,
2020-01-06 23:18:06 +00:00
audio_codec , width , height , framerate , bitrate , studio_id , cover ,
created_at , updated_at )
2019-02-09 12:30:49 +00:00
VALUES ( : checksum , : path , : title , : details , : url , : date , : rating , : size , : duration , : video_codec ,
2020-01-06 23:18:06 +00:00
: audio_codec , : width , : height , : framerate , : bitrate , : studio_id , : cover ,
: created_at , : updated_at )
2019-02-09 12:30:49 +00:00
` ,
newScene ,
)
if err != nil {
return nil , err
}
sceneID , err := result . LastInsertId ( )
if err != nil {
return nil , err
}
if err := tx . Get ( & newScene , ` SELECT * FROM scenes WHERE id = ? LIMIT 1 ` , sceneID ) ; err != nil {
return nil , err
}
return & newScene , nil
}
2019-10-14 21:57:53 +00:00
func ( qb * SceneQueryBuilder ) Update ( updatedScene ScenePartial , tx * sqlx . Tx ) ( * Scene , error ) {
2019-02-09 12:30:49 +00:00
ensureTx ( tx )
_ , err := tx . NamedExec (
2019-10-14 21:57:53 +00:00
` UPDATE scenes SET ` + SQLGenKeysPartial ( updatedScene ) + ` WHERE scenes.id = :id ` ,
2019-02-09 12:30:49 +00:00
updatedScene ,
)
if err != nil {
return nil , err
}
2019-10-14 23:54:05 +00:00
return qb . find ( updatedScene . ID , tx )
2019-02-09 12:30:49 +00:00
}
2019-08-15 07:32:57 +00:00
func ( qb * SceneQueryBuilder ) Destroy ( id string , tx * sqlx . Tx ) error {
return executeDeleteQuery ( "scenes" , id , tx )
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) Find ( id int ) ( * Scene , error ) {
2019-10-14 23:54:05 +00:00
return qb . find ( id , nil )
}
func ( qb * SceneQueryBuilder ) find ( id int , tx * sqlx . Tx ) ( * Scene , error ) {
2019-02-09 12:30:49 +00:00
query := "SELECT * FROM scenes WHERE id = ? LIMIT 1"
args := [ ] interface { } { id }
2019-10-14 23:54:05 +00:00
return qb . queryScene ( query , args , tx )
2019-02-09 12:30:49 +00:00
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) FindByChecksum ( checksum string ) ( * Scene , error ) {
2019-02-09 12:30:49 +00:00
query := "SELECT * FROM scenes WHERE checksum = ? LIMIT 1"
args := [ ] interface { } { checksum }
return qb . queryScene ( query , args , nil )
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) FindByPath ( path string ) ( * Scene , error ) {
2019-02-09 12:30:49 +00:00
query := "SELECT * FROM scenes WHERE path = ? LIMIT 1"
args := [ ] interface { } { path }
return qb . queryScene ( query , args , nil )
}
2019-05-27 19:34:26 +00:00
func ( qb * SceneQueryBuilder ) FindByPerformerID ( performerID int ) ( [ ] * Scene , error ) {
2019-02-09 12:30:49 +00:00
args := [ ] interface { } { performerID }
return qb . queryScenes ( scenesForPerformerQuery , args , nil )
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) CountByPerformerID ( performerID int ) ( int , error ) {
2019-02-09 12:30:49 +00:00
args := [ ] interface { } { performerID }
return runCountQuery ( buildCountQuery ( scenesForPerformerQuery ) , args )
}
2019-05-27 19:34:26 +00:00
func ( qb * SceneQueryBuilder ) FindByStudioID ( studioID int ) ( [ ] * Scene , error ) {
2019-02-09 12:30:49 +00:00
args := [ ] interface { } { studioID }
return qb . queryScenes ( scenesForStudioQuery , args , nil )
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) Count ( ) ( int , error ) {
2019-02-11 20:36:10 +00:00
return runCountQuery ( buildCountQuery ( "SELECT scenes.id FROM scenes" ) , nil )
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) CountByStudioID ( studioID int ) ( int , error ) {
2019-02-09 12:30:49 +00:00
args := [ ] interface { } { studioID }
return runCountQuery ( buildCountQuery ( scenesForStudioQuery ) , args )
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) CountByTagID ( tagID int ) ( int , error ) {
2019-02-09 12:30:49 +00:00
args := [ ] interface { } { tagID }
return runCountQuery ( buildCountQuery ( scenesForTagQuery ) , args )
}
2019-05-27 19:34:26 +00:00
func ( qb * SceneQueryBuilder ) Wall ( q * string ) ( [ ] * Scene , error ) {
2019-02-09 12:30:49 +00:00
s := ""
if q != nil {
s = * q
}
query := "SELECT scenes.* FROM scenes WHERE scenes.details LIKE '%" + s + "%' ORDER BY RANDOM() LIMIT 80"
return qb . queryScenes ( query , nil , nil )
}
2019-05-27 19:34:26 +00:00
func ( qb * SceneQueryBuilder ) All ( ) ( [ ] * Scene , error ) {
2019-02-14 22:53:32 +00:00
return qb . queryScenes ( selectAll ( "scenes" ) + qb . getSceneSort ( nil ) , nil , nil )
2019-02-09 12:30:49 +00:00
}
2019-05-27 19:34:26 +00:00
func ( qb * SceneQueryBuilder ) Query ( sceneFilter * SceneFilterType , findFilter * FindFilterType ) ( [ ] * Scene , int ) {
2019-02-09 12:30:49 +00:00
if sceneFilter == nil {
sceneFilter = & SceneFilterType { }
}
if findFilter == nil {
findFilter = & FindFilterType { }
}
2019-05-27 19:34:26 +00:00
var whereClauses [ ] string
var havingClauses [ ] string
var args [ ] interface { }
2019-02-09 12:30:49 +00:00
body := selectDistinctIDs ( "scenes" )
body = body + `
left join scene_markers on scene_markers . scene_id = scenes . id
left join performers_scenes as performers_join on performers_join . scene_id = scenes . id
left join performers on performers_join . performer_id = performers . id
left join studios as studio on studio . id = scenes . studio_id
left join galleries as gallery on gallery . scene_id = scenes . id
left join scenes_tags as tags_join on tags_join . scene_id = scenes . id
left join tags on tags_join . tag_id = tags . id
`
if q := findFilter . Q ; q != nil && * q != "" {
searchColumns := [ ] string { "scenes.title" , "scenes.details" , "scenes.path" , "scenes.checksum" , "scene_markers.title" }
whereClauses = append ( whereClauses , getSearch ( searchColumns , * q ) )
}
if rating := sceneFilter . Rating ; rating != nil {
2020-01-13 16:43:14 +00:00
clause , count := getIntCriterionWhereClause ( "scenes.rating" , * sceneFilter . Rating )
2019-03-24 22:11:58 +00:00
whereClauses = append ( whereClauses , clause )
if count == 1 {
args = append ( args , sceneFilter . Rating . Value )
}
2019-02-09 12:30:49 +00:00
}
2020-01-13 16:43:14 +00:00
if durationFilter := sceneFilter . Duration ; durationFilter != nil {
clause , thisArgs := getDurationWhereClause ( * durationFilter )
whereClauses = append ( whereClauses , clause )
args = append ( args , thisArgs ... )
}
2019-02-09 12:30:49 +00:00
if resolutionFilter := sceneFilter . Resolution ; resolutionFilter != nil {
if resolution := resolutionFilter . String ( ) ; resolutionFilter . IsValid ( ) {
switch resolution {
case "LOW" :
whereClauses = append ( whereClauses , "(scenes.height >= 240 AND scenes.height < 480)" )
case "STANDARD" :
whereClauses = append ( whereClauses , "(scenes.height >= 480 AND scenes.height < 720)" )
case "STANDARD_HD" :
whereClauses = append ( whereClauses , "(scenes.height >= 720 AND scenes.height < 1080)" )
case "FULL_HD" :
whereClauses = append ( whereClauses , "(scenes.height >= 1080 AND scenes.height < 2160)" )
case "FOUR_K" :
whereClauses = append ( whereClauses , "scenes.height >= 2160" )
default :
whereClauses = append ( whereClauses , "scenes.height < 240" )
}
}
}
if hasMarkersFilter := sceneFilter . HasMarkers ; hasMarkersFilter != nil {
if strings . Compare ( * hasMarkersFilter , "true" ) == 0 {
havingClauses = append ( havingClauses , "count(scene_markers.scene_id) > 0" )
} else {
whereClauses = append ( whereClauses , "scene_markers.id IS NULL" )
}
}
if isMissingFilter := sceneFilter . IsMissing ; isMissingFilter != nil && * isMissingFilter != "" {
switch * isMissingFilter {
case "gallery" :
whereClauses = append ( whereClauses , "gallery.scene_id IS NULL" )
case "studio" :
whereClauses = append ( whereClauses , "scenes.studio_id IS NULL" )
case "performers" :
whereClauses = append ( whereClauses , "performers_join.scene_id IS NULL" )
2019-09-15 21:52:02 +00:00
case "date" :
whereClauses = append ( whereClauses , "scenes.date IS \"\" OR scenes.date IS \"0001-01-01\"" )
2019-02-09 12:30:49 +00:00
default :
whereClauses = append ( whereClauses , "scenes." + * isMissingFilter + " IS NULL" )
}
}
2019-10-27 13:05:54 +00:00
if tagsFilter := sceneFilter . Tags ; tagsFilter != nil && len ( tagsFilter . Value ) > 0 {
for _ , tagID := range tagsFilter . Value {
2019-02-14 22:53:32 +00:00
args = append ( args , tagID )
2019-02-09 12:30:49 +00:00
}
2019-10-27 13:05:54 +00:00
whereClause , havingClause := getMultiCriterionClause ( "tags" , "scenes_tags" , "tag_id" , tagsFilter )
whereClauses = appendClause ( whereClauses , whereClause )
havingClauses = appendClause ( havingClauses , havingClause )
2019-02-09 12:30:49 +00:00
}
2019-10-27 13:05:54 +00:00
if performersFilter := sceneFilter . Performers ; performersFilter != nil && len ( performersFilter . Value ) > 0 {
for _ , performerID := range performersFilter . Value {
args = append ( args , performerID )
}
whereClause , havingClause := getMultiCriterionClause ( "performers" , "performers_scenes" , "performer_id" , performersFilter )
whereClauses = appendClause ( whereClauses , whereClause )
havingClauses = appendClause ( havingClauses , havingClause )
2019-02-09 12:30:49 +00:00
}
2019-10-27 13:05:54 +00:00
if studiosFilter := sceneFilter . Studios ; studiosFilter != nil && len ( studiosFilter . Value ) > 0 {
for _ , studioID := range studiosFilter . Value {
args = append ( args , studioID )
}
2019-11-07 12:49:08 +00:00
2019-10-27 13:05:54 +00:00
whereClause , havingClause := getMultiCriterionClause ( "studio" , "" , "studio_id" , studiosFilter )
whereClauses = appendClause ( whereClauses , whereClause )
havingClauses = appendClause ( havingClauses , havingClause )
2019-02-09 12:30:49 +00:00
}
sortAndPagination := qb . getSceneSort ( findFilter ) + getPagination ( findFilter )
idsResult , countResult := executeFindQuery ( "scenes" , body , args , sortAndPagination , whereClauses , havingClauses )
2019-05-27 19:34:26 +00:00
var scenes [ ] * Scene
2019-02-09 12:30:49 +00:00
for _ , id := range idsResult {
scene , _ := qb . Find ( id )
2019-05-27 19:34:26 +00:00
scenes = append ( scenes , scene )
2019-02-09 12:30:49 +00:00
}
return scenes , countResult
}
2019-10-27 13:05:54 +00:00
func appendClause ( clauses [ ] string , clause string ) [ ] string {
if clause != "" {
return append ( clauses , clause )
}
return clauses
}
2020-01-13 16:43:14 +00:00
func getDurationWhereClause ( durationFilter IntCriterionInput ) ( string , [ ] interface { } ) {
// special case for duration. We accept duration as seconds as int but the
// field is floating point. Change the equals filter to return a range
// between x and x + 1
// likewise, not equals needs to be duration < x OR duration >= x
var clause string
args := [ ] interface { } { }
value := durationFilter . Value
if durationFilter . Modifier == CriterionModifierEquals {
clause = "scenes.duration >= ? AND scenes.duration < ?"
args = append ( args , value )
args = append ( args , value + 1 )
} else if durationFilter . Modifier == CriterionModifierNotEquals {
clause = "(scenes.duration < ? OR scenes.duration >= ?)"
args = append ( args , value )
args = append ( args , value + 1 )
} else {
var count int
clause , count = getIntCriterionWhereClause ( "scenes.duration" , durationFilter )
if count == 1 {
args = append ( args , value )
}
}
return clause , args
}
2019-10-27 13:05:54 +00:00
// returns where clause and having clause
func getMultiCriterionClause ( table string , joinTable string , joinTableField string , criterion * MultiCriterionInput ) ( string , string ) {
whereClause := ""
havingClause := ""
if criterion . Modifier == CriterionModifierIncludes {
// includes any of the provided ids
2019-11-07 12:49:08 +00:00
whereClause = table + ".id IN " + getInBinding ( len ( criterion . Value ) )
2019-10-27 13:05:54 +00:00
} else if criterion . Modifier == CriterionModifierIncludesAll {
// includes all of the provided ids
2019-11-07 12:49:08 +00:00
whereClause = table + ".id IN " + getInBinding ( len ( criterion . Value ) )
2019-10-27 13:05:54 +00:00
havingClause = "count(distinct " + table + ".id) IS " + strconv . Itoa ( len ( criterion . Value ) )
} else if criterion . Modifier == CriterionModifierExcludes {
// excludes all of the provided ids
2019-11-07 12:49:08 +00:00
if joinTable != "" {
2019-10-27 13:05:54 +00:00
whereClause = "not exists (select " + joinTable + ".scene_id from " + joinTable + " where " + joinTable + ".scene_id = scenes.id and " + joinTable + "." + joinTableField + " in " + getInBinding ( len ( criterion . Value ) ) + ")"
} else {
whereClause = "not exists (select s.id from scenes as s where s.id = scenes.id and s." + joinTableField + " in " + getInBinding ( len ( criterion . Value ) ) + ")"
}
}
return whereClause , havingClause
}
2019-12-01 16:18:44 +00:00
func ( qb * SceneQueryBuilder ) QueryAllByPathRegex ( regex string ) ( [ ] * Scene , error ) {
var args [ ] interface { }
2020-01-06 18:02:25 +00:00
body := selectDistinctIDs ( "scenes" ) + " WHERE scenes.path regexp ?"
args = append ( args , "(?i)" + regex )
2019-12-01 16:18:44 +00:00
idsResult , err := runIdsQuery ( body , args )
if err != nil {
return nil , err
}
var scenes [ ] * Scene
for _ , id := range idsResult {
scene , err := qb . Find ( id )
if err != nil {
return nil , err
}
scenes = append ( scenes , scene )
}
return scenes , nil
}
2019-10-30 13:37:21 +00:00
func ( qb * SceneQueryBuilder ) QueryByPathRegex ( findFilter * FindFilterType ) ( [ ] * Scene , int ) {
if findFilter == nil {
findFilter = & FindFilterType { }
}
var whereClauses [ ] string
var havingClauses [ ] string
var args [ ] interface { }
body := selectDistinctIDs ( "scenes" )
if q := findFilter . Q ; q != nil && * q != "" {
2020-01-06 18:02:25 +00:00
whereClauses = append ( whereClauses , "scenes.path regexp ?" )
args = append ( args , "(?i)" + * q )
2019-10-30 13:37:21 +00:00
}
sortAndPagination := qb . getSceneSort ( findFilter ) + getPagination ( findFilter )
idsResult , countResult := executeFindQuery ( "scenes" , body , args , sortAndPagination , whereClauses , havingClauses )
var scenes [ ] * Scene
for _ , id := range idsResult {
scene , _ := qb . Find ( id )
scenes = append ( scenes , scene )
}
return scenes , countResult
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) getSceneSort ( findFilter * FindFilterType ) string {
2019-02-09 12:30:49 +00:00
if findFilter == nil {
return " ORDER BY scenes.path, scenes.date ASC "
}
2019-02-14 22:53:32 +00:00
sort := findFilter . GetSort ( "title" )
direction := findFilter . GetDirection ( )
return getSort ( sort , direction , "scenes" )
2019-02-09 12:30:49 +00:00
}
2019-02-14 22:53:32 +00:00
func ( qb * SceneQueryBuilder ) queryScene ( query string , args [ ] interface { } , tx * sqlx . Tx ) ( * Scene , error ) {
2019-02-09 12:30:49 +00:00
results , err := qb . queryScenes ( query , args , tx )
if err != nil || len ( results ) < 1 {
return nil , err
}
2019-05-27 19:34:26 +00:00
return results [ 0 ] , nil
2019-02-09 12:30:49 +00:00
}
2019-05-27 19:34:26 +00:00
func ( qb * SceneQueryBuilder ) queryScenes ( query string , args [ ] interface { } , tx * sqlx . Tx ) ( [ ] * Scene , error ) {
2019-02-09 12:30:49 +00:00
var rows * sqlx . Rows
var err error
if tx != nil {
rows , err = tx . Queryx ( query , args ... )
} else {
rows , err = database . DB . Queryx ( query , args ... )
}
if err != nil && err != sql . ErrNoRows {
return nil , err
}
defer rows . Close ( )
2019-05-27 19:34:26 +00:00
scenes := make ( [ ] * Scene , 0 )
2019-02-09 12:30:49 +00:00
for rows . Next ( ) {
2019-05-27 19:34:26 +00:00
scene := Scene { }
2019-02-09 12:30:49 +00:00
if err := rows . StructScan ( & scene ) ; err != nil {
return nil , err
}
2019-05-27 19:34:26 +00:00
scenes = append ( scenes , & scene )
2019-02-09 12:30:49 +00:00
}
if err := rows . Err ( ) ; err != nil {
return nil , err
}
return scenes , nil
}