Merge from master

This commit is contained in:
WithoutPants
2019-10-17 10:16:36 +11:00
216 changed files with 831 additions and 35960 deletions

View File

@@ -3,38 +3,47 @@ package api
import (
"context"
"database/sql"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/models"
"strconv"
"time"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/models"
)
func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (*models.Scene, error) {
// Populate scene from the input
sceneID, _ := strconv.Atoi(input.ID)
updatedTime := time.Now()
updatedScene := models.Scene{
updatedScene := models.ScenePartial{
ID: sceneID,
UpdatedAt: models.SQLiteTimestamp{Timestamp: updatedTime},
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
if input.Title != nil {
updatedScene.Title = sql.NullString{String: *input.Title, Valid: true}
updatedScene.Title = &sql.NullString{String: *input.Title, Valid: true}
}
if input.Details != nil {
updatedScene.Details = sql.NullString{String: *input.Details, Valid: true}
updatedScene.Details = &sql.NullString{String: *input.Details, Valid: true}
}
if input.URL != nil {
updatedScene.URL = sql.NullString{String: *input.URL, Valid: true}
updatedScene.URL = &sql.NullString{String: *input.URL, Valid: true}
}
if input.Date != nil {
updatedScene.Date = models.SQLiteDate{String: *input.Date, Valid: true}
updatedScene.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
}
if input.Rating != nil {
updatedScene.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
updatedScene.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
} else {
// rating must be nullable
updatedScene.Rating = &sql.NullInt64{Valid: false}
}
if input.StudioID != nil {
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
updatedScene.StudioID = sql.NullInt64{Int64: studioID, Valid: true}
updatedScene.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
} else {
// studio must be nullable
updatedScene.StudioID = &sql.NullInt64{Valid: false}
}
// Start the transaction and save the scene marker
@@ -47,6 +56,14 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
return nil, err
}
// Clear the existing gallery value
gqb := models.NewGalleryQueryBuilder()
err = gqb.ClearGalleryId(sceneID, tx)
if err != nil {
_ = tx.Rollback()
return nil, err
}
if input.GalleryID != nil {
// Save the gallery
galleryID, _ := strconv.Atoi(*input.GalleryID)

View File

@@ -2,12 +2,13 @@ package api
import (
"context"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) MetadataScan(ctx context.Context) (string, error) {
manager.GetInstance().Scan()
func (r *queryResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
manager.GetInstance().Scan(input.NameFromMetadata)
return "todo", nil
}

View File

@@ -1,17 +1,18 @@
package api
import (
"io"
"context"
"io"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
"github.com/stashapp/stash/pkg/ffmpeg"
"net/http"
"strconv"
"strings"
)
type sceneRoutes struct{}
@@ -41,7 +42,7 @@ func (rs sceneRoutes) Routes() chi.Router {
func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
// detect if not a streamable file and try to transcode it instead
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.Checksum)
@@ -58,10 +59,14 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
logger.Errorf("[stream] error reading video file: %s", err.Error())
return
}
// start stream based on query param, if provided
r.ParseForm()
startTime := r.Form.Get("start")
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
stream, process, err := encoder.StreamTranscode(*videoFile)
stream, process, err := encoder.StreamTranscode(*videoFile, startTime)
if err != nil {
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
return

View File

@@ -235,7 +235,7 @@ func BaseURLMiddleware(next http.Handler) http.Handler {
ctx := r.Context()
var scheme string
if strings.Compare("https", r.URL.Scheme) == 0 || r.Proto == "HTTP/2.0" {
if strings.Compare("https", r.URL.Scheme) == 0 || r.Proto == "HTTP/2.0" || r.Header.Get("X-Forwarded-Proto") == "https" {
scheme = "https"
} else {
scheme = "http"

View File

@@ -57,7 +57,7 @@ func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
stdoutString := string(stdoutData)
if err := cmd.Wait(); err != nil {
logger.Errorf("ffmpeg error when running command <%s>", strings.Join(cmd.Args, " "))
logger.Errorf("ffmpeg error when running command <%s>: %s", strings.Join(cmd.Args, " "), stdoutString)
return stdoutString, err
}

View File

@@ -26,17 +26,25 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
_, _ = e.run(probeResult, args)
}
func (e *Encoder) StreamTranscode(probeResult VideoFile) (io.ReadCloser, *os.Process, error) {
args := []string{
func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.ReadCloser, *os.Process, error) {
args := []string{}
if startTime != "" {
args = append(args, "-ss", startTime)
}
args = append(args,
"-i", probeResult.Path,
"-c:v", "libvpx-vp9",
"-vf", "scale=iw:-2",
"-deadline", "realtime",
"-cpu-used", "5",
"-row-mt", "1",
"-crf", "30",
"-b:v", "0",
"-f", "webm",
"pipe:",
}
)
return e.stream(probeResult, args)
}

View File

@@ -89,7 +89,7 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
if result.Title == "" {
// default title to filename
result.Title = filepath.Base(result.Path)
result.SetTitleFromPath()
}
result.Comment = probeJSON.Format.Tags.Comment
@@ -161,3 +161,7 @@ func (v *VideoFile) getStreamIndex(fileType string, probeJSON FFProbeJSON) int {
return -1
}
func (v *VideoFile) SetTitleFromPath() {
v.Title = filepath.Base(v.Path)
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
func (s *singleton) Scan() {
func (s *singleton) Scan(nameFromMetadata bool) {
if s.Status != Idle {
return
}
@@ -31,7 +31,7 @@ func (s *singleton) Scan() {
var wg sync.WaitGroup
for _, path := range results {
wg.Add(1)
task := ScanTask{FilePath: path}
task := ScanTask{FilePath: path, NameFromMetadata: nameFromMetadata}
go task.Start(&wg)
wg.Wait()
}

View File

@@ -16,7 +16,8 @@ import (
)
type ScanTask struct {
FilePath string
FilePath string
NameFromMetadata bool
}
func (t *ScanTask) Start(wg *sync.WaitGroup) {
@@ -90,6 +91,11 @@ func (t *ScanTask) scanScene() {
return
}
// Override title to be filename if nameFromMetadata is false
if !t.NameFromMetadata {
videoFile.SetTitleFromPath()
}
checksum, err := t.calculateChecksum()
if err != nil {
logger.Error(err.Error())
@@ -107,8 +113,11 @@ func (t *ScanTask) scanScene() {
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, scene.Path)
} else {
logger.Infof("%s already exists. Updating path...", t.FilePath)
scene.Path = t.FilePath
_, err = qb.Update(*scene, tx)
scenePartial := models.ScenePartial{
ID: scene.ID,
Path: &t.FilePath,
}
_, err = qb.Update(scenePartial, tx)
}
} else {
logger.Infof("%s doesn't exist. Creating new item...", t.FilePath)

View File

@@ -2,6 +2,8 @@ package manager
import (
"fmt"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
@@ -10,15 +12,11 @@ func IsStreamable(scene *models.Scene) (bool, error) {
if scene == nil {
return false, fmt.Errorf("nil scene")
}
fileType, err := utils.FileType(scene.Path)
if err != nil {
return false, err
}
switch fileType.MIME.Value {
case "video/quicktime", "video/mp4", "video/webm", "video/x-m4v":
videoCodec := scene.VideoCodec.String
if ffmpeg.IsValidCodec(videoCodec) {
return true, nil
default:
} else {
hasTranscode, _ := HasTranscode(scene)
return hasTranscode, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,454 +0,0 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package models
import (
"fmt"
"io"
"strconv"
)
type ConfigGeneralInput struct {
// Array of file paths to content
Stashes []string `json:"stashes"`
// Path to the SQLite database
DatabasePath *string `json:"databasePath"`
// Path to generated files
GeneratedPath *string `json:"generatedPath"`
}
type ConfigGeneralResult struct {
// Array of file paths to content
Stashes []string `json:"stashes"`
// Path to the SQLite database
DatabasePath string `json:"databasePath"`
// Path to generated files
GeneratedPath string `json:"generatedPath"`
}
type ConfigInterfaceInput struct {
// Custom CSS
CSS *string `json:"css"`
CSSEnabled *bool `json:"cssEnabled"`
}
type ConfigInterfaceResult struct {
// Custom CSS
CSS *string `json:"css"`
CSSEnabled *bool `json:"cssEnabled"`
}
// All configuration settings
type ConfigResult struct {
General *ConfigGeneralResult `json:"general"`
Interface *ConfigInterfaceResult `json:"interface"`
}
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 GenerateMetadataInput struct {
Sprites bool `json:"sprites"`
Previews bool `json:"previews"`
Markers bool `json:"markers"`
Transcodes bool `json:"transcodes"`
}
type IntCriterionInput struct {
Value int `json:"value"`
Modifier CriterionModifier `json:"modifier"`
}
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 PerformerDestroyInput struct {
ID string `json:"id"`
}
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 *IntCriterionInput `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 StudioDestroyInput struct {
ID string `json:"id"`
}
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 Version struct {
Hash string `json:"hash"`
BuildTime string `json:"build_time"`
}
type CriterionModifier string
const (
// =
CriterionModifierEquals CriterionModifier = "EQUALS"
// !=
CriterionModifierNotEquals CriterionModifier = "NOT_EQUALS"
// >
CriterionModifierGreaterThan CriterionModifier = "GREATER_THAN"
// <
CriterionModifierLessThan CriterionModifier = "LESS_THAN"
// IS NULL
CriterionModifierIsNull CriterionModifier = "IS_NULL"
// IS NOT NULL
CriterionModifierNotNull CriterionModifier = "NOT_NULL"
CriterionModifierIncludes CriterionModifier = "INCLUDES"
CriterionModifierExcludes CriterionModifier = "EXCLUDES"
)
var AllCriterionModifier = []CriterionModifier{
CriterionModifierEquals,
CriterionModifierNotEquals,
CriterionModifierGreaterThan,
CriterionModifierLessThan,
CriterionModifierIsNull,
CriterionModifierNotNull,
CriterionModifierIncludes,
CriterionModifierExcludes,
}
func (e CriterionModifier) IsValid() bool {
switch e {
case CriterionModifierEquals, CriterionModifierNotEquals, CriterionModifierGreaterThan, CriterionModifierLessThan, CriterionModifierIsNull, CriterionModifierNotNull, CriterionModifierIncludes, CriterionModifierExcludes:
return true
}
return false
}
func (e CriterionModifier) String() string {
return string(e)
}
func (e *CriterionModifier) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = CriterionModifier(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid CriterionModifier", str)
}
return nil
}
func (e CriterionModifier) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
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()))
}

View File

@@ -25,3 +25,25 @@ type Scene struct {
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}
type ScenePartial 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 *SQLiteDate `db:"date" json:"date"`
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

@@ -2,9 +2,10 @@ package models
import (
"database/sql"
"path/filepath"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"path/filepath"
)
type GalleryQueryBuilder struct{}
@@ -50,6 +51,24 @@ func (qb *GalleryQueryBuilder) Update(updatedGallery Gallery, tx *sqlx.Tx) (*Gal
return &updatedGallery, nil
}
type GalleryNullSceneID struct {
SceneID sql.NullInt64
}
func (qb *GalleryQueryBuilder) ClearGalleryId(sceneID int, tx *sqlx.Tx) error {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE galleries SET scene_id = null WHERE scene_id = :sceneid`,
GalleryNullSceneID{
SceneID: sql.NullInt64{
Int64: int64(sceneID),
Valid: true,
},
},
)
return err
}
func (qb *GalleryQueryBuilder) Find(id int) (*Gallery, error) {
query := "SELECT * FROM galleries WHERE id = ? LIMIT 1"
args := []interface{}{id}

View File

@@ -2,10 +2,11 @@ package models
import (
"database/sql"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
)
const scenesForPerformerQuery = `
@@ -60,26 +61,27 @@ func (qb *SceneQueryBuilder) Create(newScene Scene, tx *sqlx.Tx) (*Scene, error)
return &newScene, nil
}
func (qb *SceneQueryBuilder) Update(updatedScene Scene, tx *sqlx.Tx) (*Scene, error) {
func (qb *SceneQueryBuilder) Update(updatedScene ScenePartial, tx *sqlx.Tx) (*Scene, error) {
ensureTx(tx)
_, err := tx.NamedExec(
`UPDATE scenes SET `+SQLGenKeys(updatedScene)+` WHERE scenes.id = :id`,
`UPDATE scenes SET `+SQLGenKeysPartial(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
return qb.find(updatedScene.ID, tx)
}
func (qb *SceneQueryBuilder) Find(id int) (*Scene, error) {
return qb.find(id, nil)
}
func (qb *SceneQueryBuilder) find(id int, tx *sqlx.Tx) (*Scene, error) {
query := "SELECT * FROM scenes WHERE id = ? LIMIT 1"
args := []interface{}{id}
return qb.queryScene(query, args, nil)
return qb.queryScene(query, args, tx)
}
func (qb *SceneQueryBuilder) FindByChecksum(checksum string) (*Scene, error) {
@@ -206,6 +208,8 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
whereClauses = append(whereClauses, "scenes.studio_id IS NULL")
case "performers":
whereClauses = append(whereClauses, "performers_join.scene_id IS NULL")
case "date":
whereClauses = append(whereClauses, "scenes.date IS \"\" OR scenes.date IS \"0001-01-01\"")
default:
whereClauses = append(whereClauses, "scenes."+*isMissingFilter+" IS NULL")
}

View File

@@ -3,13 +3,14 @@ package models
import (
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/logger"
"math/rand"
"reflect"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/logger"
)
var randomSortFloat = rand.Float64()
@@ -235,6 +236,17 @@ func ensureTx(tx *sqlx.Tx) {
// of keys for non empty key:values. These keys are formated
// keyname=:keyname with a comma seperating them
func SQLGenKeys(i interface{}) string {
return sqlGenKeys(i, false)
}
// support a partial interface. When a partial interface is provided,
// keys will always be included if the value is not null. The partial
// interface must therefore consist of pointers
func SQLGenKeysPartial(i interface{}) string {
return sqlGenKeys(i, true)
}
func sqlGenKeys(i interface{}, partial bool) string {
var query []string
v := reflect.ValueOf(i)
for i := 0; i < v.NumField(); i++ {
@@ -246,46 +258,45 @@ func SQLGenKeys(i interface{}) string {
}
switch t := v.Field(i).Interface().(type) {
case string:
if t != "" {
if partial || t != "" {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case int:
if t != 0 {
if partial || t != 0 {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case float64:
if t != 0 {
if partial || t != 0 {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case SQLiteTimestamp:
if !t.Timestamp.IsZero() {
if partial || !t.Timestamp.IsZero() {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case SQLiteDate:
if t.Valid {
if partial || t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullString:
if t.Valid {
if partial || t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullBool:
if t.Valid {
if partial || t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullInt64:
if t.Valid {
if partial || t.Valid {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
case sql.NullFloat64:
if t.Valid {
if partial || 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 {
if !isNil {
query = append(query, fmt.Sprintf("%s=:%s", key, key))
}
}

View File

@@ -65,6 +65,10 @@ func GetPerformer(performerName string) (*models.ScrapedPerformer, error) {
if strings.ToLower(s.Text()) == strings.ToLower(performerName) {
return true
}
alias := s.ParentsFiltered(".babeNameBlock").Find(".babeAlias").First();
if strings.EqualFold(alias.Text(), "aka " + performerName) {
return true
}
return false
})