From a4016543af34a2c52660157d0520c76f57ce8835 Mon Sep 17 00:00:00 2001 From: Emilo2 <99644577+Emilo2@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:55:19 +0200 Subject: [PATCH 1/2] Scene production date field --- graphql/schema/types/filters.graphql | 2 + graphql/schema/types/scene.graphql | 5 + graphql/schema/types/scraper.graphql | 2 + graphql/stash-box/query.graphql | 1 + internal/api/resolver_model_scene.go | 8 + internal/api/resolver_mutation_scene.go | 12 ++ pkg/models/filename_parser.go | 29 +-- pkg/models/jsonschema/scene.go | 9 +- pkg/models/model_scene.go | 62 +++--- pkg/models/model_scene_test.go | 52 ++--- pkg/models/model_scraped_item.go | 32 ++-- pkg/models/scene.go | 34 ++-- pkg/scene/export.go | 4 + pkg/scene/import.go | 6 + pkg/scraper/query_url.go | 1 + pkg/scraper/stash.go | 19 +- pkg/sqlite/database.go | 2 +- .../76_scene_production_date.up.sql | 2 + pkg/sqlite/scene.go | 39 ++-- pkg/sqlite/scene_test.go | 177 ++++++++++-------- pkg/sqlite/setup_test.go | 21 ++- pkg/stashbox/graphql/generated_client.go | 38 ++-- pkg/stashbox/scene.go | 24 ++- ui/v2.5/graphql/data/scene-slim.graphql | 1 + ui/v2.5/graphql/data/scene.graphql | 2 + ui/v2.5/graphql/data/scrapers.graphql | 2 + .../Scenes/SceneDetails/SceneDetailPanel.tsx | 6 +- .../Scenes/SceneDetails/SceneEditPanel.tsx | 8 + .../Scenes/SceneDetails/SceneScrapeDialog.tsx | 12 ++ .../Tagger/scenes/StashSearchResult.tsx | 18 ++ ui/v2.5/src/locales/en-GB.json | 1 + .../models/list-filter/criteria/is-missing.ts | 1 + ui/v2.5/src/models/list-filter/scenes.ts | 2 + ui/v2.5/src/models/list-filter/types.ts | 1 + 34 files changed, 403 insertions(+), 232 deletions(-) create mode 100644 pkg/sqlite/migrations/76_scene_production_date.up.sql diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 4e70b7353..f9be79ac6 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -328,6 +328,8 @@ input SceneFilterType { last_played_at: TimestampCriterionInput "Filter by date" date: DateCriterionInput + "Filter by production date" + production_date: DateCriterionInput "Filter by creation time" created_at: TimestampCriterionInput "Filter by last update time" diff --git a/graphql/schema/types/scene.graphql b/graphql/schema/types/scene.graphql index 5fba3819d..f580e874a 100644 --- a/graphql/schema/types/scene.graphql +++ b/graphql/schema/types/scene.graphql @@ -45,6 +45,7 @@ type Scene { url: String @deprecated(reason: "Use urls") urls: [String!]! date: String + production_date: String # rating expressed as 1-100 rating100: Int organized: Boolean! @@ -101,6 +102,7 @@ input SceneCreateInput { url: String @deprecated(reason: "Use urls") urls: [String!] date: String + production_date: String # rating expressed as 1-100 rating100: Int organized: Boolean @@ -132,6 +134,7 @@ input SceneUpdateInput { url: String @deprecated(reason: "Use urls") urls: [String!] date: String + production_date: String # rating expressed as 1-100 rating100: Int o_counter: Int @@ -181,6 +184,7 @@ input BulkSceneUpdateInput { url: String @deprecated(reason: "Use urls") urls: BulkUpdateStrings date: String + production_date: String # rating expressed as 1-100 rating100: Int organized: Boolean @@ -237,6 +241,7 @@ type SceneParserResult { director: String url: String date: String + production_date: String # rating expressed as 1-5 rating: Int @deprecated(reason: "Use 1-100 range with rating100") # rating expressed as 1-100 diff --git a/graphql/schema/types/scraper.graphql b/graphql/schema/types/scraper.graphql index 9c0e33fdf..85d7bab87 100644 --- a/graphql/schema/types/scraper.graphql +++ b/graphql/schema/types/scraper.graphql @@ -83,6 +83,7 @@ type ScrapedScene { url: String @deprecated(reason: "use urls") urls: [String!] date: String + production_date: String "This should be a base64 encoded data URL" image: String @@ -107,6 +108,7 @@ input ScrapedSceneInput { url: String @deprecated(reason: "use urls") urls: [String!] date: String + production_date: String # no image, file, duration or relationships diff --git a/graphql/stash-box/query.graphql b/graphql/stash-box/query.graphql index 2367e85cf..ebafdde78 100644 --- a/graphql/stash-box/query.graphql +++ b/graphql/stash-box/query.graphql @@ -100,6 +100,7 @@ fragment SceneFragment on Scene { director duration date + production_date urls { ...URLFragment } diff --git a/internal/api/resolver_model_scene.go b/internal/api/resolver_model_scene.go index 2600c9538..c252aad55 100644 --- a/internal/api/resolver_model_scene.go +++ b/internal/api/resolver_model_scene.go @@ -74,6 +74,14 @@ func (r *sceneResolver) Date(ctx context.Context, obj *models.Scene) (*string, e return nil, nil } +func (r *sceneResolver) ProductionDate(ctx context.Context, obj *models.Scene) (*string, error) { + if obj.ProductionDate != nil { + result := obj.ProductionDate.String() + return &result, nil + } + return nil, nil +} + func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoFile, error) { files, err := r.getFiles(ctx, obj) if err != nil { diff --git a/internal/api/resolver_mutation_scene.go b/internal/api/resolver_mutation_scene.go index 6ac5b0227..fd392ca2b 100644 --- a/internal/api/resolver_mutation_scene.go +++ b/internal/api/resolver_mutation_scene.go @@ -57,6 +57,10 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr if err != nil { return nil, fmt.Errorf("converting date: %w", err) } + newScene.ProductionDate, err = translator.datePtr(input.ProductionDate) + if err != nil { + return nil, fmt.Errorf("converting date: %w", err) + } newScene.StudioID, err = translator.intPtrFromString(input.StudioID) if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) @@ -200,6 +204,10 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr if err != nil { return nil, fmt.Errorf("converting date: %w", err) } + updatedScene.ProductionDate, err = translator.optionalDate(input.ProductionDate, "production_date") + if err != nil { + return nil, fmt.Errorf("converting production date: %w", err) + } updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) @@ -355,6 +363,10 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU if err != nil { return nil, fmt.Errorf("converting date: %w", err) } + updatedScene.ProductionDate, err = translator.optionalDate(input.ProductionDate, "production_date") + if err != nil { + return nil, fmt.Errorf("converting production date: %w", err) + } updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) diff --git a/pkg/models/filename_parser.go b/pkg/models/filename_parser.go index 584ae72cb..dafa768c7 100644 --- a/pkg/models/filename_parser.go +++ b/pkg/models/filename_parser.go @@ -8,20 +8,21 @@ type SceneParserInput struct { } type SceneParserResult struct { - Scene *Scene `json:"scene"` - 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"` - Rating100 *int `json:"rating100"` - StudioID *string `json:"studio_id"` - GalleryIds []string `json:"gallery_ids"` - PerformerIds []string `json:"performer_ids"` - Movies []*SceneMovieID `json:"movies"` - TagIds []string `json:"tag_ids"` + Scene *Scene `json:"scene"` + Title *string `json:"title"` + Code *string `json:"code"` + Details *string `json:"details"` + Director *string `json:"director"` + URL *string `json:"url"` + Date *string `json:"date"` + ProductionDate *string `json:"production_date"` + Rating *int `json:"rating"` + Rating100 *int `json:"rating100"` + StudioID *string `json:"studio_id"` + GalleryIds []string `json:"gallery_ids"` + PerformerIds []string `json:"performer_ids"` + Movies []*SceneMovieID `json:"movies"` + TagIds []string `json:"tag_ids"` } type SceneMovieID struct { diff --git a/pkg/models/jsonschema/scene.go b/pkg/models/jsonschema/scene.go index c2f266d5c..a3c12173d 100644 --- a/pkg/models/jsonschema/scene.go +++ b/pkg/models/jsonschema/scene.go @@ -47,10 +47,11 @@ type Scene struct { // deprecated - for import only URL string `json:"url,omitempty"` - URLs []string `json:"urls,omitempty"` - Date string `json:"date,omitempty"` - Rating int `json:"rating,omitempty"` - Organized bool `json:"organized,omitempty"` + URLs []string `json:"urls,omitempty"` + Date string `json:"date,omitempty"` + ProductionDate string `json:"production_date,omitempty"` + Rating int `json:"rating,omitempty"` + Organized bool `json:"organized,omitempty"` // deprecated - for import only OCounter int `json:"o_counter,omitempty"` diff --git a/pkg/models/model_scene.go b/pkg/models/model_scene.go index cf0499388..19b438424 100644 --- a/pkg/models/model_scene.go +++ b/pkg/models/model_scene.go @@ -10,12 +10,13 @@ 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"` - Date *Date `json:"date"` + ID int `json:"id"` + Title string `json:"title"` + Code string `json:"code"` + Details string `json:"details"` + Director string `json:"director"` + Date *Date `json:"date"` + ProductionDate *Date `json:"production_date"` // Rating expressed in 1-100 scale Rating *int `json:"rating"` Organized bool `json:"organized"` @@ -56,11 +57,12 @@ func NewScene() Scene { // 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 - Date OptionalDate + Title OptionalString + Code OptionalString + Details OptionalString + Director OptionalString + Date OptionalDate + ProductionDate OptionalDate // Rating expressed in 1-100 scale Rating OptionalInt Organized OptionalBool @@ -192,27 +194,35 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput { dateStr = &v } + var productionDateStr *string + if s.ProductionDate.Set { + d := s.ProductionDate.Value + v := d.String() + productionDateStr = &v + } + var stashIDs StashIDs if s.StashIDs != nil { stashIDs = StashIDs(s.StashIDs.StashIDs) } ret := SceneUpdateInput{ - ID: strconv.Itoa(id), - Title: s.Title.Ptr(), - Code: s.Code.Ptr(), - Details: s.Details.Ptr(), - Director: s.Director.Ptr(), - Urls: s.URLs.Strings(), - Date: dateStr, - Rating100: s.Rating.Ptr(), - Organized: s.Organized.Ptr(), - StudioID: s.StudioID.StringPtr(), - GalleryIds: s.GalleryIDs.IDStrings(), - PerformerIds: s.PerformerIDs.IDStrings(), - Movies: s.GroupIDs.SceneMovieInputs(), - TagIds: s.TagIDs.IDStrings(), - StashIds: stashIDs.ToStashIDInputs(), + ID: strconv.Itoa(id), + Title: s.Title.Ptr(), + Code: s.Code.Ptr(), + Details: s.Details.Ptr(), + Director: s.Director.Ptr(), + Urls: s.URLs.Strings(), + Date: dateStr, + ProductionDate: productionDateStr, + Rating100: s.Rating.Ptr(), + Organized: s.Organized.Ptr(), + StudioID: s.StudioID.StringPtr(), + GalleryIds: s.GalleryIDs.IDStrings(), + PerformerIds: s.PerformerIDs.IDStrings(), + Movies: s.GroupIDs.SceneMovieInputs(), + TagIds: s.TagIDs.IDStrings(), + StashIds: stashIDs.ToStashIDInputs(), } return ret diff --git a/pkg/models/model_scene_test.go b/pkg/models/model_scene_test.go index 4eb5c1833..5c9468d58 100644 --- a/pkg/models/model_scene_test.go +++ b/pkg/models/model_scene_test.go @@ -12,19 +12,21 @@ func TestScenePartial_UpdateInput(t *testing.T) { ) var ( - title = "title" - code = "1337" - details = "details" - director = "director" - url = "url" - date = "2001-02-03" - rating100 = 80 - organized = true - studioID = 2 - studioIDStr = "2" + title = "title" + code = "1337" + details = "details" + director = "director" + url = "url" + date = "2001-02-03" + productionDate = "2001-01-02" + rating100 = 80 + organized = true + studioID = 2 + studioIDStr = "2" ) dateObj, _ := ParseDate(date) + productionDateObj, _ := ParseDate(productionDate) tests := []struct { name string @@ -44,22 +46,24 @@ func TestScenePartial_UpdateInput(t *testing.T) { Values: []string{url}, Mode: RelationshipUpdateModeSet, }, - Date: NewOptionalDate(dateObj), - Rating: NewOptionalInt(rating100), - Organized: NewOptionalBool(organized), - StudioID: NewOptionalInt(studioID), + Date: NewOptionalDate(dateObj), + ProductionDate: NewOptionalDate(productionDateObj), + Rating: NewOptionalInt(rating100), + Organized: NewOptionalBool(organized), + StudioID: NewOptionalInt(studioID), }, SceneUpdateInput{ - ID: idStr, - Title: &title, - Code: &code, - Details: &details, - Director: &director, - Urls: []string{url}, - Date: &date, - Rating100: &rating100, - Organized: &organized, - StudioID: &studioIDStr, + ID: idStr, + Title: &title, + Code: &code, + Details: &details, + Director: &director, + Urls: []string{url}, + Date: &date, + ProductionDate: &productionDate, + Rating100: &rating100, + Organized: &organized, + StudioID: &studioIDStr, }, }, { diff --git a/pkg/models/model_scraped_item.go b/pkg/models/model_scraped_item.go index 4254a9876..bbd743003 100644 --- a/pkg/models/model_scraped_item.go +++ b/pkg/models/model_scraped_item.go @@ -576,13 +576,14 @@ func (g ScrapedGroup) ScrapedMovie() ScrapedMovie { } type ScrapedScene struct { - Title *string `json:"title"` - Code *string `json:"code"` - Details *string `json:"details"` - Director *string `json:"director"` - URL *string `json:"url"` - URLs []string `json:"urls"` - Date *string `json:"date"` + Title *string `json:"title"` + Code *string `json:"code"` + Details *string `json:"details"` + Director *string `json:"director"` + URL *string `json:"url"` + URLs []string `json:"urls"` + Date *string `json:"date"` + ProductionDate *string `json:"production_date"` // This should be a base64 encoded data URL Image *string `json:"image"` File *SceneFileType `json:"file"` @@ -599,14 +600,15 @@ type ScrapedScene struct { func (ScrapedScene) IsScrapedContent() {} type ScrapedSceneInput struct { - Title *string `json:"title"` - Code *string `json:"code"` - Details *string `json:"details"` - Director *string `json:"director"` - URL *string `json:"url"` - URLs []string `json:"urls"` - Date *string `json:"date"` - RemoteSiteID *string `json:"remote_site_id"` + Title *string `json:"title"` + Code *string `json:"code"` + Details *string `json:"details"` + Director *string `json:"director"` + URL *string `json:"url"` + URLs []string `json:"urls"` + Date *string `json:"date"` + ProductionDate *string `json:"production_date"` + RemoteSiteID *string `json:"remote_site_id"` } type ScrapedImage struct { diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 434659cbe..f15f31069 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -101,6 +101,8 @@ type SceneFilterType struct { LastPlayedAt *TimestampCriterionInput `json:"last_played_at"` // Filter by date Date *DateCriterionInput `json:"date"` + // Filter by production date + ProductionDate *DateCriterionInput `json:"production_date"` // Filter by related galleries that meet this criteria GalleriesFilter *GalleryFilterType `json:"galleries_filter"` // Filter by related performers that meet this criteria @@ -153,21 +155,22 @@ type SceneGroupInput struct { } type SceneCreateInput struct { - Title *string `json:"title"` - Code *string `json:"code"` - Details *string `json:"details"` - Director *string `json:"director"` - URL *string `json:"url"` - Urls []string `json:"urls"` - Date *string `json:"date"` - Rating100 *int `json:"rating100"` - Organized *bool `json:"organized"` - StudioID *string `json:"studio_id"` - GalleryIds []string `json:"gallery_ids"` - PerformerIds []string `json:"performer_ids"` - Movies []SceneMovieInput `json:"movies"` - Groups []SceneGroupInput `json:"groups"` - TagIds []string `json:"tag_ids"` + Title *string `json:"title"` + Code *string `json:"code"` + Details *string `json:"details"` + Director *string `json:"director"` + URL *string `json:"url"` + Urls []string `json:"urls"` + Date *string `json:"date"` + ProductionDate *string `json:"production_date"` + Rating100 *int `json:"rating100"` + Organized *bool `json:"organized"` + StudioID *string `json:"studio_id"` + GalleryIds []string `json:"gallery_ids"` + PerformerIds []string `json:"performer_ids"` + Movies []SceneMovieInput `json:"movies"` + Groups []SceneGroupInput `json:"groups"` + TagIds []string `json:"tag_ids"` // This should be a URL or a base64 encoded data URL CoverImage *string `json:"cover_image"` StashIds []StashIDInput `json:"stash_ids"` @@ -187,6 +190,7 @@ type SceneUpdateInput struct { URL *string `json:"url"` Urls []string `json:"urls"` Date *string `json:"date"` + ProductionDate *string `json:"production_date"` Rating100 *int `json:"rating100"` OCounter *int `json:"o_counter"` Organized *bool `json:"organized"` diff --git a/pkg/scene/export.go b/pkg/scene/export.go index a012d1850..5ff0fb37f 100644 --- a/pkg/scene/export.go +++ b/pkg/scene/export.go @@ -44,6 +44,10 @@ func ToBasicJSON(ctx context.Context, reader ExportGetter, scene *models.Scene) newSceneJSON.Date = scene.Date.String() } + if scene.ProductionDate != nil { + newSceneJSON.ProductionDate = scene.ProductionDate.String() + } + if scene.Rating != nil { newSceneJSON.Rating = *scene.Rating } diff --git a/pkg/scene/import.go b/pkg/scene/import.go index efffd380d..dcc933169 100644 --- a/pkg/scene/import.go +++ b/pkg/scene/import.go @@ -106,6 +106,12 @@ func (i *Importer) sceneJSONToScene(sceneJSON jsonschema.Scene) models.Scene { newScene.Date = &d } } + if sceneJSON.ProductionDate != "" { + d, err := models.ParseDate(sceneJSON.ProductionDate) + if err == nil { + newScene.ProductionDate = &d + } + } if sceneJSON.Rating != 0 { newScene.Rating = &sceneJSON.Rating } diff --git a/pkg/scraper/query_url.go b/pkg/scraper/query_url.go index 91adb7d67..34771e776 100644 --- a/pkg/scraper/query_url.go +++ b/pkg/scraper/query_url.go @@ -43,6 +43,7 @@ func queryURLParametersFromScrapedScene(scene models.ScrapedSceneInput) queryURL setField("url", scene.URL) } setField("date", scene.Date) + setField("production_date", scene.ProductionDate) setField("details", scene.Details) setField("director", scene.Director) setField("remote_site_id", scene.RemoteSiteID) diff --git a/pkg/scraper/stash.go b/pkg/scraper/stash.go index 5c5cab9fc..e4e9c4a72 100644 --- a/pkg/scraper/stash.go +++ b/pkg/scraper/stash.go @@ -330,15 +330,16 @@ func (f stashVideoFile) SceneFileType() models.SceneFileType { } type scrapedSceneStash struct { - ID string `graphql:"id" json:"id"` - Title *string `graphql:"title" json:"title"` - Details *string `graphql:"details" json:"details"` - URLs []string `graphql:"urls" json:"urls"` - Date *string `graphql:"date" json:"date"` - Files []stashVideoFile `graphql:"files" json:"files"` - Studio *scrapedStudioStash `graphql:"studio" json:"studio"` - Tags []*scrapedTagStash `graphql:"tags" json:"tags"` - Performers []*scrapedPerformerStash `graphql:"performers" json:"performers"` + ID string `graphql:"id" json:"id"` + Title *string `graphql:"title" json:"title"` + Details *string `graphql:"details" json:"details"` + URLs []string `graphql:"urls" json:"urls"` + Date *string `graphql:"date" json:"date"` + ProductionDate *string `graphql:"production_date" json:"production_date"` + Files []stashVideoFile `graphql:"files" json:"files"` + Studio *scrapedStudioStash `graphql:"studio" json:"studio"` + Tags []*scrapedTagStash `graphql:"tags" json:"tags"` + Performers []*scrapedPerformerStash `graphql:"performers" json:"performers"` } func (s *stashScraper) scrapeSceneByScene(ctx context.Context, scene *models.Scene) (*models.ScrapedScene, error) { diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index 0ea3d7170..a87f6706f 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -34,7 +34,7 @@ const ( cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE" ) -var appSchemaVersion uint = 75 +var appSchemaVersion uint = 76 //go:embed migrations/*.sql var migrationsBox embed.FS diff --git a/pkg/sqlite/migrations/76_scene_production_date.up.sql b/pkg/sqlite/migrations/76_scene_production_date.up.sql new file mode 100644 index 000000000..f788b1a6b --- /dev/null +++ b/pkg/sqlite/migrations/76_scene_production_date.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE `scenes` ADD COLUMN `production_date` date; +ALTER TABLE `scenes` ADD COLUMN `production_date_precision` TINYINT; \ No newline at end of file diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index d92800317..16b56eb58 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -77,13 +77,15 @@ 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"` - Date NullDate `db:"date"` - DatePrecision null.Int `db:"date_precision"` + 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"` + Date NullDate `db:"date"` + DatePrecision null.Int `db:"date_precision"` + ProductionDate NullDate `db:"production_date"` + ProductionDatePrecision null.Int `db:"production_date_precision"` // expressed as 1-100 Rating null.Int `db:"rating"` Organized bool `db:"organized"` @@ -105,6 +107,8 @@ func (r *sceneRow) fromScene(o models.Scene) { r.Director = zero.StringFrom(o.Director) r.Date = NullDateFromDatePtr(o.Date) r.DatePrecision = datePrecisionFromDatePtr(o.Date) + r.ProductionDate = NullDateFromDatePtr(o.ProductionDate) + r.ProductionDatePrecision = datePrecisionFromDatePtr(o.ProductionDate) r.Rating = intFromPtr(o.Rating) r.Organized = o.Organized r.StudioID = intFromPtr(o.StudioID) @@ -125,15 +129,16 @@ type sceneQueryRow struct { func (r *sceneQueryRow) resolve() *models.Scene { ret := &models.Scene{ - ID: r.ID, - Title: r.Title.String, - Code: r.Code.String, - Details: r.Details.String, - Director: r.Director.String, - Date: r.Date.DatePtr(r.DatePrecision), - Rating: nullIntPtr(r.Rating), - Organized: r.Organized, - StudioID: nullIntPtr(r.StudioID), + ID: r.ID, + Title: r.Title.String, + Code: r.Code.String, + Details: r.Details.String, + Director: r.Director.String, + Date: r.Date.DatePtr(r.DatePrecision), + ProductionDate: r.ProductionDate.DatePtr(r.ProductionDatePrecision), + Rating: nullIntPtr(r.Rating), + Organized: r.Organized, + StudioID: nullIntPtr(r.StudioID), PrimaryFileID: nullIntFileIDPtr(r.PrimaryFileID), OSHash: r.PrimaryFileOshash.String, @@ -163,6 +168,7 @@ func (r *sceneRowRecord) fromPartial(o models.ScenePartial) { r.setNullString("details", o.Details) r.setNullString("director", o.Director) r.setNullDate("date", "date_precision", o.Date) + r.setNullDate("production_date", "production_date_precision", o.ProductionDate) r.setNullInt("rating", o.Rating) r.setBool("organized", o.Organized) r.setNullInt("studio_id", o.StudioID) @@ -1117,6 +1123,7 @@ var sceneSortOptions = sortOptions{ "created_at", "code", "date", + "production_date", "file_count", "filesize", "duration", diff --git a/pkg/sqlite/scene_test.go b/pkg/sqlite/scene_test.go index ae9ba56cf..11f6c5489 100644 --- a/pkg/sqlite/scene_test.go +++ b/pkg/sqlite/scene_test.go @@ -94,7 +94,8 @@ func Test_sceneQueryBuilder_Create(t *testing.T) { stashID1 = "stashid1" stashID2 = "stashid2" - date, _ = models.ParseDate("2003-02-01") + date, _ = models.ParseDate("2003-02-01") + productionDate, _ = models.ParseDate("2003-01-02") videoFile = makeFileWithID(fileIdxStartVideoFiles) ) @@ -107,20 +108,21 @@ func Test_sceneQueryBuilder_Create(t *testing.T) { { "full", models.Scene{ - Title: title, - Code: code, - Details: details, - Director: director, - URLs: models.NewRelatedStrings([]string{url}), - Date: &date, - Rating: &rating, - Organized: true, - StudioID: &studioIDs[studioIdxWithScene], - CreatedAt: createdAt, - UpdatedAt: updatedAt, - GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), - TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), - PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), + Title: title, + Code: code, + Details: details, + Director: director, + URLs: models.NewRelatedStrings([]string{url}), + Date: &date, + ProductionDate: &productionDate, + Rating: &rating, + Organized: true, + StudioID: &studioIDs[studioIdxWithScene], + CreatedAt: createdAt, + UpdatedAt: updatedAt, + GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), + PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], @@ -151,15 +153,16 @@ func Test_sceneQueryBuilder_Create(t *testing.T) { { "with file", models.Scene{ - Title: title, - Code: code, - Details: details, - Director: director, - URLs: models.NewRelatedStrings([]string{url}), - Date: &date, - Rating: &rating, - Organized: true, - StudioID: &studioIDs[studioIdxWithScene], + Title: title, + Code: code, + Details: details, + Director: director, + URLs: models.NewRelatedStrings([]string{url}), + Date: &date, + ProductionDate: &productionDate, + Rating: &rating, + Organized: true, + StudioID: &studioIDs[studioIdxWithScene], Files: models.NewRelatedVideoFiles([]*models.VideoFile{ videoFile.(*models.VideoFile), }), @@ -328,7 +331,8 @@ func Test_sceneQueryBuilder_Update(t *testing.T) { stashID1 = "stashid1" stashID2 = "stashid2" - date, _ = models.ParseDate("2003-02-01") + date, _ = models.ParseDate("2003-02-01") + productionDate, _ = models.ParseDate("2003-01-02") ) tests := []struct { @@ -339,21 +343,22 @@ func Test_sceneQueryBuilder_Update(t *testing.T) { { "full", &models.Scene{ - ID: sceneIDs[sceneIdxWithGallery], - Title: title, - Code: code, - Details: details, - Director: director, - URLs: models.NewRelatedStrings([]string{url}), - Date: &date, - Rating: &rating, - Organized: true, - StudioID: &studioIDs[studioIdxWithScene], - CreatedAt: createdAt, - UpdatedAt: updatedAt, - GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), - TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), - PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), + ID: sceneIDs[sceneIdxWithGallery], + Title: title, + Code: code, + Details: details, + Director: director, + URLs: models.NewRelatedStrings([]string{url}), + Date: &date, + ProductionDate: &productionDate, + Rating: &rating, + Organized: true, + StudioID: &studioIDs[studioIdxWithScene], + CreatedAt: createdAt, + UpdatedAt: updatedAt, + GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), + PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], @@ -506,18 +511,19 @@ func Test_sceneQueryBuilder_Update(t *testing.T) { func clearScenePartial() models.ScenePartial { // leave mandatory fields return models.ScenePartial{ - Title: models.OptionalString{Set: true, Null: true}, - Code: models.OptionalString{Set: true, Null: true}, - Details: models.OptionalString{Set: true, Null: true}, - Director: models.OptionalString{Set: true, Null: true}, - URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet}, - Date: models.OptionalDate{Set: true, Null: true}, - Rating: models.OptionalInt{Set: true, Null: true}, - StudioID: models.OptionalInt{Set: true, Null: true}, - GalleryIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, - TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, - PerformerIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, - StashIDs: &models.UpdateStashIDs{Mode: models.RelationshipUpdateModeSet}, + Title: models.OptionalString{Set: true, Null: true}, + Code: models.OptionalString{Set: true, Null: true}, + Details: models.OptionalString{Set: true, Null: true}, + Director: models.OptionalString{Set: true, Null: true}, + URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet}, + Date: models.OptionalDate{Set: true, Null: true}, + ProductionDate: models.OptionalDate{Set: true, Null: true}, + Rating: models.OptionalInt{Set: true, Null: true}, + StudioID: models.OptionalInt{Set: true, Null: true}, + GalleryIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, + TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, + PerformerIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, + StashIDs: &models.UpdateStashIDs{Mode: models.RelationshipUpdateModeSet}, } } @@ -540,7 +546,8 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) { stashID1 = "stashid1" stashID2 = "stashid2" - date, _ = models.ParseDate("2003-02-01") + date, _ = models.ParseDate("2003-02-01") + productionDate, _ = models.ParseDate("2003-01-02") ) tests := []struct { @@ -562,12 +569,13 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) { Values: []string{url}, Mode: models.RelationshipUpdateModeSet, }, - Date: models.NewOptionalDate(date), - Rating: models.NewOptionalInt(rating), - Organized: models.NewOptionalBool(true), - StudioID: models.NewOptionalInt(studioIDs[studioIdxWithScene]), - CreatedAt: models.NewOptionalTime(createdAt), - UpdatedAt: models.NewOptionalTime(updatedAt), + Date: models.NewOptionalDate(date), + ProductionDate: models.NewOptionalDate(productionDate), + Rating: models.NewOptionalInt(rating), + Organized: models.NewOptionalBool(true), + StudioID: models.NewOptionalInt(studioIDs[studioIdxWithScene]), + CreatedAt: models.NewOptionalTime(createdAt), + UpdatedAt: models.NewOptionalTime(updatedAt), GalleryIDs: &models.UpdateIDs{ IDs: []int{galleryIDs[galleryIdxWithScene]}, Mode: models.RelationshipUpdateModeSet, @@ -616,20 +624,21 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) { Files: models.NewRelatedVideoFiles([]*models.VideoFile{ makeSceneFile(sceneIdxWithSpacedName), }), - Title: title, - Code: code, - Details: details, - Director: director, - URLs: models.NewRelatedStrings([]string{url}), - Date: &date, - Rating: &rating, - Organized: true, - StudioID: &studioIDs[studioIdxWithScene], - CreatedAt: createdAt, - UpdatedAt: updatedAt, - GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), - TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), - PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), + Title: title, + Code: code, + Details: details, + Director: director, + URLs: models.NewRelatedStrings([]string{url}), + Date: &date, + ProductionDate: &productionDate, + Rating: &rating, + Organized: true, + StudioID: &studioIDs[studioIdxWithScene], + CreatedAt: createdAt, + UpdatedAt: updatedAt, + GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}), + TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithDupName], tagIDs[tagIdx1WithScene]}), + PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}), Groups: models.NewRelatedGroups([]models.GroupsScenes{ { GroupID: groupIDs[groupIdxWithScene], @@ -3292,6 +3301,28 @@ func TestSceneQueryIsMissingDate(t *testing.T) { }) } +func TestSceneQueryIsMissingProductionDate(t *testing.T) { + withTxn(func(ctx context.Context) error { + sqb := db.Scene + isMissing := "production_date" + sceneFilter := models.SceneFilterType{ + IsMissing: &isMissing, + } + + scenes := queryScene(ctx, t, sqb, &sceneFilter, nil) + + // one in four scenes have no production date + assert.Len(t, scenes, int(math.Ceil(float64(totalScenes)/4))) + + // ensure production date is null + for _, scene := range scenes { + assert.Nil(t, scene.ProductionDate) + } + + return nil + }) +} + func TestSceneQueryIsMissingTags(t *testing.T) { withTxn(func(ctx context.Context) error { sqb := db.Scene diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 843b8b4c2..7b696810b 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -1174,16 +1174,17 @@ func makeScene(i int) *models.Scene { URLs: models.NewRelatedStrings([]string{ getSceneEmptyString(i, urlField), }), - Rating: getIntPtr(rating), - Date: getObjectDate(i), - StudioID: studioID, - GalleryIDs: models.NewRelatedIDs(gids), - PerformerIDs: models.NewRelatedIDs(pids), - TagIDs: models.NewRelatedIDs(tids), - Groups: models.NewRelatedGroups(groups), - StashIDs: models.NewRelatedStashIDs(sceneStashIDs(i)), - PlayDuration: getScenePlayDuration(i), - ResumeTime: getSceneResumeTime(i), + Rating: getIntPtr(rating), + Date: getObjectDate(i), + ProductionDate: getObjectDate(i), + StudioID: studioID, + GalleryIDs: models.NewRelatedIDs(gids), + PerformerIDs: models.NewRelatedIDs(pids), + TagIDs: models.NewRelatedIDs(tids), + Groups: models.NewRelatedGroups(groups), + StashIDs: models.NewRelatedStashIDs(sceneStashIDs(i)), + PlayDuration: getScenePlayDuration(i), + ResumeTime: getSceneResumeTime(i), } } diff --git a/pkg/stashbox/graphql/generated_client.go b/pkg/stashbox/graphql/generated_client.go index 640a1c893..c95649f26 100644 --- a/pkg/stashbox/graphql/generated_client.go +++ b/pkg/stashbox/graphql/generated_client.go @@ -406,19 +406,20 @@ func (t *FingerprintFragment) GetDuration() int { } type SceneFragment struct { - ID string "json:\"id\" graphql:\"id\"" - Title *string "json:\"title,omitempty\" graphql:\"title\"" - Code *string "json:\"code,omitempty\" graphql:\"code\"" - Details *string "json:\"details,omitempty\" graphql:\"details\"" - Director *string "json:\"director,omitempty\" graphql:\"director\"" - Duration *int "json:\"duration,omitempty\" graphql:\"duration\"" - Date *string "json:\"date,omitempty\" graphql:\"date\"" - Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" - Images []*ImageFragment "json:\"images\" graphql:\"images\"" - Studio *StudioFragment "json:\"studio,omitempty\" graphql:\"studio\"" - Tags []*TagFragment "json:\"tags\" graphql:\"tags\"" - Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\"" - Fingerprints []*FingerprintFragment "json:\"fingerprints\" graphql:\"fingerprints\"" + ID string "json:\"id\" graphql:\"id\"" + Title *string "json:\"title,omitempty\" graphql:\"title\"" + Code *string "json:\"code,omitempty\" graphql:\"code\"" + Details *string "json:\"details,omitempty\" graphql:\"details\"" + Director *string "json:\"director,omitempty\" graphql:\"director\"" + Duration *int "json:\"duration,omitempty\" graphql:\"duration\"" + Date *string "json:\"date,omitempty\" graphql:\"date\"" + ProductionDate *string "json:\"production_date,omitempty\" graphql:\"production_date\"" + Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" + Images []*ImageFragment "json:\"images\" graphql:\"images\"" + Studio *StudioFragment "json:\"studio,omitempty\" graphql:\"studio\"" + Tags []*TagFragment "json:\"tags\" graphql:\"tags\"" + Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\"" + Fingerprints []*FingerprintFragment "json:\"fingerprints\" graphql:\"fingerprints\"" } func (t *SceneFragment) GetID() string { @@ -463,6 +464,12 @@ func (t *SceneFragment) GetDate() *string { } return t.Date } +func (t *SceneFragment) GetProductionDate() *string { + if t == nil { + t = &SceneFragment{} + } + return t.ProductionDate +} func (t *SceneFragment) GetUrls() []*URLFragment { if t == nil { t = &SceneFragment{} @@ -862,6 +869,7 @@ fragment SceneFragment on Scene { director duration date + production_date urls { ... URLFragment } @@ -998,6 +1006,7 @@ fragment SceneFragment on Scene { director duration date + production_date urls { ... URLFragment } @@ -1134,6 +1143,7 @@ fragment SceneFragment on Scene { director duration date + production_date urls { ... URLFragment } @@ -1270,6 +1280,7 @@ fragment SceneFragment on Scene { director duration date + production_date urls { ... URLFragment } @@ -1564,6 +1575,7 @@ fragment SceneFragment on Scene { director duration date + production_date urls { ... URLFragment } diff --git a/pkg/stashbox/scene.go b/pkg/stashbox/scene.go index 64c4defa2..af207561a 100644 --- a/pkg/stashbox/scene.go +++ b/pkg/stashbox/scene.go @@ -158,15 +158,16 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen stashID := s.ID ss := &models.ScrapedScene{ - Title: s.Title, - Code: s.Code, - Date: s.Date, - Details: s.Details, - Director: s.Director, - URL: findURL(s.Urls, "STUDIO"), - Duration: s.Duration, - RemoteSiteID: &stashID, - Fingerprints: getFingerprints(s), + Title: s.Title, + Code: s.Code, + Date: s.Date, + ProductionDate: s.ProductionDate, + Details: s.Details, + Director: s.Director, + URL: findURL(s.Urls, "STUDIO"), + Duration: s.Duration, + RemoteSiteID: &stashID, + Fingerprints: getFingerprints(s), // Image // stash_id } @@ -296,6 +297,11 @@ func newSceneDraftInput(d SceneDraft, endpoint string) graphql.SceneDraftInput { draft.Date = &v } + if scene.ProductionDate != nil { + v := scene.ProductionDate.String() + draft.ProductionDate = &v + } + if d.Studio != nil { studio := d.Studio diff --git a/ui/v2.5/graphql/data/scene-slim.graphql b/ui/v2.5/graphql/data/scene-slim.graphql index d5899a247..5e339bab7 100644 --- a/ui/v2.5/graphql/data/scene-slim.graphql +++ b/ui/v2.5/graphql/data/scene-slim.graphql @@ -6,6 +6,7 @@ fragment SlimSceneData on Scene { director urls date + production_date rating100 o_counter organized diff --git a/ui/v2.5/graphql/data/scene.graphql b/ui/v2.5/graphql/data/scene.graphql index e4a6e5cc6..934dcb049 100644 --- a/ui/v2.5/graphql/data/scene.graphql +++ b/ui/v2.5/graphql/data/scene.graphql @@ -6,6 +6,7 @@ fragment SceneData on Scene { director urls date + production_date rating100 o_counter organized @@ -85,6 +86,7 @@ fragment SelectSceneData on Scene { id title date + production_date code studio { name diff --git a/ui/v2.5/graphql/data/scrapers.graphql b/ui/v2.5/graphql/data/scrapers.graphql index 4a0f588a4..361473c6b 100644 --- a/ui/v2.5/graphql/data/scrapers.graphql +++ b/ui/v2.5/graphql/data/scrapers.graphql @@ -168,6 +168,7 @@ fragment ScrapedSceneData on ScrapedScene { director urls date + production_date image remote_site_id @@ -254,6 +255,7 @@ fragment ScrapedStashBoxSceneData on ScrapedScene { director url date + production_date image remote_site_id duration diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx index ad7663e9d..461f3c64e 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx @@ -78,9 +78,9 @@ export const SceneDetailPanel: React.FC = (props) => {
- :{" "} - {TextUtils.formatDateTime(intl, props.scene.created_at)}{" "} -
+ :{" "} + {TextUtils.formatDateTime(intl, props.scene.created_at)}{" "} +
:{" "} {TextUtils.formatDateTime(intl, props.scene.updated_at)}{" "} diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 54bf5b573..720fa4141 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -124,6 +124,7 @@ export const SceneEditPanel: React.FC = ({ code: yup.string().ensure(), urls: yupUniqueStringList(intl), date: yupDateString(intl), + production_date: yupDateString(intl), director: yup.string().ensure(), gallery_ids: yup.array(yup.string().required()).defined(), studio_id: yup.string().required().nullable(), @@ -148,6 +149,7 @@ export const SceneEditPanel: React.FC = ({ code: scene.code ?? "", urls: scene.urls ?? [], date: scene.date ?? "", + production_date: scene.production_date ?? "", director: scene.director ?? "", gallery_ids: (scene.galleries ?? []).map((g) => g.id), studio_id: scene.studio?.id ?? null, @@ -332,6 +334,7 @@ export const SceneEditPanel: React.FC = ({ try { const input: GQL.ScrapedSceneInput = { date: fragment.date, + production_date: fragment.production_date, code: fragment.code, details: fragment.details, director: fragment.director, @@ -464,6 +467,10 @@ export const SceneEditPanel: React.FC = ({ formik.setFieldValue("date", updatedScene.date); } + if (updatedScene.production_date) { + formik.setFieldValue("production_date", updatedScene.production_date); + } + if (updatedScene.urls) { formik.setFieldValue("urls", updatedScene.urls); } @@ -825,6 +832,7 @@ export const SceneEditPanel: React.FC = ({ )} {renderDateField("date")} + {renderDateField("production_date")} {renderInputField("director")} {renderGalleriesField()} diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx index 9b9a6bc40..045b82080 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx @@ -72,6 +72,9 @@ export const SceneScrapeDialog: React.FC = ({ const [date, setDate] = useState>( new ScrapeResult(scene.date, scraped.date) ); + const [production_date, setProductionDate] = useState>( + new ScrapeResult(scene.production_date, scraped.production_date) + ); const [director, setDirector] = useState>( new ScrapeResult(scene.director, scraped.director) ); @@ -177,6 +180,7 @@ export const SceneScrapeDialog: React.FC = ({ code, urls, date, + production_date, director, studio, performers, @@ -203,6 +207,7 @@ export const SceneScrapeDialog: React.FC = ({ code: code.getNewValue(), urls: urls.getNewValue(), date: date.getNewValue(), + production_date: production_date.getNewValue(), director: director.getNewValue(), studio: newStudioValue, performers: performers.getNewValue(), @@ -242,6 +247,13 @@ export const SceneScrapeDialog: React.FC = ({ result={date} onChange={(value) => setDate(value)} /> + setProductionDate(value)} + /> = ({ title: resolveField("title", stashScene.title, scene.title), details: resolveField("details", stashScene.details, scene.details), date: resolveField("date", stashScene.date, scene.date), + production_date: resolveField("production_date", stashScene.production_date, scene.production_date), performer_ids: uniq( stashScene.performers.map((p) => p.id).concat(filteredPerformerIDs) ), @@ -514,6 +515,7 @@ const StashSearchResult: React.FC = ({ cover_image: "cover_image", title: "title", date: "date", + production_date: "production_date", url: "url", details: "details", studio: "studio", @@ -630,6 +632,21 @@ const StashSearchResult: React.FC = ({ } }; + const maybeRenderProductionDateField = () => { + if (isActive && scene.production_date) { + return ( +
+ setExcludedField(fields.production_date, v)} + > + {scene.production_date} + +
+ ); + } + }; + const maybeRenderDirector = () => { if (scene.director) { return ( @@ -833,6 +850,7 @@ const StashSearchResult: React.FC = ({ {maybeRenderStudioCode()} {maybeRenderDateField()} + {maybeRenderProductionDateField()} {getDurationStatus(scene, stashSceneFile?.duration)} {getFingerprintStatus(scene, stashScene)}
diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 76df6cf33..a260d012c 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -924,6 +924,7 @@ "value": "Value" }, "date": "Date", + "production_date": "Production Date", "date_format": "YYYY-MM-DD", "datetime_format": "YYYY-MM-DD HH:MM", "death_date": "Death Date", diff --git a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts index 58e3535a6..bdc42603a 100644 --- a/ui/v2.5/src/models/list-filter/criteria/is-missing.ts +++ b/ui/v2.5/src/models/list-filter/criteria/is-missing.ts @@ -30,6 +30,7 @@ export const SceneIsMissingCriterionOption = new IsMissingCriterionOption( "details", "url", "date", + "production_date", "galleries", "studio", "group", diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index 251e2592d..7f70636ec 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -40,6 +40,7 @@ const defaultSortBy = "date"; const sortByOptions = [ "organized", "date", + "production_date", "file_count", "filesize", "duration", @@ -139,6 +140,7 @@ const criterionOptions = [ createMandatoryNumberCriterionOption("interactive_speed"), createMandatoryNumberCriterionOption("file_count"), createDateCriterionOption("date"), + createDateCriterionOption("production_date"), createMandatoryTimestampCriterionOption("created_at"), createMandatoryTimestampCriterionOption("updated_at"), ]; diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index bf5fff4d9..1063e3554 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -202,6 +202,7 @@ export type CriterionType = | "stash_id_endpoint" | "stash_id_count" | "date" + | "production_date" | "created_at" | "updated_at" | "birthdate" From 1c8f53a122ed2990d3707567bff98cefa4909eb7 Mon Sep 17 00:00:00 2001 From: Emilo2 <99644577+Emilo2@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:06:59 +0200 Subject: [PATCH 2/2] Fixed formatting --- .../src/components/Scenes/SceneDetails/SceneDetailPanel.tsx | 6 +++--- ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx index 461f3c64e..ad7663e9d 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx @@ -78,9 +78,9 @@ export const SceneDetailPanel: React.FC = (props) => {
- :{" "} - {TextUtils.formatDateTime(intl, props.scene.created_at)}{" "} -
+ :{" "} + {TextUtils.formatDateTime(intl, props.scene.created_at)}{" "} +
:{" "} {TextUtils.formatDateTime(intl, props.scene.updated_at)}{" "} diff --git a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx index 44347b18c..7243bf3a9 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx @@ -381,7 +381,11 @@ const StashSearchResult: React.FC = ({ title: resolveField("title", stashScene.title, scene.title), details: resolveField("details", stashScene.details, scene.details), date: resolveField("date", stashScene.date, scene.date), - production_date: resolveField("production_date", stashScene.production_date, scene.production_date), + production_date: resolveField( + "production_date", + stashScene.production_date, + scene.production_date + ), performer_ids: uniq( stashScene.performers.map((p) => p.id).concat(filteredPerformerIDs) ),