This commit is contained in:
Stash Dev
2019-02-14 15:42:52 -08:00
parent 1d00b2b36f
commit b488c1ed7d
111 changed files with 141 additions and 141 deletions

View File

@@ -0,0 +1,25 @@
package models
func (ff FindFilterType) GetSort(defaultSort string) string {
var sort string
if ff.Sort == nil {
sort = defaultSort
} else {
sort = *ff.Sort
}
return sort
}
func (ff FindFilterType) GetDirection() string {
var direction string
if directionFilter := ff.Direction; directionFilter != nil {
if dir := directionFilter.String(); directionFilter.IsValid() {
direction = dir
} else {
direction = "ASC"
}
} else {
direction = "ASC"
}
return direction
}

11893
pkg/models/generated_exec.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,334 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package models
import (
"fmt"
"io"
"strconv"
)
type FindFilterType struct {
Q *string `json:"q"`
Page *int `json:"page"`
PerPage *int `json:"per_page"`
Sort *string `json:"sort"`
Direction *SortDirectionEnum `json:"direction"`
}
type FindGalleriesResultType struct {
Count int `json:"count"`
Galleries []Gallery `json:"galleries"`
}
type FindPerformersResultType struct {
Count int `json:"count"`
Performers []Performer `json:"performers"`
}
type FindSceneMarkersResultType struct {
Count int `json:"count"`
SceneMarkers []SceneMarker `json:"scene_markers"`
}
type FindScenesResultType struct {
Count int `json:"count"`
Scenes []Scene `json:"scenes"`
}
type FindStudiosResultType struct {
Count int `json:"count"`
Studios []Studio `json:"studios"`
}
type GalleryFilesType struct {
Index int `json:"index"`
Name *string `json:"name"`
Path *string `json:"path"`
}
type MarkerStringsResultType struct {
Count int `json:"count"`
ID string `json:"id"`
Title string `json:"title"`
}
type PerformerCreateInput struct {
Name *string `json:"name"`
URL *string `json:"url"`
Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"`
Country *string `json:"country"`
EyeColor *string `json:"eye_color"`
Height *string `json:"height"`
Measurements *string `json:"measurements"`
FakeTits *string `json:"fake_tits"`
CareerLength *string `json:"career_length"`
Tattoos *string `json:"tattoos"`
Piercings *string `json:"piercings"`
Aliases *string `json:"aliases"`
Twitter *string `json:"twitter"`
Instagram *string `json:"instagram"`
Favorite *bool `json:"favorite"`
// This should be base64 encoded
Image string `json:"image"`
}
type PerformerFilterType struct {
// Filter by favorite
FilterFavorites *bool `json:"filter_favorites"`
}
type PerformerUpdateInput struct {
ID string `json:"id"`
Name *string `json:"name"`
URL *string `json:"url"`
Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"`
Country *string `json:"country"`
EyeColor *string `json:"eye_color"`
Height *string `json:"height"`
Measurements *string `json:"measurements"`
FakeTits *string `json:"fake_tits"`
CareerLength *string `json:"career_length"`
Tattoos *string `json:"tattoos"`
Piercings *string `json:"piercings"`
Aliases *string `json:"aliases"`
Twitter *string `json:"twitter"`
Instagram *string `json:"instagram"`
Favorite *bool `json:"favorite"`
// This should be base64 encoded
Image *string `json:"image"`
}
type SceneFileType struct {
Size *string `json:"size"`
Duration *float64 `json:"duration"`
VideoCodec *string `json:"video_codec"`
AudioCodec *string `json:"audio_codec"`
Width *int `json:"width"`
Height *int `json:"height"`
Framerate *float64 `json:"framerate"`
Bitrate *int `json:"bitrate"`
}
type SceneFilterType struct {
// Filter by rating
Rating *int `json:"rating"`
// Filter by resolution
Resolution *ResolutionEnum `json:"resolution"`
// Filter to only include scenes which have markers. `true` or `false`
HasMarkers *string `json:"has_markers"`
// Filter to only include scenes missing this property
IsMissing *string `json:"is_missing"`
// Filter to only include scenes with this studio
StudioID *string `json:"studio_id"`
// Filter to only include scenes with these tags
Tags []string `json:"tags"`
// Filter to only include scenes with this performer
PerformerID *string `json:"performer_id"`
}
type SceneMarkerCreateInput struct {
Title string `json:"title"`
Seconds float64 `json:"seconds"`
SceneID string `json:"scene_id"`
PrimaryTagID string `json:"primary_tag_id"`
TagIds []string `json:"tag_ids"`
}
type SceneMarkerFilterType struct {
// Filter to only include scene markers with this tag
TagID *string `json:"tag_id"`
// Filter to only include scene markers with these tags
Tags []string `json:"tags"`
// Filter to only include scene markers attached to a scene with these tags
SceneTags []string `json:"scene_tags"`
// Filter to only include scene markers with these performers
Performers []string `json:"performers"`
}
type SceneMarkerTag struct {
Tag Tag `json:"tag"`
SceneMarkers []SceneMarker `json:"scene_markers"`
}
type SceneMarkerUpdateInput struct {
ID string `json:"id"`
Title string `json:"title"`
Seconds float64 `json:"seconds"`
SceneID string `json:"scene_id"`
PrimaryTagID string `json:"primary_tag_id"`
TagIds []string `json:"tag_ids"`
}
type ScenePathsType struct {
Screenshot *string `json:"screenshot"`
Preview *string `json:"preview"`
Stream *string `json:"stream"`
Webp *string `json:"webp"`
Vtt *string `json:"vtt"`
ChaptersVtt *string `json:"chapters_vtt"`
}
type SceneUpdateInput struct {
ClientMutationID *string `json:"clientMutationId"`
ID string `json:"id"`
Title *string `json:"title"`
Details *string `json:"details"`
URL *string `json:"url"`
Date *string `json:"date"`
Rating *int `json:"rating"`
StudioID *string `json:"studio_id"`
GalleryID *string `json:"gallery_id"`
PerformerIds []string `json:"performer_ids"`
TagIds []string `json:"tag_ids"`
}
// A performer from a scraping operation...
type ScrapedPerformer struct {
Name *string `json:"name"`
URL *string `json:"url"`
Twitter *string `json:"twitter"`
Instagram *string `json:"instagram"`
Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"`
Country *string `json:"country"`
EyeColor *string `json:"eye_color"`
Height *string `json:"height"`
Measurements *string `json:"measurements"`
FakeTits *string `json:"fake_tits"`
CareerLength *string `json:"career_length"`
Tattoos *string `json:"tattoos"`
Piercings *string `json:"piercings"`
Aliases *string `json:"aliases"`
}
type StatsResultType struct {
SceneCount int `json:"scene_count"`
GalleryCount int `json:"gallery_count"`
PerformerCount int `json:"performer_count"`
StudioCount int `json:"studio_count"`
TagCount int `json:"tag_count"`
}
type StudioCreateInput struct {
Name string `json:"name"`
URL *string `json:"url"`
// This should be base64 encoded
Image string `json:"image"`
}
type StudioUpdateInput struct {
ID string `json:"id"`
Name *string `json:"name"`
URL *string `json:"url"`
// This should be base64 encoded
Image *string `json:"image"`
}
type TagCreateInput struct {
Name string `json:"name"`
}
type TagDestroyInput struct {
ID string `json:"id"`
}
type TagUpdateInput struct {
ID string `json:"id"`
Name string `json:"name"`
}
type ResolutionEnum string
const (
// 240p
ResolutionEnumLow ResolutionEnum = "LOW"
// 480p
ResolutionEnumStandard ResolutionEnum = "STANDARD"
// 720p
ResolutionEnumStandardHd ResolutionEnum = "STANDARD_HD"
// 1080p
ResolutionEnumFullHd ResolutionEnum = "FULL_HD"
// 4k
ResolutionEnumFourK ResolutionEnum = "FOUR_K"
)
var AllResolutionEnum = []ResolutionEnum{
ResolutionEnumLow,
ResolutionEnumStandard,
ResolutionEnumStandardHd,
ResolutionEnumFullHd,
ResolutionEnumFourK,
}
func (e ResolutionEnum) IsValid() bool {
switch e {
case ResolutionEnumLow, ResolutionEnumStandard, ResolutionEnumStandardHd, ResolutionEnumFullHd, ResolutionEnumFourK:
return true
}
return false
}
func (e ResolutionEnum) String() string {
return string(e)
}
func (e *ResolutionEnum) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ResolutionEnum(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ResolutionEnum", str)
}
return nil
}
func (e ResolutionEnum) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type SortDirectionEnum string
const (
SortDirectionEnumAsc SortDirectionEnum = "ASC"
SortDirectionEnumDesc SortDirectionEnum = "DESC"
)
var AllSortDirectionEnum = []SortDirectionEnum{
SortDirectionEnumAsc,
SortDirectionEnumDesc,
}
func (e SortDirectionEnum) IsValid() bool {
switch e {
case SortDirectionEnumAsc, SortDirectionEnumDesc:
return true
}
return false
}
func (e SortDirectionEnum) String() string {
return string(e)
}
func (e *SortDirectionEnum) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = SortDirectionEnum(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid SortDirectionEnum", str)
}
return nil
}
func (e SortDirectionEnum) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

116
pkg/models/model_gallery.go Normal file
View File

@@ -0,0 +1,116 @@
package models
import (
"archive/zip"
"bytes"
"database/sql"
"github.com/disintegration/imaging"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/utils"
"image"
"image/jpeg"
"io/ioutil"
"path/filepath"
"sort"
"strings"
)
type Gallery struct {
ID int `db:"id" json:"id"`
Path string `db:"path" json:"path"`
Checksum string `db:"checksum" json:"checksum"`
SceneID sql.NullInt64 `db:"scene_id,omitempty" json:"scene_id"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}
func (g *Gallery) GetFiles(baseURL string) []GalleryFilesType {
var galleryFiles []GalleryFilesType
filteredFiles, readCloser, err := g.listZipContents()
if err != nil {
return nil
}
defer readCloser.Close()
builder := urlbuilders.NewGalleryURLBuilder(baseURL, g.ID)
for i, file := range filteredFiles {
galleryURL := builder.GetGalleryImageURL(i)
galleryFile := GalleryFilesType{
Index: i,
Name: &file.Name,
Path: &galleryURL,
}
galleryFiles = append(galleryFiles, galleryFile)
}
return galleryFiles
}
func (g *Gallery) GetImage(index int) []byte {
data, _ := g.readZipFile(index)
return data
}
func (g *Gallery) GetThumbnail(index int) []byte {
data, _ := g.readZipFile(index)
srcImage, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return data
}
resizedImage := imaging.Resize(srcImage, 512, 0, imaging.Lanczos)
buf := new(bytes.Buffer)
err = jpeg.Encode(buf, resizedImage, nil)
if err != nil {
return data
}
return buf.Bytes()
}
func (g *Gallery) readZipFile(index int) ([]byte, error) {
filteredFiles, readCloser, err := g.listZipContents()
if err != nil {
return nil, err
}
defer readCloser.Close()
zipFile := filteredFiles[index]
zipFileReadCloser, err := zipFile.Open()
if err != nil {
logger.Warn("failed to read file inside zip file")
return nil, err
}
defer zipFileReadCloser.Close()
return ioutil.ReadAll(zipFileReadCloser)
}
func (g *Gallery) listZipContents() ([]*zip.File, *zip.ReadCloser, error) {
readCloser, err := zip.OpenReader(g.Path)
if err != nil {
logger.Warn("failed to read zip file")
return nil, nil, err
}
filteredFiles := make([]*zip.File, 0)
for _, file := range readCloser.File {
if file.FileInfo().IsDir() {
continue
}
ext := filepath.Ext(file.Name)
if ext != ".jpg" && ext != ".png" && ext != ".gif" {
continue
}
if strings.Contains(file.Name, "__MACOSX") {
continue
}
filteredFiles = append(filteredFiles, file)
}
sort.Slice(filteredFiles, func(i, j int) bool {
a := filteredFiles[i]
b := filteredFiles[j]
return utils.NaturalCompare(a.Name, b.Name)
})
return filteredFiles, readCloser, nil
}

16
pkg/models/model_joins.go Normal file
View File

@@ -0,0 +1,16 @@
package models
type PerformersScenes struct {
PerformerID int `db:"performer_id" json:"performer_id"`
SceneID int `db:"scene_id" json:"scene_id"`
}
type ScenesTags struct {
SceneID int `db:"scene_id" json:"scene_id"`
TagID int `db:"tag_id" json:"tag_id"`
}
type SceneMarkersTags struct {
SceneMarkerID int `db:"scene_marker_id" json:"scene_marker_id"`
TagID int `db:"tag_id" json:"tag_id"`
}

View File

@@ -0,0 +1,29 @@
package models
import (
"database/sql"
)
type Performer struct {
ID int `db:"id" json:"id"`
Image []byte `db:"image" json:"image"`
Checksum string `db:"checksum" json:"checksum"`
Name sql.NullString `db:"name" json:"name"`
URL sql.NullString `db:"url" json:"url"`
Twitter sql.NullString `db:"twitter" json:"twitter"`
Instagram sql.NullString `db:"instagram" json:"instagram"`
Birthdate sql.NullString `db:"birthdate" json:"birthdate"` // TODO dates?
Ethnicity sql.NullString `db:"ethnicity" json:"ethnicity"`
Country sql.NullString `db:"country" json:"country"`
EyeColor sql.NullString `db:"eye_color" json:"eye_color"`
Height sql.NullString `db:"height" json:"height"`
Measurements sql.NullString `db:"measurements" json:"measurements"`
FakeTits sql.NullString `db:"fake_tits" json:"fake_tits"`
CareerLength sql.NullString `db:"career_length" json:"career_length"`
Tattoos sql.NullString `db:"tattoos" json:"tattoos"`
Piercings sql.NullString `db:"piercings" json:"piercings"`
Aliases sql.NullString `db:"aliases" json:"aliases"`
Favorite sql.NullBool `db:"favorite" json:"favorite"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}

27
pkg/models/model_scene.go Normal file
View File

@@ -0,0 +1,27 @@
package models
import (
"database/sql"
)
type Scene struct {
ID int `db:"id" json:"id"`
Checksum string `db:"checksum" json:"checksum"`
Path string `db:"path" json:"path"`
Title sql.NullString `db:"title" json:"title"`
Details sql.NullString `db:"details" json:"details"`
URL sql.NullString `db:"url" json:"url"`
Date sql.NullString `db:"date" json:"date"` // TODO dates?
Rating sql.NullInt64 `db:"rating" json:"rating"`
Size sql.NullString `db:"size" json:"size"`
Duration sql.NullFloat64 `db:"duration" json:"duration"`
VideoCodec sql.NullString `db:"video_codec" json:"video_codec"`
AudioCodec sql.NullString `db:"audio_codec" json:"audio_codec"`
Width sql.NullInt64 `db:"width" json:"width"`
Height sql.NullInt64 `db:"height" json:"height"`
Framerate sql.NullFloat64 `db:"framerate" json:"framerate"`
Bitrate sql.NullInt64 `db:"bitrate" json:"bitrate"`
StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}

View File

@@ -0,0 +1,15 @@
package models
import (
"database/sql"
)
type SceneMarker struct {
ID int `db:"id" json:"id"`
Title string `db:"title" json:"title"`
Seconds float64 `db:"seconds" json:"seconds"`
PrimaryTagID sql.NullInt64 `db:"primary_tag_id,omitempty" json:"primary_tag_id"`
SceneID sql.NullInt64 `db:"scene_id,omitempty" json:"scene_id"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}

View File

@@ -0,0 +1,24 @@
package models
import (
"database/sql"
)
type ScrapedItem struct {
ID int `db:"id" json:"id"`
Title sql.NullString `db:"title" json:"title"`
Description sql.NullString `db:"description" json:"description"`
URL sql.NullString `db:"url" json:"url"`
Date sql.NullString `db:"date" json:"date"` // TODO dates?
Rating sql.NullString `db:"rating" json:"rating"`
Tags sql.NullString `db:"tags" json:"tags"`
Models sql.NullString `db:"models" json:"models"`
Episode sql.NullInt64 `db:"episode" json:"episode"`
GalleryFilename sql.NullString `db:"gallery_filename" json:"gallery_filename"`
GalleryURL sql.NullString `db:"gallery_url" json:"gallery_url"`
VideoFilename sql.NullString `db:"video_filename" json:"video_filename"`
VideoURL sql.NullString `db:"video_url" json:"video_url"`
StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}

View File

@@ -0,0 +1,15 @@
package models
import (
"database/sql"
)
type Studio struct {
ID int `db:"id" json:"id"`
Image []byte `db:"image" json:"image"`
Checksum string `db:"checksum" json:"checksum"`
Name sql.NullString `db:"name" json:"name"`
URL sql.NullString `db:"url" json:"url"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}

8
pkg/models/model_tag.go Normal file
View File

@@ -0,0 +1,8 @@
package models
type Tag struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"` // TODO make schema not null
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}

View File

@@ -0,0 +1,167 @@
package models
import (
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"path/filepath"
)
type GalleryQueryBuilder struct{}
func NewGalleryQueryBuilder() GalleryQueryBuilder {
return GalleryQueryBuilder{}
}
func (qb *GalleryQueryBuilder) Create(newGallery Gallery, tx *sqlx.Tx) (*Gallery, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO galleries (path, checksum, scene_id, created_at, updated_at)
VALUES (:path, :checksum, :scene_id, :created_at, :updated_at)
`,
newGallery,
)
if err != nil {
return nil, err
}
galleryID, err := result.LastInsertId()
if err != nil {
return nil, err
}
if err := tx.Get(&newGallery, `SELECT * FROM galleries WHERE id = ? LIMIT 1`, galleryID); err != nil {
return nil, err
}
return &newGallery, nil
}
func (qb *GalleryQueryBuilder) Update(updatedGallery Gallery, tx *sqlx.Tx) (*Gallery, error) {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE galleries SET `+SQLGenKeys(updatedGallery)+` WHERE galleries.id = :id`,
updatedGallery,
)
if err != nil {
return nil, err
}
if err := tx.Get(&updatedGallery, `SELECT * FROM galleries WHERE id = ? LIMIT 1`, updatedGallery.ID); err != nil {
return nil, err
}
return &updatedGallery, nil
}
func (qb *GalleryQueryBuilder) Find(id int) (*Gallery, error) {
query := "SELECT * FROM galleries WHERE id = ? LIMIT 1"
args := []interface{}{id}
return qb.queryGallery(query, args, nil)
}
func (qb *GalleryQueryBuilder) FindByChecksum(checksum string, tx *sqlx.Tx) (*Gallery, error) {
query := "SELECT * FROM galleries WHERE checksum = ? LIMIT 1"
args := []interface{}{checksum}
return qb.queryGallery(query, args, tx)
}
func (qb *GalleryQueryBuilder) FindByPath(path string) (*Gallery, error) {
query := "SELECT * FROM galleries WHERE path = ? LIMIT 1"
args := []interface{}{path}
return qb.queryGallery(query, args, nil)
}
func (qb *GalleryQueryBuilder) FindBySceneID(sceneID int, tx *sqlx.Tx) (*Gallery, error) {
query := "SELECT galleries.* FROM galleries JOIN scenes ON scenes.id = galleries.scene_id WHERE scenes.id = ? LIMIT 1"
args := []interface{}{sceneID}
return qb.queryGallery(query, args, tx)
}
func (qb *GalleryQueryBuilder) ValidGalleriesForScenePath(scenePath string) ([]Gallery, error) {
sceneDirPath := filepath.Dir(scenePath)
query := "SELECT galleries.* FROM galleries WHERE galleries.scene_id IS NULL AND galleries.path LIKE '" + sceneDirPath + "%' ORDER BY path ASC"
return qb.queryGalleries(query, nil, nil)
}
func (qb *GalleryQueryBuilder) Count() (int, error) {
return runCountQuery(buildCountQuery("SELECT galleries.id FROM galleries"), nil)
}
func (qb *GalleryQueryBuilder) All() ([]Gallery, error) {
return qb.queryGalleries(selectAll("galleries")+qb.getGallerySort(nil), nil, nil)
}
func (qb *GalleryQueryBuilder) Query(findFilter *FindFilterType) ([]Gallery, int) {
if findFilter == nil {
findFilter = &FindFilterType{}
}
whereClauses := []string{}
havingClauses := []string{}
args := []interface{}{}
body := selectDistinctIDs("galleries")
if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"galleries.path", "galleries.checksum"}
whereClauses = append(whereClauses, getSearch(searchColumns, *q))
}
sortAndPagination := qb.getGallerySort(findFilter) + getPagination(findFilter)
idsResult, countResult := executeFindQuery("galleries", body, args, sortAndPagination, whereClauses, havingClauses)
var galleries []Gallery
for _, id := range idsResult {
gallery, _ := qb.Find(id)
galleries = append(galleries, *gallery)
}
return galleries, countResult
}
func (qb *GalleryQueryBuilder) getGallerySort(findFilter *FindFilterType) string {
var sort string
var direction string
//if findFilter == nil { // TODO temp until title is removed from schema and UI
sort = "path"
direction = "ASC"
//} else {
// sort = findFilter.getSort("path")
// direction = findFilter.getDirection()
//}
return getSort(sort, direction, "galleries")
}
func (qb *GalleryQueryBuilder) queryGallery(query string, args []interface{}, tx *sqlx.Tx) (*Gallery, error) {
results, err := qb.queryGalleries(query, args, tx)
if err != nil || len(results) < 1 {
return nil, err
}
return &results[0], nil
}
func (qb *GalleryQueryBuilder) queryGalleries(query string, args []interface{}, tx *sqlx.Tx) ([]Gallery, error) {
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()
galleries := make([]Gallery, 0)
gallery := Gallery{}
for rows.Next() {
if err := rows.StructScan(&gallery); err != nil {
return nil, err
}
galleries = append(galleries, gallery)
}
if err := rows.Err(); err != nil {
return nil, err
}
return galleries, nil
}

View File

@@ -0,0 +1,84 @@
package models
import "github.com/jmoiron/sqlx"
type JoinsQueryBuilder struct{}
func NewJoinsQueryBuilder() JoinsQueryBuilder {
return JoinsQueryBuilder{}
}
func (qb *JoinsQueryBuilder) CreatePerformersScenes(newJoins []PerformersScenes, tx *sqlx.Tx) error {
ensureTx(tx)
for _, join := range newJoins {
_, err := tx.NamedExec(
`INSERT INTO performers_scenes (performer_id, scene_id) VALUES (:performer_id, :scene_id)`,
join,
)
if err != nil {
return err
}
}
return nil
}
func (qb *JoinsQueryBuilder) UpdatePerformersScenes(sceneID int, updatedJoins []PerformersScenes, tx *sqlx.Tx) error {
ensureTx(tx)
// Delete the existing joins and then create new ones
_, err := tx.Exec("DELETE FROM performers_scenes WHERE scene_id = ?", sceneID)
if err != nil {
return err
}
return qb.CreatePerformersScenes(updatedJoins, tx)
}
func (qb *JoinsQueryBuilder) CreateScenesTags(newJoins []ScenesTags, tx *sqlx.Tx) error {
ensureTx(tx)
for _, join := range newJoins {
_, err := tx.NamedExec(
`INSERT INTO scenes_tags (scene_id, tag_id) VALUES (:scene_id, :tag_id)`,
join,
)
if err != nil {
return err
}
}
return nil
}
func (qb *JoinsQueryBuilder) UpdateScenesTags(sceneID int, updatedJoins []ScenesTags, tx *sqlx.Tx) error {
ensureTx(tx)
// Delete the existing joins and then create new ones
_, err := tx.Exec("DELETE FROM scenes_tags WHERE scene_id = ?", sceneID)
if err != nil {
return err
}
return qb.CreateScenesTags(updatedJoins, tx)
}
func (qb *JoinsQueryBuilder) CreateSceneMarkersTags(newJoins []SceneMarkersTags, tx *sqlx.Tx) error {
ensureTx(tx)
for _, join := range newJoins {
_, err := tx.NamedExec(
`INSERT INTO scene_markers_tags (scene_marker_id, tag_id) VALUES (:scene_marker_id, :tag_id)`,
join,
)
if err != nil {
return err
}
}
return nil
}
func (qb *JoinsQueryBuilder) UpdateSceneMarkersTags(sceneMarkerID int, updatedJoins []SceneMarkersTags, tx *sqlx.Tx) error {
ensureTx(tx)
// Delete the existing joins and then create new ones
_, err := tx.Exec("DELETE FROM scene_markers_tags WHERE scene_marker_id = ?", sceneMarkerID)
if err != nil {
return err
}
return qb.CreateSceneMarkersTags(updatedJoins, tx)
}

View File

@@ -0,0 +1,179 @@
package models
import (
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
)
type PerformerQueryBuilder struct{}
func NewPerformerQueryBuilder() PerformerQueryBuilder {
return PerformerQueryBuilder{}
}
func (qb *PerformerQueryBuilder) Create(newPerformer Performer, tx *sqlx.Tx) (*Performer, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO performers (image, checksum, name, url, twitter, instagram, birthdate, ethnicity, country,
eye_color, height, measurements, fake_tits, career_length, tattoos, piercings,
aliases, favorite, created_at, updated_at)
VALUES (:image, :checksum, :name, :url, :twitter, :instagram, :birthdate, :ethnicity, :country,
:eye_color, :height, :measurements, :fake_tits, :career_length, :tattoos, :piercings,
:aliases, :favorite, :created_at, :updated_at)
`,
newPerformer,
)
if err != nil {
return nil, err
}
performerID, err := result.LastInsertId()
if err != nil {
return nil, err
}
if err := tx.Get(&newPerformer, `SELECT * FROM performers WHERE id = ? LIMIT 1`, performerID); err != nil {
return nil, err
}
return &newPerformer, nil
}
func (qb *PerformerQueryBuilder) Update(updatedPerformer Performer, tx *sqlx.Tx) (*Performer, error) {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE performers SET `+SQLGenKeys(updatedPerformer)+` WHERE performers.id = :id`,
updatedPerformer,
)
if err != nil {
return nil, err
}
if err := tx.Get(&updatedPerformer, `SELECT * FROM performers WHERE id = ? LIMIT 1`, updatedPerformer.ID); err != nil {
return nil, err
}
return &updatedPerformer, nil
}
func (qb *PerformerQueryBuilder) Find(id int) (*Performer, error) {
query := "SELECT * FROM performers WHERE id = ? LIMIT 1"
args := []interface{}{id}
results, err := qb.queryPerformers(query, args, nil)
if err != nil || len(results) < 1 {
return nil, err
}
return &results[0], nil
}
func (qb *PerformerQueryBuilder) FindBySceneID(sceneID int, tx *sqlx.Tx) ([]Performer, error) {
query := `
SELECT performers.* FROM performers
LEFT JOIN performers_scenes as scenes_join on scenes_join.performer_id = performers.id
LEFT JOIN scenes on scenes_join.scene_id = scenes.id
WHERE scenes.id = ?
GROUP BY performers.id
`
args := []interface{}{sceneID}
return qb.queryPerformers(query, args, tx)
}
func (qb *PerformerQueryBuilder) FindByNames(names []string, tx *sqlx.Tx) ([]Performer, error) {
query := "SELECT * FROM performers WHERE name IN " + getInBinding(len(names))
var args []interface{}
for _, name := range names {
args = append(args, name)
}
return qb.queryPerformers(query, args, tx)
}
func (qb *PerformerQueryBuilder) Count() (int, error) {
return runCountQuery(buildCountQuery("SELECT performers.id FROM performers"), nil)
}
func (qb *PerformerQueryBuilder) All() ([]Performer, error) {
return qb.queryPerformers(selectAll("performers")+qb.getPerformerSort(nil), nil, nil)
}
func (qb *PerformerQueryBuilder) Query(performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]Performer, int) {
if performerFilter == nil {
performerFilter = &PerformerFilterType{}
}
if findFilter == nil {
findFilter = &FindFilterType{}
}
whereClauses := []string{}
havingClauses := []string{}
args := []interface{}{}
body := selectDistinctIDs("performers")
body += `
left join performers_scenes as scenes_join on scenes_join.performer_id = performers.id
left join scenes on scenes_join.scene_id = scenes.id
`
if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"performers.name", "performers.checksum", "performers.birthdate", "performers.ethnicity"}
whereClauses = append(whereClauses, getSearch(searchColumns, *q))
}
if favoritesFilter := performerFilter.FilterFavorites; favoritesFilter != nil {
if *favoritesFilter == true {
whereClauses = append(whereClauses, "performers.favorite = 1")
} else {
whereClauses = append(whereClauses, "performers.favorite = 0")
}
}
sortAndPagination := qb.getPerformerSort(findFilter) + getPagination(findFilter)
idsResult, countResult := executeFindQuery("performers", body, args, sortAndPagination, whereClauses, havingClauses)
var performers []Performer
for _, id := range idsResult {
performer, _ := qb.Find(id)
performers = append(performers, *performer)
}
return performers, countResult
}
func (qb *PerformerQueryBuilder) getPerformerSort(findFilter *FindFilterType) string {
var sort string
var direction string
if findFilter == nil {
sort = "name"
direction = "ASC"
} else {
sort = findFilter.GetSort("name")
direction = findFilter.GetDirection()
}
return getSort(sort, direction, "performers")
}
func (qb *PerformerQueryBuilder) queryPerformers(query string, args []interface{}, tx *sqlx.Tx) ([]Performer, error) {
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()
performers := make([]Performer, 0)
performer := Performer{}
for rows.Next() {
if err := rows.StructScan(&performer); err != nil {
return nil, err
}
performers = append(performers, performer)
}
if err := rows.Err(); err != nil {
return nil, err
}
return performers, nil
}

View File

@@ -0,0 +1,287 @@
package models
import (
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"strconv"
"strings"
)
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
`
type SceneQueryBuilder struct{}
func NewSceneQueryBuilder() SceneQueryBuilder {
return SceneQueryBuilder{}
}
func (qb *SceneQueryBuilder) Create(newScene Scene, tx *sqlx.Tx) (*Scene, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO scenes (checksum, path, title, details, url, date, rating, size, duration, video_codec,
audio_codec, width, height, framerate, bitrate, studio_id, created_at, updated_at)
VALUES (:checksum, :path, :title, :details, :url, :date, :rating, :size, :duration, :video_codec,
:audio_codec, :width, :height, :framerate, :bitrate, :studio_id, :created_at, :updated_at)
`,
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
}
func (qb *SceneQueryBuilder) Update(updatedScene Scene, tx *sqlx.Tx) (*Scene, error) {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE scenes SET `+SQLGenKeys(updatedScene)+` WHERE scenes.id = :id`,
updatedScene,
)
if err != nil {
return nil, err
}
if err := tx.Get(&updatedScene, `SELECT * FROM scenes WHERE id = ? LIMIT 1`, updatedScene.ID); err != nil {
return nil, err
}
return &updatedScene, nil
}
func (qb *SceneQueryBuilder) Find(id int) (*Scene, error) {
query := "SELECT * FROM scenes WHERE id = ? LIMIT 1"
args := []interface{}{id}
return qb.queryScene(query, args, nil)
}
func (qb *SceneQueryBuilder) FindByChecksum(checksum string) (*Scene, error) {
query := "SELECT * FROM scenes WHERE checksum = ? LIMIT 1"
args := []interface{}{checksum}
return qb.queryScene(query, args, nil)
}
func (qb *SceneQueryBuilder) FindByPath(path string) (*Scene, error) {
query := "SELECT * FROM scenes WHERE path = ? LIMIT 1"
args := []interface{}{path}
return qb.queryScene(query, args, nil)
}
func (qb *SceneQueryBuilder) FindByPerformerID(performerID int) ([]Scene, error) {
args := []interface{}{performerID}
return qb.queryScenes(scenesForPerformerQuery, args, nil)
}
func (qb *SceneQueryBuilder) CountByPerformerID(performerID int) (int, error) {
args := []interface{}{performerID}
return runCountQuery(buildCountQuery(scenesForPerformerQuery), args)
}
func (qb *SceneQueryBuilder) FindByStudioID(studioID int) ([]Scene, error) {
args := []interface{}{studioID}
return qb.queryScenes(scenesForStudioQuery, args, nil)
}
func (qb *SceneQueryBuilder) Count() (int, error) {
return runCountQuery(buildCountQuery("SELECT scenes.id FROM scenes"), nil)
}
func (qb *SceneQueryBuilder) CountByStudioID(studioID int) (int, error) {
args := []interface{}{studioID}
return runCountQuery(buildCountQuery(scenesForStudioQuery), args)
}
func (qb *SceneQueryBuilder) CountByTagID(tagID int) (int, error) {
args := []interface{}{tagID}
return runCountQuery(buildCountQuery(scenesForTagQuery), args)
}
func (qb *SceneQueryBuilder) Wall(q *string) ([]Scene, error) {
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)
}
func (qb *SceneQueryBuilder) All() ([]Scene, error) {
return qb.queryScenes(selectAll("scenes")+qb.getSceneSort(nil), nil, nil)
}
func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *FindFilterType) ([]Scene, int) {
if sceneFilter == nil {
sceneFilter = &SceneFilterType{}
}
if findFilter == nil {
findFilter = &FindFilterType{}
}
whereClauses := []string{}
havingClauses := []string{}
args := []interface{}{}
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 {
whereClauses = append(whereClauses, "rating = ?")
args = append(args, *sceneFilter.Rating)
}
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")
default:
whereClauses = append(whereClauses, "scenes."+*isMissingFilter+" IS NULL")
}
}
if tagsFilter := sceneFilter.Tags; len(tagsFilter) > 0 {
for _, tagID := range tagsFilter {
args = append(args, tagID)
}
whereClauses = append(whereClauses, "tags.id IN "+getInBinding(len(tagsFilter)))
havingClauses = append(havingClauses, "count(distinct tags.id) IS "+strconv.Itoa(len(tagsFilter)))
}
if performerID := sceneFilter.PerformerID; performerID != nil {
whereClauses = append(whereClauses, "performers.id = ?")
args = append(args, *performerID)
}
if studioID := sceneFilter.StudioID; studioID != nil {
whereClauses = append(whereClauses, "studio.id = ?")
args = append(args, *studioID)
}
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
}
func (qb *SceneQueryBuilder) getSceneSort(findFilter *FindFilterType) string {
if findFilter == nil {
return " ORDER BY scenes.path, scenes.date ASC "
}
sort := findFilter.GetSort("title")
direction := findFilter.GetDirection()
return getSort(sort, direction, "scenes")
}
func (qb *SceneQueryBuilder) queryScene(query string, args []interface{}, tx *sqlx.Tx) (*Scene, error) {
results, err := qb.queryScenes(query, args, tx)
if err != nil || len(results) < 1 {
return nil, err
}
return &results[0], nil
}
func (qb *SceneQueryBuilder) queryScenes(query string, args []interface{}, tx *sqlx.Tx) ([]Scene, error) {
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()
scenes := make([]Scene, 0)
scene := Scene{}
for rows.Next() {
if err := rows.StructScan(&scene); err != nil {
return nil, err
}
scenes = append(scenes, scene)
}
if err := rows.Err(); err != nil {
return nil, err
}
return scenes, nil
}

View File

@@ -0,0 +1,255 @@
package models
import (
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"strconv"
)
const sceneMarkersForTagQuery = `
SELECT scene_markers.* FROM scene_markers
LEFT JOIN scene_markers_tags as tags_join on tags_join.scene_marker_id = scene_markers.id
LEFT JOIN tags on tags_join.tag_id = tags.id
WHERE tags.id = ?
GROUP BY scene_markers.id
`
type SceneMarkerQueryBuilder struct{}
func NewSceneMarkerQueryBuilder() SceneMarkerQueryBuilder {
return SceneMarkerQueryBuilder{}
}
func (qb *SceneMarkerQueryBuilder) Create(newSceneMarker SceneMarker, tx *sqlx.Tx) (*SceneMarker, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO scene_markers (title, seconds, primary_tag_id, scene_id, created_at, updated_at)
VALUES (:title, :seconds, :primary_tag_id, :scene_id, :created_at, :updated_at)
`,
newSceneMarker,
)
if err != nil {
return nil, err
}
sceneMarkerID, err := result.LastInsertId()
if err != nil {
return nil, err
}
if err := tx.Get(&newSceneMarker, `SELECT * FROM scene_markers WHERE id = ? LIMIT 1`, sceneMarkerID); err != nil {
return nil, err
}
return &newSceneMarker, nil
}
func (qb *SceneMarkerQueryBuilder) Update(updatedSceneMarker SceneMarker, tx *sqlx.Tx) (*SceneMarker, error) {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE scene_markers SET `+SQLGenKeys(updatedSceneMarker)+` WHERE scene_markers.id = :id`,
updatedSceneMarker,
)
if err != nil {
return nil, err
}
if err := tx.Get(&updatedSceneMarker, `SELECT * FROM scene_markers WHERE id = ? LIMIT 1`, updatedSceneMarker.ID); err != nil {
return nil, err
}
return &updatedSceneMarker, nil
}
func (qb *SceneMarkerQueryBuilder) Destroy(id string, tx *sqlx.Tx) error {
return executeDeleteQuery("scene_markers", id, tx)
}
func (qb *SceneMarkerQueryBuilder) Find(id int) (*SceneMarker, error) {
query := "SELECT * FROM scene_markers WHERE id = ? LIMIT 1"
args := []interface{}{id}
results, err := qb.querySceneMarkers(query, args, nil)
if err != nil || len(results) < 1 {
return nil, err
}
return &results[0], nil
}
func (qb *SceneMarkerQueryBuilder) FindBySceneID(sceneID int, tx *sqlx.Tx) ([]SceneMarker, error) {
query := `
SELECT scene_markers.* FROM scene_markers
JOIN scenes ON scenes.id = scene_markers.scene_id
WHERE scenes.id = ?
GROUP BY scene_markers.id
ORDER BY scene_markers.seconds ASC
`
args := []interface{}{sceneID}
return qb.querySceneMarkers(query, args, tx)
}
func (qb *SceneMarkerQueryBuilder) CountByTagID(tagID int) (int, error) {
args := []interface{}{tagID}
return runCountQuery(buildCountQuery(sceneMarkersForTagQuery), args)
}
func (qb *SceneMarkerQueryBuilder) GetMarkerStrings(q *string, sort *string) ([]*MarkerStringsResultType, error) {
query := "SELECT count(*) as `count`, scene_markers.id as id, scene_markers.title as title FROM scene_markers"
if q != nil {
query = query + " WHERE title LIKE '%" + *q + "%'"
}
query = query + " GROUP BY title"
if sort != nil && *sort == "count" {
query = query + " ORDER BY `count` DESC"
} else {
query = query + " ORDER BY title ASC"
}
args := []interface{}{}
return qb.queryMarkerStringsResultType(query, args)
}
func (qb *SceneMarkerQueryBuilder) Wall(q *string) ([]SceneMarker, error) {
s := ""
if q != nil {
s = *q
}
query := "SELECT scene_markers.* FROM scene_markers WHERE scene_markers.title LIKE '%" + s + "%' ORDER BY RANDOM() LIMIT 80"
return qb.querySceneMarkers(query, nil, nil)
}
func (qb *SceneMarkerQueryBuilder) Query(sceneMarkerFilter *SceneMarkerFilterType, findFilter *FindFilterType) ([]SceneMarker, int) {
if sceneMarkerFilter == nil {
sceneMarkerFilter = &SceneMarkerFilterType{}
}
if findFilter == nil {
findFilter = &FindFilterType{}
}
whereClauses := []string{}
havingClauses := []string{}
args := []interface{}{}
body := selectDistinctIDs("scene_markers")
body = body + `
left join tags as primary_tag on primary_tag.id = scene_markers.primary_tag_id
left join scenes as scene on scene.id = scene_markers.scene_id
left join scene_markers_tags as tags_join on tags_join.scene_marker_id = scene_markers.id
left join tags on tags_join.tag_id = tags.id
`
if tagIDs := sceneMarkerFilter.Tags; tagIDs != nil {
//select `scene_markers`.* from `scene_markers`
//left join `tags` as `primary_tags_join`
// on `primary_tags_join`.`id` = `scene_markers`.`primary_tag_id`
// and `primary_tags_join`.`id` in ('3', '37', '9', '89')
//left join `scene_markers_tags` as `tags_join`
// on `tags_join`.`scene_marker_id` = `scene_markers`.`id`
// and `tags_join`.`tag_id` in ('3', '37', '9', '89')
//group by `scene_markers`.`id`
//having ((count(distinct `primary_tags_join`.`id`) + count(distinct `tags_join`.`tag_id`)) = 4)
length := len(tagIDs)
body += " LEFT JOIN tags AS ptj ON ptj.id = scene_markers.primary_tag_id AND ptj.id IN " + getInBinding(length)
body += " LEFT JOIN scene_markers_tags AS tj ON tj.scene_marker_id = scene_markers.id AND tj.tag_id IN " + getInBinding(length)
havingClauses = append(havingClauses, "((COUNT(DISTINCT ptj.id) + COUNT(DISTINCT tj.tag_id)) = "+strconv.Itoa(length)+")")
for _, tagID := range tagIDs {
args = append(args, tagID)
}
for _, tagID := range tagIDs {
args = append(args, tagID)
}
}
if sceneTagIDs := sceneMarkerFilter.SceneTags; sceneTagIDs != nil {
length := len(sceneTagIDs)
body += " LEFT JOIN scenes_tags AS scene_tags_join ON scene_tags_join.scene_id = scene.id AND scene_tags_join.tag_id IN " + getInBinding(length)
havingClauses = append(havingClauses, "COUNT(DISTINCT scene_tags_join.tag_id) = "+strconv.Itoa(length))
for _, tagID := range sceneTagIDs {
args = append(args, tagID)
}
}
if performerIDs := sceneMarkerFilter.Performers; performerIDs != nil {
length := len(performerIDs)
body += " LEFT JOIN performers_scenes as scene_performers ON scene.id = scene_performers.scene_id"
whereClauses = append(whereClauses, "scene_performers.performer_id IN "+getInBinding(length))
for _, performerID := range performerIDs {
args = append(args, performerID)
}
}
if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"scene_markers.title", "scene.title"}
whereClauses = append(whereClauses, getSearch(searchColumns, *q))
}
if tagID := sceneMarkerFilter.TagID; tagID != nil {
whereClauses = append(whereClauses, "(scene_markers.primary_tag_id = "+*tagID+" OR tags.id = "+*tagID+")")
}
sortAndPagination := qb.getSceneMarkerSort(findFilter) + getPagination(findFilter)
idsResult, countResult := executeFindQuery("scene_markers", body, args, sortAndPagination, whereClauses, havingClauses)
var sceneMarkers []SceneMarker
for _, id := range idsResult {
sceneMarker, _ := qb.Find(id)
sceneMarkers = append(sceneMarkers, *sceneMarker)
}
return sceneMarkers, countResult
}
func (qb *SceneMarkerQueryBuilder) getSceneMarkerSort(findFilter *FindFilterType) string {
sort := findFilter.GetSort("title")
direction := findFilter.GetDirection()
return getSort(sort, direction, "scene_markers")
}
func (qb *SceneMarkerQueryBuilder) querySceneMarkers(query string, args []interface{}, tx *sqlx.Tx) ([]SceneMarker, error) {
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()
sceneMarkers := make([]SceneMarker, 0)
sceneMarker := SceneMarker{}
for rows.Next() {
if err := rows.StructScan(&sceneMarker); err != nil {
return nil, err
}
sceneMarkers = append(sceneMarkers, sceneMarker)
}
if err := rows.Err(); err != nil {
return nil, err
}
return sceneMarkers, nil
}
func (qb *SceneMarkerQueryBuilder) queryMarkerStringsResultType(query string, args []interface{}) ([]*MarkerStringsResultType, error) {
rows, err := database.DB.Queryx(query, args...)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
defer rows.Close()
markerStrings := make([]*MarkerStringsResultType, 0)
for rows.Next() {
markerString := MarkerStringsResultType{}
if err := rows.StructScan(&markerString); err != nil {
return nil, err
}
markerStrings = append(markerStrings, &markerString)
}
if err := rows.Err(); err != nil {
return nil, err
}
return markerStrings, nil
}

View File

@@ -0,0 +1,113 @@
package models
import (
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
)
type ScrapedItemQueryBuilder struct{}
func NewScrapedItemQueryBuilder() ScrapedItemQueryBuilder {
return ScrapedItemQueryBuilder{}
}
func (qb *ScrapedItemQueryBuilder) Create(newScrapedItem ScrapedItem, tx *sqlx.Tx) (*ScrapedItem, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO scraped_items (title, description, url, date, rating, tags, models, episode, gallery_filename,
gallery_url, video_filename, video_url, studio_id, created_at, updated_at)
VALUES (:title, :description, :url, :date, :rating, :tags, :models, :episode, :gallery_filename,
:gallery_url, :video_filename, :video_url, :studio_id, :created_at, :updated_at)
`,
newScrapedItem,
)
if err != nil {
return nil, err
}
scrapedItemID, err := result.LastInsertId()
if err != nil {
return nil, err
}
if err := tx.Get(&newScrapedItem, `SELECT * FROM scraped_items WHERE id = ? LIMIT 1`, scrapedItemID); err != nil {
return nil, err
}
return &newScrapedItem, nil
}
func (qb *ScrapedItemQueryBuilder) Update(updatedScrapedItem ScrapedItem, tx *sqlx.Tx) (*ScrapedItem, error) {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE scraped_items SET `+SQLGenKeys(updatedScrapedItem)+` WHERE scraped_items.id = :id`,
updatedScrapedItem,
)
if err != nil {
return nil, err
}
if err := tx.Get(&updatedScrapedItem, `SELECT * FROM scraped_items WHERE id = ? LIMIT 1`, updatedScrapedItem.ID); err != nil {
return nil, err
}
return &updatedScrapedItem, nil
}
func (qb *ScrapedItemQueryBuilder) Find(id int) (*ScrapedItem, error) {
query := "SELECT * FROM scraped_items WHERE id = ? LIMIT 1"
args := []interface{}{id}
return qb.queryScrapedItem(query, args, nil)
}
func (qb *ScrapedItemQueryBuilder) All() ([]ScrapedItem, error) {
return qb.queryScrapedItems(selectAll("scraped_items")+qb.getScrapedItemsSort(nil), nil, nil)
}
func (qb *ScrapedItemQueryBuilder) getScrapedItemsSort(findFilter *FindFilterType) string {
var sort string
var direction string
if findFilter == nil {
sort = "id" // TODO studio_id and title
direction = "ASC"
} else {
sort = findFilter.GetSort("id")
direction = findFilter.GetDirection()
}
return getSort(sort, direction, "scraped_items")
}
func (qb *ScrapedItemQueryBuilder) queryScrapedItem(query string, args []interface{}, tx *sqlx.Tx) (*ScrapedItem, error) {
results, err := qb.queryScrapedItems(query, args, tx)
if err != nil || len(results) < 1 {
return nil, err
}
return &results[0], nil
}
func (qb *ScrapedItemQueryBuilder) queryScrapedItems(query string, args []interface{}, tx *sqlx.Tx) ([]ScrapedItem, error) {
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()
scrapedItems := make([]ScrapedItem, 0)
scrapedItem := ScrapedItem{}
for rows.Next() {
if err := rows.StructScan(&scrapedItem); err != nil {
return nil, err
}
scrapedItems = append(scrapedItems, scrapedItem)
}
if err := rows.Err(); err != nil {
return nil, err
}
return scrapedItems, nil
}

View File

@@ -0,0 +1,234 @@
package models
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"reflect"
"strconv"
"strings"
)
func selectAll(tableName string) string {
idColumn := getColumn(tableName, "*")
return "SELECT " + idColumn + " FROM " + tableName + " "
}
func selectDistinctIDs(tableName string) string {
idColumn := getColumn(tableName, "id")
return "SELECT DISTINCT " + idColumn + " FROM " + tableName + " "
}
func buildCountQuery(query string) string {
return "SELECT COUNT(*) as count FROM (" + query + ") as temp"
}
func getColumn(tableName string, columnName string) string {
return tableName + "." + columnName
}
func getPagination(findFilter *FindFilterType) string {
if findFilter == nil {
panic("nil find filter for pagination")
}
var page int
if findFilter.Page == nil || *findFilter.Page < 1 {
page = 1
} else {
page = *findFilter.Page
}
var perPage int
if findFilter.PerPage == nil {
perPage = 25
} else {
perPage = *findFilter.PerPage
}
if perPage > 120 {
perPage = 120
} else if perPage < 1 {
perPage = 1
}
page = (page - 1) * perPage
return " LIMIT " + strconv.Itoa(perPage) + " OFFSET " + strconv.Itoa(page) + " "
}
func getSort(sort string, direction string, tableName string) string {
if direction != "ASC" && direction != "DESC" {
direction = "ASC"
}
if strings.Contains(sort, "_count") {
var relationTableName = strings.Split(sort, "_")[0] // TODO: pluralize?
colName := getColumn(relationTableName, "id")
return " ORDER BY COUNT(distinct " + colName + ") " + direction
} else if strings.Compare(sort, "filesize") == 0 {
colName := getColumn(tableName, "size")
return " ORDER BY cast(" + colName + " as integer) " + direction
} else if strings.Compare(sort, "random") == 0 {
return " ORDER BY RANDOM() "
} else {
colName := getColumn(tableName, sort)
return " ORDER BY " + colName + " " + direction
}
}
func getSearch(columns []string, q string) string {
var likeClauses []string
queryWords := strings.Split(q, " ")
trimmedQuery := strings.Trim(q, "\"")
if trimmedQuery == q {
// Search for any word
for _, word := range queryWords {
for _, column := range columns {
likeClauses = append(likeClauses, column+" LIKE '%"+word+"%'")
}
}
} else {
// Search the exact query
for _, column := range columns {
likeClauses = append(likeClauses, column+" LIKE '%"+trimmedQuery+"%'")
}
}
likes := strings.Join(likeClauses, " OR ")
return "(" + likes + ")"
}
func getInBinding(length int) string {
bindings := strings.Repeat("?, ", length)
bindings = strings.TrimRight(bindings, ", ")
return "(" + bindings + ")"
}
func runIdsQuery(query string, args []interface{}) ([]int, error) {
var result []struct {
Int int `db:"id"`
}
if err := database.DB.Select(&result, query, args...); err != nil && err != sql.ErrNoRows {
return []int{}, err
}
vsm := make([]int, len(result))
for i, v := range result {
vsm[i] = v.Int
}
return vsm, nil
}
func runCountQuery(query string, args []interface{}) (int, error) {
// Perform query and fetch result
result := struct {
Int int `db:"count"`
}{0}
if err := database.DB.Get(&result, query, args...); err != nil && err != sql.ErrNoRows {
return 0, err
}
return result.Int, nil
}
func executeFindQuery(tableName string, body string, args []interface{}, sortAndPagination string, whereClauses []string, havingClauses []string) ([]int, int) {
if len(whereClauses) > 0 {
body = body + " WHERE " + strings.Join(whereClauses, " AND ") // TODO handle AND or OR
}
body = body + " GROUP BY " + tableName + ".id "
if len(havingClauses) > 0 {
body = body + " HAVING " + strings.Join(havingClauses, " AND ") // TODO handle AND or OR
}
countQuery := buildCountQuery(body)
countResult, countErr := runCountQuery(countQuery, args)
idsQuery := body + sortAndPagination
idsResult, idsErr := runIdsQuery(idsQuery, args)
if countErr != nil {
panic(countErr)
}
if idsErr != nil {
panic(idsErr)
}
return idsResult, countResult
}
func executeDeleteQuery(tableName string, id string, tx *sqlx.Tx) error {
if tx == nil {
panic("must use a transaction")
}
idColumnName := getColumn(tableName, "id")
_, err := tx.Exec(
`DELETE FROM `+tableName+` WHERE `+idColumnName+` = ?`,
id,
)
return err
}
func ensureTx(tx *sqlx.Tx) {
if tx == nil {
panic("must use a transaction")
}
}
// https://github.com/jmoiron/sqlx/issues/410
// sqlGenKeys is used for passing a struct and returning a string
// of keys for non empty key:values. These keys are formated
// keyname=:keyname with a comma seperating them
func SQLGenKeys(i interface{}) string {
var query []string
v := reflect.ValueOf(i)
for i := 0; i < v.NumField(); i++ {
//get key for struct tag
rawKey := v.Type().Field(i).Tag.Get("db")
key := strings.Split(rawKey, ",")[0]
if key == "id" {
continue
}
switch t := v.Field(i).Interface().(type) {
case string:
if t != "" {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case int:
if t != 0 {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case float64:
if t != 0 {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case SQLiteTimestamp:
if !t.Timestamp.IsZero() {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullString:
if t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullBool:
if t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullInt64:
if t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullFloat64:
if t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
default:
reflectValue := reflect.ValueOf(t)
kind := reflectValue.Kind()
isNil := reflectValue.IsNil()
if kind != reflect.Ptr && !isNil {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
}
}
return strings.Join(query, ", ")
}

View File

@@ -0,0 +1,155 @@
package models
import (
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
)
type StudioQueryBuilder struct{}
func NewStudioQueryBuilder() StudioQueryBuilder {
return StudioQueryBuilder{}
}
func (qb *StudioQueryBuilder) Create(newStudio Studio, tx *sqlx.Tx) (*Studio, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO studios (image, checksum, name, url, created_at, updated_at)
VALUES (:image, :checksum, :name, :url, :created_at, :updated_at)
`,
newStudio,
)
if err != nil {
return nil, err
}
studioID, err := result.LastInsertId()
if err != nil {
return nil, err
}
if err := tx.Get(&newStudio, `SELECT * FROM studios WHERE id = ? LIMIT 1`, studioID); err != nil {
return nil, err
}
return &newStudio, nil
}
func (qb *StudioQueryBuilder) Update(updatedStudio Studio, tx *sqlx.Tx) (*Studio, error) {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE studios SET `+SQLGenKeys(updatedStudio)+` WHERE studios.id = :id`,
updatedStudio,
)
if err != nil {
return nil, err
}
if err := tx.Get(&updatedStudio, `SELECT * FROM studios WHERE id = ? LIMIT 1`, updatedStudio.ID); err != nil {
return nil, err
}
return &updatedStudio, nil
}
func (qb *StudioQueryBuilder) Find(id int, tx *sqlx.Tx) (*Studio, error) {
query := "SELECT * FROM studios WHERE id = ? LIMIT 1"
args := []interface{}{id}
return qb.queryStudio(query, args, tx)
}
func (qb *StudioQueryBuilder) FindBySceneID(sceneID int) (*Studio, error) {
query := "SELECT studios.* FROM studios JOIN scenes ON studios.id = scenes.studio_id WHERE scenes.id = ? LIMIT 1"
args := []interface{}{sceneID}
return qb.queryStudio(query, args, nil)
}
func (qb *StudioQueryBuilder) FindByName(name string, tx *sqlx.Tx) (*Studio, error) {
query := "SELECT * FROM studios WHERE name = ? LIMIT 1"
args := []interface{}{name}
return qb.queryStudio(query, args, tx)
}
func (qb *StudioQueryBuilder) Count() (int, error) {
return runCountQuery(buildCountQuery("SELECT studios.id FROM studios"), nil)
}
func (qb *StudioQueryBuilder) All() ([]Studio, error) {
return qb.queryStudios(selectAll("studios")+qb.getStudioSort(nil), nil, nil)
}
func (qb *StudioQueryBuilder) Query(findFilter *FindFilterType) ([]Studio, int) {
if findFilter == nil {
findFilter = &FindFilterType{}
}
whereClauses := []string{}
havingClauses := []string{}
args := []interface{}{}
body := selectDistinctIDs("studios")
if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"studios.name"}
whereClauses = append(whereClauses, getSearch(searchColumns, *q))
}
sortAndPagination := qb.getStudioSort(findFilter) + getPagination(findFilter)
idsResult, countResult := executeFindQuery("studios", body, args, sortAndPagination, whereClauses, havingClauses)
var studios []Studio
for _, id := range idsResult {
studio, _ := qb.Find(id, nil)
studios = append(studios, *studio)
}
return studios, countResult
}
func (qb *StudioQueryBuilder) getStudioSort(findFilter *FindFilterType) string {
var sort string
var direction string
if findFilter == nil {
sort = "name"
direction = "ASC"
} else {
sort = findFilter.GetSort("name")
direction = findFilter.GetDirection()
}
return getSort(sort, direction, "studios")
}
func (qb *StudioQueryBuilder) queryStudio(query string, args []interface{}, tx *sqlx.Tx) (*Studio, error) {
results, err := qb.queryStudios(query, args, tx)
if err != nil || len(results) < 1 {
return nil, err
}
return &results[0], nil
}
func (qb *StudioQueryBuilder) queryStudios(query string, args []interface{}, tx *sqlx.Tx) ([]Studio, error) {
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()
studios := make([]Studio, 0)
studio := Studio{}
for rows.Next() {
if err := rows.StructScan(&studio); err != nil {
return nil, err
}
studios = append(studios, studio)
}
if err := rows.Err(); err != nil {
return nil, err
}
return studios, nil
}

View File

@@ -0,0 +1,162 @@
package models
import (
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
)
type TagQueryBuilder struct{}
func NewTagQueryBuilder() TagQueryBuilder {
return TagQueryBuilder{}
}
func (qb *TagQueryBuilder) Create(newTag Tag, tx *sqlx.Tx) (*Tag, error) {
ensureTx(tx)
result, err := tx.NamedExec(
`INSERT INTO tags (name, created_at, updated_at)
VALUES (:name, :created_at, :updated_at)
`,
newTag,
)
if err != nil {
return nil, err
}
studioID, err := result.LastInsertId()
if err != nil {
return nil, err
}
if err := tx.Get(&newTag, `SELECT * FROM tags WHERE id = ? LIMIT 1`, studioID); err != nil {
return nil, err
}
return &newTag, nil
}
func (qb *TagQueryBuilder) Update(updatedTag Tag, tx *sqlx.Tx) (*Tag, error) {
ensureTx(tx)
query := `UPDATE tags SET ` + SQLGenKeys(updatedTag) + ` WHERE tags.id = :id`
_, err := tx.NamedExec(
query,
updatedTag,
)
if err != nil {
return nil, err
}
if err := tx.Get(&updatedTag, `SELECT * FROM tags WHERE id = ? LIMIT 1`, updatedTag.ID); err != nil {
return nil, err
}
return &updatedTag, nil
}
func (qb *TagQueryBuilder) Destroy(id string, tx *sqlx.Tx) error {
return executeDeleteQuery("tags", id, tx)
}
func (qb *TagQueryBuilder) Find(id int, tx *sqlx.Tx) (*Tag, error) {
query := "SELECT * FROM tags WHERE id = ? LIMIT 1"
args := []interface{}{id}
return qb.queryTag(query, args, tx)
}
func (qb *TagQueryBuilder) FindBySceneID(sceneID int, tx *sqlx.Tx) ([]Tag, error) {
query := `
SELECT tags.* FROM tags
LEFT JOIN scenes_tags as scenes_join on scenes_join.tag_id = tags.id
LEFT JOIN scenes on scenes_join.scene_id = scenes.id
WHERE scenes.id = ?
GROUP BY tags.id
`
query += qb.getTagSort(nil)
args := []interface{}{sceneID}
return qb.queryTags(query, args, tx)
}
func (qb *TagQueryBuilder) FindBySceneMarkerID(sceneMarkerID int, tx *sqlx.Tx) ([]Tag, error) {
query := `
SELECT tags.* FROM tags
LEFT JOIN scene_markers_tags as scene_markers_join on scene_markers_join.tag_id = tags.id
LEFT JOIN scene_markers on scene_markers_join.scene_marker_id = scene_markers.id
WHERE scene_markers.id = ?
GROUP BY tags.id
`
query += qb.getTagSort(nil)
args := []interface{}{sceneMarkerID}
return qb.queryTags(query, args, tx)
}
func (qb *TagQueryBuilder) FindByName(name string, tx *sqlx.Tx) (*Tag, error) {
query := "SELECT * FROM tags WHERE name = ? LIMIT 1"
args := []interface{}{name}
return qb.queryTag(query, args, tx)
}
func (qb *TagQueryBuilder) FindByNames(names []string, tx *sqlx.Tx) ([]Tag, error) {
query := "SELECT * FROM tags WHERE name IN " + getInBinding(len(names))
var args []interface{}
for _, name := range names {
args = append(args, name)
}
return qb.queryTags(query, args, tx)
}
func (qb *TagQueryBuilder) Count() (int, error) {
return runCountQuery(buildCountQuery("SELECT tags.id FROM tags"), nil)
}
func (qb *TagQueryBuilder) All() ([]Tag, error) {
return qb.queryTags(selectAll("tags")+qb.getTagSort(nil), nil, nil)
}
func (qb *TagQueryBuilder) getTagSort(findFilter *FindFilterType) string {
var sort string
var direction string
if findFilter == nil {
sort = "name"
direction = "ASC"
} else {
sort = findFilter.GetSort("name")
direction = findFilter.GetDirection()
}
return getSort(sort, direction, "tags")
}
func (qb *TagQueryBuilder) queryTag(query string, args []interface{}, tx *sqlx.Tx) (*Tag, error) {
results, err := qb.queryTags(query, args, tx)
if err != nil || len(results) < 1 {
return nil, err
}
return &results[0], nil
}
func (qb *TagQueryBuilder) queryTags(query string, args []interface{}, tx *sqlx.Tx) ([]Tag, error) {
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()
tags := make([]Tag, 0)
tag := Tag{}
for rows.Next() {
if err := rows.StructScan(&tag); err != nil {
return nil, err
}
tags = append(tags, tag)
}
if err := rows.Err(); err != nil {
return nil, err
}
return tags, nil
}

View File

@@ -0,0 +1,21 @@
package models
import (
"database/sql/driver"
"time"
)
type SQLiteTimestamp struct {
Timestamp time.Time
}
// Scan implements the Scanner interface.
func (t *SQLiteTimestamp) Scan(value interface{}) error {
t.Timestamp = value.(time.Time)
return nil
}
// Value implements the driver Valuer interface.
func (t SQLiteTimestamp) Value() (driver.Value, error) {
return t.Timestamp.Format(time.RFC3339), nil
}