diff --git a/go.sum b/go.sum index 9cee877f8..97938f981 100644 --- a/go.sum +++ b/go.sum @@ -321,8 +321,10 @@ github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= github.com/golang-migrate/migrate/v4 v4.3.1 h1:3eR1NY+pplX+m6yJ1fQf5dFWX3fBgUtZfDiaS/kJVu4= github.com/golang-migrate/migrate/v4 v4.3.1/go.mod h1:mJ89KBgbXmM3P49BqOxRL3riNF/ATlg5kMhm17GA0dE= +github.com/golang-migrate/migrate/v4 v4.10.0 h1:76R6UL3BGnDTpYeittMtfpaNvGBH5zMZatO/fCzIjWo= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/graphql/documents/data/movie.graphql b/graphql/documents/data/movie.graphql index 11a88c934..ef3ab3f9f 100644 --- a/graphql/documents/data/movie.graphql +++ b/graphql/documents/data/movie.graphql @@ -7,6 +7,11 @@ fragment MovieData on Movie { date rating director + + studio { + ...StudioData + } + synopsis url front_image_path diff --git a/graphql/documents/mutations/movie.graphql b/graphql/documents/mutations/movie.graphql index 257fa3ce5..253d2f8ac 100644 --- a/graphql/documents/mutations/movie.graphql +++ b/graphql/documents/mutations/movie.graphql @@ -1,16 +1,17 @@ mutation MovieCreate( $name: String!, $aliases: String, - $duration: String, + $duration: Int, $date: String, - $rating: String, + $rating: Int, + $studio_id: ID, $director: String, $synopsis: String, $url: String, $front_image: String, $back_image: String) { - movieCreate(input: { name: $name, aliases: $aliases, duration: $duration, date: $date, rating: $rating, director: $director, synopsis: $synopsis, url: $url, front_image: $front_image, back_image: $back_image }) { + movieCreate(input: { name: $name, aliases: $aliases, duration: $duration, date: $date, rating: $rating, studio_id: $studio_id, director: $director, synopsis: $synopsis, url: $url, front_image: $front_image, back_image: $back_image }) { ...MovieData } } @@ -19,16 +20,17 @@ mutation MovieUpdate( $id: ID! $name: String, $aliases: String, - $duration: String, + $duration: Int, $date: String, - $rating: String, + $rating: Int, + $studio_id: ID, $director: String, $synopsis: String, $url: String, $front_image: String, $back_image: String) { - movieUpdate(input: { id: $id, name: $name, aliases: $aliases, duration: $duration, date: $date, rating: $rating, director: $director, synopsis: $synopsis, url: $url, front_image: $front_image, back_image: $back_image }) { + movieUpdate(input: { id: $id, name: $name, aliases: $aliases, duration: $duration, date: $date, rating: $rating, studio_id: $studio_id, director: $director, synopsis: $synopsis, url: $url, front_image: $front_image, back_image: $back_image }) { ...MovieData } } diff --git a/graphql/documents/queries/movie.graphql b/graphql/documents/queries/movie.graphql index 93704eb9b..c22b61b5b 100644 --- a/graphql/documents/queries/movie.graphql +++ b/graphql/documents/queries/movie.graphql @@ -1,5 +1,5 @@ -query FindMovies($filter: FindFilterType) { - findMovies(filter: $filter) { +query FindMovies($filter: FindFilterType, $movie_filter: MovieFilterType) { + findMovies(filter: $filter, movie_filter: $movie_filter) { count movies { ...MovieData diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 77960ebf0..9b6f14094 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -25,7 +25,7 @@ type Query { """Find a movie by ID""" findMovie(id: ID!): Movie """A function which queries Movie objects""" - findMovies(filter: FindFilterType): FindMoviesResultType! + findMovies(movie_filter: MovieFilterType, filter: FindFilterType): FindMoviesResultType! findGallery(id: ID!): Gallery findGalleries(filter: FindFilterType): FindGalleriesResultType! diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index efc3552dc..92ac58e2c 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -86,6 +86,11 @@ input SceneFilterType { performers: MultiCriterionInput } +input MovieFilterType { + """Filter to only include movies with this studio""" + studios: MultiCriterionInput +} + enum CriterionModifier { """=""" EQUALS, diff --git a/graphql/schema/types/movie.graphql b/graphql/schema/types/movie.graphql index 28b828678..0b41af0c8 100644 --- a/graphql/schema/types/movie.graphql +++ b/graphql/schema/types/movie.graphql @@ -3,9 +3,11 @@ type Movie { checksum: String! name: String! aliases: String - duration: String + """Duration in seconds""" + duration: Int date: String - rating: String + rating: Int + studio: Studio director: String synopsis: String url: String @@ -18,9 +20,11 @@ type Movie { input MovieCreateInput { name: String! aliases: String - duration: String + """Duration in seconds""" + duration: Int date: String - rating: String + rating: Int + studio_id: ID director: String synopsis: String url: String @@ -33,9 +37,10 @@ input MovieUpdateInput { id: ID! name: String aliases: String - duration: String + duration: Int date: String - rating: String + rating: Int + studio_id: ID director: String synopsis: String url: String diff --git a/graphql/schema/types/scene.graphql b/graphql/schema/types/scene.graphql index e6b5a0e03..8afdd4b43 100644 --- a/graphql/schema/types/scene.graphql +++ b/graphql/schema/types/scene.graphql @@ -20,7 +20,7 @@ type ScenePathsType { type SceneMovie { movie: Movie! - scene_index: String + scene_index: Int } type Scene { @@ -48,7 +48,7 @@ type Scene { input SceneMovieInput { movie_id: ID! - scene_index: String + scene_index: Int } input SceneUpdateInput { diff --git a/pkg/api/migrate.go b/pkg/api/migrate.go index cb21efe72..6305f47f8 100644 --- a/pkg/api/migrate.go +++ b/pkg/api/migrate.go @@ -4,8 +4,10 @@ import ( "fmt" "html/template" "net/http" + "os" "github.com/stashapp/stash/pkg/database" + "github.com/stashapp/stash/pkg/logger" ) type migrateData struct { @@ -47,20 +49,44 @@ func doMigrateHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("error: %s", err), 500) } - backupPath := r.Form.Get("backuppath") + formBackupPath := r.Form.Get("backuppath") + + // always backup so that we can roll back to the previous version if + // migration fails + backupPath := formBackupPath + if formBackupPath == "" { + backupPath = database.DatabaseBackupPath() + } // perform database backup - if backupPath != "" { - if err = database.Backup(backupPath); err != nil { - http.Error(w, fmt.Sprintf("error backing up database: %s", err), 500) - return - } + if err = database.Backup(backupPath); err != nil { + http.Error(w, fmt.Sprintf("error backing up database: %s", err), 500) + return } err = database.RunMigrations() if err != nil { - http.Error(w, fmt.Sprintf("error performing migration: %s", err), 500) + errStr := fmt.Sprintf("error performing migration: %s", err) + + // roll back to the backed up version + restoreErr := database.RestoreFromBackup(backupPath) + if restoreErr != nil { + errStr = fmt.Sprintf("ERROR: unable to restore database from backup after migration failure: %s\n%s", restoreErr.Error(), errStr) + } else { + errStr = "An error occurred migrating the database to the latest schema version. The backup database file was automatically renamed to restore the database.\n" + errStr + } + + http.Error(w, errStr, 500) return } + + // if no backup path was provided, then delete the created backup + if formBackupPath == "" { + err = os.Remove(backupPath) + if err != nil { + logger.Warnf("error removing unwanted database backup (%s): %s", backupPath, err.Error()) + } + } + http.Redirect(w, r, "/", 301) } diff --git a/pkg/api/resolver_model_movie.go b/pkg/api/resolver_model_movie.go index 3f1323cc8..6ab444a64 100644 --- a/pkg/api/resolver_model_movie.go +++ b/pkg/api/resolver_model_movie.go @@ -29,9 +29,10 @@ func (r *movieResolver) Aliases(ctx context.Context, obj *models.Movie) (*string return nil, nil } -func (r *movieResolver) Duration(ctx context.Context, obj *models.Movie) (*string, error) { +func (r *movieResolver) Duration(ctx context.Context, obj *models.Movie) (*int, error) { if obj.Duration.Valid { - return &obj.Duration.String, nil + rating := int(obj.Duration.Int64) + return &rating, nil } return nil, nil } @@ -44,13 +45,23 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e return nil, nil } -func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*string, error) { +func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) { if obj.Rating.Valid { - return &obj.Rating.String, nil + rating := int(obj.Rating.Int64) + return &rating, nil } return nil, nil } +func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (*models.Studio, error) { + qb := models.NewStudioQueryBuilder() + if obj.StudioID.Valid { + return qb.Find(int(obj.StudioID.Int64), nil) + } + + return nil, nil +} + func (r *movieResolver) Director(ctx context.Context, obj *models.Movie) (*string, error) { if obj.Director.Valid { return &obj.Director.String, nil diff --git a/pkg/api/resolver_model_scene.go b/pkg/api/resolver_model_scene.go index c319b4b71..36e921052 100644 --- a/pkg/api/resolver_model_scene.go +++ b/pkg/api/resolver_model_scene.go @@ -119,10 +119,17 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) ([]*model } sceneIdx := sm.SceneIndex - ret = append(ret, &models.SceneMovie{ - Movie: movie, - SceneIndex: &sceneIdx, - }) + sceneMovie := &models.SceneMovie{ + Movie: movie, + } + + if sceneIdx.Valid { + var idx int + idx = int(sceneIdx.Int64) + sceneMovie.SceneIndex = &idx + } + + ret = append(ret, sceneMovie) } return ret, nil } diff --git a/pkg/api/resolver_mutation_movie.go b/pkg/api/resolver_mutation_movie.go index 86720dc31..75acf83f6 100644 --- a/pkg/api/resolver_mutation_movie.go +++ b/pkg/api/resolver_mutation_movie.go @@ -51,7 +51,8 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr newMovie.Aliases = sql.NullString{String: *input.Aliases, Valid: true} } if input.Duration != nil { - newMovie.Duration = sql.NullString{String: *input.Duration, Valid: true} + duration := int64(*input.Duration) + newMovie.Duration = sql.NullInt64{Int64: duration, Valid: true} } if input.Date != nil { @@ -59,9 +60,10 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr } if input.Rating != nil { - newMovie.Rating = sql.NullString{String: *input.Rating, Valid: true} - } - + rating := int64(*input.Rating) + newMovie.Rating = sql.NullInt64{Int64: rating, Valid: true} + } + if input.Director != nil { newMovie.Director = sql.NullString{String: *input.Director, Valid: true} } @@ -94,57 +96,71 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUpdateInput) (*models.Movie, error) { // Populate movie from the input movieID, _ := strconv.Atoi(input.ID) - updatedMovie := models.Movie{ + + updatedMovie := models.MoviePartial{ ID: movieID, - UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()}, + UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()}, } if input.FrontImage != nil { _, frontimageData, err := utils.ProcessBase64Image(*input.FrontImage) if err != nil { return nil, err } - updatedMovie.FrontImage = frontimageData + updatedMovie.FrontImage = &frontimageData } if input.BackImage != nil { _, backimageData, err := utils.ProcessBase64Image(*input.BackImage) if err != nil { return nil, err } - updatedMovie.BackImage = backimageData + updatedMovie.BackImage = &backimageData } if input.Name != nil { // generate checksum from movie name rather than image checksum := utils.MD5FromString(*input.Name) - updatedMovie.Name = sql.NullString{String: *input.Name, Valid: true} - updatedMovie.Checksum = checksum + updatedMovie.Name = &sql.NullString{String: *input.Name, Valid: true} + updatedMovie.Checksum = &checksum } if input.Aliases != nil { - updatedMovie.Aliases = sql.NullString{String: *input.Aliases, Valid: true} + updatedMovie.Aliases = &sql.NullString{String: *input.Aliases, Valid: true} } if input.Duration != nil { - updatedMovie.Duration = sql.NullString{String: *input.Duration, Valid: true} + duration := int64(*input.Duration) + updatedMovie.Duration = &sql.NullInt64{Int64: duration, Valid: true} } if input.Date != nil { - updatedMovie.Date = models.SQLiteDate{String: *input.Date, Valid: true} + updatedMovie.Date = &models.SQLiteDate{String: *input.Date, Valid: true} } if input.Rating != nil { - updatedMovie.Rating = sql.NullString{String: *input.Rating, Valid: true} + rating := int64(*input.Rating) + updatedMovie.Rating = &sql.NullInt64{Int64: rating, Valid: true} + } else { + // rating must be nullable + updatedMovie.Rating = &sql.NullInt64{Valid: false} + } + + if input.StudioID != nil { + studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64) + updatedMovie.StudioID = &sql.NullInt64{Int64: studioID, Valid: true} + } else { + // studio must be nullable + updatedMovie.StudioID = &sql.NullInt64{Valid: false} } if input.Director != nil { - updatedMovie.Director = sql.NullString{String: *input.Director, Valid: true} + updatedMovie.Director = &sql.NullString{String: *input.Director, Valid: true} } if input.Synopsis != nil { - updatedMovie.Synopsis = sql.NullString{String: *input.Synopsis, Valid: true} + updatedMovie.Synopsis = &sql.NullString{String: *input.Synopsis, Valid: true} } if input.URL != nil { - updatedMovie.URL = sql.NullString{String: *input.URL, Valid: true} + updatedMovie.URL = &sql.NullString{String: *input.URL, Valid: true} } // Start the transaction and save the movie diff --git a/pkg/api/resolver_mutation_scene.go b/pkg/api/resolver_mutation_scene.go index e2d6067df..3cee12c11 100644 --- a/pkg/api/resolver_mutation_scene.go +++ b/pkg/api/resolver_mutation_scene.go @@ -153,16 +153,19 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.T for _, movie := range input.Movies { movieID, _ := strconv.Atoi(movie.MovieID) - sceneIdx := "" - if movie.SceneIndex != nil { - sceneIdx = *movie.SceneIndex - } movieJoin := models.MoviesScenes{ - MovieID: movieID, - SceneID: sceneID, - SceneIndex: sceneIdx, + MovieID: movieID, + SceneID: sceneID, } + + if movie.SceneIndex != nil { + movieJoin.SceneIndex = sql.NullInt64{ + Int64: int64(*movie.SceneIndex), + Valid: true, + } + } + movieJoins = append(movieJoins, movieJoin) } if err := jqb.UpdateMoviesScenes(sceneID, movieJoins, tx); err != nil { diff --git a/pkg/api/resolver_query_find_movie.go b/pkg/api/resolver_query_find_movie.go index bf983b2a0..9d23eddfc 100644 --- a/pkg/api/resolver_query_find_movie.go +++ b/pkg/api/resolver_query_find_movie.go @@ -13,12 +13,12 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (*models.Movie return qb.Find(idInt, nil) } -func (r *queryResolver) FindMovies(ctx context.Context, filter *models.FindFilterType) (*models.FindMoviesResultType, error) { +func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (*models.FindMoviesResultType, error) { qb := models.NewMovieQueryBuilder() - movies, total := qb.Query(filter) + movies, total := qb.Query(movieFilter, filter) return &models.FindMoviesResultType{ - Count: total, - Movies: movies, + Count: total, + Movies: movies, }, nil } diff --git a/pkg/database/database.go b/pkg/database/database.go index 918ce9297..63f6d9cb1 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" "os" - "regexp" "time" "github.com/gobuffalo/packr/v2" "github.com/golang-migrate/migrate/v4" + sqlite3mig "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/golang-migrate/migrate/v4/source" "github.com/jmoiron/sqlx" sqlite3 "github.com/mattn/go-sqlite3" @@ -19,14 +19,14 @@ import ( var DB *sqlx.DB var dbPath string -var appSchemaVersion uint = 7 +var appSchemaVersion uint = 8 var databaseSchemaVersion uint -const sqlite3Driver = "sqlite3_regexp" +const sqlite3Driver = "sqlite3ex" func init() { // register custom driver with regexp function - registerRegexpFunc() + registerCustomDriver() } func Initialize(databasePath string) { @@ -55,14 +55,25 @@ func Initialize(databasePath string) { } } + const disableForeignKeys = false + DB = open(databasePath, disableForeignKeys) +} + +func open(databasePath string, disableForeignKeys bool) *sqlx.DB { // https://github.com/mattn/go-sqlite3 - conn, err := sqlx.Open(sqlite3Driver, "file:"+databasePath+"?_fk=true") + url := "file:" + databasePath + if !disableForeignKeys { + url += "?_fk=true" + } + + conn, err := sqlx.Open(sqlite3Driver, url) conn.SetMaxOpenConns(25) conn.SetMaxIdleConns(4) if err != nil { logger.Fatalf("db.Open(): %q\n", err) } - DB = conn + + return conn } func Reset(databasePath string) error { @@ -97,6 +108,10 @@ func Backup(backupPath string) error { return nil } +func RestoreFromBackup(backupPath string) error { + return os.Rename(backupPath, dbPath) +} + // Migrate the database func NeedsMigration() bool { return databaseSchemaVersion != appSchemaVersion @@ -123,10 +138,21 @@ func getMigrate() (*migrate.Migrate, error) { databasePath := utils.FixWindowsPath(dbPath) s, _ := WithInstance(packrSource) - return migrate.NewWithSourceInstance( + + const disableForeignKeys = true + conn := open(databasePath, disableForeignKeys) + + driver, err := sqlite3mig.WithInstance(conn.DB, &sqlite3mig.Config{}) + if err != nil { + return nil, err + } + + // use sqlite3Driver so that migration has access to durationToTinyInt + return migrate.NewWithInstance( "packr2", s, - fmt.Sprintf("sqlite3://%s", "file:"+databasePath), + databasePath, + driver, ) } @@ -153,6 +179,8 @@ func RunMigrations() error { if stepNumber != 0 { err = m.Steps(int(stepNumber)) if err != nil { + // migration failed + m.Close() return err } } @@ -164,15 +192,23 @@ func RunMigrations() error { return nil } -func registerRegexpFunc() { - regexFn := func(re, s string) (bool, error) { - return regexp.MatchString(re, s) - } - +func registerCustomDriver() { sql.Register(sqlite3Driver, &sqlite3.SQLiteDriver{ ConnectHook: func(conn *sqlite3.SQLiteConn) error { - return conn.RegisterFunc("regexp", regexFn, true) + funcs := map[string]interface{}{ + "regexp": regexFn, + "durationToTinyInt": durationToTinyIntFn, + } + + for name, fn := range funcs { + if err := conn.RegisterFunc(name, fn, true); err != nil { + return fmt.Errorf("Error registering function %s: %s", name, err.Error()) + } + } + + return nil }, - }) + }, + ) } diff --git a/pkg/database/functions.go b/pkg/database/functions.go new file mode 100644 index 000000000..69dc8c0fc --- /dev/null +++ b/pkg/database/functions.go @@ -0,0 +1,37 @@ +package database + +import ( + "regexp" + "strconv" + "strings" +) + +func regexFn(re, s string) (bool, error) { + return regexp.MatchString(re, s) +} + +func durationToTinyIntFn(str string) (int64, error) { + splits := strings.Split(str, ":") + + if len(splits) > 3 { + return 0, nil + } + + seconds := 0 + factor := 1 + for len(splits) > 0 { + // pop the last split + var thisSplit string + thisSplit, splits = splits[len(splits)-1], splits[:len(splits)-1] + + thisInt, err := strconv.Atoi(thisSplit) + if err != nil { + return 0, nil + } + + seconds += factor * thisInt + factor *= 60 + } + + return int64(seconds), nil +} diff --git a/pkg/database/migrations/8_movie_fix.up.sql b/pkg/database/migrations/8_movie_fix.up.sql new file mode 100644 index 000000000..33414bd99 --- /dev/null +++ b/pkg/database/migrations/8_movie_fix.up.sql @@ -0,0 +1,106 @@ +ALTER TABLE `movies` rename to `_movies_old`; +ALTER TABLE `movies_scenes` rename to `_movies_scenes_old`; + +DROP INDEX IF EXISTS `movies_checksum_unique`; +DROP INDEX IF EXISTS `index_movie_id_scene_index_unique`; +DROP INDEX IF EXISTS `index_movies_scenes_on_movie_id`; +DROP INDEX IF EXISTS `index_movies_scenes_on_scene_id`; + +-- recreate the movies table with fixed column types and constraints +CREATE TABLE `movies` ( + `id` integer not null primary key autoincrement, + -- add not null + `name` varchar(255) not null, + `aliases` varchar(255), + -- varchar(6) -> integer + `duration` integer, + `date` date, + -- varchar(1) -> tinyint + `rating` tinyint, + `studio_id` integer, + `director` varchar(255), + `synopsis` text, + `checksum` varchar(255) not null, + `url` varchar(255), + `created_at` datetime not null, + `updated_at` datetime not null, + `front_image` blob not null, + `back_image` blob, + foreign key(`studio_id`) references `studios`(`id`) on delete set null +); +CREATE TABLE `movies_scenes` ( + `movie_id` integer, + `scene_id` integer, + -- varchar(2) -> tinyint + `scene_index` tinyint, + foreign key(`movie_id`) references `movies`(`id`) on delete cascade, + foreign key(`scene_id`) references `scenes`(`id`) on delete cascade +); + +-- add unique index on movie name +CREATE UNIQUE INDEX `movies_name_unique` on `movies` (`name`); +CREATE UNIQUE INDEX `movies_checksum_unique` on `movies` (`checksum`); +-- remove unique index on movies_scenes +CREATE INDEX `index_movies_scenes_on_movie_id` on `movies_scenes` (`movie_id`); +CREATE INDEX `index_movies_scenes_on_scene_id` on `movies_scenes` (`scene_id`); +CREATE INDEX `index_movies_on_studio_id` on `movies` (`studio_id`); + +-- custom functions cannot accept NULL values, so massage the old data +UPDATE `_movies_old` set `duration` = 0 WHERE `duration` IS NULL; + +-- now populate from the old tables +INSERT INTO `movies` + ( + `id`, + `name`, + `aliases`, + `duration`, + `date`, + `rating`, + `director`, + `synopsis`, + `front_image`, + `back_image`, + `checksum`, + `url`, + `created_at`, + `updated_at` + ) + SELECT + `id`, + `name`, + `aliases`, + durationToTinyInt(`duration`), + `date`, + CAST(`rating` as tinyint), + `director`, + `synopsis`, + `front_image`, + `back_image`, + `checksum`, + `url`, + `created_at`, + `updated_at` + FROM `_movies_old` + -- ignore null named movies + WHERE `name` is not null; + +-- durationToTinyInt returns 0 if it cannot parse the string +-- set these values to null instead +UPDATE `movies` SET `duration` = NULL WHERE `duration` = 0; + +INSERT INTO `movies_scenes` + ( + `movie_id`, + `scene_id`, + `scene_index` + ) + SELECT + `movie_id`, + `scene_id`, + CAST(`scene_index` as tinyint) + FROM `_movies_scenes_old`; + +-- drop old tables +DROP TABLE `_movies_scenes_old`; +DROP TABLE `_movies_old`; diff --git a/pkg/manager/jsonschema/movie.go b/pkg/manager/jsonschema/movie.go index a7c7b4c18..44561bb3f 100644 --- a/pkg/manager/jsonschema/movie.go +++ b/pkg/manager/jsonschema/movie.go @@ -9,18 +9,18 @@ import ( ) type Movie struct { - Name string `json:"name,omitempty"` - Aliases string `json:"aliases,omitempty"` - Duration string `json:"duration,omitempty"` - Date string `json:"date,omitempty"` - Rating string `json:"rating,omitempty"` - Director string `json:"director,omitempty"` - Synopsis string `json:"sypnopsis,omitempty"` - FrontImage string `json:"front_image,omitempty"` - BackImage string `json:"back_image,omitempty"` - URL string `json:"url,omitempty"` - CreatedAt models.JSONTime `json:"created_at,omitempty"` - UpdatedAt models.JSONTime `json:"updated_at,omitempty"` + Name string `json:"name,omitempty"` + Aliases string `json:"aliases,omitempty"` + Duration int `json:"duration,omitempty"` + Date string `json:"date,omitempty"` + Rating int `json:"rating,omitempty"` + Director string `json:"director,omitempty"` + Synopsis string `json:"sypnopsis,omitempty"` + FrontImage string `json:"front_image,omitempty"` + BackImage string `json:"back_image,omitempty"` + URL string `json:"url,omitempty"` + CreatedAt models.JSONTime `json:"created_at,omitempty"` + UpdatedAt models.JSONTime `json:"updated_at,omitempty"` } func LoadMovieFile(filePath string) (*Movie, error) { diff --git a/pkg/manager/jsonschema/scene.go b/pkg/manager/jsonschema/scene.go index c69d44660..8f3c581e6 100644 --- a/pkg/manager/jsonschema/scene.go +++ b/pkg/manager/jsonschema/scene.go @@ -31,7 +31,7 @@ type SceneFile struct { type SceneMovie struct { MovieName string `json:"movieName,omitempty"` - SceneIndex string `json:"scene_index,omitempty"` + SceneIndex int `json:"scene_index,omitempty"` } type Scene struct { diff --git a/pkg/manager/task_export.go b/pkg/manager/task_export.go index 4321e78b7..cdc15e1b1 100644 --- a/pkg/manager/task_export.go +++ b/pkg/manager/task_export.go @@ -146,7 +146,7 @@ func (t *ExportTask) ExportScenes(ctx context.Context) { if movie.Name.Valid { sceneMovieJSON := jsonschema.SceneMovie{ MovieName: movie.Name.String, - SceneIndex: sceneMovie.SceneIndex, + SceneIndex: int(sceneMovie.SceneIndex.Int64), } newSceneJSON.Movies = append(newSceneJSON.Movies, sceneMovieJSON) } @@ -381,10 +381,10 @@ func (t *ExportTask) ExportMovies(ctx context.Context) { newMovieJSON.Date = utils.GetYMDFromDatabaseDate(movie.Date.String) } if movie.Rating.Valid { - newMovieJSON.Rating = movie.Rating.String + newMovieJSON.Rating = int(movie.Rating.Int64) } if movie.Duration.Valid { - newMovieJSON.Duration = movie.Duration.String + newMovieJSON.Duration = int(movie.Duration.Int64) } if movie.Director.Valid { diff --git a/pkg/manager/task_import.go b/pkg/manager/task_import.go index 1c49b40a3..9d483f59e 100644 --- a/pkg/manager/task_import.go +++ b/pkg/manager/task_import.go @@ -250,8 +250,6 @@ func (t *ImportTask) ImportMovies(ctx context.Context) { Name: sql.NullString{String: movieJSON.Name, Valid: true}, Aliases: sql.NullString{String: movieJSON.Aliases, Valid: true}, Date: models.SQLiteDate{String: movieJSON.Date, Valid: true}, - Duration: sql.NullString{String: movieJSON.Duration, Valid: true}, - Rating: sql.NullString{String: movieJSON.Rating, Valid: true}, Director: sql.NullString{String: movieJSON.Director, Valid: true}, Synopsis: sql.NullString{String: movieJSON.Synopsis, Valid: true}, URL: sql.NullString{String: movieJSON.URL, Valid: true}, @@ -259,6 +257,13 @@ func (t *ImportTask) ImportMovies(ctx context.Context) { UpdatedAt: models.SQLiteTimestamp{Timestamp: t.getTimeFromJSONTime(movieJSON.UpdatedAt)}, } + if movieJSON.Rating != 0 { + newMovie.Rating = sql.NullInt64{Int64: int64(movieJSON.Rating), Valid: true} + } + if movieJSON.Duration != 0 { + newMovie.Duration = sql.NullInt64{Int64: int64(movieJSON.Duration), Valid: true} + } + _, err = qb.Create(newMovie, tx) if err != nil { _ = tx.Rollback() @@ -712,11 +717,19 @@ func (t *ImportTask) getMoviesScenes(input []jsonschema.SceneMovie, sceneID int, if movie == nil { logger.Warnf("[scenes] movie %s does not exist", inputMovie.MovieName) } else { - movies = append(movies, models.MoviesScenes{ - MovieID: movie.ID, - SceneID: sceneID, - SceneIndex: inputMovie.SceneIndex, - }) + toAdd := models.MoviesScenes{ + MovieID: movie.ID, + SceneID: sceneID, + } + + if inputMovie.SceneIndex != 0 { + toAdd.SceneIndex = sql.NullInt64{ + Int64: int64(inputMovie.SceneIndex), + Valid: true, + } + } + + movies = append(movies, toAdd) } } diff --git a/pkg/models/model_joins.go b/pkg/models/model_joins.go index 059007455..f69be8946 100644 --- a/pkg/models/model_joins.go +++ b/pkg/models/model_joins.go @@ -1,14 +1,16 @@ package models +import "database/sql" + type PerformersScenes struct { PerformerID int `db:"performer_id" json:"performer_id"` SceneID int `db:"scene_id" json:"scene_id"` } type MoviesScenes struct { - MovieID int `db:"movie_id" json:"movie_id"` - SceneID int `db:"scene_id" json:"scene_id"` - SceneIndex string `db:"scene_index" json:"scene_index"` + MovieID int `db:"movie_id" json:"movie_id"` + SceneID int `db:"scene_id" json:"scene_id"` + SceneIndex sql.NullInt64 `db:"scene_index" json:"scene_index"` } type ScenesTags struct { diff --git a/pkg/models/model_movie.go b/pkg/models/model_movie.go index 0efdb8af6..f4260c9ba 100644 --- a/pkg/models/model_movie.go +++ b/pkg/models/model_movie.go @@ -5,20 +5,39 @@ import ( ) type Movie struct { - ID int `db:"id" json:"id"` - FrontImage []byte `db:"front_image" json:"front_image"` - BackImage []byte `db:"back_image" json:"back_image"` - Checksum string `db:"checksum" json:"checksum"` - Name sql.NullString `db:"name" json:"name"` - Aliases sql.NullString `db:"aliases" json:"aliases"` - Duration sql.NullString `db:"duration" json:"duration"` - Date SQLiteDate `db:"date" json:"date"` - Rating sql.NullString `db:"rating" json:"rating"` - Director sql.NullString `db:"director" json:"director"` - Synopsis sql.NullString `db:"synopsis" json:"synopsis"` - URL sql.NullString `db:"url" json:"url"` - CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` - UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` + ID int `db:"id" json:"id"` + FrontImage []byte `db:"front_image" json:"front_image"` + BackImage []byte `db:"back_image" json:"back_image"` + Checksum string `db:"checksum" json:"checksum"` + Name sql.NullString `db:"name" json:"name"` + Aliases sql.NullString `db:"aliases" json:"aliases"` + Duration sql.NullInt64 `db:"duration" json:"duration"` + Date SQLiteDate `db:"date" json:"date"` + Rating sql.NullInt64 `db:"rating" json:"rating"` + StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"` + Director sql.NullString `db:"director" json:"director"` + Synopsis sql.NullString `db:"synopsis" json:"synopsis"` + URL sql.NullString `db:"url" json:"url"` + CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` + UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` +} + +type MoviePartial struct { + ID int `db:"id" json:"id"` + FrontImage *[]byte `db:"front_image" json:"front_image"` + BackImage *[]byte `db:"back_image" json:"back_image"` + Checksum *string `db:"checksum" json:"checksum"` + Name *sql.NullString `db:"name" json:"name"` + Aliases *sql.NullString `db:"aliases" json:"aliases"` + Duration *sql.NullInt64 `db:"duration" json:"duration"` + Date *SQLiteDate `db:"date" json:"date"` + Rating *sql.NullInt64 `db:"rating" json:"rating"` + StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"` + Director *sql.NullString `db:"director" json:"director"` + Synopsis *sql.NullString `db:"synopsis" json:"synopsis"` + URL *sql.NullString `db:"url" json:"url"` + CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` + UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` } var DefaultMovieImage string = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC" diff --git a/pkg/models/querybuilder_joins.go b/pkg/models/querybuilder_joins.go index 578a121c0..416b85a18 100644 --- a/pkg/models/querybuilder_joins.go +++ b/pkg/models/querybuilder_joins.go @@ -161,7 +161,7 @@ func (qb *JoinsQueryBuilder) CreateMoviesScenes(newJoins []MoviesScenes, tx *sql // if the movie already exists on the scene. It returns true if scene // movie was added. -func (qb *JoinsQueryBuilder) AddMoviesScene(sceneID int, movieID int, sceneIdx string, tx *sqlx.Tx) (bool, error) { +func (qb *JoinsQueryBuilder) AddMoviesScene(sceneID int, movieID int, sceneIdx *int, tx *sqlx.Tx) (bool, error) { ensureTx(tx) existingMovies, err := qb.GetSceneMovies(sceneID, tx) @@ -178,9 +178,15 @@ func (qb *JoinsQueryBuilder) AddMoviesScene(sceneID int, movieID int, sceneIdx s } movieJoin := MoviesScenes{ - MovieID: movieID, - SceneID: sceneID, - SceneIndex: sceneIdx, + MovieID: movieID, + SceneID: sceneID, + } + + if sceneIdx != nil { + movieJoin.SceneIndex = sql.NullInt64{ + Int64: int64(*sceneIdx), + Valid: true, + } } movieJoins := append(existingMovies, movieJoin) diff --git a/pkg/models/querybuilder_movies.go b/pkg/models/querybuilder_movies.go index 37b579af4..125d75874 100644 --- a/pkg/models/querybuilder_movies.go +++ b/pkg/models/querybuilder_movies.go @@ -2,6 +2,7 @@ package models import ( "database/sql" + "strconv" "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/database" @@ -16,8 +17,8 @@ func NewMovieQueryBuilder() MovieQueryBuilder { func (qb *MovieQueryBuilder) Create(newMovie Movie, tx *sqlx.Tx) (*Movie, error) { ensureTx(tx) result, err := tx.NamedExec( - `INSERT INTO movies (front_image, back_image, checksum, name, aliases, duration, date, rating, director, synopsis, url, created_at, updated_at) - VALUES (:front_image, :back_image, :checksum, :name, :aliases, :duration, :date, :rating, :director, :synopsis, :url, :created_at, :updated_at) + `INSERT INTO movies (front_image, back_image, checksum, name, aliases, duration, date, rating, studio_id, director, synopsis, url, created_at, updated_at) + VALUES (:front_image, :back_image, :checksum, :name, :aliases, :duration, :date, :rating, :studio_id, :director, :synopsis, :url, :created_at, :updated_at) `, newMovie, ) @@ -35,20 +36,17 @@ func (qb *MovieQueryBuilder) Create(newMovie Movie, tx *sqlx.Tx) (*Movie, error) return &newMovie, nil } -func (qb *MovieQueryBuilder) Update(updatedMovie Movie, tx *sqlx.Tx) (*Movie, error) { +func (qb *MovieQueryBuilder) Update(updatedMovie MoviePartial, tx *sqlx.Tx) (*Movie, error) { ensureTx(tx) _, err := tx.NamedExec( - `UPDATE movies SET `+SQLGenKeys(updatedMovie)+` WHERE movies.id = :id`, + `UPDATE movies SET `+SQLGenKeysPartial(updatedMovie)+` WHERE movies.id = :id`, updatedMovie, ) if err != nil { return nil, err } - if err := tx.Get(&updatedMovie, `SELECT * FROM movies WHERE id = ? LIMIT 1`, updatedMovie.ID); err != nil { - return nil, err - } - return &updatedMovie, nil + return qb.Find(updatedMovie.ID, tx) } func (qb *MovieQueryBuilder) Destroy(id string, tx *sqlx.Tx) error { @@ -113,10 +111,13 @@ func (qb *MovieQueryBuilder) AllSlim() ([]*Movie, error) { return qb.queryMovies("SELECT movies.id, movies.name FROM movies "+qb.getMovieSort(nil), nil, nil) } -func (qb *MovieQueryBuilder) Query(findFilter *FindFilterType) ([]*Movie, int) { +func (qb *MovieQueryBuilder) Query(movieFilter *MovieFilterType, findFilter *FindFilterType) ([]*Movie, int) { if findFilter == nil { findFilter = &FindFilterType{} } + if movieFilter == nil { + movieFilter = &MovieFilterType{} + } var whereClauses []string var havingClauses []string @@ -125,6 +126,7 @@ func (qb *MovieQueryBuilder) Query(findFilter *FindFilterType) ([]*Movie, int) { body += ` left join movies_scenes as scenes_join on scenes_join.movie_id = movies.id left join scenes on scenes_join.scene_id = scenes.id + left join studios as studio on studio.id = movies.studio_id ` if q := findFilter.Q; q != nil && *q != "" { @@ -134,6 +136,16 @@ func (qb *MovieQueryBuilder) Query(findFilter *FindFilterType) ([]*Movie, int) { args = append(args, thisArgs...) } + if studiosFilter := movieFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 { + for _, studioID := range studiosFilter.Value { + args = append(args, studioID) + } + + whereClause, havingClause := qb.getMultiCriterionClause("studio", "", "studio_id", studiosFilter) + whereClauses = appendClause(whereClauses, whereClause) + havingClauses = appendClause(havingClauses, havingClause) + } + sortAndPagination := qb.getMovieSort(findFilter) + getPagination(findFilter) idsResult, countResult := executeFindQuery("movies", body, args, sortAndPagination, whereClauses, havingClauses) @@ -146,6 +158,29 @@ func (qb *MovieQueryBuilder) Query(findFilter *FindFilterType) ([]*Movie, int) { return movies, countResult } +// returns where clause and having clause +func (qb *MovieQueryBuilder) getMultiCriterionClause(table string, joinTable string, joinTableField string, criterion *MultiCriterionInput) (string, string) { + whereClause := "" + havingClause := "" + if criterion.Modifier == CriterionModifierIncludes { + // includes any of the provided ids + whereClause = table + ".id IN " + getInBinding(len(criterion.Value)) + } else if criterion.Modifier == CriterionModifierIncludesAll { + // includes all of the provided ids + whereClause = table + ".id IN " + getInBinding(len(criterion.Value)) + havingClause = "count(distinct " + table + ".id) IS " + strconv.Itoa(len(criterion.Value)) + } else if criterion.Modifier == CriterionModifierExcludes { + // excludes all of the provided ids + if joinTable != "" { + whereClause = "not exists (select " + joinTable + ".movie_id from " + joinTable + " where " + joinTable + ".movie_id = movies.id and " + joinTable + "." + joinTableField + " in " + getInBinding(len(criterion.Value)) + ")" + } else { + whereClause = "not exists (select m.id from movies as m where m.id = movies.id and m." + joinTableField + " in " + getInBinding(len(criterion.Value)) + ")" + } + } + + return whereClause, havingClause +} + func (qb *MovieQueryBuilder) getMovieSort(findFilter *FindFilterType) string { var sort string var direction string diff --git a/ui/v2.5/src/components/Movies/MovieCard.tsx b/ui/v2.5/src/components/Movies/MovieCard.tsx index 1cf6bc710..f029e03e8 100644 --- a/ui/v2.5/src/components/Movies/MovieCard.tsx +++ b/ui/v2.5/src/components/Movies/MovieCard.tsx @@ -5,7 +5,7 @@ import * as GQL from "src/core/generated-graphql"; interface IProps { movie: GQL.MovieDataFragment; - sceneIndex?: string; + sceneIndex?: number; } export const MovieCard: FunctionComponent = (props: IProps) => { diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index a551c9bdf..cef9e9c8c 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -8,10 +8,16 @@ import { DetailsEditNavbar, LoadingIndicator, Modal, + StudioSelect, } from "src/components/Shared"; import { useToast } from "src/hooks"; import { Table, Form } from "react-bootstrap"; -import { TableUtils, ImageUtils, EditableTextUtils, TextUtils } from "src/utils"; +import { + TableUtils, + ImageUtils, + EditableTextUtils, + TextUtils, +} from "src/utils"; import { MovieScenesPanel } from "./MovieScenesPanel"; export const Movie: React.FC = () => { @@ -29,9 +35,10 @@ export const Movie: React.FC = () => { const [backImage, setBackImage] = useState(undefined); const [name, setName] = useState(undefined); const [aliases, setAliases] = useState(undefined); - const [duration, setDuration] = useState(undefined); + const [duration, setDuration] = useState(undefined); const [date, setDate] = useState(undefined); - const [rating, setRating] = useState(undefined); + const [rating, setRating] = useState(undefined); + const [studioId, setStudioId] = useState(); const [director, setDirector] = useState(undefined); const [synopsis, setSynopsis] = useState(undefined); const [url, setUrl] = useState(undefined); @@ -63,6 +70,7 @@ export const Movie: React.FC = () => { setDuration(state.duration ?? undefined); setDate(state.date ?? undefined); setRating(state.rating ?? undefined); + setStudioId(state?.studio?.id ?? undefined); setDirector(state.director ?? undefined); setSynopsis(state.synopsis ?? undefined); setUrl(state.url ?? undefined); @@ -113,6 +121,7 @@ export const Movie: React.FC = () => { duration, date, rating, + studio_id: studioId, director, synopsis, url, @@ -213,12 +222,10 @@ export const Movie: React.FC = () => { })} {TableUtils.renderDurationInput({ title: "Duration", - value: duration, + value: duration ? duration.toString() : "", isEditing, - onChange: (value: string | undefined) => { - setDuration(value ?? ""); - }, - asString: true, + onChange: (value: string | undefined) => + setDuration(value ? Number.parseInt(value, 10) : undefined), })} {TableUtils.renderInputGroup({ title: "Date (YYYY-MM-DD)", @@ -226,6 +233,18 @@ export const Movie: React.FC = () => { isEditing, onChange: setDate, })} + + Studio + + + setStudioId(items.length > 0 ? items[0]?.id : undefined) + } + ids={studioId ? [studioId] : []} + /> + + {TableUtils.renderInputGroup({ title: "Director", value: director, @@ -234,9 +253,10 @@ export const Movie: React.FC = () => { })} {TableUtils.renderHtmlSelect({ title: "Rating", - value: rating, + value: rating ? rating : "", isEditing, - onChange: (value: string) => setRating(value), + onChange: (value: string) => + setRating(Number.parseInt(value, 10)), selectOptions: ["", "1", "2", "3", "4", "5"], })} diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx index 6f27512f0..0263ca4e7 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx @@ -5,7 +5,7 @@ import { Form } from "react-bootstrap"; type ValidTypes = GQL.SlimMovieDataFragment; -export type MovieSceneIndexMap = Map; +export type MovieSceneIndexMap = Map; export interface IProps { movieSceneIndexes: MovieSceneIndexMap; @@ -30,7 +30,7 @@ export const SceneMovieTable: React.FunctionComponent = ( return props.movieSceneIndexes.get(movie.id); }); - const updateFieldChanged = (movieId: string, value: string) => { + const updateFieldChanged = (movieId: string, value: number) => { const newMap = new Map(props.movieSceneIndexes); newMap.set(movieId, value); props.onUpdate(newMap); @@ -48,9 +48,15 @@ export const SceneMovieTable: React.FunctionComponent = ( ) => - updateFieldChanged(item.id, e.currentTarget.value) + updateFieldChanged( + item.id, + Number.parseInt( + e.currentTarget.value ? e.currentTarget.value : "0", + 10 + ) + ) } > {["", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"].map( diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 9f8165758..f04bb71b2 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -187,9 +187,12 @@ export class StashService { } public static useFindMovies(filter: ListFilterModel) { + const movieFilter = filter.makeMovieFilter(); + return GQL.useFindMoviesQuery({ variables: { filter: filter.makeFindFilter(), + movie_filter: movieFilter, }, }); } diff --git a/ui/v2.5/src/models/list-filter/filter.ts b/ui/v2.5/src/models/list-filter/filter.ts index 8fee846c7..1d2246154 100644 --- a/ui/v2.5/src/models/list-filter/filter.ts +++ b/ui/v2.5/src/models/list-filter/filter.ts @@ -6,6 +6,7 @@ import { SceneFilterType, SceneMarkerFilterType, SortDirectionEnum, + MovieFilterType, } from "src/core/generated-graphql"; import { StashService } from "src/core/StashService"; import { @@ -162,7 +163,10 @@ export class ListFilterModel { this.sortBy = "name"; this.sortByOptions = ["name", "scenes_count"]; this.displayModeOptions = [DisplayMode.Grid]; - this.criterionOptions = [new NoneCriterionOption()]; + this.criterionOptions = [ + new NoneCriterionOption(), + new StudiosCriterionOption(), + ]; break; case FilterMode.Galleries: this.sortBy = "path"; @@ -552,4 +556,22 @@ export class ListFilterModel { }); return result; } + + public makeMovieFilter(): MovieFilterType { + const result: MovieFilterType = {}; + this.criteria.forEach((criterion) => { + switch (criterion.type) { + case "studios": { + const studCrit = criterion as StudiosCriterion; + result.studios = { + value: studCrit.value.map((studio) => studio.id), + modifier: studCrit.modifier, + }; + break; + } + // no default + } + }); + return result; + } }