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:
@@ -19,28 +19,36 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp
|
||||
// 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
|
||||
@@ -53,6 +61,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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -62,9 +62,13 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
|
||||
@@ -222,7 +222,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"
|
||||
|
||||
@@ -113,7 +113,7 @@ func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
|
||||
err = waitAndDeregister(probeResult.Path, cmd)
|
||||
|
||||
if 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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,455 +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 SceneDestroyInput struct {
|
||||
ID string `json:"id"`
|
||||
DeleteFile *bool `json:"delete_file"`
|
||||
DeleteGenerated *bool `json:"delete_generated"`
|
||||
}
|
||||
|
||||
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 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}
|
||||
|
||||
@@ -61,29 +61,30 @@ 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) Destroy(id string, tx *sqlx.Tx) error {
|
||||
return executeDeleteQuery("scenes", 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) {
|
||||
@@ -210,6 +211,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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