mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
[Feature] Images new fields : URL & Date (#3015)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
cc4b0f7b11
commit
0b4b100ecc
23 changed files with 191 additions and 15 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
fragment SlimImageData on Image {
|
fragment SlimImageData on Image {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
date
|
||||||
|
url
|
||||||
rating100
|
rating100
|
||||||
organized
|
organized
|
||||||
o_counter
|
o_counter
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ fragment ImageData on Image {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
rating100
|
rating100
|
||||||
|
date
|
||||||
|
url
|
||||||
organized
|
organized
|
||||||
o_counter
|
o_counter
|
||||||
created_at
|
created_at
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,10 @@ input ImageFilterType {
|
||||||
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
|
||||||
# rating expressed as 1-100
|
# rating expressed as 1-100
|
||||||
rating100: IntCriterionInput
|
rating100: IntCriterionInput
|
||||||
|
"""Filter by date"""
|
||||||
|
date: DateCriterionInput
|
||||||
|
"""Filter by url"""
|
||||||
|
url: StringCriterionInput
|
||||||
"""Filter by organized"""
|
"""Filter by organized"""
|
||||||
organized: Boolean
|
organized: Boolean
|
||||||
"""Filter by o-counter"""
|
"""Filter by o-counter"""
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ type Image {
|
||||||
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
|
||||||
# rating expressed as 1-100
|
# rating expressed as 1-100
|
||||||
rating100: Int
|
rating100: Int
|
||||||
|
url: String
|
||||||
|
date: String
|
||||||
o_counter: Int
|
o_counter: Int
|
||||||
organized: Boolean!
|
organized: Boolean!
|
||||||
path: String! @deprecated(reason: "Use files.path")
|
path: String! @deprecated(reason: "Use files.path")
|
||||||
|
|
@ -45,6 +47,8 @@ input ImageUpdateInput {
|
||||||
# rating expressed as 1-100
|
# rating expressed as 1-100
|
||||||
rating100: Int
|
rating100: Int
|
||||||
organized: Boolean
|
organized: Boolean
|
||||||
|
url: String
|
||||||
|
date: String
|
||||||
|
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
performer_ids: [ID!]
|
performer_ids: [ID!]
|
||||||
|
|
@ -63,6 +67,8 @@ input BulkImageUpdateInput {
|
||||||
# rating expressed as 1-100
|
# rating expressed as 1-100
|
||||||
rating100: Int
|
rating100: Int
|
||||||
organized: Boolean
|
organized: Boolean
|
||||||
|
url: String
|
||||||
|
date: String
|
||||||
|
|
||||||
studio_id: ID
|
studio_id: ID
|
||||||
performer_ids: BulkUpdateIds
|
performer_ids: BulkUpdateIds
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,14 @@ func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFile
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, error) {
|
||||||
|
if obj.Date != nil {
|
||||||
|
result := obj.Date.String()
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
|
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
|
||||||
files, err := r.getFiles(ctx, obj)
|
files, err := r.getFiles(ctx, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||||
updatedImage := models.NewImagePartial()
|
updatedImage := models.NewImagePartial()
|
||||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||||
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||||
|
updatedImage.URL = translator.optionalString(input.URL, "url")
|
||||||
|
updatedImage.Date = translator.optionalDate(input.Date, "date")
|
||||||
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||||
|
|
@ -190,6 +192,8 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
|
||||||
|
|
||||||
updatedImage.Title = translator.optionalString(input.Title, "title")
|
updatedImage.Title = translator.optionalString(input.Title, "title")
|
||||||
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
|
||||||
|
updatedImage.URL = translator.optionalString(input.URL, "url")
|
||||||
|
updatedImage.Date = translator.optionalDate(input.Date, "date")
|
||||||
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("converting studio id: %w", err)
|
return nil, fmt.Errorf("converting studio id: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
func ToBasicJSON(image *models.Image) *jsonschema.Image {
|
func ToBasicJSON(image *models.Image) *jsonschema.Image {
|
||||||
newImageJSON := jsonschema.Image{
|
newImageJSON := jsonschema.Image{
|
||||||
Title: image.Title,
|
Title: image.Title,
|
||||||
|
URL: image.URL,
|
||||||
CreatedAt: json.JSONTime{Time: image.CreatedAt},
|
CreatedAt: json.JSONTime{Time: image.CreatedAt},
|
||||||
UpdatedAt: json.JSONTime{Time: image.UpdatedAt},
|
UpdatedAt: json.JSONTime{Time: image.UpdatedAt},
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +24,10 @@ func ToBasicJSON(image *models.Image) *jsonschema.Image {
|
||||||
newImageJSON.Rating = *image.Rating
|
newImageJSON.Rating = *image.Rating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if image.Date != nil {
|
||||||
|
newImageJSON.Date = image.Date.String()
|
||||||
|
}
|
||||||
|
|
||||||
newImageJSON.Organized = image.Organized
|
newImageJSON.Organized = image.Organized
|
||||||
newImageJSON.OCounter = image.OCounter
|
newImageJSON.OCounter = image.OCounter
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ const (
|
||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 5
|
rating = 5
|
||||||
|
url = "http://a.com"
|
||||||
|
date = "2001-01-01"
|
||||||
|
dateObj = models.NewDate(date)
|
||||||
organized = true
|
organized = true
|
||||||
ocounter = 2
|
ocounter = 2
|
||||||
)
|
)
|
||||||
|
|
@ -52,6 +55,8 @@ func createFullImage(id int) models.Image {
|
||||||
Title: title,
|
Title: title,
|
||||||
OCounter: ocounter,
|
OCounter: ocounter,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
|
Date: &dateObj,
|
||||||
|
URL: url,
|
||||||
Organized: organized,
|
Organized: organized,
|
||||||
CreatedAt: createTime,
|
CreatedAt: createTime,
|
||||||
UpdatedAt: updateTime,
|
UpdatedAt: updateTime,
|
||||||
|
|
@ -63,6 +68,8 @@ func createFullJSONImage() *jsonschema.Image {
|
||||||
Title: title,
|
Title: title,
|
||||||
OCounter: ocounter,
|
OCounter: ocounter,
|
||||||
Rating: rating,
|
Rating: rating,
|
||||||
|
Date: date,
|
||||||
|
URL: url,
|
||||||
Organized: organized,
|
Organized: organized,
|
||||||
Files: []string{path},
|
Files: []string{path},
|
||||||
CreatedAt: json.JSONTime{
|
CreatedAt: json.JSONTime{
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,13 @@ func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image {
|
||||||
if imageJSON.Rating != 0 {
|
if imageJSON.Rating != 0 {
|
||||||
newImage.Rating = &imageJSON.Rating
|
newImage.Rating = &imageJSON.Rating
|
||||||
}
|
}
|
||||||
|
if imageJSON.URL != "" {
|
||||||
|
newImage.URL = imageJSON.URL
|
||||||
|
}
|
||||||
|
if imageJSON.Date != "" {
|
||||||
|
d := models.NewDate(imageJSON.Date)
|
||||||
|
newImage.Date = &d
|
||||||
|
}
|
||||||
|
|
||||||
return newImage
|
return newImage
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ type ImageFilterType struct {
|
||||||
Rating *IntCriterionInput `json:"rating"`
|
Rating *IntCriterionInput `json:"rating"`
|
||||||
// Filter by rating expressed as 1-100
|
// Filter by rating expressed as 1-100
|
||||||
Rating100 *IntCriterionInput `json:"rating100"`
|
Rating100 *IntCriterionInput `json:"rating100"`
|
||||||
|
// Filter by date
|
||||||
|
Date *DateCriterionInput `json:"date"`
|
||||||
|
// Filter by url
|
||||||
|
URL *StringCriterionInput `json:"url"`
|
||||||
// Filter by organized
|
// Filter by organized
|
||||||
Organized *bool `json:"organized"`
|
Organized *bool `json:"organized"`
|
||||||
// Filter by o-counter
|
// Filter by o-counter
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ type Image struct {
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Studio string `json:"studio,omitempty"`
|
Studio string `json:"studio,omitempty"`
|
||||||
Rating int `json:"rating,omitempty"`
|
Rating int `json:"rating,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Date string `json:"date,omitempty"`
|
||||||
Organized bool `json:"organized,omitempty"`
|
Organized bool `json:"organized,omitempty"`
|
||||||
OCounter int `json:"o_counter,omitempty"`
|
OCounter int `json:"o_counter,omitempty"`
|
||||||
Galleries []GalleryRef `json:"galleries,omitempty"`
|
Galleries []GalleryRef `json:"galleries,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ type Image struct {
|
||||||
Organized bool `json:"organized"`
|
Organized bool `json:"organized"`
|
||||||
OCounter int `json:"o_counter"`
|
OCounter int `json:"o_counter"`
|
||||||
StudioID *int `json:"studio_id"`
|
StudioID *int `json:"studio_id"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Date *Date `json:"date"`
|
||||||
|
|
||||||
// transient - not persisted
|
// transient - not persisted
|
||||||
Files RelatedImageFiles
|
Files RelatedImageFiles
|
||||||
|
|
@ -117,6 +119,8 @@ type ImagePartial struct {
|
||||||
Title OptionalString
|
Title OptionalString
|
||||||
// Rating expressed in 1-100 scale
|
// Rating expressed in 1-100 scale
|
||||||
Rating OptionalInt
|
Rating OptionalInt
|
||||||
|
URL OptionalString
|
||||||
|
Date OptionalDate
|
||||||
Organized OptionalBool
|
Organized OptionalBool
|
||||||
OCounter OptionalInt
|
OCounter OptionalInt
|
||||||
StudioID OptionalInt
|
StudioID OptionalInt
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSchemaVersion uint = 42
|
var appSchemaVersion uint = 43
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsBox embed.FS
|
var migrationsBox embed.FS
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ type imageRow struct {
|
||||||
Title zero.String `db:"title"`
|
Title zero.String `db:"title"`
|
||||||
// expressed as 1-100
|
// expressed as 1-100
|
||||||
Rating null.Int `db:"rating"`
|
Rating null.Int `db:"rating"`
|
||||||
|
URL zero.String `db:"url"`
|
||||||
|
Date models.SQLiteDate `db:"date"`
|
||||||
Organized bool `db:"organized"`
|
Organized bool `db:"organized"`
|
||||||
OCounter int `db:"o_counter"`
|
OCounter int `db:"o_counter"`
|
||||||
StudioID null.Int `db:"studio_id,omitempty"`
|
StudioID null.Int `db:"studio_id,omitempty"`
|
||||||
|
|
@ -42,6 +44,10 @@ func (r *imageRow) fromImage(i models.Image) {
|
||||||
r.ID = i.ID
|
r.ID = i.ID
|
||||||
r.Title = zero.StringFrom(i.Title)
|
r.Title = zero.StringFrom(i.Title)
|
||||||
r.Rating = intFromPtr(i.Rating)
|
r.Rating = intFromPtr(i.Rating)
|
||||||
|
r.URL = zero.StringFrom(i.URL)
|
||||||
|
if i.Date != nil {
|
||||||
|
_ = r.Date.Scan(i.Date.Time)
|
||||||
|
}
|
||||||
r.Organized = i.Organized
|
r.Organized = i.Organized
|
||||||
r.OCounter = i.OCounter
|
r.OCounter = i.OCounter
|
||||||
r.StudioID = intFromPtr(i.StudioID)
|
r.StudioID = intFromPtr(i.StudioID)
|
||||||
|
|
@ -62,6 +68,8 @@ func (r *imageQueryRow) resolve() *models.Image {
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Title: r.Title.String,
|
Title: r.Title.String,
|
||||||
Rating: nullIntPtr(r.Rating),
|
Rating: nullIntPtr(r.Rating),
|
||||||
|
URL: r.URL.String,
|
||||||
|
Date: r.Date.DatePtr(),
|
||||||
Organized: r.Organized,
|
Organized: r.Organized,
|
||||||
OCounter: r.OCounter,
|
OCounter: r.OCounter,
|
||||||
StudioID: nullIntPtr(r.StudioID),
|
StudioID: nullIntPtr(r.StudioID),
|
||||||
|
|
@ -87,6 +95,8 @@ type imageRowRecord struct {
|
||||||
func (r *imageRowRecord) fromPartial(i models.ImagePartial) {
|
func (r *imageRowRecord) fromPartial(i models.ImagePartial) {
|
||||||
r.setNullString("title", i.Title)
|
r.setNullString("title", i.Title)
|
||||||
r.setNullInt("rating", i.Rating)
|
r.setNullInt("rating", i.Rating)
|
||||||
|
r.setNullString("url", i.URL)
|
||||||
|
r.setSQLiteDate("date", i.Date)
|
||||||
r.setBool("organized", i.Organized)
|
r.setBool("organized", i.Organized)
|
||||||
r.setInt("o_counter", i.OCounter)
|
r.setInt("o_counter", i.OCounter)
|
||||||
r.setNullInt("studio_id", i.StudioID)
|
r.setNullInt("studio_id", i.StudioID)
|
||||||
|
|
@ -638,6 +648,8 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF
|
||||||
query.handleCriterion(ctx, rating5CriterionHandler(imageFilter.Rating, "images.rating", nil))
|
query.handleCriterion(ctx, rating5CriterionHandler(imageFilter.Rating, "images.rating", nil))
|
||||||
query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil))
|
query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil))
|
||||||
query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil))
|
query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil))
|
||||||
|
query.handleCriterion(ctx, dateCriterionHandler(imageFilter.Date, "images.date"))
|
||||||
|
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.URL, "images.url"))
|
||||||
|
|
||||||
query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable))
|
query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable))
|
||||||
query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing))
|
query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing))
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 60
|
rating = 60
|
||||||
ocounter = 5
|
ocounter = 5
|
||||||
|
url = "url"
|
||||||
|
date = models.NewDate("2003-02-01")
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
|
@ -72,6 +74,8 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
|
||||||
models.Image{
|
models.Image{
|
||||||
Title: title,
|
Title: title,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
|
Date: &date,
|
||||||
|
URL: url,
|
||||||
Organized: true,
|
Organized: true,
|
||||||
OCounter: ocounter,
|
OCounter: ocounter,
|
||||||
StudioID: &studioIDs[studioIdxWithImage],
|
StudioID: &studioIDs[studioIdxWithImage],
|
||||||
|
|
@ -88,6 +92,8 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
|
||||||
models.Image{
|
models.Image{
|
||||||
Title: title,
|
Title: title,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
|
Date: &date,
|
||||||
|
URL: url,
|
||||||
Organized: true,
|
Organized: true,
|
||||||
OCounter: ocounter,
|
OCounter: ocounter,
|
||||||
StudioID: &studioIDs[studioIdxWithImage],
|
StudioID: &studioIDs[studioIdxWithImage],
|
||||||
|
|
@ -209,6 +215,8 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 60
|
rating = 60
|
||||||
|
url = "url"
|
||||||
|
date = models.NewDate("2003-02-01")
|
||||||
ocounter = 5
|
ocounter = 5
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
@ -225,6 +233,8 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
|
||||||
ID: imageIDs[imageIdxWithGallery],
|
ID: imageIDs[imageIdxWithGallery],
|
||||||
Title: title,
|
Title: title,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
|
URL: url,
|
||||||
|
Date: &date,
|
||||||
Organized: true,
|
Organized: true,
|
||||||
OCounter: ocounter,
|
OCounter: ocounter,
|
||||||
StudioID: &studioIDs[studioIdxWithImage],
|
StudioID: &studioIDs[studioIdxWithImage],
|
||||||
|
|
@ -372,6 +382,8 @@ func clearImagePartial() models.ImagePartial {
|
||||||
return models.ImagePartial{
|
return models.ImagePartial{
|
||||||
Title: models.OptionalString{Set: true, Null: true},
|
Title: models.OptionalString{Set: true, Null: true},
|
||||||
Rating: models.OptionalInt{Set: true, Null: true},
|
Rating: models.OptionalInt{Set: true, Null: true},
|
||||||
|
URL: models.OptionalString{Set: true, Null: true},
|
||||||
|
Date: models.OptionalDate{Set: true, Null: true},
|
||||||
StudioID: models.OptionalInt{Set: true, Null: true},
|
StudioID: models.OptionalInt{Set: true, Null: true},
|
||||||
GalleryIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet},
|
GalleryIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet},
|
||||||
TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet},
|
TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet},
|
||||||
|
|
@ -383,6 +395,8 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
title = "title"
|
title = "title"
|
||||||
rating = 60
|
rating = 60
|
||||||
|
url = "url"
|
||||||
|
date = models.NewDate("2003-02-01")
|
||||||
ocounter = 5
|
ocounter = 5
|
||||||
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
@ -401,6 +415,8 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
|
||||||
models.ImagePartial{
|
models.ImagePartial{
|
||||||
Title: models.NewOptionalString(title),
|
Title: models.NewOptionalString(title),
|
||||||
Rating: models.NewOptionalInt(rating),
|
Rating: models.NewOptionalInt(rating),
|
||||||
|
URL: models.NewOptionalString(url),
|
||||||
|
Date: models.NewOptionalDate(date),
|
||||||
Organized: models.NewOptionalBool(true),
|
Organized: models.NewOptionalBool(true),
|
||||||
OCounter: models.NewOptionalInt(ocounter),
|
OCounter: models.NewOptionalInt(ocounter),
|
||||||
StudioID: models.NewOptionalInt(studioIDs[studioIdxWithImage]),
|
StudioID: models.NewOptionalInt(studioIDs[studioIdxWithImage]),
|
||||||
|
|
@ -423,6 +439,8 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
|
||||||
ID: imageIDs[imageIdx1WithGallery],
|
ID: imageIDs[imageIdx1WithGallery],
|
||||||
Title: title,
|
Title: title,
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
|
URL: url,
|
||||||
|
Date: &date,
|
||||||
Organized: true,
|
Organized: true,
|
||||||
OCounter: ocounter,
|
OCounter: ocounter,
|
||||||
StudioID: &studioIDs[studioIdxWithImage],
|
StudioID: &studioIDs[studioIdxWithImage],
|
||||||
|
|
@ -943,7 +961,8 @@ func Test_imageQueryBuilder_Destroy(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeImageWithID(index int) *models.Image {
|
func makeImageWithID(index int) *models.Image {
|
||||||
ret := makeImage(index)
|
const fromDB = true
|
||||||
|
ret := makeImage(index, true)
|
||||||
ret.ID = imageIDs[index]
|
ret.ID = imageIDs[index]
|
||||||
|
|
||||||
ret.Files = models.NewRelatedImageFiles([]*file.ImageFile{makeImageFile(index)})
|
ret.Files = models.NewRelatedImageFiles([]*file.ImageFile{makeImageFile(index)})
|
||||||
|
|
@ -2560,6 +2579,13 @@ func TestImageQuerySorting(t *testing.T) {
|
||||||
-1,
|
-1,
|
||||||
-1,
|
-1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"date",
|
||||||
|
"date",
|
||||||
|
models.SortDirectionEnumDesc,
|
||||||
|
imageIdxWithTwoGalleries,
|
||||||
|
imageIdxWithGrandChildStudio,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
qb := db.Image
|
qb := db.Image
|
||||||
|
|
|
||||||
2
pkg/sqlite/migrations/43_image_date_url.up.sql
Normal file
2
pkg/sqlite/migrations/43_image_date_url.up.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE `images` ADD COLUMN `url` varchar(255);
|
||||||
|
ALTER TABLE `images` ADD COLUMN `date` date;
|
||||||
|
|
@ -885,9 +885,9 @@ func getObjectDate(index int) models.SQLiteDate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getObjectDateObject(index int) *models.Date {
|
func getObjectDateObject(index int, fromDB bool) *models.Date {
|
||||||
d := getObjectDate(index)
|
d := getObjectDate(index)
|
||||||
if !d.Valid {
|
if !d.Valid || (fromDB && (d.String == "" || d.String == "0001-01-01")) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -998,7 +998,7 @@ func makeScene(i int) *models.Scene {
|
||||||
URL: getSceneEmptyString(i, urlField),
|
URL: getSceneEmptyString(i, urlField),
|
||||||
Rating: getIntPtr(rating),
|
Rating: getIntPtr(rating),
|
||||||
OCounter: getOCounter(i),
|
OCounter: getOCounter(i),
|
||||||
Date: getObjectDateObject(i),
|
Date: getObjectDateObject(i, false),
|
||||||
StudioID: studioID,
|
StudioID: studioID,
|
||||||
GalleryIDs: models.NewRelatedIDs(gids),
|
GalleryIDs: models.NewRelatedIDs(gids),
|
||||||
PerformerIDs: models.NewRelatedIDs(pids),
|
PerformerIDs: models.NewRelatedIDs(pids),
|
||||||
|
|
@ -1063,7 +1063,7 @@ func makeImageFile(i int) *file.ImageFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeImage(i int) *models.Image {
|
func makeImage(i int, fromDB bool) *models.Image {
|
||||||
title := getImageStringValue(i, titleField)
|
title := getImageStringValue(i, titleField)
|
||||||
var studioID *int
|
var studioID *int
|
||||||
if _, ok := imageStudios[i]; ok {
|
if _, ok := imageStudios[i]; ok {
|
||||||
|
|
@ -1078,6 +1078,8 @@ func makeImage(i int) *models.Image {
|
||||||
return &models.Image{
|
return &models.Image{
|
||||||
Title: title,
|
Title: title,
|
||||||
Rating: getIntPtr(getRating(i)),
|
Rating: getIntPtr(getRating(i)),
|
||||||
|
Date: getObjectDateObject(i, fromDB),
|
||||||
|
URL: getImageStringValue(i, urlField),
|
||||||
OCounter: getOCounter(i),
|
OCounter: getOCounter(i),
|
||||||
StudioID: studioID,
|
StudioID: studioID,
|
||||||
GalleryIDs: models.NewRelatedIDs(gids),
|
GalleryIDs: models.NewRelatedIDs(gids),
|
||||||
|
|
@ -1101,7 +1103,7 @@ func createImages(ctx context.Context, n int) error {
|
||||||
}
|
}
|
||||||
imageFileIDs = append(imageFileIDs, f.ID)
|
imageFileIDs = append(imageFileIDs, f.ID)
|
||||||
|
|
||||||
image := makeImage(i)
|
image := makeImage(i, false)
|
||||||
|
|
||||||
err := qb.Create(ctx, &models.ImageCreateInput{
|
err := qb.Create(ctx, &models.ImageCreateInput{
|
||||||
Image: image,
|
Image: image,
|
||||||
|
|
@ -1162,7 +1164,7 @@ func makeGallery(i int, includeScenes bool) *models.Gallery {
|
||||||
Title: getGalleryStringValue(i, titleField),
|
Title: getGalleryStringValue(i, titleField),
|
||||||
URL: getGalleryNullStringValue(i, urlField).String,
|
URL: getGalleryNullStringValue(i, urlField).String,
|
||||||
Rating: getIntPtr(getRating(i)),
|
Rating: getIntPtr(getRating(i)),
|
||||||
Date: getObjectDateObject(i),
|
Date: getObjectDateObject(i, false),
|
||||||
StudioID: studioID,
|
StudioID: studioID,
|
||||||
PerformerIDs: models.NewRelatedIDs(pids),
|
PerformerIDs: models.NewRelatedIDs(pids),
|
||||||
TagIDs: models.NewRelatedIDs(tids),
|
TagIDs: models.NewRelatedIDs(tids),
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@ import { TagLink, TruncatedText } from "src/components/Shared";
|
||||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import { sortPerformers } from "src/core/performers";
|
import { sortPerformers } from "src/core/performers";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedDate, FormattedMessage, useIntl } from "react-intl";
|
||||||
import { objectTitle } from "src/core/files";
|
import { objectTitle } from "src/core/files";
|
||||||
|
|
||||||
interface IImageDetailProps {
|
interface IImageDetailProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +90,15 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
||||||
<TruncatedText text={objectTitle(props.image)} />
|
<TruncatedText text={objectTitle(props.image)} />
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
{props.image.date ? (
|
||||||
|
<h5>
|
||||||
|
<FormattedDate
|
||||||
|
value={props.image.date}
|
||||||
|
format="long"
|
||||||
|
timeZone="utc"
|
||||||
|
/>
|
||||||
|
</h5>
|
||||||
|
) : undefined}
|
||||||
{props.image.rating100 ? (
|
{props.image.rating100 ? (
|
||||||
<h6>
|
<h6>
|
||||||
<FormattedMessage id="rating" />:{" "}
|
<FormattedMessage id="rating" />:{" "}
|
||||||
|
|
@ -99,6 +107,7 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{renderGalleries()}
|
{renderGalleries()}
|
||||||
{file?.width && file?.height ? (
|
{file?.width && file?.height ? (
|
||||||
<h6>
|
<h6>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
TagSelect,
|
TagSelect,
|
||||||
StudioSelect,
|
StudioSelect,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
|
URLField,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormUtils } from "src/utils";
|
import { FormUtils } from "src/utils";
|
||||||
|
|
@ -39,6 +40,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().optional().nullable(),
|
title: yup.string().optional().nullable(),
|
||||||
rating100: yup.number().optional().nullable(),
|
rating100: yup.number().optional().nullable(),
|
||||||
|
url: yup.string().optional().nullable(),
|
||||||
|
date: yup.string().optional().nullable(),
|
||||||
studio_id: yup.string().optional().nullable(),
|
studio_id: yup.string().optional().nullable(),
|
||||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||||
|
|
@ -47,6 +50,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
title: image.title ?? "",
|
title: image.title ?? "",
|
||||||
rating100: image.rating100 ?? null,
|
rating100: image.rating100 ?? null,
|
||||||
|
url: image?.url ?? "",
|
||||||
|
date: image?.date ?? "",
|
||||||
studio_id: image.studio?.id,
|
studio_id: image.studio?.id,
|
||||||
performer_ids: (image.performers ?? []).map((p) => p.id),
|
performer_ids: (image.performers ?? []).map((p) => p.id),
|
||||||
tag_ids: (image.tags ?? []).map((t) => t.id),
|
tag_ids: (image.tags ?? []).map((t) => t.id),
|
||||||
|
|
@ -189,6 +194,28 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
<div className="form-container row px-3">
|
<div className="form-container row px-3">
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
<div className="col-12 col-lg-6 col-xl-12">
|
||||||
{renderTextField("title", intl.formatMessage({ id: "title" }))}
|
{renderTextField("title", intl.formatMessage({ id: "title" }))}
|
||||||
|
<Form.Group controlId="url" as={Row}>
|
||||||
|
<Col xs={3} className="pr-0 url-label">
|
||||||
|
<Form.Label className="col-form-label">
|
||||||
|
<FormattedMessage id="url" />
|
||||||
|
</Form.Label>
|
||||||
|
</Col>
|
||||||
|
<Col xs={9}>
|
||||||
|
<URLField
|
||||||
|
{...formik.getFieldProps("url")}
|
||||||
|
onScrapeClick={() => {}}
|
||||||
|
urlScrapable={() => {
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
isInvalid={!!formik.getFieldMeta("url").error}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
{renderTextField(
|
||||||
|
"date",
|
||||||
|
intl.formatMessage({ id: "date" }),
|
||||||
|
"YYYY-MM-DD"
|
||||||
|
)}
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: intl.formatMessage({ id: "rating" }),
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,24 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.image.files.length === 1) {
|
if (props.image.files.length === 1) {
|
||||||
return <FileInfoPanel file={props.image.files[0]} />;
|
return (
|
||||||
|
<>
|
||||||
|
<FileInfoPanel file={props.image.files[0]} />
|
||||||
|
|
||||||
|
{props.image.url ? (
|
||||||
|
<dl className="container image-file-info details-list">
|
||||||
|
<URLField
|
||||||
|
id="media_info.downloaded_from"
|
||||||
|
url={TextUtils.sanitiseURL(props.image.url)}
|
||||||
|
value={TextUtils.domainFromURL(props.image.url)}
|
||||||
|
truncate
|
||||||
|
/>
|
||||||
|
</dl>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSetPrimaryFile(fileID: string) {
|
async function onSetPrimaryFile(fileID: string) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* Performer autotagging does not currently match on performer aliases. This will be addressed when finer control over the matching is implemented.
|
* Performer autotagging does not currently match on performer aliases. This will be addressed when finer control over the matching is implemented.
|
||||||
|
|
||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Added URL and Date fields to Images. ([#3015](https://github.com/stashapp/stash/pull/3015))
|
||||||
* Added support for plugins to add injected CSS and Javascript to the UI. ([#3195](https://github.com/stashapp/stash/pull/3195))
|
* Added support for plugins to add injected CSS and Javascript to the UI. ([#3195](https://github.com/stashapp/stash/pull/3195))
|
||||||
* Added disambiguation field to Performers, to differentiate between performers with the same name. ([#3113](https://github.com/stashapp/stash/pull/3113))
|
* Added disambiguation field to Performers, to differentiate between performers with the same name. ([#3113](https://github.com/stashapp/stash/pull/3113))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
createStringCriterionOption,
|
createStringCriterionOption,
|
||||||
NullNumberCriterionOption,
|
NullNumberCriterionOption,
|
||||||
createMandatoryTimestampCriterionOption,
|
createMandatoryTimestampCriterionOption,
|
||||||
|
createDateCriterionOption,
|
||||||
} from "./criteria/criterion";
|
} from "./criteria/criterion";
|
||||||
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
|
||||||
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
|
import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
|
||||||
|
|
@ -24,6 +25,7 @@ const sortByOptions = [
|
||||||
"o_counter",
|
"o_counter",
|
||||||
"filesize",
|
"filesize",
|
||||||
"file_count",
|
"file_count",
|
||||||
|
"date",
|
||||||
...MediaSortByOptions,
|
...MediaSortByOptions,
|
||||||
].map(ListFilterOptions.createSortBy);
|
].map(ListFilterOptions.createSortBy);
|
||||||
|
|
||||||
|
|
@ -44,6 +46,8 @@ const criterionOptions = [
|
||||||
createMandatoryNumberCriterionOption("performer_count"),
|
createMandatoryNumberCriterionOption("performer_count"),
|
||||||
PerformerFavoriteCriterionOption,
|
PerformerFavoriteCriterionOption,
|
||||||
StudiosCriterionOption,
|
StudiosCriterionOption,
|
||||||
|
createStringCriterionOption("url"),
|
||||||
|
createDateCriterionOption("date"),
|
||||||
createMandatoryNumberCriterionOption("file_count"),
|
createMandatoryNumberCriterionOption("file_count"),
|
||||||
createMandatoryTimestampCriterionOption("created_at"),
|
createMandatoryTimestampCriterionOption("created_at"),
|
||||||
createMandatoryTimestampCriterionOption("updated_at"),
|
createMandatoryTimestampCriterionOption("updated_at"),
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,26 @@ const sanitiseURL = (url?: string, siteURL?: URL) => {
|
||||||
return `https://${url}`;
|
return `https://${url}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const domainFromURL = (urlString?: string, url?: URL) => {
|
||||||
|
if (url) {
|
||||||
|
return url.hostname;
|
||||||
|
} else if (urlString) {
|
||||||
|
var urlDomain = "";
|
||||||
|
try {
|
||||||
|
var sanitizedUrl = sanitiseURL(urlString);
|
||||||
|
if (sanitizedUrl) {
|
||||||
|
urlString = sanitizedUrl;
|
||||||
|
}
|
||||||
|
urlDomain = new URL(urlString).hostname;
|
||||||
|
} catch {
|
||||||
|
urlDomain = urlString; // We cant determine the hostname so we return the base string
|
||||||
|
}
|
||||||
|
return urlDomain;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatDate = (intl: IntlShape, date?: string, utc = true) => {
|
const formatDate = (intl: IntlShape, date?: string, utc = true) => {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -339,6 +359,7 @@ const TextUtils = {
|
||||||
bitRate,
|
bitRate,
|
||||||
resolution,
|
resolution,
|
||||||
sanitiseURL,
|
sanitiseURL,
|
||||||
|
domainFromURL,
|
||||||
twitterURL,
|
twitterURL,
|
||||||
instagramURL,
|
instagramURL,
|
||||||
formatDate,
|
formatDate,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue