mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Add Studio to movie and fix movie schema (#458)
* Add movie migration * Update server and UI code for type changes * Add studio to movies * Movie blobs to end * Document movie duration * Add filtering on movie studio
This commit is contained in:
2
go.sum
2
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.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.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
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 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.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/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 h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ fragment MovieData on Movie {
|
|||||||
date
|
date
|
||||||
rating
|
rating
|
||||||
director
|
director
|
||||||
|
|
||||||
|
studio {
|
||||||
|
...StudioData
|
||||||
|
}
|
||||||
|
|
||||||
synopsis
|
synopsis
|
||||||
url
|
url
|
||||||
front_image_path
|
front_image_path
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
mutation MovieCreate(
|
mutation MovieCreate(
|
||||||
$name: String!,
|
$name: String!,
|
||||||
$aliases: String,
|
$aliases: String,
|
||||||
$duration: String,
|
$duration: Int,
|
||||||
$date: String,
|
$date: String,
|
||||||
$rating: String,
|
$rating: Int,
|
||||||
|
$studio_id: ID,
|
||||||
$director: String,
|
$director: String,
|
||||||
$synopsis: String,
|
$synopsis: String,
|
||||||
$url: String,
|
$url: String,
|
||||||
$front_image: String,
|
$front_image: String,
|
||||||
$back_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
|
...MovieData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,16 +20,17 @@ mutation MovieUpdate(
|
|||||||
$id: ID!
|
$id: ID!
|
||||||
$name: String,
|
$name: String,
|
||||||
$aliases: String,
|
$aliases: String,
|
||||||
$duration: String,
|
$duration: Int,
|
||||||
$date: String,
|
$date: String,
|
||||||
$rating: String,
|
$rating: Int,
|
||||||
|
$studio_id: ID,
|
||||||
$director: String,
|
$director: String,
|
||||||
$synopsis: String,
|
$synopsis: String,
|
||||||
$url: String,
|
$url: String,
|
||||||
$front_image: String,
|
$front_image: String,
|
||||||
$back_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
|
...MovieData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query FindMovies($filter: FindFilterType) {
|
query FindMovies($filter: FindFilterType, $movie_filter: MovieFilterType) {
|
||||||
findMovies(filter: $filter) {
|
findMovies(filter: $filter, movie_filter: $movie_filter) {
|
||||||
count
|
count
|
||||||
movies {
|
movies {
|
||||||
...MovieData
|
...MovieData
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type Query {
|
|||||||
"""Find a movie by ID"""
|
"""Find a movie by ID"""
|
||||||
findMovie(id: ID!): Movie
|
findMovie(id: ID!): Movie
|
||||||
"""A function which queries Movie objects"""
|
"""A function which queries Movie objects"""
|
||||||
findMovies(filter: FindFilterType): FindMoviesResultType!
|
findMovies(movie_filter: MovieFilterType, filter: FindFilterType): FindMoviesResultType!
|
||||||
|
|
||||||
findGallery(id: ID!): Gallery
|
findGallery(id: ID!): Gallery
|
||||||
findGalleries(filter: FindFilterType): FindGalleriesResultType!
|
findGalleries(filter: FindFilterType): FindGalleriesResultType!
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ input SceneFilterType {
|
|||||||
performers: MultiCriterionInput
|
performers: MultiCriterionInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input MovieFilterType {
|
||||||
|
"""Filter to only include movies with this studio"""
|
||||||
|
studios: MultiCriterionInput
|
||||||
|
}
|
||||||
|
|
||||||
enum CriterionModifier {
|
enum CriterionModifier {
|
||||||
"""="""
|
"""="""
|
||||||
EQUALS,
|
EQUALS,
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ type Movie {
|
|||||||
checksum: String!
|
checksum: String!
|
||||||
name: String!
|
name: String!
|
||||||
aliases: String
|
aliases: String
|
||||||
duration: String
|
"""Duration in seconds"""
|
||||||
|
duration: Int
|
||||||
date: String
|
date: String
|
||||||
rating: String
|
rating: Int
|
||||||
|
studio: Studio
|
||||||
director: String
|
director: String
|
||||||
synopsis: String
|
synopsis: String
|
||||||
url: String
|
url: String
|
||||||
@@ -18,9 +20,11 @@ type Movie {
|
|||||||
input MovieCreateInput {
|
input MovieCreateInput {
|
||||||
name: String!
|
name: String!
|
||||||
aliases: String
|
aliases: String
|
||||||
duration: String
|
"""Duration in seconds"""
|
||||||
|
duration: Int
|
||||||
date: String
|
date: String
|
||||||
rating: String
|
rating: Int
|
||||||
|
studio_id: ID
|
||||||
director: String
|
director: String
|
||||||
synopsis: String
|
synopsis: String
|
||||||
url: String
|
url: String
|
||||||
@@ -33,9 +37,10 @@ input MovieUpdateInput {
|
|||||||
id: ID!
|
id: ID!
|
||||||
name: String
|
name: String
|
||||||
aliases: String
|
aliases: String
|
||||||
duration: String
|
duration: Int
|
||||||
date: String
|
date: String
|
||||||
rating: String
|
rating: Int
|
||||||
|
studio_id: ID
|
||||||
director: String
|
director: String
|
||||||
synopsis: String
|
synopsis: String
|
||||||
url: String
|
url: String
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type ScenePathsType {
|
|||||||
|
|
||||||
type SceneMovie {
|
type SceneMovie {
|
||||||
movie: Movie!
|
movie: Movie!
|
||||||
scene_index: String
|
scene_index: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Scene {
|
type Scene {
|
||||||
@@ -48,7 +48,7 @@ type Scene {
|
|||||||
|
|
||||||
input SceneMovieInput {
|
input SceneMovieInput {
|
||||||
movie_id: ID!
|
movie_id: ID!
|
||||||
scene_index: String
|
scene_index: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
input SceneUpdateInput {
|
input SceneUpdateInput {
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type migrateData struct {
|
type migrateData struct {
|
||||||
@@ -47,20 +49,44 @@ func doMigrateHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, fmt.Sprintf("error: %s", err), 500)
|
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
|
// perform database backup
|
||||||
if backupPath != "" {
|
|
||||||
if err = database.Backup(backupPath); err != nil {
|
if err = database.Backup(backupPath); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("error backing up database: %s", err), 500)
|
http.Error(w, fmt.Sprintf("error backing up database: %s", err), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
err = database.RunMigrations()
|
err = database.RunMigrations()
|
||||||
if err != nil {
|
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
|
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)
|
http.Redirect(w, r, "/", 301)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ func (r *movieResolver) Aliases(ctx context.Context, obj *models.Movie) (*string
|
|||||||
return nil, nil
|
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 {
|
if obj.Duration.Valid {
|
||||||
return &obj.Duration.String, nil
|
rating := int(obj.Duration.Int64)
|
||||||
|
return &rating, nil
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -44,13 +45,23 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e
|
|||||||
return nil, nil
|
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 {
|
if obj.Rating.Valid {
|
||||||
return &obj.Rating.String, nil
|
rating := int(obj.Rating.Int64)
|
||||||
|
return &rating, nil
|
||||||
}
|
}
|
||||||
return nil, 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) {
|
func (r *movieResolver) Director(ctx context.Context, obj *models.Movie) (*string, error) {
|
||||||
if obj.Director.Valid {
|
if obj.Director.Valid {
|
||||||
return &obj.Director.String, nil
|
return &obj.Director.String, nil
|
||||||
|
|||||||
@@ -119,10 +119,17 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) ([]*model
|
|||||||
}
|
}
|
||||||
|
|
||||||
sceneIdx := sm.SceneIndex
|
sceneIdx := sm.SceneIndex
|
||||||
ret = append(ret, &models.SceneMovie{
|
sceneMovie := &models.SceneMovie{
|
||||||
Movie: movie,
|
Movie: movie,
|
||||||
SceneIndex: &sceneIdx,
|
}
|
||||||
})
|
|
||||||
|
if sceneIdx.Valid {
|
||||||
|
var idx int
|
||||||
|
idx = int(sceneIdx.Int64)
|
||||||
|
sceneMovie.SceneIndex = &idx
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, sceneMovie)
|
||||||
}
|
}
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
|||||||
newMovie.Aliases = sql.NullString{String: *input.Aliases, Valid: true}
|
newMovie.Aliases = sql.NullString{String: *input.Aliases, Valid: true}
|
||||||
}
|
}
|
||||||
if input.Duration != nil {
|
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 {
|
if input.Date != nil {
|
||||||
@@ -59,7 +60,8 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.Rating != nil {
|
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 {
|
if input.Director != nil {
|
||||||
@@ -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) {
|
func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUpdateInput) (*models.Movie, error) {
|
||||||
// Populate movie from the input
|
// Populate movie from the input
|
||||||
movieID, _ := strconv.Atoi(input.ID)
|
movieID, _ := strconv.Atoi(input.ID)
|
||||||
updatedMovie := models.Movie{
|
|
||||||
|
updatedMovie := models.MoviePartial{
|
||||||
ID: movieID,
|
ID: movieID,
|
||||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()},
|
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||||
}
|
}
|
||||||
if input.FrontImage != nil {
|
if input.FrontImage != nil {
|
||||||
_, frontimageData, err := utils.ProcessBase64Image(*input.FrontImage)
|
_, frontimageData, err := utils.ProcessBase64Image(*input.FrontImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
updatedMovie.FrontImage = frontimageData
|
updatedMovie.FrontImage = &frontimageData
|
||||||
}
|
}
|
||||||
if input.BackImage != nil {
|
if input.BackImage != nil {
|
||||||
_, backimageData, err := utils.ProcessBase64Image(*input.BackImage)
|
_, backimageData, err := utils.ProcessBase64Image(*input.BackImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
updatedMovie.BackImage = backimageData
|
updatedMovie.BackImage = &backimageData
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Name != nil {
|
if input.Name != nil {
|
||||||
// generate checksum from movie name rather than image
|
// generate checksum from movie name rather than image
|
||||||
checksum := utils.MD5FromString(*input.Name)
|
checksum := utils.MD5FromString(*input.Name)
|
||||||
updatedMovie.Name = sql.NullString{String: *input.Name, Valid: true}
|
updatedMovie.Name = &sql.NullString{String: *input.Name, Valid: true}
|
||||||
updatedMovie.Checksum = checksum
|
updatedMovie.Checksum = &checksum
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Aliases != nil {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
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
|
// Start the transaction and save the movie
|
||||||
|
|||||||
@@ -153,16 +153,19 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.T
|
|||||||
for _, movie := range input.Movies {
|
for _, movie := range input.Movies {
|
||||||
|
|
||||||
movieID, _ := strconv.Atoi(movie.MovieID)
|
movieID, _ := strconv.Atoi(movie.MovieID)
|
||||||
sceneIdx := ""
|
|
||||||
if movie.SceneIndex != nil {
|
|
||||||
sceneIdx = *movie.SceneIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
movieJoin := models.MoviesScenes{
|
movieJoin := models.MoviesScenes{
|
||||||
MovieID: movieID,
|
MovieID: movieID,
|
||||||
SceneID: sceneID,
|
SceneID: sceneID,
|
||||||
SceneIndex: sceneIdx,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if movie.SceneIndex != nil {
|
||||||
|
movieJoin.SceneIndex = sql.NullInt64{
|
||||||
|
Int64: int64(*movie.SceneIndex),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
movieJoins = append(movieJoins, movieJoin)
|
movieJoins = append(movieJoins, movieJoin)
|
||||||
}
|
}
|
||||||
if err := jqb.UpdateMoviesScenes(sceneID, movieJoins, tx); err != nil {
|
if err := jqb.UpdateMoviesScenes(sceneID, movieJoins, tx); err != nil {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (*models.Movie
|
|||||||
return qb.Find(idInt, nil)
|
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()
|
qb := models.NewMovieQueryBuilder()
|
||||||
movies, total := qb.Query(filter)
|
movies, total := qb.Query(movieFilter, filter)
|
||||||
return &models.FindMoviesResultType{
|
return &models.FindMoviesResultType{
|
||||||
Count: total,
|
Count: total,
|
||||||
Movies: movies,
|
Movies: movies,
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gobuffalo/packr/v2"
|
"github.com/gobuffalo/packr/v2"
|
||||||
"github.com/golang-migrate/migrate/v4"
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
sqlite3mig "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||||
"github.com/golang-migrate/migrate/v4/source"
|
"github.com/golang-migrate/migrate/v4/source"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
sqlite3 "github.com/mattn/go-sqlite3"
|
sqlite3 "github.com/mattn/go-sqlite3"
|
||||||
@@ -19,14 +19,14 @@ import (
|
|||||||
|
|
||||||
var DB *sqlx.DB
|
var DB *sqlx.DB
|
||||||
var dbPath string
|
var dbPath string
|
||||||
var appSchemaVersion uint = 7
|
var appSchemaVersion uint = 8
|
||||||
var databaseSchemaVersion uint
|
var databaseSchemaVersion uint
|
||||||
|
|
||||||
const sqlite3Driver = "sqlite3_regexp"
|
const sqlite3Driver = "sqlite3ex"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// register custom driver with regexp function
|
// register custom driver with regexp function
|
||||||
registerRegexpFunc()
|
registerCustomDriver()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Initialize(databasePath string) {
|
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
|
// 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.SetMaxOpenConns(25)
|
||||||
conn.SetMaxIdleConns(4)
|
conn.SetMaxIdleConns(4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("db.Open(): %q\n", err)
|
logger.Fatalf("db.Open(): %q\n", err)
|
||||||
}
|
}
|
||||||
DB = conn
|
|
||||||
|
return conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func Reset(databasePath string) error {
|
func Reset(databasePath string) error {
|
||||||
@@ -97,6 +108,10 @@ func Backup(backupPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RestoreFromBackup(backupPath string) error {
|
||||||
|
return os.Rename(backupPath, dbPath)
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate the database
|
// Migrate the database
|
||||||
func NeedsMigration() bool {
|
func NeedsMigration() bool {
|
||||||
return databaseSchemaVersion != appSchemaVersion
|
return databaseSchemaVersion != appSchemaVersion
|
||||||
@@ -123,10 +138,21 @@ func getMigrate() (*migrate.Migrate, error) {
|
|||||||
|
|
||||||
databasePath := utils.FixWindowsPath(dbPath)
|
databasePath := utils.FixWindowsPath(dbPath)
|
||||||
s, _ := WithInstance(packrSource)
|
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",
|
"packr2",
|
||||||
s,
|
s,
|
||||||
fmt.Sprintf("sqlite3://%s", "file:"+databasePath),
|
databasePath,
|
||||||
|
driver,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +179,8 @@ func RunMigrations() error {
|
|||||||
if stepNumber != 0 {
|
if stepNumber != 0 {
|
||||||
err = m.Steps(int(stepNumber))
|
err = m.Steps(int(stepNumber))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// migration failed
|
||||||
|
m.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,15 +192,23 @@ func RunMigrations() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerRegexpFunc() {
|
func registerCustomDriver() {
|
||||||
regexFn := func(re, s string) (bool, error) {
|
|
||||||
return regexp.MatchString(re, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
sql.Register(sqlite3Driver,
|
sql.Register(sqlite3Driver,
|
||||||
&sqlite3.SQLiteDriver{
|
&sqlite3.SQLiteDriver{
|
||||||
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
37
pkg/database/functions.go
Normal file
37
pkg/database/functions.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
106
pkg/database/migrations/8_movie_fix.up.sql
Normal file
106
pkg/database/migrations/8_movie_fix.up.sql
Normal file
@@ -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`;
|
||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
type Movie struct {
|
type Movie struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Aliases string `json:"aliases,omitempty"`
|
Aliases string `json:"aliases,omitempty"`
|
||||||
Duration string `json:"duration,omitempty"`
|
Duration int `json:"duration,omitempty"`
|
||||||
Date string `json:"date,omitempty"`
|
Date string `json:"date,omitempty"`
|
||||||
Rating string `json:"rating,omitempty"`
|
Rating int `json:"rating,omitempty"`
|
||||||
Director string `json:"director,omitempty"`
|
Director string `json:"director,omitempty"`
|
||||||
Synopsis string `json:"sypnopsis,omitempty"`
|
Synopsis string `json:"sypnopsis,omitempty"`
|
||||||
FrontImage string `json:"front_image,omitempty"`
|
FrontImage string `json:"front_image,omitempty"`
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type SceneFile struct {
|
|||||||
|
|
||||||
type SceneMovie struct {
|
type SceneMovie struct {
|
||||||
MovieName string `json:"movieName,omitempty"`
|
MovieName string `json:"movieName,omitempty"`
|
||||||
SceneIndex string `json:"scene_index,omitempty"`
|
SceneIndex int `json:"scene_index,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Scene struct {
|
type Scene struct {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ func (t *ExportTask) ExportScenes(ctx context.Context) {
|
|||||||
if movie.Name.Valid {
|
if movie.Name.Valid {
|
||||||
sceneMovieJSON := jsonschema.SceneMovie{
|
sceneMovieJSON := jsonschema.SceneMovie{
|
||||||
MovieName: movie.Name.String,
|
MovieName: movie.Name.String,
|
||||||
SceneIndex: sceneMovie.SceneIndex,
|
SceneIndex: int(sceneMovie.SceneIndex.Int64),
|
||||||
}
|
}
|
||||||
newSceneJSON.Movies = append(newSceneJSON.Movies, sceneMovieJSON)
|
newSceneJSON.Movies = append(newSceneJSON.Movies, sceneMovieJSON)
|
||||||
}
|
}
|
||||||
@@ -381,10 +381,10 @@ func (t *ExportTask) ExportMovies(ctx context.Context) {
|
|||||||
newMovieJSON.Date = utils.GetYMDFromDatabaseDate(movie.Date.String)
|
newMovieJSON.Date = utils.GetYMDFromDatabaseDate(movie.Date.String)
|
||||||
}
|
}
|
||||||
if movie.Rating.Valid {
|
if movie.Rating.Valid {
|
||||||
newMovieJSON.Rating = movie.Rating.String
|
newMovieJSON.Rating = int(movie.Rating.Int64)
|
||||||
}
|
}
|
||||||
if movie.Duration.Valid {
|
if movie.Duration.Valid {
|
||||||
newMovieJSON.Duration = movie.Duration.String
|
newMovieJSON.Duration = int(movie.Duration.Int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
if movie.Director.Valid {
|
if movie.Director.Valid {
|
||||||
|
|||||||
@@ -250,8 +250,6 @@ func (t *ImportTask) ImportMovies(ctx context.Context) {
|
|||||||
Name: sql.NullString{String: movieJSON.Name, Valid: true},
|
Name: sql.NullString{String: movieJSON.Name, Valid: true},
|
||||||
Aliases: sql.NullString{String: movieJSON.Aliases, Valid: true},
|
Aliases: sql.NullString{String: movieJSON.Aliases, Valid: true},
|
||||||
Date: models.SQLiteDate{String: movieJSON.Date, 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},
|
Director: sql.NullString{String: movieJSON.Director, Valid: true},
|
||||||
Synopsis: sql.NullString{String: movieJSON.Synopsis, Valid: true},
|
Synopsis: sql.NullString{String: movieJSON.Synopsis, Valid: true},
|
||||||
URL: sql.NullString{String: movieJSON.URL, 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)},
|
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)
|
_, err = qb.Create(newMovie, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
@@ -712,11 +717,19 @@ func (t *ImportTask) getMoviesScenes(input []jsonschema.SceneMovie, sceneID int,
|
|||||||
if movie == nil {
|
if movie == nil {
|
||||||
logger.Warnf("[scenes] movie %s does not exist", inputMovie.MovieName)
|
logger.Warnf("[scenes] movie %s does not exist", inputMovie.MovieName)
|
||||||
} else {
|
} else {
|
||||||
movies = append(movies, models.MoviesScenes{
|
toAdd := models.MoviesScenes{
|
||||||
MovieID: movie.ID,
|
MovieID: movie.ID,
|
||||||
SceneID: sceneID,
|
SceneID: sceneID,
|
||||||
SceneIndex: inputMovie.SceneIndex,
|
}
|
||||||
})
|
|
||||||
|
if inputMovie.SceneIndex != 0 {
|
||||||
|
toAdd.SceneIndex = sql.NullInt64{
|
||||||
|
Int64: int64(inputMovie.SceneIndex),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
movies = append(movies, toAdd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
|
||||||
type PerformersScenes struct {
|
type PerformersScenes struct {
|
||||||
PerformerID int `db:"performer_id" json:"performer_id"`
|
PerformerID int `db:"performer_id" json:"performer_id"`
|
||||||
SceneID int `db:"scene_id" json:"scene_id"`
|
SceneID int `db:"scene_id" json:"scene_id"`
|
||||||
@@ -8,7 +10,7 @@ type PerformersScenes struct {
|
|||||||
type MoviesScenes struct {
|
type MoviesScenes struct {
|
||||||
MovieID int `db:"movie_id" json:"movie_id"`
|
MovieID int `db:"movie_id" json:"movie_id"`
|
||||||
SceneID int `db:"scene_id" json:"scene_id"`
|
SceneID int `db:"scene_id" json:"scene_id"`
|
||||||
SceneIndex string `db:"scene_index" json:"scene_index"`
|
SceneIndex sql.NullInt64 `db:"scene_index" json:"scene_index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScenesTags struct {
|
type ScenesTags struct {
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ type Movie struct {
|
|||||||
Checksum string `db:"checksum" json:"checksum"`
|
Checksum string `db:"checksum" json:"checksum"`
|
||||||
Name sql.NullString `db:"name" json:"name"`
|
Name sql.NullString `db:"name" json:"name"`
|
||||||
Aliases sql.NullString `db:"aliases" json:"aliases"`
|
Aliases sql.NullString `db:"aliases" json:"aliases"`
|
||||||
Duration sql.NullString `db:"duration" json:"duration"`
|
Duration sql.NullInt64 `db:"duration" json:"duration"`
|
||||||
Date SQLiteDate `db:"date" json:"date"`
|
Date SQLiteDate `db:"date" json:"date"`
|
||||||
Rating sql.NullString `db:"rating" json:"rating"`
|
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
||||||
|
StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
|
||||||
Director sql.NullString `db:"director" json:"director"`
|
Director sql.NullString `db:"director" json:"director"`
|
||||||
Synopsis sql.NullString `db:"synopsis" json:"synopsis"`
|
Synopsis sql.NullString `db:"synopsis" json:"synopsis"`
|
||||||
URL sql.NullString `db:"url" json:"url"`
|
URL sql.NullString `db:"url" json:"url"`
|
||||||
@@ -21,4 +22,22 @@ type Movie struct {
|
|||||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_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"
|
var DefaultMovieImage string = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC"
|
||||||
|
|||||||
@@ -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
|
// if the movie already exists on the scene. It returns true if scene
|
||||||
// movie was added.
|
// 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)
|
ensureTx(tx)
|
||||||
|
|
||||||
existingMovies, err := qb.GetSceneMovies(sceneID, tx)
|
existingMovies, err := qb.GetSceneMovies(sceneID, tx)
|
||||||
@@ -180,7 +180,13 @@ func (qb *JoinsQueryBuilder) AddMoviesScene(sceneID int, movieID int, sceneIdx s
|
|||||||
movieJoin := MoviesScenes{
|
movieJoin := MoviesScenes{
|
||||||
MovieID: movieID,
|
MovieID: movieID,
|
||||||
SceneID: sceneID,
|
SceneID: sceneID,
|
||||||
SceneIndex: sceneIdx,
|
}
|
||||||
|
|
||||||
|
if sceneIdx != nil {
|
||||||
|
movieJoin.SceneIndex = sql.NullInt64{
|
||||||
|
Int64: int64(*sceneIdx),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
movieJoins := append(existingMovies, movieJoin)
|
movieJoins := append(existingMovies, movieJoin)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
@@ -16,8 +17,8 @@ func NewMovieQueryBuilder() MovieQueryBuilder {
|
|||||||
func (qb *MovieQueryBuilder) Create(newMovie Movie, tx *sqlx.Tx) (*Movie, error) {
|
func (qb *MovieQueryBuilder) Create(newMovie Movie, tx *sqlx.Tx) (*Movie, error) {
|
||||||
ensureTx(tx)
|
ensureTx(tx)
|
||||||
result, err := tx.NamedExec(
|
result, err := tx.NamedExec(
|
||||||
`INSERT INTO movies (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, :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,
|
newMovie,
|
||||||
)
|
)
|
||||||
@@ -35,20 +36,17 @@ func (qb *MovieQueryBuilder) Create(newMovie Movie, tx *sqlx.Tx) (*Movie, error)
|
|||||||
return &newMovie, nil
|
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)
|
ensureTx(tx)
|
||||||
_, err := tx.NamedExec(
|
_, err := tx.NamedExec(
|
||||||
`UPDATE movies SET `+SQLGenKeys(updatedMovie)+` WHERE movies.id = :id`,
|
`UPDATE movies SET `+SQLGenKeysPartial(updatedMovie)+` WHERE movies.id = :id`,
|
||||||
updatedMovie,
|
updatedMovie,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Get(&updatedMovie, `SELECT * FROM movies WHERE id = ? LIMIT 1`, updatedMovie.ID); err != nil {
|
return qb.Find(updatedMovie.ID, tx)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &updatedMovie, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *MovieQueryBuilder) Destroy(id string, tx *sqlx.Tx) error {
|
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)
|
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 {
|
if findFilter == nil {
|
||||||
findFilter = &FindFilterType{}
|
findFilter = &FindFilterType{}
|
||||||
}
|
}
|
||||||
|
if movieFilter == nil {
|
||||||
|
movieFilter = &MovieFilterType{}
|
||||||
|
}
|
||||||
|
|
||||||
var whereClauses []string
|
var whereClauses []string
|
||||||
var havingClauses []string
|
var havingClauses []string
|
||||||
@@ -125,6 +126,7 @@ func (qb *MovieQueryBuilder) Query(findFilter *FindFilterType) ([]*Movie, int) {
|
|||||||
body += `
|
body += `
|
||||||
left join movies_scenes as scenes_join on scenes_join.movie_id = movies.id
|
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 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 != "" {
|
if q := findFilter.Q; q != nil && *q != "" {
|
||||||
@@ -134,6 +136,16 @@ func (qb *MovieQueryBuilder) Query(findFilter *FindFilterType) ([]*Movie, int) {
|
|||||||
args = append(args, thisArgs...)
|
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)
|
sortAndPagination := qb.getMovieSort(findFilter) + getPagination(findFilter)
|
||||||
idsResult, countResult := executeFindQuery("movies", body, args, sortAndPagination, whereClauses, havingClauses)
|
idsResult, countResult := executeFindQuery("movies", body, args, sortAndPagination, whereClauses, havingClauses)
|
||||||
|
|
||||||
@@ -146,6 +158,29 @@ func (qb *MovieQueryBuilder) Query(findFilter *FindFilterType) ([]*Movie, int) {
|
|||||||
return movies, countResult
|
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 {
|
func (qb *MovieQueryBuilder) getMovieSort(findFilter *FindFilterType) string {
|
||||||
var sort string
|
var sort string
|
||||||
var direction string
|
var direction string
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
movie: GQL.MovieDataFragment;
|
movie: GQL.MovieDataFragment;
|
||||||
sceneIndex?: string;
|
sceneIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
|
export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
|
||||||
|
|||||||
@@ -8,10 +8,16 @@ import {
|
|||||||
DetailsEditNavbar,
|
DetailsEditNavbar,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
Modal,
|
Modal,
|
||||||
|
StudioSelect,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Table, Form } from "react-bootstrap";
|
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";
|
import { MovieScenesPanel } from "./MovieScenesPanel";
|
||||||
|
|
||||||
export const Movie: React.FC = () => {
|
export const Movie: React.FC = () => {
|
||||||
@@ -29,9 +35,10 @@ export const Movie: React.FC = () => {
|
|||||||
const [backImage, setBackImage] = useState<string | undefined>(undefined);
|
const [backImage, setBackImage] = useState<string | undefined>(undefined);
|
||||||
const [name, setName] = useState<string | undefined>(undefined);
|
const [name, setName] = useState<string | undefined>(undefined);
|
||||||
const [aliases, setAliases] = useState<string | undefined>(undefined);
|
const [aliases, setAliases] = useState<string | undefined>(undefined);
|
||||||
const [duration, setDuration] = useState<string | undefined>(undefined);
|
const [duration, setDuration] = useState<number | undefined>(undefined);
|
||||||
const [date, setDate] = useState<string | undefined>(undefined);
|
const [date, setDate] = useState<string | undefined>(undefined);
|
||||||
const [rating, setRating] = useState<string | undefined>(undefined);
|
const [rating, setRating] = useState<number | undefined>(undefined);
|
||||||
|
const [studioId, setStudioId] = useState<string>();
|
||||||
const [director, setDirector] = useState<string | undefined>(undefined);
|
const [director, setDirector] = useState<string | undefined>(undefined);
|
||||||
const [synopsis, setSynopsis] = useState<string | undefined>(undefined);
|
const [synopsis, setSynopsis] = useState<string | undefined>(undefined);
|
||||||
const [url, setUrl] = useState<string | undefined>(undefined);
|
const [url, setUrl] = useState<string | undefined>(undefined);
|
||||||
@@ -63,6 +70,7 @@ export const Movie: React.FC = () => {
|
|||||||
setDuration(state.duration ?? undefined);
|
setDuration(state.duration ?? undefined);
|
||||||
setDate(state.date ?? undefined);
|
setDate(state.date ?? undefined);
|
||||||
setRating(state.rating ?? undefined);
|
setRating(state.rating ?? undefined);
|
||||||
|
setStudioId(state?.studio?.id ?? undefined);
|
||||||
setDirector(state.director ?? undefined);
|
setDirector(state.director ?? undefined);
|
||||||
setSynopsis(state.synopsis ?? undefined);
|
setSynopsis(state.synopsis ?? undefined);
|
||||||
setUrl(state.url ?? undefined);
|
setUrl(state.url ?? undefined);
|
||||||
@@ -113,6 +121,7 @@ export const Movie: React.FC = () => {
|
|||||||
duration,
|
duration,
|
||||||
date,
|
date,
|
||||||
rating,
|
rating,
|
||||||
|
studio_id: studioId,
|
||||||
director,
|
director,
|
||||||
synopsis,
|
synopsis,
|
||||||
url,
|
url,
|
||||||
@@ -213,12 +222,10 @@ export const Movie: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
{TableUtils.renderDurationInput({
|
{TableUtils.renderDurationInput({
|
||||||
title: "Duration",
|
title: "Duration",
|
||||||
value: duration,
|
value: duration ? duration.toString() : "",
|
||||||
isEditing,
|
isEditing,
|
||||||
onChange: (value: string | undefined) => {
|
onChange: (value: string | undefined) =>
|
||||||
setDuration(value ?? "");
|
setDuration(value ? Number.parseInt(value, 10) : undefined),
|
||||||
},
|
|
||||||
asString: true,
|
|
||||||
})}
|
})}
|
||||||
{TableUtils.renderInputGroup({
|
{TableUtils.renderInputGroup({
|
||||||
title: "Date (YYYY-MM-DD)",
|
title: "Date (YYYY-MM-DD)",
|
||||||
@@ -226,6 +233,18 @@ export const Movie: React.FC = () => {
|
|||||||
isEditing,
|
isEditing,
|
||||||
onChange: setDate,
|
onChange: setDate,
|
||||||
})}
|
})}
|
||||||
|
<tr>
|
||||||
|
<td>Studio</td>
|
||||||
|
<td>
|
||||||
|
<StudioSelect
|
||||||
|
isDisabled={!isEditing}
|
||||||
|
onSelect={(items) =>
|
||||||
|
setStudioId(items.length > 0 ? items[0]?.id : undefined)
|
||||||
|
}
|
||||||
|
ids={studioId ? [studioId] : []}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{TableUtils.renderInputGroup({
|
{TableUtils.renderInputGroup({
|
||||||
title: "Director",
|
title: "Director",
|
||||||
value: director,
|
value: director,
|
||||||
@@ -234,9 +253,10 @@ export const Movie: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
{TableUtils.renderHtmlSelect({
|
{TableUtils.renderHtmlSelect({
|
||||||
title: "Rating",
|
title: "Rating",
|
||||||
value: rating,
|
value: rating ? rating : "",
|
||||||
isEditing,
|
isEditing,
|
||||||
onChange: (value: string) => setRating(value),
|
onChange: (value: string) =>
|
||||||
|
setRating(Number.parseInt(value, 10)),
|
||||||
selectOptions: ["", "1", "2", "3", "4", "5"],
|
selectOptions: ["", "1", "2", "3", "4", "5"],
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Form } from "react-bootstrap";
|
|||||||
|
|
||||||
type ValidTypes = GQL.SlimMovieDataFragment;
|
type ValidTypes = GQL.SlimMovieDataFragment;
|
||||||
|
|
||||||
export type MovieSceneIndexMap = Map<string, string | undefined>;
|
export type MovieSceneIndexMap = Map<string, number | undefined>;
|
||||||
|
|
||||||
export interface IProps {
|
export interface IProps {
|
||||||
movieSceneIndexes: MovieSceneIndexMap;
|
movieSceneIndexes: MovieSceneIndexMap;
|
||||||
@@ -30,7 +30,7 @@ export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
|||||||
return props.movieSceneIndexes.get(movie.id);
|
return props.movieSceneIndexes.get(movie.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateFieldChanged = (movieId: string, value: string) => {
|
const updateFieldChanged = (movieId: string, value: number) => {
|
||||||
const newMap = new Map(props.movieSceneIndexes);
|
const newMap = new Map(props.movieSceneIndexes);
|
||||||
newMap.set(movieId, value);
|
newMap.set(movieId, value);
|
||||||
props.onUpdate(newMap);
|
props.onUpdate(newMap);
|
||||||
@@ -48,9 +48,15 @@ export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
|||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
className="input-control"
|
className="input-control"
|
||||||
value={storeIdx[index] ?? ""}
|
value={storeIdx[index] ? storeIdx[index]?.toString() : ""}
|
||||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||||
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(
|
{["", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"].map(
|
||||||
|
|||||||
@@ -187,9 +187,12 @@ export class StashService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static useFindMovies(filter: ListFilterModel) {
|
public static useFindMovies(filter: ListFilterModel) {
|
||||||
|
const movieFilter = filter.makeMovieFilter();
|
||||||
|
|
||||||
return GQL.useFindMoviesQuery({
|
return GQL.useFindMoviesQuery({
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter.makeFindFilter(),
|
||||||
|
movie_filter: movieFilter,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
SceneFilterType,
|
SceneFilterType,
|
||||||
SceneMarkerFilterType,
|
SceneMarkerFilterType,
|
||||||
SortDirectionEnum,
|
SortDirectionEnum,
|
||||||
|
MovieFilterType,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
@@ -162,7 +163,10 @@ export class ListFilterModel {
|
|||||||
this.sortBy = "name";
|
this.sortBy = "name";
|
||||||
this.sortByOptions = ["name", "scenes_count"];
|
this.sortByOptions = ["name", "scenes_count"];
|
||||||
this.displayModeOptions = [DisplayMode.Grid];
|
this.displayModeOptions = [DisplayMode.Grid];
|
||||||
this.criterionOptions = [new NoneCriterionOption()];
|
this.criterionOptions = [
|
||||||
|
new NoneCriterionOption(),
|
||||||
|
new StudiosCriterionOption(),
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
case FilterMode.Galleries:
|
case FilterMode.Galleries:
|
||||||
this.sortBy = "path";
|
this.sortBy = "path";
|
||||||
@@ -552,4 +556,22 @@ export class ListFilterModel {
|
|||||||
});
|
});
|
||||||
return result;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user