mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Merge from master
This commit is contained in:
@@ -56,3 +56,23 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
|
||||
return makeConfigGeneralResult(), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) {
|
||||
css := ""
|
||||
|
||||
if input.CSS != nil {
|
||||
css = *input.CSS
|
||||
}
|
||||
|
||||
config.SetCSS(css)
|
||||
|
||||
if input.CSSEnabled != nil {
|
||||
config.Set(config.CSSEnabled, *input.CSSEnabled)
|
||||
}
|
||||
|
||||
if err := config.Write(); err != nil {
|
||||
return makeConfigInterfaceResult(), err
|
||||
}
|
||||
|
||||
return makeConfigInterfaceResult(), nil
|
||||
}
|
||||
|
||||
@@ -175,3 +175,17 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
|
||||
return performer, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) PerformerDestroy(ctx context.Context, input models.PerformerDestroyInput) (bool, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
if err := qb.Destroy(input.ID, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,11 +3,12 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) StudioCreate(ctx context.Context, input models.StudioCreateInput) (*models.Studio, error) {
|
||||
@@ -85,3 +86,16 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
||||
|
||||
return studio, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) StudioDestroy(ctx context.Context, input models.StudioDestroyInput) (bool, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
if err := qb.Destroy(input.ID, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ func (r *queryResolver) Directories(ctx context.Context, path *string) ([]string
|
||||
|
||||
func makeConfigResult() *models.ConfigResult {
|
||||
return &models.ConfigResult{
|
||||
General: makeConfigGeneralResult(),
|
||||
General: makeConfigGeneralResult(),
|
||||
Interface: makeConfigInterfaceResult(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,3 +36,12 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
Password: config.GetPasswordHash(),
|
||||
}
|
||||
}
|
||||
|
||||
func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
css := config.GetCSS()
|
||||
cssEnabled := config.GetCSSEnabled()
|
||||
return &models.ConfigInterfaceResult{
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -100,6 +100,21 @@ func Start() {
|
||||
r.Mount("/scene", sceneRoutes{}.Routes())
|
||||
r.Mount("/studio", studioRoutes{}.Routes())
|
||||
|
||||
r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !config.GetCSSEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
// search for custom.css in current directory, then $HOME/.stash
|
||||
fn := config.GetCSSPath()
|
||||
exists, _ := utils.FileExists(fn)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, fn)
|
||||
})
|
||||
|
||||
// Serve the setup UI
|
||||
r.HandleFunc("/setup*", func(w http.ResponseWriter, r *http.Request) {
|
||||
ext := path.Ext(r.URL.Path)
|
||||
@@ -234,7 +249,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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -28,6 +29,8 @@ type VideoFile struct {
|
||||
VideoStream *FFProbeStream
|
||||
|
||||
Path string
|
||||
Title string
|
||||
Comment string
|
||||
Container string
|
||||
Duration float64
|
||||
StartTime float64
|
||||
@@ -82,6 +85,14 @@ func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
|
||||
//} // TODO nil_or_unsupported.(video_stream) && nil_or_unsupported.(audio_stream)
|
||||
|
||||
result.Path = filePath
|
||||
result.Title = probeJSON.Format.Tags.Title
|
||||
|
||||
if result.Title == "" {
|
||||
// default title to filename
|
||||
result.SetTitleFromPath()
|
||||
}
|
||||
|
||||
result.Comment = probeJSON.Format.Tags.Comment
|
||||
|
||||
result.Bitrate, _ = strconv.ParseInt(probeJSON.Format.BitRate, 10, 64)
|
||||
result.Container = probeJSON.Format.FormatName
|
||||
@@ -150,3 +161,7 @@ func (v *VideoFile) getStreamIndex(fileType string, probeJSON FFProbeJSON) int {
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (v *VideoFile) SetTitleFromPath() {
|
||||
v.Title = filepath.Base(v.Path)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ type FFProbeJSON struct {
|
||||
Encoder string `json:"encoder"`
|
||||
MajorBrand string `json:"major_brand"`
|
||||
MinorVersion string `json:"minor_version"`
|
||||
Title string `json:"title"`
|
||||
Comment string `json:"comment"`
|
||||
} `json:"tags"`
|
||||
} `json:"format"`
|
||||
Streams []FFProbeStream `json:"streams"`
|
||||
|
||||
@@ -3,7 +3,10 @@ package config
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"io/ioutil"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
const Stash = "stash"
|
||||
@@ -19,6 +22,8 @@ const Database = "database"
|
||||
const Host = "host"
|
||||
const Port = "port"
|
||||
|
||||
const CSSEnabled = "cssEnabled"
|
||||
|
||||
func Set(key string, value interface{}) {
|
||||
viper.Set(key, value)
|
||||
}
|
||||
@@ -110,6 +115,46 @@ func ValidateCredentials(username string, password string) bool {
|
||||
return username == authUser && err == nil
|
||||
}
|
||||
|
||||
func GetCSSPath() string {
|
||||
// search for custom.css in current directory, then $HOME/.stash
|
||||
fn := "custom.css"
|
||||
exists, _ := utils.FileExists(fn)
|
||||
if !exists {
|
||||
fn = "$HOME/.stash/" + fn
|
||||
}
|
||||
|
||||
return fn
|
||||
}
|
||||
|
||||
func GetCSS() string {
|
||||
fn := GetCSSPath()
|
||||
|
||||
exists, _ := utils.FileExists(fn)
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
buf, err := ioutil.ReadFile(fn)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func SetCSS(css string) {
|
||||
fn := GetCSSPath()
|
||||
|
||||
buf := []byte(css)
|
||||
|
||||
ioutil.WriteFile(fn, buf, 0777)
|
||||
}
|
||||
|
||||
func GetCSSEnabled() bool {
|
||||
return viper.GetBool(CSSEnabled)
|
||||
}
|
||||
|
||||
func IsValid() bool {
|
||||
setPaths := viper.IsSet(Stash) && viper.IsSet(Cache) && viper.IsSet(Generated) && viper.IsSet(Metadata)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
@@ -10,8 +11,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/manager/paths"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type singleton struct {
|
||||
@@ -71,12 +70,15 @@ func initConfig() {
|
||||
// Set generated to the metadata path for backwards compat
|
||||
viper.SetDefault(config.Generated, viper.GetString(config.Metadata))
|
||||
|
||||
// Disabling config watching due to race condition issue
|
||||
// See: https://github.com/spf13/viper/issues/174
|
||||
// Changes to the config outside the system will require a restart
|
||||
// Watch for changes
|
||||
viper.WatchConfig()
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
fmt.Println("Config file changed:", e.Name)
|
||||
instance.refreshConfig()
|
||||
})
|
||||
// viper.WatchConfig()
|
||||
// viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
// fmt.Println("Config file changed:", e.Name)
|
||||
// instance.refreshConfig()
|
||||
// })
|
||||
|
||||
//viper.Set("stash", []string{"/", "/stuff"})
|
||||
//viper.WriteConfig()
|
||||
@@ -92,15 +94,15 @@ func initFlags() {
|
||||
}
|
||||
}
|
||||
|
||||
func initEnvs() {
|
||||
func initEnvs() {
|
||||
viper.SetEnvPrefix("stash") // will be uppercased automatically
|
||||
viper.BindEnv("host") // STASH_HOST
|
||||
viper.BindEnv("port") // STASH_PORT
|
||||
viper.BindEnv("stash") // STASH_STASH
|
||||
viper.BindEnv("generated") // STASH_GENERATED
|
||||
viper.BindEnv("metadata") // STASH_METADATA
|
||||
viper.BindEnv("cache") // STASH_CACHE
|
||||
}
|
||||
viper.BindEnv("host") // STASH_HOST
|
||||
viper.BindEnv("port") // STASH_PORT
|
||||
viper.BindEnv("stash") // STASH_STASH
|
||||
viper.BindEnv("generated") // STASH_GENERATED
|
||||
viper.BindEnv("metadata") // STASH_METADATA
|
||||
viper.BindEnv("cache") // STASH_CACHE
|
||||
}
|
||||
|
||||
func initFFMPEG() {
|
||||
configDirectory := paths.GetConfigDirectory()
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/bmatcuk/doublestar"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func (s *singleton) Scan() {
|
||||
func (s *singleton) Scan(nameFromMetadata bool) {
|
||||
if s.Status != Idle {
|
||||
return
|
||||
}
|
||||
@@ -30,10 +31,12 @@ 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()
|
||||
}
|
||||
|
||||
logger.Info("Finished scan")
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,19 +3,21 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ScanTask struct {
|
||||
FilePath string
|
||||
FilePath string
|
||||
NameFromMetadata bool
|
||||
}
|
||||
|
||||
func (t *ScanTask) Start(wg *sync.WaitGroup) {
|
||||
@@ -46,9 +48,15 @@ func (t *ScanTask) scanGallery() {
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
gallery, _ = qb.FindByChecksum(checksum, tx)
|
||||
if gallery != nil {
|
||||
logger.Infof("%s already exists. Updating path...", t.FilePath)
|
||||
gallery.Path = t.FilePath
|
||||
_, err = qb.Update(*gallery, tx)
|
||||
exists, _ := utils.FileExists(gallery.Path)
|
||||
if exists {
|
||||
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, gallery.Path)
|
||||
} else {
|
||||
|
||||
logger.Infof("%s already exists. Updating path...", t.FilePath)
|
||||
gallery.Path = t.FilePath
|
||||
_, err = qb.Update(*gallery, tx)
|
||||
}
|
||||
} else {
|
||||
logger.Infof("%s doesn't exist. Creating new item...", t.FilePath)
|
||||
currentTime := time.Now()
|
||||
@@ -83,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())
|
||||
@@ -95,15 +108,26 @@ func (t *ScanTask) scanScene() {
|
||||
ctx := context.TODO()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
if scene != nil {
|
||||
logger.Infof("%s already exists. Updating path...", t.FilePath)
|
||||
scene.Path = t.FilePath
|
||||
_, err = qb.Update(*scene, tx)
|
||||
exists, _ := utils.FileExists(scene.Path)
|
||||
if exists {
|
||||
logger.Infof("%s already exists. Duplicate of %s ", t.FilePath, scene.Path)
|
||||
} else {
|
||||
logger.Infof("%s already exists. Updating path...", t.FilePath)
|
||||
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)
|
||||
currentTime := time.Now()
|
||||
newScene := models.Scene{
|
||||
Checksum: checksum,
|
||||
Path: t.FilePath,
|
||||
Title: sql.NullString{String: videoFile.Title, Valid: true},
|
||||
Details: sql.NullString{String: videoFile.Comment, Valid: true},
|
||||
Date: models.SQLiteDate{String: videoFile.CreationTime.Format("2006-01-02")},
|
||||
Duration: sql.NullFloat64{Float64: videoFile.Duration, Valid: true},
|
||||
VideoCodec: sql.NullString{String: videoFile.VideoCodec, Valid: true},
|
||||
AudioCodec: sql.NullString{String: videoFile.AudioCodec, Valid: true},
|
||||
|
||||
@@ -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
@@ -1,436 +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"`
|
||||
// Username
|
||||
Username *string `json:"username"`
|
||||
// Password
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
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"`
|
||||
// Username
|
||||
Username string `json:"username"`
|
||||
// Password
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// All configuration settings
|
||||
type ConfigResult struct {
|
||||
General *ConfigGeneralResult `json:"general"`
|
||||
}
|
||||
|
||||
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 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 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 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()))
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
)
|
||||
@@ -54,6 +55,15 @@ func (qb *PerformerQueryBuilder) Update(updatedPerformer Performer, tx *sqlx.Tx)
|
||||
return &updatedPerformer, nil
|
||||
}
|
||||
|
||||
func (qb *PerformerQueryBuilder) Destroy(id string, tx *sqlx.Tx) error {
|
||||
_, err := tx.Exec("DELETE FROM performers_scenes WHERE performer_id = ?", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return executeDeleteQuery("performers", id, tx)
|
||||
}
|
||||
|
||||
func (qb *PerformerQueryBuilder) Find(id int) (*Performer, error) {
|
||||
query := "SELECT * FROM performers WHERE id = ? LIMIT 1"
|
||||
args := []interface{}{id}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
)
|
||||
@@ -50,6 +51,22 @@ func (qb *StudioQueryBuilder) Update(updatedStudio Studio, tx *sqlx.Tx) (*Studio
|
||||
return &updatedStudio, nil
|
||||
}
|
||||
|
||||
func (qb *StudioQueryBuilder) Destroy(id string, tx *sqlx.Tx) error {
|
||||
// remove studio from scenes
|
||||
_, err := tx.Exec("UPDATE scenes SET studio_id = null WHERE studio_id = ?", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove studio from scraped items
|
||||
_, err = tx.Exec("UPDATE scraped_items SET studio_id = null WHERE studio_id = ?", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return executeDeleteQuery("studios", id, tx)
|
||||
}
|
||||
|
||||
func (qb *StudioQueryBuilder) Find(id int, tx *sqlx.Tx) (*Studio, error) {
|
||||
query := "SELECT * FROM studios WHERE id = ? LIMIT 1"
|
||||
args := []interface{}{id}
|
||||
@@ -86,7 +103,7 @@ func (qb *StudioQueryBuilder) Query(findFilter *FindFilterType) ([]*Studio, int)
|
||||
var args []interface{}
|
||||
body := selectDistinctIDs("studios")
|
||||
body += `
|
||||
join scenes on studios.id = scenes.studio_id
|
||||
left join scenes on studios.id = scenes.studio_id
|
||||
`
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user