diff --git a/.gitignore b/.gitignore index 34494cd8d..d3a1c21f0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,12 @@ ui/v2.5/src/core/generated-*.tsx # Jetbrains #### + +#### +# Visual Studio +#### +/.vs + # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml @@ -57,4 +63,4 @@ node_modules /stash dist -.DS_Store +.DS_Store \ No newline at end of file diff --git a/graphql/documents/data/gallery-slim.graphql b/graphql/documents/data/gallery-slim.graphql index ea98d30f0..c49ef2c11 100644 --- a/graphql/documents/data/gallery-slim.graphql +++ b/graphql/documents/data/gallery-slim.graphql @@ -4,7 +4,7 @@ fragment SlimGalleryData on Gallery { date url details - rating + rating100 organized files { ...GalleryFileData diff --git a/graphql/documents/data/gallery.graphql b/graphql/documents/data/gallery.graphql index 9d43244e9..f23e34b52 100644 --- a/graphql/documents/data/gallery.graphql +++ b/graphql/documents/data/gallery.graphql @@ -6,7 +6,7 @@ fragment GalleryData on Gallery { date url details - rating + rating100 organized files { diff --git a/graphql/documents/data/image-slim.graphql b/graphql/documents/data/image-slim.graphql index 37b0bc86f..b9f891fa8 100644 --- a/graphql/documents/data/image-slim.graphql +++ b/graphql/documents/data/image-slim.graphql @@ -1,7 +1,7 @@ fragment SlimImageData on Image { id title - rating + rating100 organized o_counter diff --git a/graphql/documents/data/image.graphql b/graphql/documents/data/image.graphql index 4fe1f0d0e..8142d9d49 100644 --- a/graphql/documents/data/image.graphql +++ b/graphql/documents/data/image.graphql @@ -1,7 +1,7 @@ fragment ImageData on Image { id title - rating + rating100 organized o_counter created_at diff --git a/graphql/documents/data/movie-slim.graphql b/graphql/documents/data/movie-slim.graphql index 8150986a8..28986b232 100644 --- a/graphql/documents/data/movie-slim.graphql +++ b/graphql/documents/data/movie-slim.graphql @@ -2,4 +2,5 @@ fragment SlimMovieData on Movie { id name front_image_path + rating100 } diff --git a/graphql/documents/data/movie.graphql b/graphql/documents/data/movie.graphql index f566e535d..1605e039e 100644 --- a/graphql/documents/data/movie.graphql +++ b/graphql/documents/data/movie.graphql @@ -5,7 +5,7 @@ fragment MovieData on Movie { aliases duration date - rating + rating100 director studio { diff --git a/graphql/documents/data/performer-slim.graphql b/graphql/documents/data/performer-slim.graphql index 62d1b9b7a..6479717f2 100644 --- a/graphql/documents/data/performer-slim.graphql +++ b/graphql/documents/data/performer-slim.graphql @@ -26,7 +26,7 @@ fragment SlimPerformerData on Performer { endpoint stash_id } - rating + rating100 death_date weight } diff --git a/graphql/documents/data/performer.graphql b/graphql/documents/data/performer.graphql index 2c27fb6cc..ecdb9eacc 100644 --- a/graphql/documents/data/performer.graphql +++ b/graphql/documents/data/performer.graphql @@ -33,7 +33,7 @@ fragment PerformerData on Performer { stash_id endpoint } - rating + rating100 details death_date hair_color diff --git a/graphql/documents/data/scene-slim.graphql b/graphql/documents/data/scene-slim.graphql index 574bafb9e..cd5cfd556 100644 --- a/graphql/documents/data/scene-slim.graphql +++ b/graphql/documents/data/scene-slim.graphql @@ -6,7 +6,7 @@ fragment SlimSceneData on Scene { director url date - rating + rating100 o_counter organized interactive diff --git a/graphql/documents/data/scene.graphql b/graphql/documents/data/scene.graphql index 2f9422f02..790b2f2c1 100644 --- a/graphql/documents/data/scene.graphql +++ b/graphql/documents/data/scene.graphql @@ -6,7 +6,7 @@ fragment SceneData on Scene { director url date - rating + rating100 o_counter organized interactive diff --git a/graphql/documents/data/studio-slim.graphql b/graphql/documents/data/studio-slim.graphql index 36b0fd287..c37513194 100644 --- a/graphql/documents/data/studio-slim.graphql +++ b/graphql/documents/data/studio-slim.graphql @@ -10,6 +10,6 @@ fragment SlimStudioData on Studio { id } details - rating + rating100 aliases } diff --git a/graphql/documents/data/studio.graphql b/graphql/documents/data/studio.graphql index 94e118b6b..52fd8b418 100644 --- a/graphql/documents/data/studio.graphql +++ b/graphql/documents/data/studio.graphql @@ -25,6 +25,6 @@ fragment StudioData on Studio { endpoint } details - rating + rating100 aliases } diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 21ac7ad7f..6185c9895 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -92,7 +92,9 @@ input PerformerFilterType { """Filter by StashID""" stash_id: StringCriterionInput """Filter by rating""" - rating: IntCriterionInput + rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: IntCriterionInput """Filter by url""" url: StringCriterionInput """Filter by hair color""" @@ -158,7 +160,9 @@ input SceneFilterType { """Filter by file count""" file_count: IntCriterionInput """Filter by rating""" - rating: IntCriterionInput + rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: IntCriterionInput """Filter by organized""" organized: Boolean """Filter by o-counter""" @@ -218,7 +222,9 @@ input MovieFilterType { """Filter by duration (in seconds)""" duration: IntCriterionInput """Filter by rating""" - rating: IntCriterionInput + rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: IntCriterionInput """Filter to only include movies with this studio""" studios: HierarchicalMultiCriterionInput """Filter to only include movies missing this property""" @@ -249,7 +255,9 @@ input StudioFilterType { """Filter to only include studios missing this property""" is_missing: String """Filter by rating""" - rating: IntCriterionInput + rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: IntCriterionInput """Filter by scene count""" scene_count: IntCriterionInput """Filter by image count""" @@ -288,7 +296,9 @@ input GalleryFilterType { """Filter to include/exclude galleries that were created from zip""" is_zip: Boolean """Filter by rating""" - rating: IntCriterionInput + rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: IntCriterionInput """Filter by organized""" organized: Boolean """Filter by average image resolution""" @@ -391,7 +401,9 @@ input ImageFilterType { """Filter by file count""" file_count: IntCriterionInput """Filter by rating""" - rating: IntCriterionInput + rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: IntCriterionInput """Filter by organized""" organized: Boolean """Filter by o-counter""" diff --git a/graphql/schema/types/gallery.graphql b/graphql/schema/types/gallery.graphql index 993f5e010..2f7916a14 100644 --- a/graphql/schema/types/gallery.graphql +++ b/graphql/schema/types/gallery.graphql @@ -7,7 +7,10 @@ type Gallery { url: String date: String details: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int organized: Boolean! created_at: Time! updated_at: Time! @@ -32,7 +35,10 @@ input GalleryCreateInput { url: String date: String details: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int organized: Boolean scene_ids: [ID!] studio_id: ID @@ -47,7 +53,10 @@ input GalleryUpdateInput { url: String date: String details: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int organized: Boolean scene_ids: [ID!] studio_id: ID @@ -63,7 +72,10 @@ input BulkGalleryUpdateInput { url: String date: String details: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int organized: Boolean scene_ids: BulkUpdateIds studio_id: ID diff --git a/graphql/schema/types/image.graphql b/graphql/schema/types/image.graphql index 82aa1e443..3eed1ee85 100644 --- a/graphql/schema/types/image.graphql +++ b/graphql/schema/types/image.graphql @@ -2,7 +2,10 @@ type Image { id: ID! checksum: String @deprecated(reason: "Use files.fingerprints") title: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int o_counter: Int organized: Boolean! path: String! @deprecated(reason: "Use files.path") @@ -37,7 +40,10 @@ input ImageUpdateInput { clientMutationId: String id: ID! title: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int organized: Boolean studio_id: ID @@ -52,7 +58,10 @@ input BulkImageUpdateInput { clientMutationId: String ids: [ID!] title: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int organized: Boolean studio_id: ID diff --git a/graphql/schema/types/movie.graphql b/graphql/schema/types/movie.graphql index 3d100e141..14910c003 100644 --- a/graphql/schema/types/movie.graphql +++ b/graphql/schema/types/movie.graphql @@ -6,7 +6,10 @@ type Movie { """Duration in seconds""" duration: Int date: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int studio: Studio director: String synopsis: String @@ -26,7 +29,10 @@ input MovieCreateInput { """Duration in seconds""" duration: Int date: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int studio_id: ID director: String synopsis: String @@ -43,7 +49,10 @@ input MovieUpdateInput { aliases: String duration: Int date: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int studio_id: ID director: String synopsis: String @@ -57,7 +66,10 @@ input MovieUpdateInput { input BulkMovieUpdateInput { clientMutationId: String ids: [ID!] - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int studio_id: ID director: String } diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index 1a2002610..651341fc2 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -37,7 +37,10 @@ type Performer { gallery_count: Int # Resolver scenes: [Scene!]! stash_ids: [StashID!]! - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int details: String death_date: String hair_color: String @@ -72,7 +75,10 @@ input PerformerCreateInput { """This should be a URL or a base64 encoded data URL""" image: String stash_ids: [StashIDInput!] - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int details: String death_date: String hair_color: String @@ -105,7 +111,10 @@ input PerformerUpdateInput { """This should be a URL or a base64 encoded data URL""" image: String stash_ids: [StashIDInput!] - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int details: String death_date: String hair_color: String @@ -135,7 +144,10 @@ input BulkPerformerUpdateInput { instagram: String favorite: Boolean tag_ids: BulkUpdateIds - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int details: String death_date: String hair_color: String diff --git a/graphql/schema/types/scene.graphql b/graphql/schema/types/scene.graphql index 6360c3204..27fd2b52f 100644 --- a/graphql/schema/types/scene.graphql +++ b/graphql/schema/types/scene.graphql @@ -41,7 +41,10 @@ type Scene { director: String url: String date: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int organized: Boolean! o_counter: Int path: String! @deprecated(reason: "Use files.path") @@ -106,7 +109,10 @@ input SceneUpdateInput { director: String url: String date: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int o_counter: Int organized: Boolean studio_id: ID @@ -141,7 +147,10 @@ input BulkSceneUpdateInput { director: String url: String date: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int organized: Boolean studio_id: ID gallery_ids: BulkUpdateIds @@ -191,7 +200,10 @@ type SceneParserResult { director: String url: String date: String - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int studio_id: ID gallery_ids: [ID!] performer_ids: [ID!] diff --git a/graphql/schema/types/studio.graphql b/graphql/schema/types/studio.graphql index 7bf4bb355..097ea8340 100644 --- a/graphql/schema/types/studio.graphql +++ b/graphql/schema/types/studio.graphql @@ -13,7 +13,10 @@ type Studio { image_count: Int # Resolver gallery_count: Int # Resolver stash_ids: [StashID!]! - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int details: String created_at: Time! updated_at: Time! @@ -28,7 +31,10 @@ input StudioCreateInput { """This should be a URL or a base64 encoded data URL""" image: String stash_ids: [StashIDInput!] - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int details: String aliases: [String!] ignore_auto_tag: Boolean @@ -42,7 +48,10 @@ input StudioUpdateInput { """This should be a URL or a base64 encoded data URL""" image: String stash_ids: [StashIDInput!] - rating: Int + # rating expressed as 1-5 + rating: Int @deprecated(reason: "Use 1-100 range with rating100") + # rating expressed as 1-100 + rating100: Int details: String aliases: [String!] ignore_auto_tag: Boolean diff --git a/internal/api/changeset_translator.go b/internal/api/changeset_translator.go index 3cbb0ea48..3f88a6483 100644 --- a/internal/api/changeset_translator.go +++ b/internal/api/changeset_translator.go @@ -189,6 +189,36 @@ func (t changesetTranslator) nullInt64(value *int, field string) *sql.NullInt64 return ret } +func (t changesetTranslator) ratingConversion(legacyValue *int, rating100Value *int) *sql.NullInt64 { + const ( + legacyField = "rating" + rating100Field = "rating100" + ) + + legacyRating := t.nullInt64(legacyValue, legacyField) + if legacyRating != nil { + if legacyRating.Valid { + legacyRating.Int64 = int64(models.Rating5To100(int(legacyRating.Int64))) + } + return legacyRating + } + return t.nullInt64(rating100Value, rating100Field) +} + +func (t changesetTranslator) ratingConversionOptional(legacyValue *int, rating100Value *int) models.OptionalInt { + const ( + legacyField = "rating" + rating100Field = "rating100" + ) + + legacyRating := t.optionalInt(legacyValue, legacyField) + if legacyRating.Set && !(legacyRating.Null) { + legacyRating.Value = int(models.Rating5To100(int(legacyRating.Value))) + return legacyRating + } + return t.optionalInt(rating100Value, rating100Field) +} + func (t changesetTranslator) optionalInt(value *int, field string) models.OptionalInt { if !t.hasField(field) { return models.OptionalInt{} diff --git a/internal/api/resolver_model_gallery.go b/internal/api/resolver_model_gallery.go index 8a75b8a28..1d1518b9b 100644 --- a/internal/api/resolver_model_gallery.go +++ b/internal/api/resolver_model_gallery.go @@ -189,6 +189,18 @@ func (r *galleryResolver) Checksum(ctx context.Context, obj *models.Gallery) (st return obj.PrimaryChecksum(), nil } +func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) { + if obj.Rating != nil { + rating := models.Rating100To5(*obj.Rating) + return &rating, nil + } + return nil, nil +} + +func (r *galleryResolver) Rating100(ctx context.Context, obj *models.Gallery) (*int, error) { + return obj.Rating, nil +} + func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) { if !obj.SceneIDs.Loaded() { if err := r.withTxn(ctx, func(ctx context.Context) error { diff --git a/internal/api/resolver_model_image.go b/internal/api/resolver_model_image.go index 136a46622..4e6ef8605 100644 --- a/internal/api/resolver_model_image.go +++ b/internal/api/resolver_model_image.go @@ -144,6 +144,18 @@ func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret [ return ret, firstError(errs) } +func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) { + if obj.Rating != nil { + rating := models.Rating100To5(*obj.Rating) + return &rating, nil + } + return nil, nil +} + +func (r *imageResolver) Rating100(ctx context.Context, obj *models.Image) (*int, error) { + return obj.Rating, nil +} + func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *models.Studio, err error) { if obj.StudioID == nil { return nil, nil diff --git a/internal/api/resolver_model_movie.go b/internal/api/resolver_model_movie.go index e587e3ba5..5101dd4f9 100644 --- a/internal/api/resolver_model_movie.go +++ b/internal/api/resolver_model_movie.go @@ -48,6 +48,14 @@ func (r *movieResolver) Date(ctx context.Context, obj *models.Movie) (*string, e } func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, error) { + if obj.Rating.Valid { + rating := models.Rating100To5(int(obj.Rating.Int64)) + return &rating, nil + } + return nil, nil +} + +func (r *movieResolver) Rating100(ctx context.Context, obj *models.Movie) (*int, error) { if obj.Rating.Valid { rating := int(obj.Rating.Int64) return &rating, nil diff --git a/internal/api/resolver_model_performer.go b/internal/api/resolver_model_performer.go index f80cd4c35..2bb297a3d 100644 --- a/internal/api/resolver_model_performer.go +++ b/internal/api/resolver_model_performer.go @@ -107,6 +107,18 @@ func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) return stashIDsSliceToPtrSlice(ret), nil } +func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) { + if obj.Rating != nil { + rating := models.Rating100To5(*obj.Rating) + return &rating, nil + } + return nil, nil +} + +func (r *performerResolver) Rating100(ctx context.Context, obj *models.Performer) (*int, error) { + return obj.Rating, nil +} + func (r *performerResolver) DeathDate(ctx context.Context, obj *models.Performer) (*string, error) { if obj.DeathDate != nil { ret := obj.DeathDate.String() diff --git a/internal/api/resolver_model_scene.go b/internal/api/resolver_model_scene.go index 62213e7f0..7f0d64d98 100644 --- a/internal/api/resolver_model_scene.go +++ b/internal/api/resolver_model_scene.go @@ -141,6 +141,18 @@ func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoF return ret, nil } +func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) { + if obj.Rating != nil { + rating := models.Rating100To5(*obj.Rating) + return &rating, nil + } + return nil, nil +} + +func (r *sceneResolver) Rating100(ctx context.Context, obj *models.Scene) (*int, error) { + return obj.Rating, nil +} + func resolveFingerprints(f *file.BaseFile) []*Fingerprint { ret := make([]*Fingerprint, len(f.Fingerprints)) diff --git a/internal/api/resolver_model_studio.go b/internal/api/resolver_model_studio.go index 79ef8259e..5b8b15d21 100644 --- a/internal/api/resolver_model_studio.go +++ b/internal/api/resolver_model_studio.go @@ -126,6 +126,14 @@ func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*m } func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) { + if obj.Rating.Valid { + rating := models.Rating100To5(int(obj.Rating.Int64)) + return &rating, nil + } + return nil, nil +} + +func (r *studioResolver) Rating100(ctx context.Context, obj *models.Studio) (*int, error) { if obj.Rating.Valid { rating := int(obj.Rating.Int64) return &rating, nil diff --git a/internal/api/resolver_mutation_gallery.go b/internal/api/resolver_mutation_gallery.go index 43f554d22..51ff989a3 100644 --- a/internal/api/resolver_mutation_gallery.go +++ b/internal/api/resolver_mutation_gallery.go @@ -68,7 +68,13 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat d := models.NewDate(*input.Date) newGallery.Date = &d } - newGallery.Rating = input.Rating + + if input.Rating100 != nil { + newGallery.Rating = input.Rating100 + } else if input.Rating != nil { + rating := models.Rating5To100(*input.Rating) + newGallery.Rating = &rating + } if input.StudioID != nil { studioID, _ := strconv.Atoi(*input.StudioID) @@ -187,7 +193,7 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle updatedGallery.Details = translator.optionalString(input.Details, "details") updatedGallery.URL = translator.optionalString(input.URL, "url") updatedGallery.Date = translator.optionalDate(input.Date, "date") - updatedGallery.Rating = translator.optionalInt(input.Rating, "rating") + updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) @@ -262,8 +268,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall updatedGallery.Details = translator.optionalString(input.Details, "details") updatedGallery.URL = translator.optionalString(input.URL, "url") updatedGallery.Date = translator.optionalDate(input.Date, "date") - updatedGallery.Rating = translator.optionalInt(input.Rating, "rating") - + updatedGallery.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) var err error updatedGallery.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { diff --git a/internal/api/resolver_mutation_image.go b/internal/api/resolver_mutation_image.go index a6d0577f7..135888f8f 100644 --- a/internal/api/resolver_mutation_image.go +++ b/internal/api/resolver_mutation_image.go @@ -103,7 +103,7 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp updatedImage := models.NewImagePartial() updatedImage.Title = translator.optionalString(input.Title, "title") - updatedImage.Rating = translator.optionalInt(input.Rating, "rating") + updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) @@ -189,7 +189,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU } updatedImage.Title = translator.optionalString(input.Title, "title") - updatedImage.Rating = translator.optionalInt(input.Rating, "rating") + updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) diff --git a/internal/api/resolver_mutation_movie.go b/internal/api/resolver_mutation_movie.go index 0a22350b6..f3d3e529d 100644 --- a/internal/api/resolver_mutation_movie.go +++ b/internal/api/resolver_mutation_movie.go @@ -76,9 +76,11 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp newMovie.Date = models.SQLiteDate{String: *input.Date, Valid: true} } - if input.Rating != nil { - rating := int64(*input.Rating) - newMovie.Rating = sql.NullInt64{Int64: rating, Valid: true} + if input.Rating100 != nil { + newMovie.Rating = sql.NullInt64{Int64: int64(*input.Rating100), Valid: true} + } else if input.Rating != nil { + rating := models.Rating5To100(*input.Rating) + newMovie.Rating = sql.NullInt64{Int64: int64(rating), Valid: true} } if input.StudioID != nil { @@ -166,7 +168,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp updatedMovie.Aliases = translator.nullString(input.Aliases, "aliases") updatedMovie.Duration = translator.nullInt64(input.Duration, "duration") updatedMovie.Date = translator.sqliteDate(input.Date, "date") - updatedMovie.Rating = translator.nullInt64(input.Rating, "rating") + updatedMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100) updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id") updatedMovie.Director = translator.nullString(input.Director, "director") updatedMovie.Synopsis = translator.nullString(input.Synopsis, "synopsis") @@ -239,7 +241,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime}, } - updatedMovie.Rating = translator.nullInt64(input.Rating, "rating") + updatedMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100) updatedMovie.StudioID = translator.nullInt64FromString(input.StudioID, "studio_id") updatedMovie.Director = translator.nullString(input.Director, "director") diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index 15c6610a8..33e440fd7 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -114,8 +114,11 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC if input.Favorite != nil { newPerformer.Favorite = *input.Favorite } - if input.Rating != nil { - newPerformer.Rating = input.Rating + if input.Rating100 != nil { + newPerformer.Rating = input.Rating100 + } else if input.Rating != nil { + rating := models.Rating5To100(*input.Rating) + newPerformer.Rating = &rating } if input.Details != nil { newPerformer.Details = *input.Details @@ -239,7 +242,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter") updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram") updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite") - updatedPerformer.Rating = translator.optionalInt(input.Rating, "rating") + updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) updatedPerformer.Details = translator.optionalString(input.Details, "details") updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date") updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color") @@ -352,7 +355,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter") updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram") updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite") - updatedPerformer.Rating = translator.optionalInt(input.Rating, "rating") + updatedPerformer.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) updatedPerformer.Details = translator.optionalString(input.Details, "details") updatedPerformer.DeathDate = translator.optionalDate(input.DeathDate, "death_date") updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color") diff --git a/internal/api/resolver_mutation_scene.go b/internal/api/resolver_mutation_scene.go index 15aff6c6e..2d3bf2725 100644 --- a/internal/api/resolver_mutation_scene.go +++ b/internal/api/resolver_mutation_scene.go @@ -172,7 +172,7 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr updatedScene.Director = translator.optionalString(input.Director, "director") updatedScene.URL = translator.optionalString(input.URL, "url") updatedScene.Date = translator.optionalDate(input.Date, "date") - updatedScene.Rating = translator.optionalInt(input.Rating, "rating") + updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter") var err error updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") @@ -348,7 +348,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU updatedScene.Director = translator.optionalString(input.Director, "director") updatedScene.URL = translator.optionalString(input.URL, "url") updatedScene.Date = translator.optionalDate(input.Date, "date") - updatedScene.Rating = translator.optionalInt(input.Rating, "rating") + updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) diff --git a/internal/api/resolver_mutation_studio.go b/internal/api/resolver_mutation_studio.go index e9ee8965b..98c871323 100644 --- a/internal/api/resolver_mutation_studio.go +++ b/internal/api/resolver_mutation_studio.go @@ -58,11 +58,18 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateI newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true} } - if input.Rating != nil { - newStudio.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true} - } else { - newStudio.Rating = sql.NullInt64{Valid: false} + if input.Rating100 != nil { + newStudio.Rating = sql.NullInt64{ + Int64: int64(*input.Rating100), + Valid: true, + } + } else if input.Rating != nil { + newStudio.Rating = sql.NullInt64{ + Int64: int64(models.Rating5To100(*input.Rating)), + Valid: true, + } } + if input.Details != nil { newStudio.Details = sql.NullString{String: *input.Details, Valid: true} } @@ -150,7 +157,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI updatedStudio.URL = translator.nullString(input.URL, "url") updatedStudio.Details = translator.nullString(input.Details, "details") updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id") - updatedStudio.Rating = translator.nullInt64(input.Rating, "rating") + updatedStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100) updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag // Start the transaction and save the studio diff --git a/internal/manager/config/map.go b/internal/manager/config/map.go index 3394d7040..b13cf73ac 100644 --- a/internal/manager/config/map.go +++ b/internal/manager/config/map.go @@ -32,15 +32,19 @@ func toSnakeCase(v string) string { func fromSnakeCase(v string) string { var buf bytes.Buffer + leadingUnderscore := true capvar := false for i, c := range v { switch { - case c == '_' && i > 0: + case c == '_' && !leadingUnderscore && i > 0: capvar = true + case c == '_' && leadingUnderscore: + buf.WriteRune(c) case capvar: buf.WriteRune(unicode.ToUpper(c)) capvar = false default: + leadingUnderscore = false buf.WriteRune(c) } } @@ -54,7 +58,13 @@ func toSnakeCaseMap(m map[string]interface{}) map[string]interface{} { for key, val := range m { adjKey := toSnakeCase(key) - nm[adjKey] = val + + switch v := val.(type) { + case map[string]interface{}: + nm[adjKey] = toSnakeCaseMap(v) + default: + nm[adjKey] = val + } } return nm @@ -68,13 +78,15 @@ func convertMapValue(val interface{}) interface{} { case map[interface{}]interface{}: ret := cast.ToStringMap(v) for k, vv := range ret { - ret[k] = convertMapValue(vv) + adjKey := fromSnakeCase(k) + ret[adjKey] = convertMapValue(vv) } return ret case map[string]interface{}: ret := make(map[string]interface{}) for k, vv := range v { - ret[k] = convertMapValue(vv) + adjKey := fromSnakeCase(k) + ret[adjKey] = convertMapValue(vv) } return ret case []interface{}: diff --git a/internal/manager/filename_parser.go b/internal/manager/filename_parser.go index bf5a05f91..9ee876a8c 100644 --- a/internal/manager/filename_parser.go +++ b/internal/manager/filename_parser.go @@ -32,6 +32,7 @@ type SceneParserResult struct { URL *string `json:"url"` Date *string `json:"date"` Rating *int `json:"rating"` + Rating100 *int `json:"rating100"` StudioID *string `json:"studio_id"` GalleryIds []string `json:"gallery_ids"` PerformerIds []string `json:"performer_ids"` @@ -113,6 +114,7 @@ func initParserFields() { ret["d"] = newParserField("d", `(?:\.|-|_)`, false) ret["rating"] = newParserField("rating", `\d`, true) + ret["rating100"] = newParserField("rating100", `\d`, true) ret["performer"] = newParserField("performer", ".*", true) ret["studio"] = newParserField("studio", ".*", true) ret["movie"] = newParserField("movie", ".*", true) @@ -256,6 +258,10 @@ func validateRating(rating int) bool { return rating >= 1 && rating <= 5 } +func validateRating100(rating100 int) bool { + return rating100 >= 1 && rating100 <= 100 +} + func validateDate(dateStr string) bool { splits := strings.Split(dateStr, "-") if len(splits) != 3 { @@ -347,6 +353,13 @@ func (h *sceneHolder) setField(field parserField, value interface{}) { case "rating": rating, _ := strconv.Atoi(value.(string)) if validateRating(rating) { + // convert to 1-100 scale + rating = models.Rating5To100(rating) + h.result.Rating = &rating + } + case "rating100": + rating, _ := strconv.Atoi(value.(string)) + if validateRating100(rating) { h.result.Rating = &rating } case "performer": diff --git a/pkg/models/gallery.go b/pkg/models/gallery.go index 4818e1ab9..fa95cc559 100644 --- a/pkg/models/gallery.go +++ b/pkg/models/gallery.go @@ -23,8 +23,10 @@ type GalleryFilterType struct { IsMissing *string `json:"is_missing"` // Filter to include/exclude galleries that were created from zip IsZip *bool `json:"is_zip"` - // Filter by rating + // Filter by rating expressed as 1-5 Rating *IntCriterionInput `json:"rating"` + // Filter by rating expressed as 1-100 + Rating100 *IntCriterionInput `json:"rating100"` // Filter by organized Organized *bool `json:"organized"` // Filter by average image resolution @@ -65,6 +67,7 @@ type GalleryUpdateInput struct { Date *string `json:"date"` Details *string `json:"details"` Rating *int `json:"rating"` + Rating100 *int `json:"rating100"` Organized *bool `json:"organized"` SceneIds []string `json:"scene_ids"` StudioID *string `json:"studio_id"` diff --git a/pkg/models/image.go b/pkg/models/image.go index 57e47ad30..2b908dcb6 100644 --- a/pkg/models/image.go +++ b/pkg/models/image.go @@ -14,8 +14,10 @@ type ImageFilterType struct { Path *StringCriterionInput `json:"path"` // Filter by file count FileCount *IntCriterionInput `json:"file_count"` - // Filter by rating + // Filter by rating expressed as 1-5 Rating *IntCriterionInput `json:"rating"` + // Filter by rating expressed as 1-100 + Rating100 *IntCriterionInput `json:"rating100"` // Filter by organized Organized *bool `json:"organized"` // Filter by o-counter diff --git a/pkg/models/model_gallery.go b/pkg/models/model_gallery.go index d5c648853..932d5cd17 100644 --- a/pkg/models/model_gallery.go +++ b/pkg/models/model_gallery.go @@ -12,13 +12,14 @@ import ( type Gallery struct { ID int `json:"id"` - Title string `json:"title"` - URL string `json:"url"` - Date *Date `json:"date"` - Details string `json:"details"` - Rating *int `json:"rating"` - Organized bool `json:"organized"` - StudioID *int `json:"studio_id"` + Title string `json:"title"` + URL string `json:"url"` + Date *Date `json:"date"` + Details string `json:"details"` + // Rating expressed in 1-100 scale + Rating *int `json:"rating"` + Organized bool `json:"organized"` + StudioID *int `json:"studio_id"` // transient - not persisted Files RelatedFiles @@ -104,10 +105,11 @@ type GalleryPartial struct { // Path OptionalString // Checksum OptionalString // Zip OptionalBool - Title OptionalString - URL OptionalString - Date OptionalDate - Details OptionalString + Title OptionalString + URL OptionalString + Date OptionalDate + Details OptionalString + // Rating expressed in 1-100 scale Rating OptionalInt Organized OptionalBool StudioID OptionalInt diff --git a/pkg/models/model_image.go b/pkg/models/model_image.go index 377e0cc5a..dcece55bb 100644 --- a/pkg/models/model_image.go +++ b/pkg/models/model_image.go @@ -14,11 +14,12 @@ import ( type Image struct { ID int `json:"id"` - Title string `json:"title"` - Rating *int `json:"rating"` - Organized bool `json:"organized"` - OCounter int `json:"o_counter"` - StudioID *int `json:"studio_id"` + Title string `json:"title"` + // Rating expressed in 1-100 scale + Rating *int `json:"rating"` + Organized bool `json:"organized"` + OCounter int `json:"o_counter"` + StudioID *int `json:"studio_id"` // transient - not persisted Files RelatedImageFiles @@ -113,7 +114,8 @@ type ImageCreateInput struct { } type ImagePartial struct { - Title OptionalString + Title OptionalString + // Rating expressed in 1-100 scale Rating OptionalInt Organized OptionalBool OCounter OptionalInt diff --git a/pkg/models/model_movie.go b/pkg/models/model_movie.go index b2e2631e3..756a6c936 100644 --- a/pkg/models/model_movie.go +++ b/pkg/models/model_movie.go @@ -8,12 +8,13 @@ import ( ) type Movie struct { - ID int `db:"id" json:"id"` - 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"` + ID int `db:"id" json:"id"` + 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 expressed in 1-100 scale Rating sql.NullInt64 `db:"rating" json:"rating"` StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"` Director sql.NullString `db:"director" json:"director"` @@ -24,12 +25,13 @@ type Movie struct { } type MoviePartial struct { - ID int `db:"id" json:"id"` - 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"` + ID int `db:"id" json:"id"` + 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 expressed in 1-100 scale Rating *sql.NullInt64 `db:"rating" json:"rating"` StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"` Director *sql.NullString `db:"director" json:"director"` diff --git a/pkg/models/model_performer.go b/pkg/models/model_performer.go index b6c9eff4d..18c864fc4 100644 --- a/pkg/models/model_performer.go +++ b/pkg/models/model_performer.go @@ -7,59 +7,61 @@ import ( ) type Performer struct { - ID int `json:"id"` - Checksum string `json:"checksum"` - Name string `json:"name"` - Gender GenderEnum `json:"gender"` - URL string `json:"url"` - Twitter string `json:"twitter"` - Instagram string `json:"instagram"` - Birthdate *Date `json:"birthdate"` - Ethnicity string `json:"ethnicity"` - Country string `json:"country"` - EyeColor string `json:"eye_color"` - Height *int `json:"height"` - Measurements string `json:"measurements"` - FakeTits string `json:"fake_tits"` - CareerLength string `json:"career_length"` - Tattoos string `json:"tattoos"` - Piercings string `json:"piercings"` - Aliases string `json:"aliases"` - Favorite bool `json:"favorite"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Rating *int `json:"rating"` - Details string `json:"details"` - DeathDate *Date `json:"death_date"` - HairColor string `json:"hair_color"` - Weight *int `json:"weight"` - IgnoreAutoTag bool `json:"ignore_auto_tag"` + ID int `json:"id"` + Checksum string `json:"checksum"` + Name string `json:"name"` + Gender GenderEnum `json:"gender"` + URL string `json:"url"` + Twitter string `json:"twitter"` + Instagram string `json:"instagram"` + Birthdate *Date `json:"birthdate"` + Ethnicity string `json:"ethnicity"` + Country string `json:"country"` + EyeColor string `json:"eye_color"` + Height *int `json:"height"` + Measurements string `json:"measurements"` + FakeTits string `json:"fake_tits"` + CareerLength string `json:"career_length"` + Tattoos string `json:"tattoos"` + Piercings string `json:"piercings"` + Aliases string `json:"aliases"` + Favorite bool `json:"favorite"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + // Rating expressed in 1-100 scale + Rating *int `json:"rating"` + Details string `json:"details"` + DeathDate *Date `json:"death_date"` + HairColor string `json:"hair_color"` + Weight *int `json:"weight"` + IgnoreAutoTag bool `json:"ignore_auto_tag"` } // PerformerPartial represents part of a Performer object. It is used to update // the database entry. type PerformerPartial struct { - ID int - Checksum OptionalString - Name OptionalString - Gender OptionalString - URL OptionalString - Twitter OptionalString - Instagram OptionalString - Birthdate OptionalDate - Ethnicity OptionalString - Country OptionalString - EyeColor OptionalString - Height OptionalInt - Measurements OptionalString - FakeTits OptionalString - CareerLength OptionalString - Tattoos OptionalString - Piercings OptionalString - Aliases OptionalString - Favorite OptionalBool - CreatedAt OptionalTime - UpdatedAt OptionalTime + ID int + Checksum OptionalString + Name OptionalString + Gender OptionalString + URL OptionalString + Twitter OptionalString + Instagram OptionalString + Birthdate OptionalDate + Ethnicity OptionalString + Country OptionalString + EyeColor OptionalString + Height OptionalInt + Measurements OptionalString + FakeTits OptionalString + CareerLength OptionalString + Tattoos OptionalString + Piercings OptionalString + Aliases OptionalString + Favorite OptionalBool + CreatedAt OptionalTime + UpdatedAt OptionalTime + // Rating expressed in 1-100 scale Rating OptionalInt Details OptionalString DeathDate OptionalDate diff --git a/pkg/models/model_scene.go b/pkg/models/model_scene.go index b4dc45128..16df1a67f 100644 --- a/pkg/models/model_scene.go +++ b/pkg/models/model_scene.go @@ -12,17 +12,18 @@ import ( // Scene stores the metadata for a single video scene. type Scene struct { - ID int `json:"id"` - Title string `json:"title"` - Code string `json:"code"` - Details string `json:"details"` - Director string `json:"director"` - URL string `json:"url"` - Date *Date `json:"date"` - Rating *int `json:"rating"` - Organized bool `json:"organized"` - OCounter int `json:"o_counter"` - StudioID *int `json:"studio_id"` + ID int `json:"id"` + Title string `json:"title"` + Code string `json:"code"` + Details string `json:"details"` + Director string `json:"director"` + URL string `json:"url"` + Date *Date `json:"date"` + // Rating expressed in 1-100 scale + Rating *int `json:"rating"` + Organized bool `json:"organized"` + OCounter int `json:"o_counter"` + StudioID *int `json:"studio_id"` // transient - not persisted Files RelatedVideoFiles @@ -134,12 +135,13 @@ func (s *Scene) LoadRelationships(ctx context.Context, l SceneReader) error { // ScenePartial represents part of a Scene object. It is used to update // the database entry. type ScenePartial struct { - Title OptionalString - Code OptionalString - Details OptionalString - Director OptionalString - URL OptionalString - Date OptionalDate + Title OptionalString + Code OptionalString + Details OptionalString + Director OptionalString + URL OptionalString + Date OptionalDate + // Rating expressed in 1-100 scale Rating OptionalInt Organized OptionalBool OCounter OptionalInt @@ -168,22 +170,25 @@ type SceneMovieInput struct { } type SceneUpdateInput struct { - ClientMutationID *string `json:"clientMutationId"` - ID string `json:"id"` - Title *string `json:"title"` - Code *string `json:"code"` - Details *string `json:"details"` - Director *string `json:"director"` - URL *string `json:"url"` - Date *string `json:"date"` - Rating *int `json:"rating"` - OCounter *int `json:"o_counter"` - Organized *bool `json:"organized"` - StudioID *string `json:"studio_id"` - GalleryIds []string `json:"gallery_ids"` - PerformerIds []string `json:"performer_ids"` - Movies []*SceneMovieInput `json:"movies"` - TagIds []string `json:"tag_ids"` + ClientMutationID *string `json:"clientMutationId"` + ID string `json:"id"` + Title *string `json:"title"` + Code *string `json:"code"` + Details *string `json:"details"` + Director *string `json:"director"` + URL *string `json:"url"` + Date *string `json:"date"` + // Rating expressed in 1-5 scale + Rating *int `json:"rating"` + // Rating expressed in 1-100 scale + Rating100 *int `json:"rating100"` + OCounter *int `json:"o_counter"` + Organized *bool `json:"organized"` + StudioID *string `json:"studio_id"` + GalleryIds []string `json:"gallery_ids"` + PerformerIds []string `json:"performer_ids"` + Movies []*SceneMovieInput `json:"movies"` + TagIds []string `json:"tag_ids"` // This should be a URL or a base64 encoded data URL CoverImage *string `json:"cover_image"` StashIds []StashID `json:"stash_ids"` @@ -204,7 +209,7 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput { stashIDs = s.StashIDs.StashIDs } - return SceneUpdateInput{ + ret := SceneUpdateInput{ ID: strconv.Itoa(id), Title: s.Title.Ptr(), Code: s.Code.Ptr(), @@ -212,7 +217,7 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput { Director: s.Director.Ptr(), URL: s.URL.Ptr(), Date: dateStr, - Rating: s.Rating.Ptr(), + Rating100: s.Rating.Ptr(), Organized: s.Organized.Ptr(), StudioID: s.StudioID.StringPtr(), GalleryIds: s.GalleryIDs.IDStrings(), @@ -221,6 +226,14 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput { TagIds: s.TagIDs.IDStrings(), StashIds: stashIDs, } + + if s.Rating.Set && !s.Rating.Null { + // convert to 1-100 scale + rating := Rating100To5(s.Rating.Value) + ret.Rating = &rating + } + + return ret } // GetTitle returns the title of the scene. If the Title field is empty, diff --git a/pkg/models/model_scene_test.go b/pkg/models/model_scene_test.go index db6d6f119..910991971 100644 --- a/pkg/models/model_scene_test.go +++ b/pkg/models/model_scene_test.go @@ -12,16 +12,17 @@ func TestScenePartial_UpdateInput(t *testing.T) { ) var ( - title = "title" - code = "1337" - details = "details" - director = "director" - url = "url" - date = "2001-02-03" - rating = 4 - organized = true - studioID = 2 - studioIDStr = "2" + title = "title" + code = "1337" + details = "details" + director = "director" + url = "url" + date = "2001-02-03" + ratingLegacy = 4 + rating100 = 80 + organized = true + studioID = 2 + studioIDStr = "2" ) dateObj := NewDate(date) @@ -42,7 +43,7 @@ func TestScenePartial_UpdateInput(t *testing.T) { Director: NewOptionalString(director), URL: NewOptionalString(url), Date: NewOptionalDate(dateObj), - Rating: NewOptionalInt(rating), + Rating: NewOptionalInt(rating100), Organized: NewOptionalBool(organized), StudioID: NewOptionalInt(studioID), }, @@ -54,7 +55,8 @@ func TestScenePartial_UpdateInput(t *testing.T) { Director: &director, URL: &url, Date: &date, - Rating: &rating, + Rating: &ratingLegacy, + Rating100: &rating100, Organized: &organized, StudioID: &studioIDStr, }, diff --git a/pkg/models/model_studio.go b/pkg/models/model_studio.go index 55b0e03aa..51a8d332c 100644 --- a/pkg/models/model_studio.go +++ b/pkg/models/model_studio.go @@ -8,29 +8,31 @@ import ( ) type Studio struct { - ID int `db:"id" json:"id"` - Checksum string `db:"checksum" json:"checksum"` - Name sql.NullString `db:"name" json:"name"` - URL sql.NullString `db:"url" json:"url"` - ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"` - CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` - UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` - Rating sql.NullInt64 `db:"rating" json:"rating"` - Details sql.NullString `db:"details" json:"details"` - IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"` + ID int `db:"id" json:"id"` + Checksum string `db:"checksum" json:"checksum"` + Name sql.NullString `db:"name" json:"name"` + URL sql.NullString `db:"url" json:"url"` + ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"` + CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` + UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` + // Rating expressed in 1-100 scale + Rating sql.NullInt64 `db:"rating" json:"rating"` + Details sql.NullString `db:"details" json:"details"` + IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"` } type StudioPartial struct { - ID int `db:"id" json:"id"` - Checksum *string `db:"checksum" json:"checksum"` - Name *sql.NullString `db:"name" json:"name"` - URL *sql.NullString `db:"url" json:"url"` - ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"` - CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` - UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` - Rating *sql.NullInt64 `db:"rating" json:"rating"` - Details *sql.NullString `db:"details" json:"details"` - IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"` + ID int `db:"id" json:"id"` + Checksum *string `db:"checksum" json:"checksum"` + Name *sql.NullString `db:"name" json:"name"` + URL *sql.NullString `db:"url" json:"url"` + ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"` + CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` + UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` + // Rating expressed in 1-100 scale + Rating *sql.NullInt64 `db:"rating" json:"rating"` + Details *sql.NullString `db:"details" json:"details"` + IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"` } var DefaultStudioImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC" diff --git a/pkg/models/movie.go b/pkg/models/movie.go index e577d79e7..8d58d70dd 100644 --- a/pkg/models/movie.go +++ b/pkg/models/movie.go @@ -8,8 +8,10 @@ type MovieFilterType struct { Synopsis *StringCriterionInput `json:"synopsis"` // Filter by duration (in seconds) Duration *IntCriterionInput `json:"duration"` - // Filter by rating + // Filter by rating expressed as 1-5 Rating *IntCriterionInput `json:"rating"` + // Filter by rating expressed as 1-100 + Rating100 *IntCriterionInput `json:"rating100"` // Filter to only include movies with this studio Studios *HierarchicalMultiCriterionInput `json:"studios"` // Filter to only include movies missing this property diff --git a/pkg/models/performer.go b/pkg/models/performer.go index 99b2c84c0..b4379d41c 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -111,8 +111,10 @@ type PerformerFilterType struct { GalleryCount *IntCriterionInput `json:"gallery_count"` // Filter by StashID StashID *StringCriterionInput `json:"stash_id"` - // Filter by rating + // Filter by rating expressed as 1-5 Rating *IntCriterionInput `json:"rating"` + // Filter by rating expressed as 1-100 + Rating100 *IntCriterionInput `json:"rating100"` // Filter by url URL *StringCriterionInput `json:"url"` // Filter by hair color diff --git a/pkg/models/rating.go b/pkg/models/rating.go new file mode 100644 index 000000000..66219b50a --- /dev/null +++ b/pkg/models/rating.go @@ -0,0 +1,69 @@ +package models + +import ( + "fmt" + "io" + "math" + "strconv" +) + +type RatingSystem string + +const ( + FiveStar = "FiveStar" + FivePointFiveStar = "FivePointFiveStar" + FivePointTwoFiveStar = "FivePointTwoFiveStar" + // TenStar = "TenStar" + // TenPointFiveStar = "TenPointFiveStar" + // TenPointTwoFiveStar = "TenPointTwoFiveStar" + TenPointDecimal = "TenPointDecimal" +) + +func (e RatingSystem) IsValid() bool { + switch e { + // case FiveStar, FivePointFiveStar, FivePointTwoFiveStar, TenStar, TenPointFiveStar, TenPointTwoFiveStar, TenPointDecimal: + case FiveStar, FivePointFiveStar, FivePointTwoFiveStar, TenPointDecimal: + return true + } + return false +} + +func (e RatingSystem) String() string { + return string(e) +} + +func (e *RatingSystem) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = RatingSystem(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid RatingSystem", str) + } + return nil +} + +func (e RatingSystem) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + +const ( + maxRating100 = 100 + maxRating5 = 5 + minRating5 = 1 + minRating100 = 20 +) + +// Rating100To5 converts a 1-100 rating to a 1-5 rating. +// Values <= 30 are converted to 1. Otherwise, rating is divided by 20 and rounded to the nearest integer. +func Rating100To5(rating100 int) int { + val := math.Round((float64(rating100) / 20)) + return int(math.Max(minRating5, math.Min(maxRating5, val))) +} + +// Rating5To100 converts a 1-5 rating to a 1-100 rating +func Rating5To100(rating5 int) int { + return int(math.Max(minRating100, math.Min(maxRating100, float64(rating5*20)))) +} diff --git a/pkg/models/rating_test.go b/pkg/models/rating_test.go new file mode 100644 index 000000000..ad04ca11d --- /dev/null +++ b/pkg/models/rating_test.go @@ -0,0 +1,55 @@ +package models + +import ( + "testing" +) + +func TestRating100To5(t *testing.T) { + tests := []struct { + name string + rating100 int + want int + }{ + {"20", 20, 1}, + {"100", 100, 5}, + {"1", 1, 1}, + {"10", 10, 1}, + {"11", 11, 1}, + {"21", 21, 1}, + {"31", 31, 2}, + {"0", 0, 1}, + {"-100", -100, 1}, + {"120", 120, 5}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Rating100To5(tt.rating100); got != tt.want { + t.Errorf("Rating100To5() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRating5To100(t *testing.T) { + tests := []struct { + name string + rating5 int + want int + }{ + {"1", 1, 20}, + {"5", 5, 100}, + {"2", 2, 40}, + {"3", 3, 60}, + {"4", 4, 80}, + {"6", 6, 100}, + {"0", 0, 20}, + {"-1", -1, 20}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Rating5To100(tt.rating5); got != tt.want { + t.Errorf("Rating5To100() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 6483a5c69..60220c6f0 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -31,8 +31,10 @@ type SceneFilterType struct { Path *StringCriterionInput `json:"path"` // Filter by file count FileCount *IntCriterionInput `json:"file_count"` - // Filter by rating + // Filter by rating expressed as 1-5 Rating *IntCriterionInput `json:"rating"` + // Filter by rating expressed as 1-100 + Rating100 *IntCriterionInput `json:"rating100"` // Filter by organized Organized *bool `json:"organized"` // Filter by o-counter diff --git a/pkg/models/studio.go b/pkg/models/studio.go index 661e80806..336898d29 100644 --- a/pkg/models/studio.go +++ b/pkg/models/studio.go @@ -14,8 +14,10 @@ type StudioFilterType struct { StashID *StringCriterionInput `json:"stash_id"` // Filter to only include studios missing this property IsMissing *string `json:"is_missing"` - // Filter by rating + // Filter by rating expressed as 1-5 Rating *IntCriterionInput `json:"rating"` + // Filter by rating expressed as 1-100 + Rating100 *IntCriterionInput `json:"rating100"` // Filter by scene count SceneCount *IntCriterionInput `json:"scene_count"` // Filter by image count diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index 11de78540..550c66763 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -22,7 +22,7 @@ import ( "github.com/stashapp/stash/pkg/logger" ) -var appSchemaVersion uint = 39 +var appSchemaVersion uint = 40 //go:embed migrations/*.sql var migrationsBox embed.FS diff --git a/pkg/sqlite/filter.go b/pkg/sqlite/filter.go index bce9ad52b..c1eba93eb 100644 --- a/pkg/sqlite/filter.go +++ b/pkg/sqlite/filter.go @@ -543,6 +543,25 @@ func boolCriterionHandler(c *bool, column string, addJoinFn func(f *filterBuilde } } +func rating5CriterionHandler(c *models.IntCriterionInput, column string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { + return func(ctx context.Context, f *filterBuilder) { + if c != nil { + // make a copy so we can adjust it + cc := *c + if cc.Value != 0 { + cc.Value = models.Rating5To100(cc.Value) + } + if cc.Value2 != nil { + val := models.Rating5To100(*cc.Value2) + cc.Value2 = &val + } + + clause, args := getIntCriterionWhereClause(column, cc) + f.addWhere(clause, args...) + } + } +} + func dateCriterionHandler(c *models.DateCriterionInput, column string) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if c != nil { diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index 829dab5ae..e45d7cb9f 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -30,11 +30,12 @@ const ( ) type galleryRow struct { - ID int `db:"id" goqu:"skipinsert"` - Title zero.String `db:"title"` - URL zero.String `db:"url"` - Date models.SQLiteDate `db:"date"` - Details zero.String `db:"details"` + ID int `db:"id" goqu:"skipinsert"` + Title zero.String `db:"title"` + URL zero.String `db:"url"` + Date models.SQLiteDate `db:"date"` + Details zero.String `db:"details"` + // expressed as 1-100 Rating null.Int `db:"rating"` Organized bool `db:"organized"` StudioID null.Int `db:"studio_id,omitempty"` @@ -651,7 +652,9 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga query.handleCriterion(ctx, qb.galleryPathCriterionHandler(galleryFilter.Path)) query.handleCriterion(ctx, galleryFileCountCriterionHandler(qb, galleryFilter.FileCount)) - query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating, "galleries.rating", nil)) + query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating100, "galleries.rating", nil)) + // legacy rating handler + query.handleCriterion(ctx, rating5CriterionHandler(galleryFilter.Rating, "galleries.rating", nil)) query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.URL, "galleries.url")) query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil)) query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing)) diff --git a/pkg/sqlite/gallery_test.go b/pkg/sqlite/gallery_test.go index 88c016d4c..80e25b6d1 100644 --- a/pkg/sqlite/gallery_test.go +++ b/pkg/sqlite/gallery_test.go @@ -54,7 +54,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) { var ( title = "title" url = "url" - rating = 3 + rating = 60 details = "details" createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -205,7 +205,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) { var ( title = "title" url = "url" - rating = 3 + rating = 60 details = "details" createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -399,7 +399,7 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) { title = "title" details = "details" url = "url" - rating = 3 + rating = 60 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -1547,7 +1547,7 @@ func TestGalleryQueryPathAndRating(t *testing.T) { Modifier: models.CriterionModifierEquals, }, And: &models.GalleryFilterType{ - Rating: &models.IntCriterionInput{ + Rating100: &models.IntCriterionInput{ Value: *galleryRating, Modifier: models.CriterionModifierEquals, }, @@ -1588,7 +1588,7 @@ func TestGalleryQueryPathNotRating(t *testing.T) { galleryFilter := models.GalleryFilterType{ Path: &pathCriterion, Not: &models.GalleryFilterType{ - Rating: &ratingCriterion, + Rating100: &ratingCriterion, }, } @@ -1699,32 +1699,32 @@ func verifyGalleryQuery(t *testing.T, filter models.GalleryFilterType, verifyFn }) } -func TestGalleryQueryRating(t *testing.T) { +func TestGalleryQueryLegacyRating(t *testing.T) { const rating = 3 ratingCriterion := models.IntCriterionInput{ Value: rating, Modifier: models.CriterionModifierEquals, } - verifyGalleriesRating(t, ratingCriterion) + verifyGalleriesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotEquals - verifyGalleriesRating(t, ratingCriterion) + verifyGalleriesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierGreaterThan - verifyGalleriesRating(t, ratingCriterion) + verifyGalleriesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierLessThan - verifyGalleriesRating(t, ratingCriterion) + verifyGalleriesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierIsNull - verifyGalleriesRating(t, ratingCriterion) + verifyGalleriesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotNull - verifyGalleriesRating(t, ratingCriterion) + verifyGalleriesLegacyRating(t, ratingCriterion) } -func verifyGalleriesRating(t *testing.T, ratingCriterion models.IntCriterionInput) { +func verifyGalleriesLegacyRating(t *testing.T, ratingCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { sqb := db.Gallery galleryFilter := models.GalleryFilterType{ @@ -1736,6 +1736,54 @@ func verifyGalleriesRating(t *testing.T, ratingCriterion models.IntCriterionInpu t.Errorf("Error querying gallery: %s", err.Error()) } + // convert criterion value to the 100 value + ratingCriterion.Value = models.Rating5To100(ratingCriterion.Value) + + for _, gallery := range galleries { + verifyIntPtr(t, gallery.Rating, ratingCriterion) + } + + return nil + }) +} + +func TestGalleryQueryRating100(t *testing.T) { + const rating = 60 + ratingCriterion := models.IntCriterionInput{ + Value: rating, + Modifier: models.CriterionModifierEquals, + } + + verifyGalleriesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierNotEquals + verifyGalleriesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierGreaterThan + verifyGalleriesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierLessThan + verifyGalleriesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierIsNull + verifyGalleriesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierNotNull + verifyGalleriesRating100(t, ratingCriterion) +} + +func verifyGalleriesRating100(t *testing.T, ratingCriterion models.IntCriterionInput) { + withTxn(func(ctx context.Context) error { + sqb := db.Gallery + galleryFilter := models.GalleryFilterType{ + Rating100: &ratingCriterion, + } + + galleries, _, err := sqb.Query(ctx, &galleryFilter, nil) + if err != nil { + t.Errorf("Error querying gallery: %s", err.Error()) + } + for _, gallery := range galleries { verifyIntPtr(t, gallery.Rating, ratingCriterion) } diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 0a1d72be5..9cc0e957a 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -27,8 +27,9 @@ const ( ) type imageRow struct { - ID int `db:"id" goqu:"skipinsert"` - Title zero.String `db:"title"` + ID int `db:"id" goqu:"skipinsert"` + Title zero.String `db:"title"` + // expressed as 1-100 Rating null.Int `db:"rating"` Organized bool `db:"organized"` OCounter int `db:"o_counter"` @@ -632,7 +633,9 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF query.handleCriterion(ctx, pathCriterionHandler(imageFilter.Path, "folders.path", "files.basename", qb.addFoldersTable)) query.handleCriterion(ctx, imageFileCountCriterionHandler(qb, imageFilter.FileCount)) - query.handleCriterion(ctx, intCriterionHandler(imageFilter.Rating, "images.rating", nil)) + query.handleCriterion(ctx, intCriterionHandler(imageFilter.Rating100, "images.rating", nil)) + // legacy rating handler + query.handleCriterion(ctx, rating5CriterionHandler(imageFilter.Rating, "images.rating", nil)) query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil)) query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil)) diff --git a/pkg/sqlite/image_test.go b/pkg/sqlite/image_test.go index b748dbe49..cb89152e0 100644 --- a/pkg/sqlite/image_test.go +++ b/pkg/sqlite/image_test.go @@ -54,7 +54,7 @@ func loadImageRelationships(ctx context.Context, expected models.Image, actual * func Test_imageQueryBuilder_Create(t *testing.T) { var ( title = "title" - rating = 3 + rating = 60 ocounter = 5 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -208,7 +208,7 @@ func makeImageFileWithID(i int) *file.ImageFile { func Test_imageQueryBuilder_Update(t *testing.T) { var ( title = "title" - rating = 3 + rating = 60 ocounter = 5 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -382,7 +382,7 @@ func clearImagePartial() models.ImagePartial { func Test_imageQueryBuilder_UpdatePartial(t *testing.T) { var ( title = "title" - rating = 3 + rating = 60 ocounter = 5 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -1595,7 +1595,7 @@ func TestImageQueryPathAndRating(t *testing.T) { Modifier: models.CriterionModifierEquals, }, And: &models.ImageFilterType{ - Rating: &models.IntCriterionInput{ + Rating100: &models.IntCriterionInput{ Value: int(imageRating.Int64), Modifier: models.CriterionModifierEquals, }, @@ -1607,7 +1607,10 @@ func TestImageQueryPathAndRating(t *testing.T) { images := queryImages(ctx, t, sqb, &imageFilter, nil) - assert.Len(t, images, 1) + if !assert.Len(t, images, 1) { + return nil + } + assert.Equal(t, imagePath, images[0].Path) assert.Equal(t, int(imageRating.Int64), *images[0].Rating) @@ -1633,7 +1636,7 @@ func TestImageQueryPathNotRating(t *testing.T) { imageFilter := models.ImageFilterType{ Path: &pathCriterion, Not: &models.ImageFilterType{ - Rating: &ratingCriterion, + Rating100: &ratingCriterion, }, } @@ -1688,32 +1691,32 @@ func TestImageIllegalQuery(t *testing.T) { }) } -func TestImageQueryRating(t *testing.T) { +func TestImageQueryLegacyRating(t *testing.T) { const rating = 3 ratingCriterion := models.IntCriterionInput{ Value: rating, Modifier: models.CriterionModifierEquals, } - verifyImagesRating(t, ratingCriterion) + verifyImagesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotEquals - verifyImagesRating(t, ratingCriterion) + verifyImagesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierGreaterThan - verifyImagesRating(t, ratingCriterion) + verifyImagesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierLessThan - verifyImagesRating(t, ratingCriterion) + verifyImagesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierIsNull - verifyImagesRating(t, ratingCriterion) + verifyImagesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotNull - verifyImagesRating(t, ratingCriterion) + verifyImagesLegacyRating(t, ratingCriterion) } -func verifyImagesRating(t *testing.T, ratingCriterion models.IntCriterionInput) { +func verifyImagesLegacyRating(t *testing.T, ratingCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { sqb := db.Image imageFilter := models.ImageFilterType{ @@ -1725,6 +1728,54 @@ func verifyImagesRating(t *testing.T, ratingCriterion models.IntCriterionInput) t.Errorf("Error querying image: %s", err.Error()) } + // convert criterion value to the 100 value + ratingCriterion.Value = models.Rating5To100(ratingCriterion.Value) + + for _, image := range images { + verifyIntPtr(t, image.Rating, ratingCriterion) + } + + return nil + }) +} + +func TestImageQueryRating100(t *testing.T) { + const rating = 60 + ratingCriterion := models.IntCriterionInput{ + Value: rating, + Modifier: models.CriterionModifierEquals, + } + + verifyImagesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierNotEquals + verifyImagesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierGreaterThan + verifyImagesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierLessThan + verifyImagesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierIsNull + verifyImagesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierNotNull + verifyImagesRating100(t, ratingCriterion) +} + +func verifyImagesRating100(t *testing.T, ratingCriterion models.IntCriterionInput) { + withTxn(func(ctx context.Context) error { + sqb := db.Image + imageFilter := models.ImageFilterType{ + Rating100: &ratingCriterion, + } + + images, _, err := queryImagesWithCount(ctx, sqb, &imageFilter, nil) + if err != nil { + t.Errorf("Error querying image: %s", err.Error()) + } + for _, image := range images { verifyIntPtr(t, image.Rating, ratingCriterion) } diff --git a/pkg/sqlite/migrations/40_newratings.up.sql b/pkg/sqlite/migrations/40_newratings.up.sql new file mode 100644 index 000000000..37b7ade9f --- /dev/null +++ b/pkg/sqlite/migrations/40_newratings.up.sql @@ -0,0 +1,6 @@ +UPDATE `scenes` SET `rating` = (`rating` * 20) WHERE `rating` < 6; +UPDATE `galleries` SET `rating` = (`rating` * 20) WHERE `rating` < 6; +UPDATE `images` SET `rating` = (`rating` * 20) WHERE `rating` < 6; +UPDATE `movies` SET `rating` = (`rating` * 20) WHERE `rating` < 6; +UPDATE `performers` SET `rating` = (`rating` * 20) WHERE `rating` < 6; +UPDATE `studios` SET `rating` = (`rating` * 20) WHERE `rating` < 6; \ No newline at end of file diff --git a/pkg/sqlite/movies.go b/pkg/sqlite/movies.go index 4cc19c1e6..d39a45472 100644 --- a/pkg/sqlite/movies.go +++ b/pkg/sqlite/movies.go @@ -147,7 +147,9 @@ func (qb *movieQueryBuilder) makeFilter(ctx context.Context, movieFilter *models query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Name, "movies.name")) query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Director, "movies.director")) query.handleCriterion(ctx, stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis")) - query.handleCriterion(ctx, intCriterionHandler(movieFilter.Rating, "movies.rating", nil)) + query.handleCriterion(ctx, intCriterionHandler(movieFilter.Rating100, "movies.rating", nil)) + // legacy rating handler + query.handleCriterion(ctx, rating5CriterionHandler(movieFilter.Rating, "movies.rating", nil)) query.handleCriterion(ctx, durationCriterionHandler(movieFilter.Duration, "movies.duration", nil)) query.handleCriterion(ctx, movieIsMissingCriterionHandler(qb, movieFilter.IsMissing)) query.handleCriterion(ctx, stringCriterionHandler(movieFilter.URL, "movies.url")) diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index 501db6e32..fb938df4d 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -23,33 +23,34 @@ const performersTagsTable = "performers_tags" const performersImageTable = "performers_image" // performer cover image type performerRow struct { - ID int `db:"id" goqu:"skipinsert"` - Checksum string `db:"checksum"` - Name zero.String `db:"name"` - Gender zero.String `db:"gender"` - URL zero.String `db:"url"` - Twitter zero.String `db:"twitter"` - Instagram zero.String `db:"instagram"` - Birthdate models.SQLiteDate `db:"birthdate"` - Ethnicity zero.String `db:"ethnicity"` - Country zero.String `db:"country"` - EyeColor zero.String `db:"eye_color"` - Height null.Int `db:"height"` - Measurements zero.String `db:"measurements"` - FakeTits zero.String `db:"fake_tits"` - CareerLength zero.String `db:"career_length"` - Tattoos zero.String `db:"tattoos"` - Piercings zero.String `db:"piercings"` - Aliases zero.String `db:"aliases"` - Favorite sql.NullBool `db:"favorite"` - CreatedAt models.SQLiteTimestamp `db:"created_at"` - UpdatedAt models.SQLiteTimestamp `db:"updated_at"` - Rating null.Int `db:"rating"` - Details zero.String `db:"details"` - DeathDate models.SQLiteDate `db:"death_date"` - HairColor zero.String `db:"hair_color"` - Weight null.Int `db:"weight"` - IgnoreAutoTag bool `db:"ignore_auto_tag"` + ID int `db:"id" goqu:"skipinsert"` + Checksum string `db:"checksum"` + Name zero.String `db:"name"` + Gender zero.String `db:"gender"` + URL zero.String `db:"url"` + Twitter zero.String `db:"twitter"` + Instagram zero.String `db:"instagram"` + Birthdate models.SQLiteDate `db:"birthdate"` + Ethnicity zero.String `db:"ethnicity"` + Country zero.String `db:"country"` + EyeColor zero.String `db:"eye_color"` + Height null.Int `db:"height"` + Measurements zero.String `db:"measurements"` + FakeTits zero.String `db:"fake_tits"` + CareerLength zero.String `db:"career_length"` + Tattoos zero.String `db:"tattoos"` + Piercings zero.String `db:"piercings"` + Aliases zero.String `db:"aliases"` + Favorite sql.NullBool `db:"favorite"` + CreatedAt models.SQLiteTimestamp `db:"created_at"` + UpdatedAt models.SQLiteTimestamp `db:"updated_at"` + // expressed as 1-100 + Rating null.Int `db:"rating"` + Details zero.String `db:"details"` + DeathDate models.SQLiteDate `db:"death_date"` + HairColor zero.String `db:"hair_color"` + Weight null.Int `db:"weight"` + IgnoreAutoTag bool `db:"ignore_auto_tag"` } func (r *performerRow) fromPerformer(o models.Performer) { @@ -90,27 +91,28 @@ func (r *performerRow) fromPerformer(o models.Performer) { func (r *performerRow) resolve() *models.Performer { ret := &models.Performer{ - ID: r.ID, - Checksum: r.Checksum, - Name: r.Name.String, - Gender: models.GenderEnum(r.Gender.String), - URL: r.URL.String, - Twitter: r.Twitter.String, - Instagram: r.Instagram.String, - Birthdate: r.Birthdate.DatePtr(), - Ethnicity: r.Ethnicity.String, - Country: r.Country.String, - EyeColor: r.EyeColor.String, - Height: nullIntPtr(r.Height), - Measurements: r.Measurements.String, - FakeTits: r.FakeTits.String, - CareerLength: r.CareerLength.String, - Tattoos: r.Tattoos.String, - Piercings: r.Piercings.String, - Aliases: r.Aliases.String, - Favorite: r.Favorite.Bool, - CreatedAt: r.CreatedAt.Timestamp, - UpdatedAt: r.UpdatedAt.Timestamp, + ID: r.ID, + Checksum: r.Checksum, + Name: r.Name.String, + Gender: models.GenderEnum(r.Gender.String), + URL: r.URL.String, + Twitter: r.Twitter.String, + Instagram: r.Instagram.String, + Birthdate: r.Birthdate.DatePtr(), + Ethnicity: r.Ethnicity.String, + Country: r.Country.String, + EyeColor: r.EyeColor.String, + Height: nullIntPtr(r.Height), + Measurements: r.Measurements.String, + FakeTits: r.FakeTits.String, + CareerLength: r.CareerLength.String, + Tattoos: r.Tattoos.String, + Piercings: r.Piercings.String, + Aliases: r.Aliases.String, + Favorite: r.Favorite.Bool, + CreatedAt: r.CreatedAt.Timestamp, + UpdatedAt: r.UpdatedAt.Timestamp, + // expressed as 1-100 Rating: nullIntPtr(r.Rating), Details: r.Details.String, DeathDate: r.DeathDate.DatePtr(), @@ -519,7 +521,9 @@ func (qb *PerformerStore) makeFilter(ctx context.Context, filter *models.Perform query.handleCriterion(ctx, stringCriterionHandler(filter.CareerLength, tableName+".career_length")) query.handleCriterion(ctx, stringCriterionHandler(filter.Tattoos, tableName+".tattoos")) query.handleCriterion(ctx, stringCriterionHandler(filter.Piercings, tableName+".piercings")) - query.handleCriterion(ctx, intCriterionHandler(filter.Rating, tableName+".rating", nil)) + query.handleCriterion(ctx, intCriterionHandler(filter.Rating100, tableName+".rating", nil)) + // legacy rating handler + query.handleCriterion(ctx, rating5CriterionHandler(filter.Rating, tableName+".rating", nil)) query.handleCriterion(ctx, stringCriterionHandler(filter.HairColor, tableName+".hair_color")) query.handleCriterion(ctx, stringCriterionHandler(filter.URL, tableName+".url")) query.handleCriterion(ctx, intCriterionHandler(filter.Weight, tableName+".weight", nil)) diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 2b089a3ef..1a59a0575 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -440,7 +440,7 @@ func TestPerformerQueryEthnicityAndRating(t *testing.T) { Modifier: models.CriterionModifierEquals, }, And: &models.PerformerFilterType{ - Rating: &models.IntCriterionInput{ + Rating100: &models.IntCriterionInput{ Value: performerRating, Modifier: models.CriterionModifierEquals, }, @@ -450,7 +450,10 @@ func TestPerformerQueryEthnicityAndRating(t *testing.T) { withTxn(func(ctx context.Context) error { performers := queryPerformers(ctx, t, &performerFilter, nil) - assert.Len(t, performers, 1) + if !assert.Len(t, performers, 1) { + return nil + } + assert.Equal(t, performerEth, performers[0].Ethnicity) if assert.NotNil(t, performers[0].Rating) { assert.Equal(t, performerRating, *performers[0].Rating) @@ -478,7 +481,7 @@ func TestPerformerQueryEthnicityNotRating(t *testing.T) { performerFilter := models.PerformerFilterType{ Ethnicity: ðCriterion, Not: &models.PerformerFilterType{ - Rating: &ratingCriterion, + Rating100: &ratingCriterion, }, } @@ -1173,32 +1176,32 @@ func TestPerformerStashIDs(t *testing.T) { t.Error(err.Error()) } } -func TestPerformerQueryRating(t *testing.T) { +func TestPerformerQueryLegacyRating(t *testing.T) { const rating = 3 ratingCriterion := models.IntCriterionInput{ Value: rating, Modifier: models.CriterionModifierEquals, } - verifyPerformersRating(t, ratingCriterion) + verifyPerformersLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotEquals - verifyPerformersRating(t, ratingCriterion) + verifyPerformersLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierGreaterThan - verifyPerformersRating(t, ratingCriterion) + verifyPerformersLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierLessThan - verifyPerformersRating(t, ratingCriterion) + verifyPerformersLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierIsNull - verifyPerformersRating(t, ratingCriterion) + verifyPerformersLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotNull - verifyPerformersRating(t, ratingCriterion) + verifyPerformersLegacyRating(t, ratingCriterion) } -func verifyPerformersRating(t *testing.T, ratingCriterion models.IntCriterionInput) { +func verifyPerformersLegacyRating(t *testing.T, ratingCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { performerFilter := models.PerformerFilterType{ Rating: &ratingCriterion, @@ -1206,6 +1209,50 @@ func verifyPerformersRating(t *testing.T, ratingCriterion models.IntCriterionInp performers := queryPerformers(ctx, t, &performerFilter, nil) + // convert criterion value to the 100 value + ratingCriterion.Value = models.Rating5To100(ratingCriterion.Value) + + for _, performer := range performers { + verifyIntPtr(t, performer.Rating, ratingCriterion) + } + + return nil + }) +} + +func TestPerformerQueryRating100(t *testing.T) { + const rating = 60 + ratingCriterion := models.IntCriterionInput{ + Value: rating, + Modifier: models.CriterionModifierEquals, + } + + verifyPerformersRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierNotEquals + verifyPerformersRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierGreaterThan + verifyPerformersRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierLessThan + verifyPerformersRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierIsNull + verifyPerformersRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierNotNull + verifyPerformersRating100(t, ratingCriterion) +} + +func verifyPerformersRating100(t *testing.T, ratingCriterion models.IntCriterionInput) { + withTxn(func(ctx context.Context) error { + performerFilter := models.PerformerFilterType{ + Rating100: &ratingCriterion, + } + + performers := queryPerformers(ctx, t, &performerFilter, nil) + for _, performer := range performers { verifyIntPtr(t, performer.Rating, ratingCriterion) } diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 79581c94d..011216eeb 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -52,13 +52,14 @@ ORDER BY files.size DESC ` type sceneRow struct { - ID int `db:"id" goqu:"skipinsert"` - Title zero.String `db:"title"` - Code zero.String `db:"code"` - Details zero.String `db:"details"` - Director zero.String `db:"director"` - URL zero.String `db:"url"` - Date models.SQLiteDate `db:"date"` + ID int `db:"id" goqu:"skipinsert"` + Title zero.String `db:"title"` + Code zero.String `db:"code"` + Details zero.String `db:"details"` + Director zero.String `db:"director"` + URL zero.String `db:"url"` + Date models.SQLiteDate `db:"date"` + // expressed as 1-100 Rating null.Int `db:"rating"` Organized bool `db:"organized"` OCounter int `db:"o_counter"` @@ -844,7 +845,9 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF } })) - query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating, "scenes.rating", nil)) + query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating100, "scenes.rating", nil)) + // legacy rating handler + query.handleCriterion(ctx, rating5CriterionHandler(sceneFilter.Rating, "scenes.rating", nil)) query.handleCriterion(ctx, intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter", nil)) query.handleCriterion(ctx, boolCriterionHandler(sceneFilter.Organized, "scenes.organized", nil)) diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index 52033abb4..fed6167e8 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -77,7 +77,7 @@ func Test_sceneQueryBuilder_Create(t *testing.T) { details = "details" director = "director" url = "url" - rating = 3 + rating = 60 ocounter = 5 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -304,7 +304,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) { details = "details" director = "director" url = "url" - rating = 3 + rating = 60 ocounter = 5 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -512,7 +512,7 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) { details = "details" director = "director" url = "url" - rating = 3 + rating = 60 ocounter = 5 createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) @@ -2295,7 +2295,7 @@ func TestSceneQueryPathAndRating(t *testing.T) { Modifier: models.CriterionModifierEquals, }, And: &models.SceneFilterType{ - Rating: &models.IntCriterionInput{ + Rating100: &models.IntCriterionInput{ Value: sceneRating, Modifier: models.CriterionModifierEquals, }, @@ -2335,7 +2335,7 @@ func TestSceneQueryPathNotRating(t *testing.T) { sceneFilter := models.SceneFilterType{ Path: &pathCriterion, Not: &models.SceneFilterType{ - Rating: &ratingCriterion, + Rating100: &ratingCriterion, }, } @@ -2522,25 +2522,25 @@ func TestSceneQueryRating(t *testing.T) { Modifier: models.CriterionModifierEquals, } - verifyScenesRating(t, ratingCriterion) + verifyScenesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotEquals - verifyScenesRating(t, ratingCriterion) + verifyScenesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierGreaterThan - verifyScenesRating(t, ratingCriterion) + verifyScenesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierLessThan - verifyScenesRating(t, ratingCriterion) + verifyScenesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierIsNull - verifyScenesRating(t, ratingCriterion) + verifyScenesLegacyRating(t, ratingCriterion) ratingCriterion.Modifier = models.CriterionModifierNotNull - verifyScenesRating(t, ratingCriterion) + verifyScenesLegacyRating(t, ratingCriterion) } -func verifyScenesRating(t *testing.T, ratingCriterion models.IntCriterionInput) { +func verifyScenesLegacyRating(t *testing.T, ratingCriterion models.IntCriterionInput) { withTxn(func(ctx context.Context) error { sqb := db.Scene sceneFilter := models.SceneFilterType{ @@ -2549,6 +2549,51 @@ func verifyScenesRating(t *testing.T, ratingCriterion models.IntCriterionInput) scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) + // convert criterion value to the 100 value + ratingCriterion.Value = models.Rating5To100(ratingCriterion.Value) + + for _, scene := range scenes { + verifyIntPtr(t, scene.Rating, ratingCriterion) + } + + return nil + }) +} + +func TestSceneQueryRating100(t *testing.T) { + const rating = 60 + ratingCriterion := models.IntCriterionInput{ + Value: rating, + Modifier: models.CriterionModifierEquals, + } + + verifyScenesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierNotEquals + verifyScenesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierGreaterThan + verifyScenesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierLessThan + verifyScenesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierIsNull + verifyScenesRating100(t, ratingCriterion) + + ratingCriterion.Modifier = models.CriterionModifierNotNull + verifyScenesRating100(t, ratingCriterion) +} + +func verifyScenesRating100(t *testing.T, ratingCriterion models.IntCriterionInput) { + withTxn(func(ctx context.Context) error { + sqb := db.Scene + sceneFilter := models.SceneFilterType{ + Rating100: &ratingCriterion, + } + + scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) + for _, scene := range scenes { verifyIntPtr(t, scene.Rating, ratingCriterion) } diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index af0c0f0a0..86169dcf2 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -823,7 +823,7 @@ func getSceneTitle(index int) string { func getRating(index int) sql.NullInt64 { rating := index % 6 - return sql.NullInt64{Int64: int64(rating), Valid: rating > 0} + return sql.NullInt64{Int64: int64(rating * 20), Valid: rating > 0} } func getIntPtr(r sql.NullInt64) *int { @@ -967,11 +967,13 @@ func makeScene(i int) *models.Scene { } } + rating := getRating(i) + return &models.Scene{ Title: title, Details: details, URL: getSceneEmptyString(i, urlField), - Rating: getIntPtr(getRating(i)), + Rating: getIntPtr(rating), OCounter: getOCounter(i), Date: getObjectDateObject(i), StudioID: studioID, diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index 901affc82..ee1b1d731 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -234,7 +234,9 @@ func (qb *studioQueryBuilder) makeFilter(ctx context.Context, studioFilter *mode query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Name, studioTable+".name")) query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Details, studioTable+".details")) query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url")) - query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating, studioTable+".rating", nil)) + query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil)) + // legacy rating handler + query.handleCriterion(ctx, rating5CriterionHandler(studioFilter.Rating, studioTable+".rating", nil)) query.handleCriterion(ctx, boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil)) query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { diff --git a/ui/v2.5/.stylelintrc b/ui/v2.5/.stylelintrc index 430e9a879..1357ed90e 100644 --- a/ui/v2.5/.stylelintrc +++ b/ui/v2.5/.stylelintrc @@ -61,7 +61,7 @@ "no-descending-specificity": null, "no-invalid-double-slash-comments": true, "no-missing-end-of-source-newline": true, - "number-max-precision": 2, + "number-max-precision": 3, "number-no-trailing-zeros": true, "order/order": [ "custom-properties", diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index afe858eb3..5995febca 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -8,7 +8,7 @@ "start": "vite", "build": "vite build", "build-ci": "yarn validate && yarn build", - "validate": "yarn lint && yarn format-check && tsc --noEmit", + "validate": "yarn lint && tsc --noEmit && yarn format-check", "lint": "yarn lint:css && yarn lint:js", "lint:js": "eslint --cache src/**/*.{ts,tsx}", "lint:css": "stylelint \"src/**/*.scss\"", diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx old mode 100755 new mode 100644 diff --git a/ui/v2.5/src/components/FrontPage/Control.tsx b/ui/v2.5/src/components/FrontPage/Control.tsx index 4175d30d5..d8e17c4ed 100644 --- a/ui/v2.5/src/components/FrontPage/Control.tsx +++ b/ui/v2.5/src/components/FrontPage/Control.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React, { useContext, useMemo } from "react"; import { useIntl } from "react-intl"; import { FrontPageContent, @@ -7,6 +7,7 @@ import { } from "src/core/config"; import * as GQL from "src/core/generated-graphql"; import { useFindSavedFilter } from "src/core/StashService"; +import { ConfigurationContext } from "src/hooks/Config"; import { ListFilterModel } from "src/models/list-filter/filter"; import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow"; import { ImageRecommendationRow } from "../Images/ImageRecommendationRow"; @@ -98,6 +99,7 @@ interface ISavedFilterResults { const SavedFilterResults: React.FC = ({ savedFilterID, }) => { + const { configuration: config } = useContext(ConfigurationContext); const { loading, data } = useFindSavedFilter(savedFilterID.toString()); const filter = useMemo(() => { @@ -105,12 +107,12 @@ const SavedFilterResults: React.FC = ({ const { mode, filter: filterJSON } = data.findSavedFilter; - const ret = new ListFilterModel(mode); + const ret = new ListFilterModel(mode, config); ret.currentPage = 1; ret.configureFromJSON(filterJSON); ret.randomSeed = -1; return ret; - }, [data?.findSavedFilter]); + }, [data?.findSavedFilter, config]); if (loading || !data?.findSavedFilter || !filter) { return <>; @@ -128,18 +130,19 @@ interface ICustomFilterProps { const CustomFilterResults: React.FC = ({ customFilter, }) => { + const { configuration: config } = useContext(ConfigurationContext); const intl = useIntl(); const filter = useMemo(() => { const itemsPerPage = 25; - const ret = new ListFilterModel(customFilter.mode); + const ret = new ListFilterModel(customFilter.mode, config); ret.sortBy = customFilter.sortBy; ret.sortDirection = customFilter.direction; ret.itemsPerPage = itemsPerPage; ret.currentPage = 1; ret.randomSeed = -1; return ret; - }, [customFilter]); + }, [customFilter, config]); const header = customFilter.message ? intl.formatMessage( diff --git a/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx index 69fb695d6..24e153acd 100644 --- a/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx @@ -8,7 +8,7 @@ import { StudioSelect, Modal } from "src/components/Shared"; import { useToast } from "src/hooks"; import { FormUtils } from "src/utils"; import MultiSet from "../Shared/MultiSet"; -import { RatingStars } from "../Scenes/SceneDetails/RatingStars"; +import { RatingSystem } from "../Shared/Rating/RatingSystem"; import { getAggregateInputIDs, getAggregateInputValue, @@ -29,7 +29,7 @@ export const EditGalleriesDialog: React.FC = ( ) => { const intl = useIntl(); const Toast = useToast(); - const [rating, setRating] = useState(); + const [rating100, setRating] = useState(); const [studioId, setStudioId] = useState(); const [ performerMode, @@ -64,7 +64,7 @@ export const EditGalleriesDialog: React.FC = ( }), }; - galleryInput.rating = getAggregateInputValue(rating, aggregateRating); + galleryInput.rating100 = getAggregateInputValue(rating100, aggregateRating); galleryInput.studio_id = getAggregateInputValue( studioId, aggregateStudioId @@ -121,7 +121,7 @@ export const EditGalleriesDialog: React.FC = ( let first = true; state.forEach((gallery: GQL.SlimGalleryDataFragment) => { - const galleryRating = gallery.rating; + const galleryRating = gallery.rating100; const GalleriestudioID = gallery?.studio?.id; const galleryPerformerIDs = (gallery.performers ?? []) .map((p) => p.id) @@ -256,14 +256,13 @@ export const EditGalleriesDialog: React.FC = ( title: intl.formatMessage({ id: "rating" }), })} - setRating(value)} disabled={isUpdating} /> - {FormUtils.renderLabel({ title: intl.formatMessage({ id: "studio" }), diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index 1b7b81c51..76673d74e 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -160,7 +160,7 @@ export const GalleryCard: React.FC = (props) => { src={`${props.gallery.cover.paths.thumbnail}`} /> ) : undefined} - + } overlays={maybeRenderSceneStudioOverlay()} diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx index 049490b35..c2cfece58 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -5,7 +5,7 @@ import * as GQL from "src/core/generated-graphql"; import { TextUtils } from "src/utils"; import { TagLink, TruncatedText } from "src/components/Shared"; import { PerformerCard } from "src/components/Performers/PerformerCard"; -import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; +import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { sortPerformers } from "src/core/performers"; import { galleryTitle } from "src/core/galleries"; @@ -94,10 +94,10 @@ export const GalleryDetailPanel: React.FC = ({ /> ) : undefined} - {gallery.rating ? ( + {gallery.rating100 ? (
:{" "} - +
) : ( "" diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index 184a0dd14..ed85d9309 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -32,7 +32,7 @@ import { import { useToast } from "src/hooks"; import { useFormik } from "formik"; import { FormUtils } from "src/utils"; -import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; +import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { GalleryScrapeDialog } from "./GalleryScrapeDialog"; import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; @@ -89,7 +89,7 @@ export const GalleryEditPanel: React.FC< details: yup.string().optional().nullable(), url: yup.string().optional().nullable(), date: yup.string().optional().nullable(), - rating: yup.number().optional().nullable(), + rating100: yup.number().optional().nullable(), studio_id: yup.string().optional().nullable(), performer_ids: yup.array(yup.string().required()).optional().nullable(), tag_ids: yup.array(yup.string().required()).optional().nullable(), @@ -101,7 +101,7 @@ export const GalleryEditPanel: React.FC< details: gallery?.details ?? "", url: gallery?.url ?? "", date: gallery?.date ?? "", - rating: gallery?.rating ?? null, + rating100: gallery?.rating100 ?? null, studio_id: gallery?.studio?.id, performer_ids: (gallery?.performers ?? []).map((p) => p.id), tag_ids: (gallery?.tags ?? []).map((t) => t.id), @@ -117,7 +117,7 @@ export const GalleryEditPanel: React.FC< }); function setRating(v: number) { - formik.setFieldValue("rating", v); + formik.setFieldValue("rating100", v); } interface ISceneSelectValue { @@ -150,11 +150,11 @@ export const GalleryEditPanel: React.FC< } Mousetrap.bind("0", () => setRating(NaN)); - Mousetrap.bind("1", () => setRating(1)); - Mousetrap.bind("2", () => setRating(2)); - Mousetrap.bind("3", () => setRating(3)); - Mousetrap.bind("4", () => setRating(4)); - Mousetrap.bind("5", () => setRating(5)); + Mousetrap.bind("1", () => setRating(20)); + Mousetrap.bind("2", () => setRating(40)); + Mousetrap.bind("3", () => setRating(60)); + Mousetrap.bind("4", () => setRating(80)); + Mousetrap.bind("5", () => setRating(100)); setTimeout(() => { Mousetrap.unbind("0"); @@ -483,15 +483,14 @@ export const GalleryEditPanel: React.FC< title: intl.formatMessage({ id: "rating" }), })} - - formik.setFieldValue("rating", value ?? null) + formik.setFieldValue("rating100", value ?? null) } />
- {FormUtils.renderLabel({ title: intl.formatMessage({ id: "studio" }), diff --git a/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx b/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx index 33f6a2dd5..a357b6722 100644 --- a/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx @@ -2,10 +2,11 @@ import React from "react"; import { useIntl } from "react-intl"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { RatingStars, TruncatedText } from "src/components/Shared"; +import { TruncatedText } from "src/components/Shared"; import { TextUtils } from "src/utils"; import { useGalleryLightbox } from "src/hooks"; import { galleryTitle } from "src/core/galleries"; +import { RatingSystem } from "../Shared/Rating/RatingSystem"; const CLASSNAME = "GalleryWallCard"; const CLASSNAME_FOOTER = `${CLASSNAME}-footer`; @@ -45,7 +46,7 @@ const GalleryWallCard: React.FC = ({ gallery }) => { role="button" tabIndex={0} > - +