diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 3d15f0c480f..d3444c34f0d 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -33,7 +33,7 @@ jobs: run: docker exec -t build /bin/bash -c "make generate-backend" - name: Run golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v3 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version version: latest @@ -42,19 +42,26 @@ jobs: # working-directory: somedir # Optional: golangci-lint command line arguments. - args: --modules-download-mode=vendor --timeout=5m + # + # Note: By default, the `.golangci.yml` file should be at the root of the repository. + # The location of the configuration file can be changed by using `--config=` + args: --timeout=5m # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true - # Optional: if set to true then the action will use pre-installed Go. - # skip-go-installation: true + # Optional: if set to true, then all caching functionality will be completely disabled, + # takes precedence over all other caching options. + # skip-cache: true - # Optional: if set to true then the action don't cache or restore ~/go/pkg. - skip-pkg-cache: true + # Optional: if set to true, then the action won't cache or restore ~/go/pkg. + # skip-pkg-cache: true - # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. - skip-build-cache: true + # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. + # skip-build-cache: true + + # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. + # install-mode: "goinstall" - name: Cleanup build container run: docker rm -f -v build diff --git a/.gitignore b/.gitignore index 08696850df1..01b895e922e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ # Go #### +# Vendored dependencies +vendor + # Binaries for programs and plugins *.exe *.exe~ diff --git a/.golangci.yml b/.golangci.yml index 43f7324a0ca..48ca4fd75a7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,6 @@ # options for analysis running run: timeout: 5m - modules-download-mode: vendor linters: disable-all: true diff --git a/cmd/stash/main.go b/cmd/stash/main.go index 4aadf4fb1d8..0fbdf2108ad 100644 --- a/cmd/stash/main.go +++ b/cmd/stash/main.go @@ -1,4 +1,4 @@ -//go:generate go run -mod=vendor github.com/99designs/gqlgen +//go:generate go run github.com/99designs/gqlgen package main import ( diff --git a/docker/build/x86_64/Dockerfile b/docker/build/x86_64/Dockerfile index 554c6ff9977..174d6f022fb 100644 --- a/docker/build/x86_64/Dockerfile +++ b/docker/build/x86_64/Dockerfile @@ -21,7 +21,6 @@ RUN apk add --no-cache make alpine-sdk WORKDIR /stash COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/ COPY ./scripts /stash/scripts/ -COPY ./vendor /stash/vendor/ COPY ./pkg /stash/pkg/ COPY ./cmd /stash/cmd COPY ./internal /stash/internal diff --git a/docker/build/x86_64/Dockerfile-CUDA b/docker/build/x86_64/Dockerfile-CUDA index 63ecf3d75bb..8195f2324c9 100644 --- a/docker/build/x86_64/Dockerfile-CUDA +++ b/docker/build/x86_64/Dockerfile-CUDA @@ -21,7 +21,6 @@ RUN apt update && apt install -y build-essential golang WORKDIR /stash COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/ COPY ./scripts /stash/scripts/ -COPY ./vendor /stash/vendor/ COPY ./pkg /stash/pkg/ COPY ./cmd /stash/cmd COPY ./internal /stash/internal diff --git a/go.mod b/go.mod index bb05736f6c4..5b82dcc4aec 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ require ( github.com/99designs/gqlgen v0.17.2 github.com/Yamashou/gqlgenc v0.0.6 github.com/anacrolix/dms v1.2.2 - github.com/antchfx/htmlquery v1.2.5-0.20211125074323-810ee8082758 + github.com/antchfx/htmlquery v1.3.0 github.com/chromedp/cdproto v0.0.0-20210622022015-fe1827b46b84 github.com/chromedp/chromedp v0.7.3 github.com/corona10/goimagehash v1.0.3 @@ -66,7 +66,7 @@ require ( require ( github.com/agnivade/levenshtein v1.1.1 // indirect - github.com/antchfx/xpath v1.2.0 // indirect + github.com/antchfx/xpath v1.2.3 // indirect github.com/asticode/go-astikit v0.20.0 // indirect github.com/asticode/go-astits v1.8.0 // indirect github.com/chromedp/sysutil v1.0.0 // indirect diff --git a/go.sum b/go.sum index b9524d0cbc1..e06dae76834 100644 --- a/go.sum +++ b/go.sum @@ -94,10 +94,10 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM= github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/antchfx/htmlquery v1.2.5-0.20211125074323-810ee8082758 h1:Ldjwcl7T8VqCKgQQ0TfPI8fNb8O/GtMXcYaHlqOu99s= -github.com/antchfx/htmlquery v1.2.5-0.20211125074323-810ee8082758/go.mod h1:2xO6iu3EVWs7R2JYqBbp8YzG50gj/ofqs5/0VZoDZLc= -github.com/antchfx/xpath v1.2.0 h1:mbwv7co+x0RwgeGAOHdrKy89GvHaGvxxBtPK0uF9Zr8= -github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= +github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= +github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes= +github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0= github.com/apache/arrow/go/arrow v0.0.0-20210521153258-78c88a9f517b/go.mod h1:R4hW3Ug0s+n4CUsWHKOj00Pu01ZqU4x/hSF5kXUcXKQ= @@ -909,7 +909,6 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -935,6 +934,7 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1064,10 +1064,12 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1079,6 +1081,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/graphql/documents/data/gallery-slim.graphql b/graphql/documents/data/gallery-slim.graphql index ebec042512c..ea44b2997ba 100644 --- a/graphql/documents/data/gallery-slim.graphql +++ b/graphql/documents/data/gallery-slim.graphql @@ -2,7 +2,7 @@ fragment SlimGalleryData on Gallery { id title date - url + urls details rating100 organized diff --git a/graphql/documents/data/gallery.graphql b/graphql/documents/data/gallery.graphql index b4df2589690..89719dfca65 100644 --- a/graphql/documents/data/gallery.graphql +++ b/graphql/documents/data/gallery.graphql @@ -4,7 +4,7 @@ fragment GalleryData on Gallery { updated_at title date - url + urls details rating100 organized diff --git a/graphql/documents/data/image-slim.graphql b/graphql/documents/data/image-slim.graphql index 9f84904dcfe..1c7784c9ede 100644 --- a/graphql/documents/data/image-slim.graphql +++ b/graphql/documents/data/image-slim.graphql @@ -2,7 +2,7 @@ fragment SlimImageData on Image { id title date - url + urls rating100 organized o_counter diff --git a/graphql/documents/data/image.graphql b/graphql/documents/data/image.graphql index d55a8108121..64c801401e7 100644 --- a/graphql/documents/data/image.graphql +++ b/graphql/documents/data/image.graphql @@ -3,7 +3,7 @@ fragment ImageData on Image { title rating100 date - url + urls organized o_counter created_at diff --git a/graphql/documents/data/scene-marker.graphql b/graphql/documents/data/scene-marker.graphql index 61439bd1e80..9fd0c7d3ded 100644 --- a/graphql/documents/data/scene-marker.graphql +++ b/graphql/documents/data/scene-marker.graphql @@ -13,12 +13,10 @@ fragment SceneMarkerData on SceneMarker { primary_tag { id name - aliases } tags { id name - aliases } } diff --git a/graphql/documents/data/scrapers.graphql b/graphql/documents/data/scrapers.graphql index 6e9ba214912..05ed76b5cff 100644 --- a/graphql/documents/data/scrapers.graphql +++ b/graphql/documents/data/scrapers.graphql @@ -110,6 +110,12 @@ fragment ScrapedSceneMovieData on ScrapedMovie { director url synopsis + front_image + back_image + + studio { + ...ScrapedMovieStudioData + } } fragment ScrapedSceneStudioData on ScrapedStudio { @@ -179,7 +185,7 @@ fragment ScrapedSceneData on ScrapedScene { fragment ScrapedGalleryData on ScrapedGallery { title details - url + urls date studio { diff --git a/graphql/documents/data/tag-slim.graphql b/graphql/documents/data/tag-slim.graphql index 26b7c277a5b..e35660de624 100644 --- a/graphql/documents/data/tag-slim.graphql +++ b/graphql/documents/data/tag-slim.graphql @@ -3,4 +3,6 @@ fragment SlimTagData on Tag { name aliases image_path + parent_count + child_count } diff --git a/graphql/schema/types/gallery.graphql b/graphql/schema/types/gallery.graphql index c2526fc6298..4f17a673842 100644 --- a/graphql/schema/types/gallery.graphql +++ b/graphql/schema/types/gallery.graphql @@ -4,7 +4,8 @@ type Gallery { checksum: String! @deprecated(reason: "Use files.fingerprints") path: String @deprecated(reason: "Use files.path") title: String - url: String + url: String @deprecated(reason: "Use urls") + urls: [String!]! date: String details: String # rating expressed as 1-5 @@ -33,7 +34,8 @@ type Gallery { input GalleryCreateInput { title: String! - url: String + url: String @deprecated(reason: "Use urls") + urls: [String!] date: String details: String # rating expressed as 1-5 @@ -51,7 +53,8 @@ input GalleryUpdateInput { clientMutationId: String id: ID! title: String - url: String + url: String @deprecated(reason: "Use urls") + urls: [String!] date: String details: String # rating expressed as 1-5 @@ -70,7 +73,8 @@ input GalleryUpdateInput { input BulkGalleryUpdateInput { clientMutationId: String ids: [ID!] - url: String + url: String @deprecated(reason: "Use urls") + urls: BulkUpdateStrings date: String details: String # rating expressed as 1-5 diff --git a/graphql/schema/types/image.graphql b/graphql/schema/types/image.graphql index 5d13cbdd6e4..f0307b962ae 100644 --- a/graphql/schema/types/image.graphql +++ b/graphql/schema/types/image.graphql @@ -6,7 +6,8 @@ type Image { rating: Int @deprecated(reason: "Use 1-100 range with rating100") # rating expressed as 1-100 rating100: Int - url: String + url: String @deprecated(reason: "Use urls") + urls: [String!]! date: String o_counter: Int organized: Boolean! @@ -48,7 +49,8 @@ input ImageUpdateInput { # rating expressed as 1-100 rating100: Int organized: Boolean - url: String + url: String @deprecated(reason: "Use urls") + urls: [String!] date: String studio_id: ID @@ -68,7 +70,8 @@ input BulkImageUpdateInput { # rating expressed as 1-100 rating100: Int organized: Boolean - url: String + url: String @deprecated(reason: "Use urls") + urls: BulkUpdateStrings date: String studio_id: ID diff --git a/graphql/schema/types/scene.graphql b/graphql/schema/types/scene.graphql index cb0831b0aea..2a8b1ddf5f2 100644 --- a/graphql/schema/types/scene.graphql +++ b/graphql/schema/types/scene.graphql @@ -41,7 +41,7 @@ type Scene { details: String director: String url: String @deprecated(reason: "Use urls") - urls: [String!] + urls: [String!]! date: String # rating expressed as 1-5 rating: Int @deprecated(reason: "Use 1-100 range with rating100") diff --git a/graphql/schema/types/scraper.graphql b/graphql/schema/types/scraper.graphql index 191feca9155..320d6065c00 100644 --- a/graphql/schema/types/scraper.graphql +++ b/graphql/schema/types/scraper.graphql @@ -100,7 +100,8 @@ input ScrapedSceneInput { type ScrapedGallery { title: String details: String - url: String + url: String @deprecated(reason: "use urls") + urls: [String!] date: String studio: ScrapedStudio @@ -111,7 +112,8 @@ type ScrapedGallery { input ScrapedGalleryInput { title: String details: String - url: String + url: String @deprecated(reason: "use urls") + urls: [String!] date: String # no studio, tags or performers diff --git a/graphql/schema/types/tag.graphql b/graphql/schema/types/tag.graphql index 6260856572c..eba9b1996ef 100644 --- a/graphql/schema/types/tag.graphql +++ b/graphql/schema/types/tag.graphql @@ -15,6 +15,9 @@ type Tag { performer_count(depth: Int): Int! # Resolver parents: [Tag!]! children: [Tag!]! + + parent_count: Int! # Resolver + child_count: Int! # Resolver } input TagCreateInput { diff --git a/internal/api/changeset_translator.go b/internal/api/changeset_translator.go index e40b8fe0e48..412f12db99e 100644 --- a/internal/api/changeset_translator.go +++ b/internal/api/changeset_translator.go @@ -7,7 +7,9 @@ import ( "strings" "github.com/99designs/gqlgen/graphql" + "github.com/stashapp/stash/pkg/models" + "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) const updateInputField = "input" @@ -91,7 +93,7 @@ func (t changesetTranslator) getFields() []string { return ret } -func (t changesetTranslator) string(value *string, field string) string { +func (t changesetTranslator) string(value *string) string { if value == nil { return "" } @@ -127,7 +129,7 @@ func (t changesetTranslator) optionalDate(value *string, field string) (models.O return models.NewOptionalDate(date), nil } -func (t changesetTranslator) datePtr(value *string, field string) (*models.Date, error) { +func (t changesetTranslator) datePtr(value *string) (*models.Date, error) { if value == nil || *value == "" { return nil, nil } @@ -139,7 +141,7 @@ func (t changesetTranslator) datePtr(value *string, field string) (*models.Date, return &date, nil } -func (t changesetTranslator) intPtrFromString(value *string, field string) (*int, error) { +func (t changesetTranslator) intPtrFromString(value *string) (*int, error) { if value == nil || *value == "" { return nil, nil } @@ -151,35 +153,35 @@ func (t changesetTranslator) intPtrFromString(value *string, field string) (*int return &vv, nil } -func (t changesetTranslator) ratingConversionInt(legacyValue *int, rating100Value *int) *int { +func (t changesetTranslator) ratingConversion(legacyValue *int, rating100Value *int) *int { const ( legacyField = "rating" rating100Field = "rating100" ) legacyRating := t.optionalInt(legacyValue, legacyField) - if legacyRating.Set && !(legacyRating.Null) { - ret := int(models.Rating5To100(int(legacyRating.Value))) + if legacyRating.Set && !legacyRating.Null { + ret := models.Rating5To100(legacyRating.Value) return &ret } o := t.optionalInt(rating100Value, rating100Field) - if o.Set && !(o.Null) { + if o.Set && !o.Null { return &o.Value } return nil } -func (t changesetTranslator) ratingConversionOptional(legacyValue *int, rating100Value *int) models.OptionalInt { +func (t changesetTranslator) optionalRatingConversion(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))) + if legacyRating.Set && !legacyRating.Null { + legacyRating.Value = models.Rating5To100(legacyRating.Value) return legacyRating } return t.optionalInt(rating100Value, rating100Field) @@ -212,7 +214,7 @@ func (t changesetTranslator) optionalIntFromString(value *string, field string) return models.NewOptionalInt(vv), nil } -func (t changesetTranslator) bool(value *bool, field string) bool { +func (t changesetTranslator) bool(value *bool) bool { if value == nil { return false } @@ -235,3 +237,191 @@ func (t changesetTranslator) optionalFloat64(value *float64, field string) model return models.NewOptionalFloat64Ptr(value) } + +func (t changesetTranslator) fileIDPtrFromString(value *string) (*models.FileID, error) { + if value == nil || *value == "" { + return nil, nil + } + + vv, err := strconv.Atoi(*value) + if err != nil { + return nil, fmt.Errorf("converting %v to int: %w", *value, err) + } + + id := models.FileID(vv) + return &id, nil +} + +func (t changesetTranslator) fileIDSliceFromStringSlice(value []string) ([]models.FileID, error) { + ints, err := stringslice.StringSliceToIntSlice(value) + if err != nil { + return nil, err + } + + fileIDs := make([]models.FileID, len(ints)) + for i, v := range ints { + fileIDs[i] = models.FileID(v) + } + + return fileIDs, nil +} + +func (t changesetTranslator) relatedIds(value []string) (models.RelatedIDs, error) { + ids, err := stringslice.StringSliceToIntSlice(value) + if err != nil { + return models.RelatedIDs{}, err + } + + return models.NewRelatedIDs(ids), nil +} + +func (t changesetTranslator) updateIds(value []string, field string) (*models.UpdateIDs, error) { + if !t.hasField(field) { + return nil, nil + } + + ids, err := stringslice.StringSliceToIntSlice(value) + if err != nil { + return nil, err + } + + return &models.UpdateIDs{ + IDs: ids, + Mode: models.RelationshipUpdateModeSet, + }, nil +} + +func (t changesetTranslator) updateIdsBulk(value *BulkUpdateIds, field string) (*models.UpdateIDs, error) { + if !t.hasField(field) || value == nil { + return nil, nil + } + + ids, err := stringslice.StringSliceToIntSlice(value.Ids) + if err != nil { + return nil, fmt.Errorf("converting ids [%v]: %w", value.Ids, err) + } + + return &models.UpdateIDs{ + IDs: ids, + Mode: value.Mode, + }, nil +} + +func (t changesetTranslator) optionalURLs(value []string, legacyValue *string) *models.UpdateStrings { + const ( + legacyField = "url" + field = "urls" + ) + + // prefer urls over url + if t.hasField(field) { + return t.updateStrings(value, field) + } else if t.hasField(legacyField) { + var valueSlice []string + if legacyValue != nil { + valueSlice = []string{*legacyValue} + } + return t.updateStrings(valueSlice, legacyField) + } + + return nil +} + +func (t changesetTranslator) optionalURLsBulk(value *BulkUpdateStrings, legacyValue *string) *models.UpdateStrings { + const ( + legacyField = "url" + field = "urls" + ) + + // prefer urls over url + if t.hasField("urls") { + return t.updateStringsBulk(value, field) + } else if t.hasField(legacyField) { + var valueSlice []string + if legacyValue != nil { + valueSlice = []string{*legacyValue} + } + return t.updateStrings(valueSlice, legacyField) + } + + return nil +} + +func (t changesetTranslator) updateStrings(value []string, field string) *models.UpdateStrings { + if !t.hasField(field) { + return nil + } + + return &models.UpdateStrings{ + Values: value, + Mode: models.RelationshipUpdateModeSet, + } +} + +func (t changesetTranslator) updateStringsBulk(value *BulkUpdateStrings, field string) *models.UpdateStrings { + if !t.hasField(field) || value == nil { + return nil + } + + return &models.UpdateStrings{ + Values: value.Values, + Mode: value.Mode, + } +} + +func (t changesetTranslator) updateStashIDs(value []models.StashID, field string) *models.UpdateStashIDs { + if !t.hasField(field) { + return nil + } + + return &models.UpdateStashIDs{ + StashIDs: value, + Mode: models.RelationshipUpdateModeSet, + } +} + +func (t changesetTranslator) relatedMovies(value []models.SceneMovieInput) (models.RelatedMovies, error) { + moviesScenes, err := models.MoviesScenesFromInput(value) + if err != nil { + return models.RelatedMovies{}, err + } + + return models.NewRelatedMovies(moviesScenes), nil +} + +func (t changesetTranslator) updateMovieIDs(value []models.SceneMovieInput, field string) (*models.UpdateMovieIDs, error) { + if !t.hasField(field) { + return nil, nil + } + + moviesScenes, err := models.MoviesScenesFromInput(value) + if err != nil { + return nil, err + } + + return &models.UpdateMovieIDs{ + Movies: moviesScenes, + Mode: models.RelationshipUpdateModeSet, + }, nil +} + +func (t changesetTranslator) updateMovieIDsBulk(value *BulkUpdateIds, field string) (*models.UpdateMovieIDs, error) { + if !t.hasField(field) || value == nil { + return nil, nil + } + + ids, err := stringslice.StringSliceToIntSlice(value.Ids) + if err != nil { + return nil, fmt.Errorf("converting ids [%v]: %w", value.Ids, err) + } + + movies := make([]models.MoviesScenes, len(ids)) + for i, id := range ids { + movies[i] = models.MoviesScenes{MovieID: id} + } + + return &models.UpdateMovieIDs{ + Movies: movies, + Mode: value.Mode, + }, nil +} diff --git a/internal/api/resolver_model_gallery.go b/internal/api/resolver_model_gallery.go index e7c0cd6a04c..d979d7b5885 100644 --- a/internal/api/resolver_model_gallery.go +++ b/internal/api/resolver_model_gallery.go @@ -226,3 +226,32 @@ func (r *galleryResolver) Chapters(ctx context.Context, obj *models.Gallery) (re return ret, nil } + +func (r *galleryResolver) URL(ctx context.Context, obj *models.Gallery) (*string, error) { + if !obj.URLs.Loaded() { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + return obj.LoadURLs(ctx, r.repository.Gallery) + }); err != nil { + return nil, err + } + } + + urls := obj.URLs.List() + if len(urls) == 0 { + return nil, nil + } + + return &urls[0], nil +} + +func (r *galleryResolver) Urls(ctx context.Context, obj *models.Gallery) ([]string, error) { + if !obj.URLs.Loaded() { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + return obj.LoadURLs(ctx, r.repository.Gallery) + }); err != nil { + return nil, err + } + } + + return obj.URLs.List(), nil +} diff --git a/internal/api/resolver_model_image.go b/internal/api/resolver_model_image.go index 246ff8b4450..f4e699b7ba6 100644 --- a/internal/api/resolver_model_image.go +++ b/internal/api/resolver_model_image.go @@ -198,3 +198,32 @@ func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List()) return ret, firstError(errs) } + +func (r *imageResolver) URL(ctx context.Context, obj *models.Image) (*string, error) { + if !obj.URLs.Loaded() { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + return obj.LoadURLs(ctx, r.repository.Image) + }); err != nil { + return nil, err + } + } + + urls := obj.URLs.List() + if len(urls) == 0 { + return nil, nil + } + + return &urls[0], nil +} + +func (r *imageResolver) Urls(ctx context.Context, obj *models.Image) ([]string, error) { + if !obj.URLs.Loaded() { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + return obj.LoadURLs(ctx, r.repository.Image) + }); err != nil { + return nil, err + } + } + + return obj.URLs.List(), nil +} diff --git a/internal/api/resolver_model_scene.go b/internal/api/resolver_model_scene.go index 27ccaf33b85..2593555472f 100644 --- a/internal/api/resolver_model_scene.go +++ b/internal/api/resolver_model_scene.go @@ -275,16 +275,6 @@ func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret return ret, firstError(errs) } -func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID { - ret := make([]*models.StashID, len(v)) - for i, vv := range v { - c := vv - ret[i] = &c - } - - return ret -} - func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) { if err := r.withReadTxn(ctx, func(ctx context.Context) error { return obj.LoadStashIDs(ctx, r.repository.Scene) diff --git a/internal/api/resolver_model_tag.go b/internal/api/resolver_model_tag.go index 778dc7fa623..9124b18f483 100644 --- a/internal/api/resolver_model_tag.go +++ b/internal/api/resolver_model_tag.go @@ -113,3 +113,25 @@ func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj).GetTagImageURL(hasImage) return &imagePath, nil } + +func (r *tagResolver) ParentCount(ctx context.Context, obj *models.Tag) (ret int, err error) { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.Tag.CountByParentTagID(ctx, obj.ID) + return err + }); err != nil { + return ret, err + } + + return ret, nil +} + +func (r *tagResolver) ChildCount(ctx context.Context, obj *models.Tag) (ret int, err error) { + if err := r.withReadTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.Tag.CountByChildTagID(ctx, obj.ID) + return err + }); err != nil { + return ret, err + } + + return ret, nil +} diff --git a/internal/api/resolver_mutation_file.go b/internal/api/resolver_mutation_file.go index 2fcf66fcf19..e8fecef80a8 100644 --- a/internal/api/resolver_mutation_file.go +++ b/internal/api/resolver_mutation_file.go @@ -26,7 +26,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) fileIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { - return fmt.Errorf("converting file ids: %w", err) + return fmt.Errorf("converting ids: %w", err) } switch { @@ -35,7 +35,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) folderID, err := strconv.Atoi(*input.DestinationFolderID) if err != nil { - return fmt.Errorf("invalid folder id %s: %w", *input.DestinationFolderID, err) + return fmt.Errorf("converting destination folder id: %w", err) } folder, err = folderStore.Find(ctx, models.FolderID(folderID)) @@ -146,7 +146,7 @@ func (r *mutationResolver) validateFileExtensionList(exts []string, oldBasename, func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret bool, err error) { fileIDs, err := stringslice.StringSliceToIntSlice(ids) if err != nil { - return false, err + return false, fmt.Errorf("converting ids: %w", err) } fileDeleter := file.NewDeleter() diff --git a/internal/api/resolver_mutation_gallery.go b/internal/api/resolver_mutation_gallery.go index ebdb94e647c..0f246d857e8 100644 --- a/internal/api/resolver_mutation_gallery.go +++ b/internal/api/resolver_mutation_gallery.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "strconv" - "time" "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/pkg/file" @@ -18,6 +17,7 @@ import ( "github.com/stashapp/stash/pkg/utils" ) +// used to refetch gallery after hooks run func (r *mutationResolver) getGallery(ctx context.Context, id int) (ret *models.Gallery, err error) { if err := r.withTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Gallery.Find(ctx, id) @@ -39,40 +39,41 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat inputMap: getUpdateInputMap(ctx), } - performerIDs, err := stringslice.StringSliceToIntSlice(input.PerformerIds) + // Populate a new gallery from the input + newGallery := models.NewGallery() + + newGallery.Title = input.Title + newGallery.Details = translator.string(input.Details) + newGallery.Rating = translator.ratingConversion(input.Rating, input.Rating100) + + var err error + + newGallery.Date, err = translator.datePtr(input.Date) + if err != nil { + return nil, fmt.Errorf("converting date: %w", err) + } + newGallery.StudioID, err = translator.intPtrFromString(input.StudioID) + if err != nil { + return nil, fmt.Errorf("converting studio id: %w", err) + } + + newGallery.PerformerIDs, err = translator.relatedIds(input.PerformerIds) if err != nil { return nil, fmt.Errorf("converting performer ids: %w", err) } - tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds) + newGallery.TagIDs, err = translator.relatedIds(input.TagIds) if err != nil { return nil, fmt.Errorf("converting tag ids: %w", err) } - sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds) + newGallery.SceneIDs, err = translator.relatedIds(input.SceneIds) if err != nil { return nil, fmt.Errorf("converting scene ids: %w", err) } - // Populate a new gallery from the input - currentTime := time.Now() - newGallery := models.Gallery{ - Title: input.Title, - URL: translator.string(input.URL, "url"), - Details: translator.string(input.Details, "details"), - Rating: translator.ratingConversionInt(input.Rating, input.Rating100), - PerformerIDs: models.NewRelatedIDs(performerIDs), - TagIDs: models.NewRelatedIDs(tagIDs), - SceneIDs: models.NewRelatedIDs(sceneIDs), - CreatedAt: currentTime, - UpdatedAt: currentTime, - } - - newGallery.Date, err = translator.datePtr(input.Date, "date") - if err != nil { - return nil, fmt.Errorf("converting date: %w", err) - } - newGallery.StudioID, err = translator.intPtrFromString(input.StudioID, "studio_id") - if err != nil { - return nil, fmt.Errorf("converting studio id: %w", err) + if input.Urls != nil { + newGallery.URLs = models.NewRelatedStrings(input.Urls) + } else if input.URL != nil { + newGallery.URLs = models.NewRelatedStrings([]string{*input.URL}) } // Start the transaction and save the gallery @@ -140,6 +141,7 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models. } r.hookExecutor.ExecutePostHooks(ctx, gallery.ID, plugin.GalleryUpdatePost, input, translator.getFields()) + gallery, err = r.getGallery(ctx, gallery.ID) if err != nil { return nil, err @@ -154,7 +156,7 @@ func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models. func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.GalleryUpdateInput, translator changesetTranslator) (*models.Gallery, error) { galleryID, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } qb := r.repository.Gallery @@ -181,26 +183,26 @@ 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.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) + updatedGallery.Organized = translator.optionalBool(input.Organized, "organized") + updatedGallery.Date, err = translator.optionalDate(input.Date, "date") if err != nil { return nil, fmt.Errorf("converting date: %w", err) } - 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) } - updatedGallery.Organized = translator.optionalBool(input.Organized, "organized") - if input.PrimaryFileID != nil { - primaryFileID, err := strconv.Atoi(*input.PrimaryFileID) - if err != nil { - return nil, fmt.Errorf("converting primary file id: %w", err) - } + updatedGallery.URLs = translator.optionalURLs(input.Urls, input.URL) - converted := models.FileID(primaryFileID) - updatedGallery.PrimaryFileID = &converted + updatedGallery.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID) + if err != nil { + return nil, fmt.Errorf("converting primary file id: %w", err) + } + if updatedGallery.PrimaryFileID != nil { + primaryFileID := *updatedGallery.PrimaryFileID if err := originalGallery.LoadFiles(ctx, r.repository.Gallery); err != nil { return nil, err @@ -209,35 +211,27 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle // ensure that new primary file is associated with gallery var f models.File for _, ff := range originalGallery.Files.List() { - if ff.Base().ID == converted { + if ff.Base().ID == primaryFileID { f = ff } } if f == nil { - return nil, fmt.Errorf("file with id %d not associated with gallery", converted) + return nil, fmt.Errorf("file with id %d not associated with gallery", primaryFileID) } } - if translator.hasField("performer_ids") { - updatedGallery.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet) - if err != nil { - return nil, fmt.Errorf("converting performer ids: %w", err) - } + updatedGallery.PerformerIDs, err = translator.updateIds(input.PerformerIds, "performer_ids") + if err != nil { + return nil, fmt.Errorf("converting performer ids: %w", err) } - - if translator.hasField("tag_ids") { - updatedGallery.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet) - if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) - } + updatedGallery.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) } - - if translator.hasField("scene_ids") { - updatedGallery.SceneIDs, err = translateUpdateIDs(input.SceneIds, models.RelationshipUpdateModeSet) - if err != nil { - return nil, fmt.Errorf("converting scene ids: %w", err) - } + updatedGallery.SceneIDs, err = translator.updateIds(input.SceneIds, "scene_ids") + if err != nil { + return nil, fmt.Errorf("converting scene ids: %w", err) } // gallery scene is set from the scene only @@ -253,7 +247,7 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGalleryUpdateInput) ([]*models.Gallery, error) { galleryIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { - return nil, err + return nil, fmt.Errorf("converting ids: %w", err) } translator := changesetTranslator{ @@ -264,37 +258,30 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall updatedGallery := models.NewGalleryPartial() updatedGallery.Details = translator.optionalString(input.Details, "details") - updatedGallery.URL = translator.optionalString(input.URL, "url") + updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) + updatedGallery.Organized = translator.optionalBool(input.Organized, "organized") + updatedGallery.URLs = translator.optionalURLsBulk(input.Urls, input.URL) + updatedGallery.Date, err = translator.optionalDate(input.Date, "date") if err != nil { return nil, fmt.Errorf("converting date: %w", err) } - 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) } - updatedGallery.Organized = translator.optionalBool(input.Organized, "organized") - if translator.hasField("performer_ids") { - updatedGallery.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting performer ids: %w", err) - } + updatedGallery.PerformerIDs, err = translator.updateIdsBulk(input.PerformerIds, "performer_ids") + if err != nil { + return nil, fmt.Errorf("converting performer ids: %w", err) } - - if translator.hasField("tag_ids") { - updatedGallery.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) - } + updatedGallery.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) } - - if translator.hasField("scene_ids") { - updatedGallery.SceneIDs, err = translateUpdateIDs(input.SceneIds.Ids, input.SceneIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting scene ids: %w", err) - } + updatedGallery.SceneIDs, err = translator.updateIdsBulk(input.SceneIds, "scene_ids") + if err != nil { + return nil, fmt.Errorf("converting scene ids: %w", err) } ret := []*models.Gallery{} @@ -336,7 +323,7 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) { galleryIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { - return false, err + return false, fmt.Errorf("converting ids: %w", err) } var galleries []*models.Gallery @@ -427,12 +414,12 @@ func isStashPath(path string) bool { func (r *mutationResolver) AddGalleryImages(ctx context.Context, input GalleryAddInput) (bool, error) { galleryID, err := strconv.Atoi(input.GalleryID) if err != nil { - return false, err + return false, fmt.Errorf("converting gallery id: %w", err) } imageIDs, err := stringslice.StringSliceToIntSlice(input.ImageIds) if err != nil { - return false, err + return false, fmt.Errorf("converting image ids: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -457,12 +444,12 @@ func (r *mutationResolver) AddGalleryImages(ctx context.Context, input GalleryAd func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input GalleryRemoveInput) (bool, error) { galleryID, err := strconv.Atoi(input.GalleryID) if err != nil { - return false, err + return false, fmt.Errorf("converting gallery id: %w", err) } imageIDs, err := stringslice.StringSliceToIntSlice(input.ImageIds) if err != nil { - return false, err + return false, fmt.Errorf("converting image ids: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -501,14 +488,12 @@ func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input Galle return nil, fmt.Errorf("converting gallery id: %w", err) } - currentTime := time.Now() - newChapter := models.GalleryChapter{ - Title: input.Title, - ImageIndex: input.ImageIndex, - GalleryID: galleryID, - CreatedAt: currentTime, - UpdatedAt: currentTime, - } + // Populate a new gallery chapter from the input + newChapter := models.NewGalleryChapter() + + newChapter.Title = input.Title + newChapter.ImageIndex = input.ImageIndex + newChapter.GalleryID = galleryID // Start the transaction and save the gallery chapter if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -534,7 +519,7 @@ func (r *mutationResolver) GalleryChapterCreate(ctx context.Context, input Galle func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input GalleryChapterUpdateInput) (*models.GalleryChapter, error) { chapterID, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } translator := changesetTranslator{ @@ -600,7 +585,7 @@ func (r *mutationResolver) GalleryChapterUpdate(ctx context.Context, input Galle func (r *mutationResolver) GalleryChapterDestroy(ctx context.Context, id string) (bool, error) { chapterID, err := strconv.Atoi(id) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { diff --git a/internal/api/resolver_mutation_image.go b/internal/api/resolver_mutation_image.go index 6ea58e211f3..8b2cf447831 100644 --- a/internal/api/resolver_mutation_image.go +++ b/internal/api/resolver_mutation_image.go @@ -15,6 +15,7 @@ import ( "github.com/stashapp/stash/pkg/utils" ) +// used to refetch image after hooks run func (r *mutationResolver) getImage(ctx context.Context, id int) (ret *models.Image, err error) { if err := r.withTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Image.Find(ctx, id) @@ -75,6 +76,7 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdat } r.hookExecutor.ExecutePostHooks(ctx, image.ID, plugin.ImageUpdatePost, input, translator.getFields()) + image, err = r.getImage(ctx, image.ID) if err != nil { return nil, err @@ -89,7 +91,7 @@ func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*ImageUpdat func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInput, translator changesetTranslator) (*models.Image, error) { imageID, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } i, err := r.repository.Image.Find(ctx, imageID) @@ -105,8 +107,9 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp updatedImage := models.NewImagePartial() updatedImage.Title = translator.optionalString(input.Title, "title") - updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) - updatedImage.URL = translator.optionalString(input.URL, "url") + updatedImage.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) + updatedImage.Organized = translator.optionalBool(input.Organized, "organized") + updatedImage.Date, err = translator.optionalDate(input.Date, "date") if err != nil { return nil, fmt.Errorf("converting date: %w", err) @@ -115,16 +118,15 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) } - updatedImage.Organized = translator.optionalBool(input.Organized, "organized") - if input.PrimaryFileID != nil { - primaryFileID, err := strconv.Atoi(*input.PrimaryFileID) - if err != nil { - return nil, fmt.Errorf("converting primary file id: %w", err) - } + updatedImage.URLs = translator.optionalURLs(input.Urls, input.URL) - converted := models.FileID(primaryFileID) - updatedImage.PrimaryFileID = &converted + updatedImage.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID) + if err != nil { + return nil, fmt.Errorf("converting primary file id: %w", err) + } + if updatedImage.PrimaryFileID != nil { + primaryFileID := *updatedImage.PrimaryFileID if err := i.LoadFiles(ctx, r.repository.Image); err != nil { return nil, err @@ -133,24 +135,23 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp // ensure that new primary file is associated with image var f models.File for _, ff := range i.Files.List() { - if ff.Base().ID == converted { + if ff.Base().ID == primaryFileID { f = ff } } if f == nil { - return nil, fmt.Errorf("file with id %d not associated with image", converted) + return nil, fmt.Errorf("file with id %d not associated with image", primaryFileID) } } var updatedGalleryIDs []int - if translator.hasField("gallery_ids") { - updatedImage.GalleryIDs, err = translateUpdateIDs(input.GalleryIds, models.RelationshipUpdateModeSet) - if err != nil { - return nil, fmt.Errorf("converting gallery ids: %w", err) - } - + updatedImage.GalleryIDs, err = translator.updateIds(input.GalleryIds, "gallery_ids") + if err != nil { + return nil, fmt.Errorf("converting gallery ids: %w", err) + } + if updatedImage.GalleryIDs != nil { // ensure gallery IDs are loaded if err := i.LoadGalleryIDs(ctx, r.repository.Image); err != nil { return nil, err @@ -163,18 +164,13 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp updatedGalleryIDs = updatedImage.GalleryIDs.ImpactedIDs(i.GalleryIDs.List()) } - if translator.hasField("performer_ids") { - updatedImage.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet) - if err != nil { - return nil, fmt.Errorf("converting performer ids: %w", err) - } + updatedImage.PerformerIDs, err = translator.updateIds(input.PerformerIds, "performer_ids") + if err != nil { + return nil, fmt.Errorf("converting performer ids: %w", err) } - - if translator.hasField("tag_ids") { - updatedImage.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet) - if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) - } + updatedImage.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) } qb := r.repository.Image @@ -196,7 +192,7 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageUpdateInput) (ret []*models.Image, err error) { imageIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { - return nil, err + return nil, fmt.Errorf("converting ids: %w", err) } translator := changesetTranslator{ @@ -207,8 +203,9 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU updatedImage := models.NewImagePartial() updatedImage.Title = translator.optionalString(input.Title, "title") - updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) - updatedImage.URL = translator.optionalString(input.URL, "url") + updatedImage.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) + updatedImage.Organized = translator.optionalBool(input.Organized, "organized") + updatedImage.Date, err = translator.optionalDate(input.Date, "date") if err != nil { return nil, fmt.Errorf("converting date: %w", err) @@ -217,27 +214,20 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) } - updatedImage.Organized = translator.optionalBool(input.Organized, "organized") - if translator.hasField("gallery_ids") { - updatedImage.GalleryIDs, err = translateUpdateIDs(input.GalleryIds.Ids, input.GalleryIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting gallery ids: %w", err) - } - } + updatedImage.URLs = translator.optionalURLsBulk(input.Urls, input.URL) - if translator.hasField("performer_ids") { - updatedImage.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting performer ids: %w", err) - } + updatedImage.GalleryIDs, err = translator.updateIdsBulk(input.GalleryIds, "gallery_ids") + if err != nil { + return nil, fmt.Errorf("converting gallery ids: %w", err) } - - if translator.hasField("tag_ids") { - updatedImage.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) - } + updatedImage.PerformerIDs, err = translator.updateIdsBulk(input.PerformerIds, "performer_ids") + if err != nil { + return nil, fmt.Errorf("converting performer ids: %w", err) + } + updatedImage.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) } // Start the transaction and save the images @@ -308,7 +298,7 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageDestroyInput) (ret bool, err error) { imageID, err := strconv.Atoi(input.ID) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } var i *models.Image @@ -348,7 +338,7 @@ func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageD func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.ImagesDestroyInput) (ret bool, err error) { imageIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { - return false, err + return false, fmt.Errorf("converting ids: %w", err) } var images []*models.Image @@ -400,7 +390,7 @@ func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.Image func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (ret int, err error) { imageID, err := strconv.Atoi(id) if err != nil { - return 0, err + return 0, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -418,7 +408,7 @@ func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (ret func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (ret int, err error) { imageID, err := strconv.Atoi(id) if err != nil { - return 0, err + return 0, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -436,7 +426,7 @@ func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (ret func (r *mutationResolver) ImageResetO(ctx context.Context, id string) (ret int, err error) { imageID, err := strconv.Atoi(id) if err != nil { - return 0, err + return 0, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { diff --git a/internal/api/resolver_mutation_job.go b/internal/api/resolver_mutation_job.go index 5417468339d..74ced81d5f7 100644 --- a/internal/api/resolver_mutation_job.go +++ b/internal/api/resolver_mutation_job.go @@ -2,17 +2,18 @@ package api import ( "context" + "fmt" "strconv" "github.com/stashapp/stash/internal/manager" ) func (r *mutationResolver) StopJob(ctx context.Context, jobID string) (bool, error) { - idInt, err := strconv.Atoi(jobID) + id, err := strconv.Atoi(jobID) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } - manager.GetInstance().JobManager.CancelJob(idInt) + manager.GetInstance().JobManager.CancelJob(id) return true, nil } diff --git a/internal/api/resolver_mutation_movie.go b/internal/api/resolver_mutation_movie.go index b06d84a7f90..ef2d2405afe 100644 --- a/internal/api/resolver_mutation_movie.go +++ b/internal/api/resolver_mutation_movie.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "strconv" - "time" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/plugin" @@ -12,6 +11,7 @@ import ( "github.com/stashapp/stash/pkg/utils" ) +// used to refetch movie after hooks run func (r *mutationResolver) getMovie(ctx context.Context, id int) (ret *models.Movie, err error) { if err := r.withTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Movie.Find(ctx, id) @@ -29,26 +29,23 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp } // Populate a new movie from the input - currentTime := time.Now() - newMovie := models.Movie{ - Name: input.Name, - CreatedAt: currentTime, - UpdatedAt: currentTime, - Aliases: translator.string(input.Aliases, "aliases"), - Duration: input.Duration, - Rating: translator.ratingConversionInt(input.Rating, input.Rating100), - Director: translator.string(input.Director, "director"), - Synopsis: translator.string(input.Synopsis, "synopsis"), - URL: translator.string(input.URL, "url"), - } + newMovie := models.NewMovie() + + newMovie.Name = input.Name + newMovie.Aliases = translator.string(input.Aliases) + newMovie.Duration = input.Duration + newMovie.Rating = translator.ratingConversion(input.Rating, input.Rating100) + newMovie.Director = translator.string(input.Director) + newMovie.Synopsis = translator.string(input.Synopsis) + newMovie.URL = translator.string(input.URL) var err error - newMovie.Date, err = translator.datePtr(input.Date, "date") + newMovie.Date, err = translator.datePtr(input.Date) if err != nil { return nil, fmt.Errorf("converting date: %w", err) } - newMovie.StudioID, err = translator.intPtrFromString(input.StudioID, "studio_id") + newMovie.StudioID, err = translator.intPtrFromString(input.StudioID) if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) } @@ -64,7 +61,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp if input.FrontImage != nil { frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage) if err != nil { - return nil, err + return nil, fmt.Errorf("processing front image: %w", err) } } @@ -73,7 +70,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp if input.BackImage != nil { backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage) if err != nil { - return nil, err + return nil, fmt.Errorf("processing back image: %w", err) } } @@ -111,7 +108,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInput) (*models.Movie, error) { movieID, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } translator := changesetTranslator{ @@ -124,14 +121,15 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp updatedMovie.Name = translator.optionalString(input.Name, "name") updatedMovie.Aliases = translator.optionalString(input.Aliases, "aliases") updatedMovie.Duration = translator.optionalInt(input.Duration, "duration") + updatedMovie.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) + updatedMovie.Director = translator.optionalString(input.Director, "director") + updatedMovie.Synopsis = translator.optionalString(input.Synopsis, "synopsis") + updatedMovie.URL = translator.optionalString(input.URL, "url") + updatedMovie.Date, err = translator.optionalDate(input.Date, "date") if err != nil { return nil, fmt.Errorf("converting date: %w", err) } - updatedMovie.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) - updatedMovie.Director = translator.optionalString(input.Director, "director") - updatedMovie.Synopsis = translator.optionalString(input.Synopsis, "synopsis") - updatedMovie.URL = translator.optionalString(input.URL, "url") updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) @@ -142,7 +140,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp if input.FrontImage != nil { frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage) if err != nil { - return nil, err + return nil, fmt.Errorf("processing front image: %w", err) } } @@ -151,7 +149,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp if input.BackImage != nil { backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage) if err != nil { - return nil, err + return nil, fmt.Errorf("processing back image: %w", err) } } @@ -189,18 +187,19 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieUpdateInput) ([]*models.Movie, error) { movieIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { - return nil, err + return nil, fmt.Errorf("converting ids: %w", err) } translator := changesetTranslator{ inputMap: getUpdateInputMap(ctx), } - // populate movie from the input + // Populate movie from the input updatedMovie := models.NewMoviePartial() - updatedMovie.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) + updatedMovie.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) updatedMovie.Director = translator.optionalString(input.Director, "director") + updatedMovie.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) @@ -243,7 +242,7 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyInput) (bool, error) { id, err := strconv.Atoi(input.ID) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -260,7 +259,7 @@ func (r *mutationResolver) MovieDestroy(ctx context.Context, input MovieDestroyI func (r *mutationResolver) MoviesDestroy(ctx context.Context, movieIDs []string) (bool, error) { ids, err := stringslice.StringSliceToIntSlice(movieIDs) if err != nil { - return false, err + return false, fmt.Errorf("converting ids: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { diff --git a/internal/api/resolver_mutation_performer.go b/internal/api/resolver_mutation_performer.go index dbdfe2160fc..9e40e7a01bf 100644 --- a/internal/api/resolver_mutation_performer.go +++ b/internal/api/resolver_mutation_performer.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "strconv" - "time" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/performer" @@ -13,6 +12,7 @@ import ( "github.com/stashapp/stash/pkg/utils" ) +// used to refetch performer after hooks run func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *models.Performer, err error) { if err := r.withTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Performer.Find(ctx, id) @@ -24,62 +24,45 @@ func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *model return ret, nil } -func stashIDPtrSliceToSlice(v []*models.StashID) []models.StashID { - ret := make([]models.StashID, len(v)) - for i, vv := range v { - c := vv - ret[i] = *c - } - - return ret -} - -func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerCreateInput) (*models.Performer, error) { +func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.PerformerCreateInput) (*models.Performer, error) { translator := changesetTranslator{ inputMap: getUpdateInputMap(ctx), } - tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds) - if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) - } - // Populate a new performer from the input - currentTime := time.Now() - newPerformer := models.Performer{ - Name: input.Name, - Disambiguation: translator.string(input.Disambiguation, "disambiguation"), - URL: translator.string(input.URL, "url"), - Gender: input.Gender, - Ethnicity: translator.string(input.Ethnicity, "ethnicity"), - Country: translator.string(input.Country, "country"), - EyeColor: translator.string(input.EyeColor, "eye_color"), - Measurements: translator.string(input.Measurements, "measurements"), - FakeTits: translator.string(input.FakeTits, "fake_tits"), - PenisLength: input.PenisLength, - Circumcised: input.Circumcised, - CareerLength: translator.string(input.CareerLength, "career_length"), - Tattoos: translator.string(input.Tattoos, "tattoos"), - Piercings: translator.string(input.Piercings, "piercings"), - Twitter: translator.string(input.Twitter, "twitter"), - Instagram: translator.string(input.Instagram, "instagram"), - Favorite: translator.bool(input.Favorite, "favorite"), - Rating: translator.ratingConversionInt(input.Rating, input.Rating100), - Details: translator.string(input.Details, "details"), - HairColor: translator.string(input.HairColor, "hair_color"), - Weight: input.Weight, - IgnoreAutoTag: translator.bool(input.IgnoreAutoTag, "ignore_auto_tag"), - CreatedAt: currentTime, - UpdatedAt: currentTime, - TagIDs: models.NewRelatedIDs(tagIDs), - StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)), - } - - newPerformer.Birthdate, err = translator.datePtr(input.Birthdate, "birthdate") + newPerformer := models.NewPerformer() + + newPerformer.Name = input.Name + newPerformer.Disambiguation = translator.string(input.Disambiguation) + newPerformer.URL = translator.string(input.URL) + newPerformer.Gender = input.Gender + newPerformer.Ethnicity = translator.string(input.Ethnicity) + newPerformer.Country = translator.string(input.Country) + newPerformer.EyeColor = translator.string(input.EyeColor) + newPerformer.Measurements = translator.string(input.Measurements) + newPerformer.FakeTits = translator.string(input.FakeTits) + newPerformer.PenisLength = input.PenisLength + newPerformer.Circumcised = input.Circumcised + newPerformer.CareerLength = translator.string(input.CareerLength) + newPerformer.Tattoos = translator.string(input.Tattoos) + newPerformer.Piercings = translator.string(input.Piercings) + newPerformer.Twitter = translator.string(input.Twitter) + newPerformer.Instagram = translator.string(input.Instagram) + newPerformer.Favorite = translator.bool(input.Favorite) + newPerformer.Rating = translator.ratingConversion(input.Rating, input.Rating100) + newPerformer.Details = translator.string(input.Details) + newPerformer.HairColor = translator.string(input.HairColor) + newPerformer.Weight = input.Weight + newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) + newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds) + + var err error + + newPerformer.Birthdate, err = translator.datePtr(input.Birthdate) if err != nil { return nil, fmt.Errorf("converting birthdate: %w", err) } - newPerformer.DeathDate, err = translator.datePtr(input.DeathDate, "death_date") + newPerformer.DeathDate, err = translator.datePtr(input.DeathDate) if err != nil { return nil, fmt.Errorf("converting death date: %w", err) } @@ -88,18 +71,24 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC if input.HeightCm != nil { newPerformer.Height = input.HeightCm } else { - newPerformer.Height, err = translator.intPtrFromString(input.Height, "height") + newPerformer.Height, err = translator.intPtrFromString(input.Height) if err != nil { return nil, fmt.Errorf("converting height: %w", err) } } + // prefer alias_list over aliases if input.AliasList != nil { newPerformer.Aliases = models.NewRelatedStrings(input.AliasList) } else if input.Aliases != nil { newPerformer.Aliases = models.NewRelatedStrings(stringslice.FromString(*input.Aliases, ",")) } + newPerformer.TagIDs, err = translator.relatedIds(input.TagIds) + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + if err := performer.ValidateDeathDate(nil, input.Birthdate, input.DeathDate); err != nil { if err != nil { return nil, err @@ -111,7 +100,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC if input.Image != nil { imageData, err = utils.ProcessImageInput(ctx, *input.Image) if err != nil { - return nil, err + return nil, fmt.Errorf("processing image: %w", err) } } @@ -140,42 +129,27 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC return r.getPerformer(ctx, newPerformer.ID) } -func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerUpdateInput) (*models.Performer, error) { +func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) { performerID, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } - // Populate performer from the input translator := changesetTranslator{ inputMap: getUpdateInputMap(ctx), } + // Populate performer from the input updatedPerformer := models.NewPerformerPartial() updatedPerformer.Name = translator.optionalString(input.Name, "name") updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation") updatedPerformer.URL = translator.optionalString(input.URL, "url") updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender") - updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate") - if err != nil { - return nil, fmt.Errorf("converting birthdate: %w", err) - } updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity") updatedPerformer.Country = translator.optionalString(input.Country, "country") updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color") updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements") - - // prefer height_cm over height - if translator.hasField("height_cm") { - updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm") - } else if translator.hasField("height") { - updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height") - if err != nil { - return nil, err - } - } - updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits") updatedPerformer.PenisLength = translator.optionalFloat64(input.PenisLength, "penis_length") updatedPerformer.Circumcised = translator.optionalString((*string)(input.Circumcised), "circumcised") @@ -185,45 +159,46 @@ 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.ratingConversionOptional(input.Rating, input.Rating100) + updatedPerformer.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) updatedPerformer.Details = translator.optionalString(input.Details, "details") - updatedPerformer.DeathDate, err = translator.optionalDate(input.DeathDate, "death_date") - if err != nil { - return nil, fmt.Errorf("converting death date: %w", err) - } updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color") updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight") updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") + updatedPerformer.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids") - if translator.hasField("alias_list") { - updatedPerformer.Aliases = &models.UpdateStrings{ - Values: input.AliasList, - Mode: models.RelationshipUpdateModeSet, - } - } else if translator.hasField("aliases") { - var values []string - if input.Aliases != nil { - values = stringslice.FromString(*input.Aliases, ",") - } - updatedPerformer.Aliases = &models.UpdateStrings{ - Values: values, - Mode: models.RelationshipUpdateModeSet, - } + updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate") + if err != nil { + return nil, fmt.Errorf("converting birthdate: %w", err) + } + updatedPerformer.DeathDate, err = translator.optionalDate(input.DeathDate, "death_date") + if err != nil { + return nil, fmt.Errorf("converting death date: %w", err) } - if translator.hasField("tag_ids") { - updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet) + // prefer height_cm over height + if translator.hasField("height_cm") { + updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm") + } else if translator.hasField("height") { + updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height") if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) + return nil, fmt.Errorf("converting height: %w", err) } } - // Save the stash_ids - if translator.hasField("stash_ids") { - updatedPerformer.StashIDs = &models.UpdateStashIDs{ - StashIDs: stashIDPtrSliceToSlice(input.StashIds), - Mode: models.RelationshipUpdateModeSet, + // prefer alias_list over aliases + if translator.hasField("alias_list") { + updatedPerformer.Aliases = translator.updateStrings(input.AliasList, "alias_list") + } else if translator.hasField("aliases") { + var aliasList []string + if input.Aliases != nil { + aliasList = stringslice.FromString(*input.Aliases, ",") } + updatedPerformer.Aliases = translator.updateStrings(aliasList, "aliases") + } + + updatedPerformer.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) } var imageData []byte @@ -231,7 +206,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU if input.Image != nil { imageData, err = utils.ProcessImageInput(ctx, *input.Image) if err != nil { - return nil, err + return nil, fmt.Errorf("processing image: %w", err) } } @@ -250,9 +225,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU } if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil { - if err != nil { - return err - } + return err } _, err = qb.UpdatePartial(ctx, performerID, updatedPerformer) @@ -279,37 +252,22 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPerformerUpdateInput) ([]*models.Performer, error) { performerIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { - return nil, err + return nil, fmt.Errorf("converting ids: %w", err) } - // Populate performer from the input translator := changesetTranslator{ inputMap: getUpdateInputMap(ctx), } + // Populate performer from the input updatedPerformer := models.NewPerformerPartial() updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation") updatedPerformer.URL = translator.optionalString(input.URL, "url") updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender") - updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate") - if err != nil { - return nil, fmt.Errorf("converting birthdate: %w", err) - } updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity") updatedPerformer.Country = translator.optionalString(input.Country, "country") updatedPerformer.EyeColor = translator.optionalString(input.EyeColor, "eye_color") - - // prefer height_cm over height - if translator.hasField("height_cm") { - updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm") - } else if translator.hasField("height") { - updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height") - if err != nil { - return nil, err - } - } - updatedPerformer.Measurements = translator.optionalString(input.Measurements, "measurements") updatedPerformer.FakeTits = translator.optionalString(input.FakeTits, "fake_tits") updatedPerformer.PenisLength = translator.optionalFloat64(input.PenisLength, "penis_length") @@ -320,37 +278,45 @@ 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.ratingConversionOptional(input.Rating, input.Rating100) + updatedPerformer.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) updatedPerformer.Details = translator.optionalString(input.Details, "details") + updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color") + updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight") + updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") + + updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate") + if err != nil { + return nil, fmt.Errorf("converting birthdate: %w", err) + } updatedPerformer.DeathDate, err = translator.optionalDate(input.DeathDate, "death_date") if err != nil { return nil, fmt.Errorf("converting death date: %w", err) } - updatedPerformer.HairColor = translator.optionalString(input.HairColor, "hair_color") - updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight") - updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") - if translator.hasField("alias_list") { - updatedPerformer.Aliases = &models.UpdateStrings{ - Values: input.AliasList.Values, - Mode: input.AliasList.Mode, + // prefer height_cm over height + if translator.hasField("height_cm") { + updatedPerformer.Height = translator.optionalInt(input.HeightCm, "height_cm") + } else if translator.hasField("height") { + updatedPerformer.Height, err = translator.optionalIntFromString(input.Height, "height") + if err != nil { + return nil, fmt.Errorf("converting height: %w", err) } + } + + // prefer alias_list over aliases + if translator.hasField("alias_list") { + updatedPerformer.Aliases = translator.updateStringsBulk(input.AliasList, "alias_list") } else if translator.hasField("aliases") { - var values []string + var aliasList []string if input.Aliases != nil { - values = stringslice.FromString(*input.Aliases, ",") - } - updatedPerformer.Aliases = &models.UpdateStrings{ - Values: values, - Mode: models.RelationshipUpdateModeSet, + aliasList = stringslice.FromString(*input.Aliases, ",") } + updatedPerformer.Aliases = translator.updateStrings(aliasList, "aliases") } - if translator.hasField("tag_ids") { - updatedPerformer.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) - } + updatedPerformer.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) } ret := []*models.Performer{} @@ -370,7 +336,8 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe return fmt.Errorf("performer with id %d not found", performerID) } - if err := performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate); err != nil { + err = performer.ValidateDeathDate(existing, input.Birthdate, input.DeathDate) + if err != nil { return err } @@ -406,7 +373,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe func (r *mutationResolver) PerformerDestroy(ctx context.Context, input PerformerDestroyInput) (bool, error) { id, err := strconv.Atoi(input.ID) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -423,7 +390,7 @@ func (r *mutationResolver) PerformerDestroy(ctx context.Context, input Performer func (r *mutationResolver) PerformersDestroy(ctx context.Context, performerIDs []string) (bool, error) { ids, err := stringslice.StringSliceToIntSlice(performerIDs) if err != nil { - return false, err + return false, fmt.Errorf("converting ids: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { diff --git a/internal/api/resolver_mutation_saved_filter.go b/internal/api/resolver_mutation_saved_filter.go index 89062227068..13b5d87fafa 100644 --- a/internal/api/resolver_mutation_saved_filter.go +++ b/internal/api/resolver_mutation_saved_filter.go @@ -3,6 +3,7 @@ package api import ( "context" "errors" + "fmt" "strconv" "strings" @@ -18,7 +19,7 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput if input.ID != nil { idv, err := strconv.Atoi(*input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } id = &idv } @@ -53,7 +54,7 @@ func (r *mutationResolver) SaveFilter(ctx context.Context, input SaveFilterInput func (r *mutationResolver) DestroySavedFilter(ctx context.Context, input DestroyFilterInput) (bool, error) { id, err := strconv.Atoi(input.ID) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { diff --git a/internal/api/resolver_mutation_scene.go b/internal/api/resolver_mutation_scene.go index 4c338586c5e..37a9be9c844 100644 --- a/internal/api/resolver_mutation_scene.go +++ b/internal/api/resolver_mutation_scene.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "strconv" - "time" "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/pkg/file" @@ -17,6 +16,7 @@ import ( "github.com/stashapp/stash/pkg/utils" ) +// used to refetch scene after hooks run func (r *mutationResolver) getScene(ctx context.Context, id int) (ret *models.Scene, err error) { if err := r.withTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Scene.Find(ctx, id) @@ -28,59 +28,32 @@ func (r *mutationResolver) getScene(ctx context.Context, id int) (ret *models.Sc return ret, nil } -func (r *mutationResolver) SceneCreate(ctx context.Context, input SceneCreateInput) (ret *models.Scene, err error) { +func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCreateInput) (ret *models.Scene, err error) { translator := changesetTranslator{ inputMap: getUpdateInputMap(ctx), } - performerIDs, err := stringslice.StringSliceToIntSlice(input.PerformerIds) - if err != nil { - return nil, fmt.Errorf("converting performer ids: %w", err) - } - tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds) - if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) - } - galleryIDs, err := stringslice.StringSliceToIntSlice(input.GalleryIds) - if err != nil { - return nil, fmt.Errorf("converting gallery ids: %w", err) - } - - moviesScenes, err := models.MoviesScenesFromInput(input.Movies) - if err != nil { - return nil, fmt.Errorf("converting movies scenes: %w", err) - } - - fileIDsInt, err := stringslice.StringSliceToIntSlice(input.FileIds) + fileIDs, err := translator.fileIDSliceFromStringSlice(input.FileIds) if err != nil { return nil, fmt.Errorf("converting file ids: %w", err) } - fileIDs := make([]models.FileID, len(fileIDsInt)) - for i, v := range fileIDsInt { - fileIDs[i] = models.FileID(v) - } - // Populate a new scene from the input - newScene := models.Scene{ - Title: translator.string(input.Title, "title"), - Code: translator.string(input.Code, "code"), - Details: translator.string(input.Details, "details"), - Director: translator.string(input.Director, "director"), - Rating: translator.ratingConversionInt(input.Rating, input.Rating100), - Organized: translator.bool(input.Organized, "organized"), - PerformerIDs: models.NewRelatedIDs(performerIDs), - TagIDs: models.NewRelatedIDs(tagIDs), - GalleryIDs: models.NewRelatedIDs(galleryIDs), - Movies: models.NewRelatedMovies(moviesScenes), - StashIDs: models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)), - } - - newScene.Date, err = translator.datePtr(input.Date, "date") + newScene := models.NewScene() + + newScene.Title = translator.string(input.Title) + newScene.Code = translator.string(input.Code) + newScene.Details = translator.string(input.Details) + newScene.Director = translator.string(input.Director) + newScene.Rating = translator.ratingConversion(input.Rating, input.Rating100) + newScene.Organized = translator.bool(input.Organized) + newScene.StashIDs = models.NewRelatedStashIDs(input.StashIds) + + newScene.Date, err = translator.datePtr(input.Date) if err != nil { return nil, fmt.Errorf("converting date: %w", err) } - newScene.StudioID, err = translator.intPtrFromString(input.StudioID, "studio_id") + newScene.StudioID, err = translator.intPtrFromString(input.StudioID) if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) } @@ -91,12 +64,30 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input SceneCreateInp newScene.URLs = models.NewRelatedStrings([]string{*input.URL}) } + newScene.PerformerIDs, err = translator.relatedIds(input.PerformerIds) + if err != nil { + return nil, fmt.Errorf("converting performer ids: %w", err) + } + newScene.TagIDs, err = translator.relatedIds(input.TagIds) + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) + } + newScene.GalleryIDs, err = translator.relatedIds(input.GalleryIds) + if err != nil { + return nil, fmt.Errorf("converting gallery ids: %w", err) + } + + newScene.Movies, err = translator.relatedMovies(input.Movies) + if err != nil { + return nil, fmt.Errorf("converting movies: %w", err) + } + var coverImageData []byte if input.CoverImage != nil { var err error coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage) if err != nil { - return nil, err + return nil, fmt.Errorf("processing cover image: %w", err) } } @@ -173,88 +164,51 @@ func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.Sce func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTranslator) (*models.ScenePartial, error) { updatedScene := models.NewScenePartial() - var err error - updatedScene.Title = translator.optionalString(input.Title, "title") updatedScene.Code = translator.optionalString(input.Code, "code") updatedScene.Details = translator.optionalString(input.Details, "details") updatedScene.Director = translator.optionalString(input.Director, "director") + updatedScene.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) + updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter") + updatedScene.PlayCount = translator.optionalInt(input.PlayCount, "play_count") + updatedScene.PlayDuration = translator.optionalFloat64(input.PlayDuration, "play_duration") + updatedScene.Organized = translator.optionalBool(input.Organized, "organized") + updatedScene.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids") + + var err error + updatedScene.Date, err = translator.optionalDate(input.Date, "date") if err != nil { return nil, fmt.Errorf("converting date: %w", err) } - updatedScene.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) - updatedScene.OCounter = translator.optionalInt(input.OCounter, "o_counter") - updatedScene.PlayCount = translator.optionalInt(input.PlayCount, "play_count") - updatedScene.PlayDuration = translator.optionalFloat64(input.PlayDuration, "play_duration") updatedScene.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") if err != nil { return nil, fmt.Errorf("converting studio id: %w", err) } - updatedScene.Organized = translator.optionalBool(input.Organized, "organized") - - if translator.hasField("urls") { - updatedScene.URLs = &models.UpdateStrings{ - Values: input.Urls, - Mode: models.RelationshipUpdateModeSet, - } - } else if translator.hasField("url") { - var values []string - if input.URL != nil { - values = []string{*input.URL} - } - updatedScene.URLs = &models.UpdateStrings{ - Values: values, - Mode: models.RelationshipUpdateModeSet, - } - } + updatedScene.URLs = translator.optionalURLs(input.Urls, input.URL) - if input.PrimaryFileID != nil { - primaryFileID, err := strconv.Atoi(*input.PrimaryFileID) - if err != nil { - return nil, fmt.Errorf("converting primary file id: %w", err) - } - - converted := models.FileID(primaryFileID) - updatedScene.PrimaryFileID = &converted - } - - if translator.hasField("performer_ids") { - updatedScene.PerformerIDs, err = translateUpdateIDs(input.PerformerIds, models.RelationshipUpdateModeSet) - if err != nil { - return nil, fmt.Errorf("converting performer ids: %w", err) - } + updatedScene.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID) + if err != nil { + return nil, fmt.Errorf("converting primary file id: %w", err) } - if translator.hasField("tag_ids") { - updatedScene.TagIDs, err = translateUpdateIDs(input.TagIds, models.RelationshipUpdateModeSet) - if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) - } + updatedScene.PerformerIDs, err = translator.updateIds(input.PerformerIds, "performer_ids") + if err != nil { + return nil, fmt.Errorf("converting performer ids: %w", err) } - - if translator.hasField("gallery_ids") { - updatedScene.GalleryIDs, err = translateUpdateIDs(input.GalleryIds, models.RelationshipUpdateModeSet) - if err != nil { - return nil, fmt.Errorf("converting gallery ids: %w", err) - } + updatedScene.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) } - - // Save the movies - if translator.hasField("movies") { - updatedScene.MovieIDs, err = models.UpdateMovieIDsFromInput(input.Movies) - if err != nil { - return nil, fmt.Errorf("converting movie ids: %w", err) - } + updatedScene.GalleryIDs, err = translator.updateIds(input.GalleryIds, "gallery_ids") + if err != nil { + return nil, fmt.Errorf("converting gallery ids: %w", err) } - // Save the stash_ids - if translator.hasField("stash_ids") { - updatedScene.StashIDs = &models.UpdateStashIDs{ - StashIDs: input.StashIds, - Mode: models.RelationshipUpdateModeSet, - } + updatedScene.MovieIDs, err = translator.updateMovieIDs(input.Movies, "movies") + if err != nil { + return nil, fmt.Errorf("converting movies: %w", err) } return &updatedScene, nil @@ -263,7 +217,7 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUpdateInput, translator changesetTranslator) (*models.Scene, error) { sceneID, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } qb := r.repository.Scene @@ -321,7 +275,7 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp var err error coverImageData, err = utils.ProcessImageInput(ctx, *input.CoverImage) if err != nil { - return nil, err + return nil, fmt.Errorf("processing cover image: %w", err) } } @@ -353,7 +307,7 @@ func (r *mutationResolver) sceneUpdateCoverImage(ctx context.Context, s *models. func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneUpdateInput) ([]*models.Scene, error) { sceneIDs, err := stringslice.StringSliceToIntSlice(input.Ids) if err != nil { - return nil, err + return nil, fmt.Errorf("converting ids: %w", err) } translator := changesetTranslator{ @@ -367,61 +321,36 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU updatedScene.Code = translator.optionalString(input.Code, "code") updatedScene.Details = translator.optionalString(input.Details, "details") updatedScene.Director = translator.optionalString(input.Director, "director") + updatedScene.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) + updatedScene.Organized = translator.optionalBool(input.Organized, "organized") + updatedScene.Date, err = translator.optionalDate(input.Date, "date") if err != nil { return nil, fmt.Errorf("converting date: %w", err) } - 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) } - updatedScene.Organized = translator.optionalBool(input.Organized, "organized") + updatedScene.URLs = translator.optionalURLsBulk(input.Urls, input.URL) - if translator.hasField("urls") { - updatedScene.URLs = &models.UpdateStrings{ - Values: input.Urls.Values, - Mode: input.Urls.Mode, - } - } else if translator.hasField("url") { - var values []string - if input.URL != nil { - values = []string{*input.URL} - } - updatedScene.URLs = &models.UpdateStrings{ - Values: values, - Mode: models.RelationshipUpdateModeSet, - } - } - - if translator.hasField("performer_ids") { - updatedScene.PerformerIDs, err = translateUpdateIDs(input.PerformerIds.Ids, input.PerformerIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting performer ids: %w", err) - } + updatedScene.PerformerIDs, err = translator.updateIdsBulk(input.PerformerIds, "performer_ids") + if err != nil { + return nil, fmt.Errorf("converting performer ids: %w", err) } - - if translator.hasField("tag_ids") { - updatedScene.TagIDs, err = translateUpdateIDs(input.TagIds.Ids, input.TagIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting tag ids: %w", err) - } + updatedScene.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids") + if err != nil { + return nil, fmt.Errorf("converting tag ids: %w", err) } - - if translator.hasField("gallery_ids") { - updatedScene.GalleryIDs, err = translateUpdateIDs(input.GalleryIds.Ids, input.GalleryIds.Mode) - if err != nil { - return nil, fmt.Errorf("converting gallery ids: %w", err) - } + updatedScene.GalleryIDs, err = translator.updateIdsBulk(input.GalleryIds, "gallery_ids") + if err != nil { + return nil, fmt.Errorf("converting gallery ids: %w", err) } - // Save the movies - if translator.hasField("movie_ids") { - updatedScene.MovieIDs, err = translateSceneMovieIDs(*input.MovieIds) - if err != nil { - return nil, fmt.Errorf("converting movie ids: %w", err) - } + updatedScene.MovieIDs, err = translator.updateMovieIDsBulk(input.MovieIds, "movie_ids") + if err != nil { + return nil, fmt.Errorf("converting movie ids: %w", err) } ret := []*models.Scene{} @@ -463,7 +392,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneDestroyInput) (bool, error) { sceneID, err := strconv.Atoi(input.ID) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm() @@ -514,6 +443,11 @@ func (r *mutationResolver) SceneDestroy(ctx context.Context, input models.SceneD } func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.ScenesDestroyInput) (bool, error) { + sceneIDs, err := stringslice.StringSliceToIntSlice(input.Ids) + if err != nil { + return false, fmt.Errorf("converting ids: %w", err) + } + var scenes []*models.Scene fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm() @@ -529,23 +463,21 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Scene - for _, id := range input.Ids { - sceneID, _ := strconv.Atoi(id) - - s, err := qb.Find(ctx, sceneID) + for _, id := range sceneIDs { + scene, err := qb.Find(ctx, id) if err != nil { return err } - if s == nil { - return fmt.Errorf("scene with id %d not found", sceneID) + if scene == nil { + return fmt.Errorf("scene with id %d not found", id) } - scenes = append(scenes, s) + scenes = append(scenes, scene) // kill any running encoders - manager.KillRunningStreams(s, fileNamingAlgo) + manager.KillRunningStreams(scene, fileNamingAlgo) - if err := r.sceneService.Destroy(ctx, s, fileDeleter, deleteGenerated, deleteFile); err != nil { + if err := r.sceneService.Destroy(ctx, scene, fileDeleter, deleteGenerated, deleteFile); err != nil { return err } } @@ -575,18 +507,16 @@ func (r *mutationResolver) ScenesDestroy(ctx context.Context, input models.Scene func (r *mutationResolver) SceneAssignFile(ctx context.Context, input AssignSceneFileInput) (bool, error) { sceneID, err := strconv.Atoi(input.SceneID) if err != nil { - return false, fmt.Errorf("converting scene ID: %w", err) + return false, fmt.Errorf("converting scene id: %w", err) } - fileIDInt, err := strconv.Atoi(input.FileID) + fileID, err := strconv.Atoi(input.FileID) if err != nil { - return false, fmt.Errorf("converting file ID: %w", err) + return false, fmt.Errorf("converting file id: %w", err) } - fileID := models.FileID(fileIDInt) - if err := r.withTxn(ctx, func(ctx context.Context) error { - return r.Resolver.sceneService.AssignFile(ctx, sceneID, fileID) + return r.Resolver.sceneService.AssignFile(ctx, sceneID, models.FileID(fileID)) }); err != nil { return false, fmt.Errorf("assigning file to scene: %w", err) } @@ -597,15 +527,17 @@ func (r *mutationResolver) SceneAssignFile(ctx context.Context, input AssignScen func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput) (*models.Scene, error) { srcIDs, err := stringslice.StringSliceToIntSlice(input.Source) if err != nil { - return nil, fmt.Errorf("converting source IDs: %w", err) + return nil, fmt.Errorf("converting source ids: %w", err) } destID, err := strconv.Atoi(input.Destination) if err != nil { - return nil, fmt.Errorf("converting destination ID %s: %w", input.Destination, err) + return nil, fmt.Errorf("converting destination id: %w", err) } var values *models.ScenePartial + var coverImageData []byte + if input.Values != nil { translator := changesetTranslator{ inputMap: getNamedUpdateInputMap(ctx, "input.values"), @@ -615,20 +547,19 @@ func (r *mutationResolver) SceneMerge(ctx context.Context, input SceneMergeInput if err != nil { return nil, err } + + if input.Values.CoverImage != nil { + var err error + coverImageData, err = utils.ProcessImageInput(ctx, *input.Values.CoverImage) + if err != nil { + return nil, fmt.Errorf("processing cover image: %w", err) + } + } } else { v := models.NewScenePartial() values = &v } - var coverImageData []byte - if input.Values.CoverImage != nil { - var err error - coverImageData, err = utils.ProcessImageInput(ctx, *input.Values.CoverImage) - if err != nil { - return nil, err - } - } - var ret *models.Scene if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.Resolver.sceneService.Merge(ctx, srcIDs, destID, *values); err != nil { @@ -673,15 +604,13 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar return nil, fmt.Errorf("converting primary tag id: %w", err) } - currentTime := time.Now() - newMarker := models.SceneMarker{ - Title: input.Title, - Seconds: input.Seconds, - PrimaryTagID: primaryTagID, - SceneID: sceneID, - CreatedAt: currentTime, - UpdatedAt: currentTime, - } + // Populate a new scene marker from the input + newMarker := models.NewSceneMarker() + + newMarker.Title = input.Title + newMarker.Seconds = input.Seconds + newMarker.PrimaryTagID = primaryTagID + newMarker.SceneID = sceneID tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds) if err != nil { @@ -711,7 +640,7 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) { markerID, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } translator := changesetTranslator{ @@ -809,7 +738,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) { markerID, err := strconv.Atoi(id) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm() @@ -860,7 +789,7 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, resumeTime *float64, playDuration *float64) (ret bool, err error) { sceneID, err := strconv.Atoi(id) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -878,7 +807,7 @@ func (r *mutationResolver) SceneSaveActivity(ctx context.Context, id string, res func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id string) (ret int, err error) { sceneID, err := strconv.Atoi(id) if err != nil { - return 0, err + return 0, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -896,7 +825,7 @@ func (r *mutationResolver) SceneIncrementPlayCount(ctx context.Context, id strin func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret int, err error) { sceneID, err := strconv.Atoi(id) if err != nil { - return 0, err + return 0, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -914,7 +843,7 @@ func (r *mutationResolver) SceneIncrementO(ctx context.Context, id string) (ret func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (ret int, err error) { sceneID, err := strconv.Atoi(id) if err != nil { - return 0, err + return 0, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -932,7 +861,7 @@ func (r *mutationResolver) SceneDecrementO(ctx context.Context, id string) (ret func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (ret int, err error) { sceneID, err := strconv.Atoi(id) if err != nil { - return 0, err + return 0, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { diff --git a/internal/api/resolver_mutation_stash_box.go b/internal/api/resolver_mutation_stash_box.go index cbcfc53401b..2f8593097f9 100644 --- a/internal/api/resolver_mutation_stash_box.go +++ b/internal/api/resolver_mutation_stash_box.go @@ -53,7 +53,7 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S id, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } var res *string @@ -95,7 +95,7 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp id, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } var res *string diff --git a/internal/api/resolver_mutation_studio.go b/internal/api/resolver_mutation_studio.go index 626e0d4f481..db314d26109 100644 --- a/internal/api/resolver_mutation_studio.go +++ b/internal/api/resolver_mutation_studio.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "strconv" - "time" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/plugin" @@ -13,19 +12,48 @@ import ( "github.com/stashapp/stash/pkg/utils" ) -func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateInput) (*models.Studio, error) { - s, err := studioFromStudioCreateInput(ctx, input) - if err != nil { +// used to refetch studio after hooks run +func (r *mutationResolver) getStudio(ctx context.Context, id int) (ret *models.Studio, err error) { + if err := r.withTxn(ctx, func(ctx context.Context) error { + ret, err = r.repository.Studio.Find(ctx, id) + return err + }); err != nil { return nil, err } + return ret, nil +} + +func (r *mutationResolver) StudioCreate(ctx context.Context, input models.StudioCreateInput) (*models.Studio, error) { + translator := changesetTranslator{ + inputMap: getUpdateInputMap(ctx), + } + + // Populate a new studio from the input + newStudio := models.NewStudio() + + newStudio.Name = input.Name + newStudio.URL = translator.string(input.URL) + newStudio.Rating = translator.ratingConversion(input.Rating, input.Rating100) + newStudio.Details = translator.string(input.Details) + newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) + newStudio.Aliases = models.NewRelatedStrings(input.Aliases) + newStudio.StashIDs = models.NewRelatedStashIDs(input.StashIds) + + var err error + + newStudio.ParentID, err = translator.intPtrFromString(input.ParentID) + if err != nil { + return nil, fmt.Errorf("converting parent id: %w", err) + } + // Process the base 64 encoded image string var imageData []byte if input.Image != nil { var err error imageData, err = utils.ProcessImageInput(ctx, *input.Image) if err != nil { - return nil, err + return nil, fmt.Errorf("processing image: %w", err) } } @@ -33,19 +61,19 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateI if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Studio - if s.Aliases.Loaded() && len(s.Aliases.List()) > 0 { - if err := studio.EnsureAliasesUnique(ctx, 0, s.Aliases.List(), qb); err != nil { + if len(input.Aliases) > 0 { + if err := studio.EnsureAliasesUnique(ctx, 0, input.Aliases, qb); err != nil { return err } } - err = qb.Create(ctx, s) + err = qb.Create(ctx, &newStudio) if err != nil { return err } if len(imageData) > 0 { - if err := qb.UpdateImage(ctx, s.ID, imageData); err != nil { + if err := qb.UpdateImage(ctx, newStudio.ID, imageData); err != nil { return err } } @@ -55,53 +83,37 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateI return nil, err } - r.hookExecutor.ExecutePostHooks(ctx, s.ID, plugin.StudioCreatePost, input, nil) - - return s, nil + r.hookExecutor.ExecutePostHooks(ctx, newStudio.ID, plugin.StudioCreatePost, input, nil) + return r.getStudio(ctx, newStudio.ID) } -func studioFromStudioCreateInput(ctx context.Context, input StudioCreateInput) (*models.Studio, error) { - translator := changesetTranslator{ - inputMap: getUpdateInputMap(ctx), +func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.StudioUpdateInput) (*models.Studio, error) { + studioID, err := strconv.Atoi(input.ID) + if err != nil { + return nil, fmt.Errorf("converting id: %w", err) } - // Populate a new studio from the input - currentTime := time.Now() - newStudio := models.Studio{ - Name: input.Name, - CreatedAt: currentTime, - UpdatedAt: currentTime, - URL: translator.string(input.URL, "url"), - Rating: translator.ratingConversionInt(input.Rating, input.Rating100), - Details: translator.string(input.Details, "details"), - IgnoreAutoTag: translator.bool(input.IgnoreAutoTag, "ignore_auto_tag"), + translator := changesetTranslator{ + inputMap: getUpdateInputMap(ctx), } - var err error - newStudio.ParentID, err = translator.intPtrFromString(input.ParentID, "parent_id") + // Populate studio from the input + updatedStudio := models.NewStudioPartial() + + updatedStudio.ID = studioID + updatedStudio.Name = translator.optionalString(input.Name, "name") + updatedStudio.URL = translator.optionalString(input.URL, "url") + updatedStudio.Details = translator.optionalString(input.Details, "details") + updatedStudio.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) + updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") + updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases") + updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids") + + updatedStudio.ParentID, err = translator.optionalIntFromString(input.ParentID, "parent_id") if err != nil { return nil, fmt.Errorf("converting parent id: %w", err) } - if input.Aliases != nil { - newStudio.Aliases = models.NewRelatedStrings(input.Aliases) - } - if input.StashIds != nil { - newStudio.StashIDs = models.NewRelatedStashIDs(stashIDPtrSliceToSlice(input.StashIds)) - } - - return &newStudio, nil -} - -func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateInput) (*models.Studio, error) { - var updatedStudio *models.Studio - var err error - - translator := changesetTranslator{ - inputMap: getNamedUpdateInputMap(ctx, updateInputField), - } - s := studioPartialFromStudioUpdateInput(input, &input.ID, translator) - // Process the base 64 encoded image string var imageData []byte imageIncluded := translator.hasField("image") @@ -109,7 +121,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI var err error imageData, err = utils.ProcessImageInput(ctx, *input.Image) if err != nil { - return nil, err + return nil, fmt.Errorf("processing image: %w", err) } } @@ -117,17 +129,17 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI if err := r.withTxn(ctx, func(ctx context.Context) error { qb := r.repository.Studio - if err := studio.ValidateModify(ctx, *s, qb); err != nil { + if err := studio.ValidateModify(ctx, updatedStudio, qb); err != nil { return err } - updatedStudio, err = qb.UpdatePartial(ctx, *s) + _, err = qb.UpdatePartial(ctx, updatedStudio) if err != nil { return err } if imageIncluded { - if err := qb.UpdateImage(ctx, s.ID, imageData); err != nil { + if err := qb.UpdateImage(ctx, studioID, imageData); err != nil { return err } } @@ -137,57 +149,14 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI return nil, err } - r.hookExecutor.ExecutePostHooks(ctx, updatedStudio.ID, plugin.StudioUpdatePost, input, translator.getFields()) - - return updatedStudio, nil -} - -// This is slightly different to studioPartialFromStudioCreateInput in that Name is handled differently -// and ImageIncluded is not hardcoded to true -func studioPartialFromStudioUpdateInput(input StudioUpdateInput, id *string, translator changesetTranslator) *models.StudioPartial { - // Populate studio from the input - updatedStudio := models.StudioPartial{ - Name: translator.optionalString(input.Name, "name"), - URL: translator.optionalString(input.URL, "url"), - Details: translator.optionalString(input.Details, "details"), - Rating: translator.ratingConversionOptional(input.Rating, input.Rating100), - IgnoreAutoTag: translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag"), - UpdatedAt: models.NewOptionalTime(time.Now()), - } - - updatedStudio.ID, _ = strconv.Atoi(*id) - - if input.ParentID != nil { - parentID, _ := strconv.Atoi(*input.ParentID) - if parentID > 0 { - // This is to be set directly as we know it has a value and the translator won't have the field - updatedStudio.ParentID = models.NewOptionalInt(parentID) - } - } else { - updatedStudio.ParentID = translator.optionalInt(nil, "parent_id") - } - - if translator.hasField("aliases") { - updatedStudio.Aliases = &models.UpdateStrings{ - Values: input.Aliases, - Mode: models.RelationshipUpdateModeSet, - } - } - - if translator.hasField("stash_ids") { - updatedStudio.StashIDs = &models.UpdateStashIDs{ - StashIDs: stashIDPtrSliceToSlice(input.StashIds), - Mode: models.RelationshipUpdateModeSet, - } - } - - return &updatedStudio + r.hookExecutor.ExecutePostHooks(ctx, studioID, plugin.StudioUpdatePost, input, translator.getFields()) + return r.getStudio(ctx, studioID) } func (r *mutationResolver) StudioDestroy(ctx context.Context, input StudioDestroyInput) (bool, error) { id, err := strconv.Atoi(input.ID) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -204,7 +173,7 @@ func (r *mutationResolver) StudioDestroy(ctx context.Context, input StudioDestro func (r *mutationResolver) StudiosDestroy(ctx context.Context, studioIDs []string) (bool, error) { ids, err := stringslice.StringSliceToIntSlice(studioIDs) if err != nil { - return false, err + return false, fmt.Errorf("converting ids: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { diff --git a/internal/api/resolver_mutation_tag.go b/internal/api/resolver_mutation_tag.go index 51c9fa7ab26..cec4a77726c 100644 --- a/internal/api/resolver_mutation_tag.go +++ b/internal/api/resolver_mutation_tag.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "strconv" - "time" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" @@ -31,14 +30,11 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput) } // Populate a new tag from the input - currentTime := time.Now() - newTag := models.Tag{ - Name: input.Name, - CreatedAt: currentTime, - UpdatedAt: currentTime, - Description: translator.string(input.Description, "description"), - IgnoreAutoTag: translator.bool(input.IgnoreAutoTag, "ignore_auto_tag"), - } + newTag := models.NewTag() + + newTag.Name = input.Name + newTag.Description = translator.string(input.Description) + newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) var err error @@ -46,7 +42,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput) if len(input.ParentIds) > 0 { parentIDs, err = stringslice.StringSliceToIntSlice(input.ParentIds) if err != nil { - return nil, err + return nil, fmt.Errorf("converting parent ids: %w", err) } } @@ -54,7 +50,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput) if len(input.ChildIds) > 0 { childIDs, err = stringslice.StringSliceToIntSlice(input.ChildIds) if err != nil { - return nil, err + return nil, fmt.Errorf("converting child ids: %w", err) } } @@ -63,7 +59,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput) if input.Image != nil { imageData, err = utils.ProcessImageInput(ctx, *input.Image) if err != nil { - return nil, err + return nil, fmt.Errorf("processing image: %w", err) } } @@ -130,7 +126,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput) func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) (*models.Tag, error) { tagID, err := strconv.Atoi(input.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("converting id: %w", err) } translator := changesetTranslator{ @@ -147,7 +143,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) if translator.hasField("parent_ids") { parentIDs, err = stringslice.StringSliceToIntSlice(input.ParentIds) if err != nil { - return nil, err + return nil, fmt.Errorf("converting parent ids: %w", err) } } @@ -155,7 +151,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) if translator.hasField("child_ids") { childIDs, err = stringslice.StringSliceToIntSlice(input.ChildIds) if err != nil { - return nil, err + return nil, fmt.Errorf("converting child ids: %w", err) } } @@ -164,7 +160,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) if input.Image != nil { imageData, err = utils.ProcessImageInput(ctx, *input.Image) if err != nil { - return nil, err + return nil, fmt.Errorf("processing image: %w", err) } } @@ -246,7 +242,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput) func (r *mutationResolver) TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error) { tagID, err := strconv.Atoi(input.ID) if err != nil { - return false, err + return false, fmt.Errorf("converting id: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -263,7 +259,7 @@ func (r *mutationResolver) TagDestroy(ctx context.Context, input TagDestroyInput func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bool, error) { ids, err := stringslice.StringSliceToIntSlice(tagIDs) if err != nil { - return false, err + return false, fmt.Errorf("converting ids: %w", err) } if err := r.withTxn(ctx, func(ctx context.Context) error { @@ -289,12 +285,12 @@ func (r *mutationResolver) TagsDestroy(ctx context.Context, tagIDs []string) (bo func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput) (*models.Tag, error) { source, err := stringslice.StringSliceToIntSlice(input.Source) if err != nil { - return nil, err + return nil, fmt.Errorf("converting source ids: %w", err) } destination, err := strconv.Atoi(input.Destination) if err != nil { - return nil, err + return nil, fmt.Errorf("converting destination id: %w", err) } if len(source) == 0 { @@ -345,5 +341,6 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input TagsMergeInput) } r.hookExecutor.ExecutePostHooks(ctx, t.ID, plugin.TagMergePost, input, nil) + return t, nil } diff --git a/internal/api/types.go b/internal/api/types.go index 79b4aa02002..372c094b8b4 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -1,11 +1,9 @@ package api import ( - "fmt" "math" "github.com/stashapp/stash/pkg/models" - "github.com/stashapp/stash/pkg/sliceutil/stringslice" ) // #1572 - Inf and NaN values cause the JSON marshaller to fail @@ -18,32 +16,12 @@ func handleFloat64(v float64) *float64 { return &v } -func translateUpdateIDs(strIDs []string, mode models.RelationshipUpdateMode) (*models.UpdateIDs, error) { - ids, err := stringslice.StringSliceToIntSlice(strIDs) - if err != nil { - return nil, fmt.Errorf("converting ids [%v]: %w", strIDs, err) - } - return &models.UpdateIDs{ - IDs: ids, - Mode: mode, - }, nil -} - -func translateSceneMovieIDs(input BulkUpdateIds) (*models.UpdateMovieIDs, error) { - ids, err := stringslice.StringSliceToIntSlice(input.Ids) - if err != nil { - return nil, fmt.Errorf("converting ids [%v]: %w", input.Ids, err) - } - - ret := &models.UpdateMovieIDs{ - Mode: input.Mode, - } - - for _, id := range ids { - ret.Movies = append(ret.Movies, models.MoviesScenes{ - MovieID: id, - }) +func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID { + ret := make([]*models.StashID, len(v)) + for i, vv := range v { + c := vv + ret[i] = &c } - return ret, nil + return ret } diff --git a/internal/autotag/gallery_test.go b/internal/autotag/gallery_test.go index b617791abea..23c3d931ee6 100644 --- a/internal/autotag/gallery_test.go +++ b/internal/autotag/gallery_test.go @@ -14,6 +14,19 @@ const galleryExt = "zip" var testCtx = context.Background() +// returns got == expected +// ignores expected.UpdatedAt, but ensures that got.UpdatedAt is set and not null +func galleryPartialsEqual(got, expected models.GalleryPartial) bool { + // updated at should be set and not null + if !got.UpdatedAt.Set || got.UpdatedAt.Null { + return false + } + // else ignore the exact value + got.UpdatedAt = models.OptionalTime{} + + return assert.ObjectsAreEqual(got, expected) +} + func TestGalleryPerformers(t *testing.T) { t.Parallel() @@ -46,12 +59,17 @@ func TestGalleryPerformers(t *testing.T) { mockPerformerReader.On("QueryForAutoTag", testCtx, mock.Anything).Return([]*models.Performer{&performer, &reversedPerformer}, nil).Once() if test.Matches { - mockGalleryReader.On("UpdatePartial", testCtx, galleryID, models.GalleryPartial{ - PerformerIDs: &models.UpdateIDs{ - IDs: []int{performerID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.GalleryPartial) bool { + expected := models.GalleryPartial{ + PerformerIDs: &models.UpdateIDs{ + IDs: []int{performerID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return galleryPartialsEqual(got, expected) + }) + mockGalleryReader.On("UpdatePartial", testCtx, galleryID, matchPartial).Return(nil, nil).Once() } gallery := models.Gallery{ @@ -91,10 +109,14 @@ func TestGalleryStudios(t *testing.T) { doTest := func(mockStudioReader *mocks.StudioReaderWriter, mockGalleryReader *mocks.GalleryReaderWriter, test pathTestTable) { if test.Matches { - expectedStudioID := studioID - mockGalleryReader.On("UpdatePartial", testCtx, galleryID, models.GalleryPartial{ - StudioID: models.NewOptionalInt(expectedStudioID), - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.GalleryPartial) bool { + expected := models.GalleryPartial{ + StudioID: models.NewOptionalInt(studioID), + } + + return galleryPartialsEqual(got, expected) + }) + mockGalleryReader.On("UpdatePartial", testCtx, galleryID, matchPartial).Return(nil, nil).Once() } gallery := models.Gallery{ @@ -162,12 +184,17 @@ func TestGalleryTags(t *testing.T) { doTest := func(mockTagReader *mocks.TagReaderWriter, mockGalleryReader *mocks.GalleryReaderWriter, test pathTestTable) { if test.Matches { - mockGalleryReader.On("UpdatePartial", testCtx, galleryID, models.GalleryPartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{tagID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.GalleryPartial) bool { + expected := models.GalleryPartial{ + TagIDs: &models.UpdateIDs{ + IDs: []int{tagID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return galleryPartialsEqual(got, expected) + }) + mockGalleryReader.On("UpdatePartial", testCtx, galleryID, matchPartial).Return(nil, nil).Once() } gallery := models.Gallery{ diff --git a/internal/autotag/image_test.go b/internal/autotag/image_test.go index 3ced047f7e2..06991beea1f 100644 --- a/internal/autotag/image_test.go +++ b/internal/autotag/image_test.go @@ -11,6 +11,19 @@ import ( const imageExt = "jpg" +// returns got == expected +// ignores expected.UpdatedAt, but ensures that got.UpdatedAt is set and not null +func imagePartialsEqual(got, expected models.ImagePartial) bool { + // updated at should be set and not null + if !got.UpdatedAt.Set || got.UpdatedAt.Null { + return false + } + // else ignore the exact value + got.UpdatedAt = models.OptionalTime{} + + return assert.ObjectsAreEqual(got, expected) +} + func TestImagePerformers(t *testing.T) { t.Parallel() @@ -43,12 +56,17 @@ func TestImagePerformers(t *testing.T) { mockPerformerReader.On("QueryForAutoTag", testCtx, mock.Anything).Return([]*models.Performer{&performer, &reversedPerformer}, nil).Once() if test.Matches { - mockImageReader.On("UpdatePartial", testCtx, imageID, models.ImagePartial{ - PerformerIDs: &models.UpdateIDs{ - IDs: []int{performerID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.ImagePartial) bool { + expected := models.ImagePartial{ + PerformerIDs: &models.UpdateIDs{ + IDs: []int{performerID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return imagePartialsEqual(got, expected) + }) + mockImageReader.On("UpdatePartial", testCtx, imageID, matchPartial).Return(nil, nil).Once() } image := models.Image{ @@ -88,10 +106,14 @@ func TestImageStudios(t *testing.T) { doTest := func(mockStudioReader *mocks.StudioReaderWriter, mockImageReader *mocks.ImageReaderWriter, test pathTestTable) { if test.Matches { - expectedStudioID := studioID - mockImageReader.On("UpdatePartial", testCtx, imageID, models.ImagePartial{ - StudioID: models.NewOptionalInt(expectedStudioID), - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.ImagePartial) bool { + expected := models.ImagePartial{ + StudioID: models.NewOptionalInt(studioID), + } + + return imagePartialsEqual(got, expected) + }) + mockImageReader.On("UpdatePartial", testCtx, imageID, matchPartial).Return(nil, nil).Once() } image := models.Image{ @@ -159,12 +181,17 @@ func TestImageTags(t *testing.T) { doTest := func(mockTagReader *mocks.TagReaderWriter, mockImageReader *mocks.ImageReaderWriter, test pathTestTable) { if test.Matches { - mockImageReader.On("UpdatePartial", testCtx, imageID, models.ImagePartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{tagID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.ImagePartial) bool { + expected := models.ImagePartial{ + TagIDs: &models.UpdateIDs{ + IDs: []int{tagID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return imagePartialsEqual(got, expected) + }) + mockImageReader.On("UpdatePartial", testCtx, imageID, matchPartial).Return(nil, nil).Once() } image := models.Image{ diff --git a/internal/autotag/integration_test.go b/internal/autotag/integration_test.go index 774a7738bab..84ae016987c 100644 --- a/internal/autotag/integration_test.go +++ b/internal/autotag/integration_test.go @@ -362,10 +362,7 @@ func makeImage(expectedResult bool) *models.Image { } func createImage(ctx context.Context, w models.ImageWriter, o *models.Image, f *models.ImageFile) error { - err := w.Create(ctx, &models.ImageCreateInput{ - Image: o, - FileIDs: []models.FileID{f.ID}, - }) + err := w.Create(ctx, o, []models.FileID{f.ID}) if err != nil { return fmt.Errorf("Failed to create image with path '%s': %s", f.Path, err.Error()) diff --git a/internal/autotag/performer_test.go b/internal/autotag/performer_test.go index 5f7b12c228d..aa0a43d92f8 100644 --- a/internal/autotag/performer_test.go +++ b/internal/autotag/performer_test.go @@ -89,12 +89,18 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) { for i := range matchingPaths { sceneID := i + 1 - mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, models.ScenePartial{ - PerformerIDs: &models.UpdateIDs{ - IDs: []int{performerID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + + matchPartial := mock.MatchedBy(func(got models.ScenePartial) bool { + expected := models.ScenePartial{ + PerformerIDs: &models.UpdateIDs{ + IDs: []int{performerID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return scenePartialsEqual(got, expected) + }) + mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, matchPartial).Return(nil, nil).Once() } tagger := Tagger{ @@ -178,12 +184,18 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) { for i := range matchingPaths { imageID := i + 1 - mockImageReader.On("UpdatePartial", mock.Anything, imageID, models.ImagePartial{ - PerformerIDs: &models.UpdateIDs{ - IDs: []int{performerID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + + matchPartial := mock.MatchedBy(func(got models.ImagePartial) bool { + expected := models.ImagePartial{ + PerformerIDs: &models.UpdateIDs{ + IDs: []int{performerID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return imagePartialsEqual(got, expected) + }) + mockImageReader.On("UpdatePartial", mock.Anything, imageID, matchPartial).Return(nil, nil).Once() } tagger := Tagger{ @@ -267,12 +279,18 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) { for i := range matchingPaths { galleryID := i + 1 - mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, models.GalleryPartial{ - PerformerIDs: &models.UpdateIDs{ - IDs: []int{performerID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + + matchPartial := mock.MatchedBy(func(got models.GalleryPartial) bool { + expected := models.GalleryPartial{ + PerformerIDs: &models.UpdateIDs{ + IDs: []int{performerID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return galleryPartialsEqual(got, expected) + }) + mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, matchPartial).Return(nil, nil).Once() } tagger := Tagger{ diff --git a/internal/autotag/scene_test.go b/internal/autotag/scene_test.go index 19ae15c9cce..a714c364c41 100644 --- a/internal/autotag/scene_test.go +++ b/internal/autotag/scene_test.go @@ -29,6 +29,19 @@ var testEndSeparators = []string{ ",", } +// asserts that got == expected +// ignores expected.UpdatedAt, but ensures that got.UpdatedAt is set and not null +func scenePartialsEqual(got, expected models.ScenePartial) bool { + // updated at should be set and not null + if !got.UpdatedAt.Set || got.UpdatedAt.Null { + return false + } + // else ignore the exact value + got.UpdatedAt = models.OptionalTime{} + + return assert.ObjectsAreEqual(got, expected) +} + func generateNamePatterns(name, separator, ext string) []string { var ret []string ret = append(ret, fmt.Sprintf("%s%saaa.%s", name, separator, ext)) @@ -182,12 +195,17 @@ func TestScenePerformers(t *testing.T) { } if test.Matches { - mockSceneReader.On("UpdatePartial", testCtx, sceneID, models.ScenePartial{ - PerformerIDs: &models.UpdateIDs{ - IDs: []int{performerID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.ScenePartial) bool { + expected := models.ScenePartial{ + PerformerIDs: &models.UpdateIDs{ + IDs: []int{performerID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return scenePartialsEqual(got, expected) + }) + mockSceneReader.On("UpdatePartial", testCtx, sceneID, matchPartial).Return(nil, nil).Once() } err := ScenePerformers(testCtx, &scene, mockSceneReader, mockPerformerReader, nil) @@ -224,10 +242,14 @@ func TestSceneStudios(t *testing.T) { doTest := func(mockStudioReader *mocks.StudioReaderWriter, mockSceneReader *mocks.SceneReaderWriter, test pathTestTable) { if test.Matches { - expectedStudioID := studioID - mockSceneReader.On("UpdatePartial", testCtx, sceneID, models.ScenePartial{ - StudioID: models.NewOptionalInt(expectedStudioID), - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.ScenePartial) bool { + expected := models.ScenePartial{ + StudioID: models.NewOptionalInt(studioID), + } + + return scenePartialsEqual(got, expected) + }) + mockSceneReader.On("UpdatePartial", testCtx, sceneID, matchPartial).Return(nil, nil).Once() } scene := models.Scene{ @@ -295,12 +317,17 @@ func TestSceneTags(t *testing.T) { doTest := func(mockTagReader *mocks.TagReaderWriter, mockSceneReader *mocks.SceneReaderWriter, test pathTestTable) { if test.Matches { - mockSceneReader.On("UpdatePartial", testCtx, sceneID, models.ScenePartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{tagID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.ScenePartial) bool { + expected := models.ScenePartial{ + TagIDs: &models.UpdateIDs{ + IDs: []int{tagID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return scenePartialsEqual(got, expected) + }) + mockSceneReader.On("UpdatePartial", testCtx, sceneID, matchPartial).Return(nil, nil).Once() } scene := models.Scene{ diff --git a/internal/autotag/studio.go b/internal/autotag/studio.go index ef5a6f0da9c..8312e0edf61 100644 --- a/internal/autotag/studio.go +++ b/internal/autotag/studio.go @@ -18,9 +18,8 @@ func addSceneStudio(ctx context.Context, sceneWriter models.SceneUpdater, o *mod } // set the studio id - scenePartial := models.ScenePartial{ - StudioID: models.NewOptionalInt(studioID), - } + scenePartial := models.NewScenePartial() + scenePartial.StudioID = models.NewOptionalInt(studioID) if _, err := sceneWriter.UpdatePartial(ctx, o.ID, scenePartial); err != nil { return false, err @@ -35,9 +34,8 @@ func addImageStudio(ctx context.Context, imageWriter models.ImageUpdater, i *mod } // set the studio id - imagePartial := models.ImagePartial{ - StudioID: models.NewOptionalInt(studioID), - } + imagePartial := models.NewImagePartial() + imagePartial.StudioID = models.NewOptionalInt(studioID) if _, err := imageWriter.UpdatePartial(ctx, i.ID, imagePartial); err != nil { return false, err @@ -52,9 +50,8 @@ func addGalleryStudio(ctx context.Context, galleryWriter GalleryFinderUpdater, o } // set the studio id - galleryPartial := models.GalleryPartial{ - StudioID: models.NewOptionalInt(studioID), - } + galleryPartial := models.NewGalleryPartial() + galleryPartial.StudioID = models.NewOptionalInt(studioID) if _, err := galleryWriter.UpdatePartial(ctx, o.ID, galleryPartial); err != nil { return false, err @@ -93,9 +90,8 @@ func (tagger *Tagger) StudioScenes(ctx context.Context, p *models.Studio, paths } // set the studio id - scenePartial := models.ScenePartial{ - StudioID: models.NewOptionalInt(p.ID), - } + scenePartial := models.NewScenePartial() + scenePartial.StudioID = models.NewOptionalInt(p.ID) if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error { _, err := rw.UpdatePartial(ctx, o.ID, scenePartial) @@ -124,9 +120,8 @@ func (tagger *Tagger) StudioImages(ctx context.Context, p *models.Studio, paths } // set the studio id - imagePartial := models.ImagePartial{ - StudioID: models.NewOptionalInt(p.ID), - } + imagePartial := models.NewImagePartial() + imagePartial.StudioID = models.NewOptionalInt(p.ID) if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error { _, err := rw.UpdatePartial(ctx, i.ID, imagePartial) @@ -155,9 +150,8 @@ func (tagger *Tagger) StudioGalleries(ctx context.Context, p *models.Studio, pat } // set the studio id - galleryPartial := models.GalleryPartial{ - StudioID: models.NewOptionalInt(p.ID), - } + galleryPartial := models.NewGalleryPartial() + galleryPartial.StudioID = models.NewOptionalInt(p.ID) if err := txn.WithTxn(ctx, tagger.TxnManager, func(ctx context.Context) error { _, err := rw.UpdatePartial(ctx, o.ID, galleryPartial) diff --git a/internal/autotag/studio_test.go b/internal/autotag/studio_test.go index 3e9eae5f5fb..aa52c9c5179 100644 --- a/internal/autotag/studio_test.go +++ b/internal/autotag/studio_test.go @@ -151,10 +151,15 @@ func testStudioScenes(t *testing.T, tc testStudioCase) { for i := range matchingPaths { sceneID := i + 1 - expectedStudioID := studioID - mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, models.ScenePartial{ - StudioID: models.NewOptionalInt(expectedStudioID), - }).Return(nil, nil).Once() + + matchPartial := mock.MatchedBy(func(got models.ScenePartial) bool { + expected := models.ScenePartial{ + StudioID: models.NewOptionalInt(studioID), + } + + return scenePartialsEqual(got, expected) + }) + mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, matchPartial).Return(nil, nil).Once() } tagger := Tagger{ @@ -249,10 +254,15 @@ func testStudioImages(t *testing.T, tc testStudioCase) { for i := range matchingPaths { imageID := i + 1 - expectedStudioID := studioID - mockImageReader.On("UpdatePartial", mock.Anything, imageID, models.ImagePartial{ - StudioID: models.NewOptionalInt(expectedStudioID), - }).Return(nil, nil).Once() + + matchPartial := mock.MatchedBy(func(got models.ImagePartial) bool { + expected := models.ImagePartial{ + StudioID: models.NewOptionalInt(studioID), + } + + return imagePartialsEqual(got, expected) + }) + mockImageReader.On("UpdatePartial", mock.Anything, imageID, matchPartial).Return(nil, nil).Once() } tagger := Tagger{ @@ -346,10 +356,15 @@ func testStudioGalleries(t *testing.T, tc testStudioCase) { for i := range matchingPaths { galleryID := i + 1 - expectedStudioID := studioID - mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, models.GalleryPartial{ - StudioID: models.NewOptionalInt(expectedStudioID), - }).Return(nil, nil).Once() + + matchPartial := mock.MatchedBy(func(got models.GalleryPartial) bool { + expected := models.GalleryPartial{ + StudioID: models.NewOptionalInt(studioID), + } + + return galleryPartialsEqual(got, expected) + }) + mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, matchPartial).Return(nil, nil).Once() } tagger := Tagger{ diff --git a/internal/autotag/tag_test.go b/internal/autotag/tag_test.go index 04f10875c2e..4b183200490 100644 --- a/internal/autotag/tag_test.go +++ b/internal/autotag/tag_test.go @@ -151,12 +151,18 @@ func testTagScenes(t *testing.T, tc testTagCase) { for i := range matchingPaths { sceneID := i + 1 - mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, models.ScenePartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{tagID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + + matchPartial := mock.MatchedBy(func(got models.ScenePartial) bool { + expected := models.ScenePartial{ + TagIDs: &models.UpdateIDs{ + IDs: []int{tagID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return scenePartialsEqual(got, expected) + }) + mockSceneReader.On("UpdatePartial", mock.Anything, sceneID, matchPartial).Return(nil, nil).Once() } tagger := Tagger{ @@ -253,12 +259,17 @@ func testTagImages(t *testing.T, tc testTagCase) { for i := range matchingPaths { imageID := i + 1 - mockImageReader.On("UpdatePartial", mock.Anything, imageID, models.ImagePartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{tagID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.ImagePartial) bool { + expected := models.ImagePartial{ + TagIDs: &models.UpdateIDs{ + IDs: []int{tagID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return imagePartialsEqual(got, expected) + }) + mockImageReader.On("UpdatePartial", mock.Anything, imageID, matchPartial).Return(nil, nil).Once() } tagger := Tagger{ @@ -355,12 +366,17 @@ func testTagGalleries(t *testing.T, tc testTagCase) { for i := range matchingPaths { galleryID := i + 1 - mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, models.GalleryPartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{tagID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }).Return(nil, nil).Once() + matchPartial := mock.MatchedBy(func(got models.GalleryPartial) bool { + expected := models.GalleryPartial{ + TagIDs: &models.UpdateIDs{ + IDs: []int{tagID}, + Mode: models.RelationshipUpdateModeAdd, + }, + } + + return galleryPartialsEqual(got, expected) + }) + mockGalleryReader.On("UpdatePartial", mock.Anything, galleryID, matchPartial).Return(nil, nil).Once() } diff --git a/internal/identify/scene.go b/internal/identify/scene.go index 9a951c13b18..eec8ce6edc2 100644 --- a/internal/identify/scene.go +++ b/internal/identify/scene.go @@ -7,7 +7,6 @@ import ( "fmt" "strconv" "strings" - "time" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" @@ -164,12 +163,9 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) { tagIDs = intslice.IntAppendUnique(tagIDs, int(tagID)) } else if createMissing { - now := time.Now() - newTag := models.Tag{ - Name: t.Name, - CreatedAt: now, - UpdatedAt: now, - } + newTag := models.NewTag() + newTag.Name = t.Name + err := g.tagCreator.Create(ctx, &newTag) if err != nil { return nil, fmt.Errorf("error creating tag: %w", err) diff --git a/internal/identify/studio.go b/internal/identify/studio.go index c822afa991e..d05967bc4f2 100644 --- a/internal/identify/studio.go +++ b/internal/identify/studio.go @@ -39,7 +39,13 @@ func createMissingStudio(ctx context.Context, endpoint string, w models.StudioRe s.Parent.StoredID = &storedId } else { // The parent studio matched an existing one and the user has chosen in the UI to link and/or update it - existingStashIDs := getStashIDsForStudio(ctx, *s.Parent.StoredID, w) + storedID, _ := strconv.Atoi(*s.Parent.StoredID) + + existingStashIDs, err := w.GetStashIDs(ctx, storedID) + if err != nil { + return nil, err + } + studioPartial := s.Parent.ToPartial(s.Parent.StoredID, endpoint, nil, existingStashIDs) parentImage, err := s.Parent.GetImage(ctx, nil) if err != nil { @@ -83,14 +89,3 @@ func createMissingStudio(ctx context.Context, endpoint string, w models.StudioRe return &newStudio.ID, nil } - -func getStashIDsForStudio(ctx context.Context, studioID string, w models.StudioReaderWriter) []models.StashID { - id, _ := strconv.Atoi(studioID) - tempStudio := &models.Studio{ID: id} - - err := tempStudio.LoadStashIDs(ctx, w) - if err != nil { - return nil - } - return tempStudio.StashIDs.List() -} diff --git a/internal/manager/manager_tasks.go b/internal/manager/manager_tasks.go index e69dccf1dfa..ed4eea17116 100644 --- a/internal/manager/manager_tasks.go +++ b/internal/manager/manager_tasks.go @@ -383,8 +383,8 @@ func (s *Manager) StashBoxBatchPerformerTag(ctx context.Context, input StashBoxB } // Check if the user wants to refresh existing or new items - if (input.Refresh && len(performer.StashIDs.List()) > 0) || - (!input.Refresh && len(performer.StashIDs.List()) == 0) { + hasStashID := performer.StashIDs.ForEndpoint(box.Endpoint) != nil + if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { tasks = append(tasks, StashBoxBatchTagTask{ performer: performer, refresh: input.Refresh, @@ -516,8 +516,8 @@ func (s *Manager) StashBoxBatchStudioTag(ctx context.Context, input StashBoxBatc } // Check if the user wants to refresh existing or new items - if (input.Refresh && len(studio.StashIDs.List()) > 0) || - (!input.Refresh && len(studio.StashIDs.List()) == 0) { + hasStashID := studio.StashIDs.ForEndpoint(box.Endpoint) != nil + if (input.Refresh && hasStashID) || (!input.Refresh && !hasStashID) { tasks = append(tasks, StashBoxBatchTagTask{ studio: studio, refresh: input.Refresh, diff --git a/internal/manager/task_clean.go b/internal/manager/task_clean.go index f5c3e1d547b..207c6381866 100644 --- a/internal/manager/task_clean.go +++ b/internal/manager/task_clean.go @@ -321,9 +321,10 @@ func (h *cleanHandler) handleRelatedScenes(ctx context.Context, fileDeleter *fil } } - if _, err := mgr.Repository.Scene.UpdatePartial(ctx, scene.ID, models.ScenePartial{ - PrimaryFileID: &newPrimaryID, - }); err != nil { + scenePartial := models.NewScenePartial() + scenePartial.PrimaryFileID = &newPrimaryID + + if _, err := mgr.Repository.Scene.UpdatePartial(ctx, scene.ID, scenePartial); err != nil { return err } } @@ -366,9 +367,10 @@ func (h *cleanHandler) handleRelatedGalleries(ctx context.Context, fileID models } } - if _, err := mgr.Repository.Gallery.UpdatePartial(ctx, g.ID, models.GalleryPartial{ - PrimaryFileID: &newPrimaryID, - }); err != nil { + galleryPartial := models.NewGalleryPartial() + galleryPartial.PrimaryFileID = &newPrimaryID + + if _, err := mgr.Repository.Gallery.UpdatePartial(ctx, g.ID, galleryPartial); err != nil { return err } } @@ -439,9 +441,10 @@ func (h *cleanHandler) handleRelatedImages(ctx context.Context, fileDeleter *fil } } - if _, err := mgr.Repository.Image.UpdatePartial(ctx, i.ID, models.ImagePartial{ - PrimaryFileID: &newPrimaryID, - }); err != nil { + imagePartial := models.NewImagePartial() + imagePartial.PrimaryFileID = &newPrimaryID + + if _, err := mgr.Repository.Image.UpdatePartial(ctx, i.ID, imagePartial); err != nil { return err } } diff --git a/internal/manager/task_export.go b/internal/manager/task_export.go index 98ae1918f31..a7278253ecc 100644 --- a/internal/manager/task_export.go +++ b/internal/manager/task_export.go @@ -647,6 +647,11 @@ func exportImage(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models continue } + if err := s.LoadURLs(ctx, repo.Image); err != nil { + logger.Errorf("[images] <%s> error getting image urls: %s", imageHash, err.Error()) + continue + } + newImageJSON := image.ToBasicJSON(s) // export files diff --git a/internal/manager/task_generate_screenshot.go b/internal/manager/task_generate_screenshot.go index 384d8740c7b..1050ebd1c05 100644 --- a/internal/manager/task_generate_screenshot.go +++ b/internal/manager/task_generate_screenshot.go @@ -72,7 +72,7 @@ func (t *GenerateCoverTask) Start(ctx context.Context) { if err := t.txnManager.WithTxn(ctx, func(ctx context.Context) error { qb := t.txnManager.Scene - updatedScene := models.NewScenePartial() + scenePartial := models.NewScenePartial() // update the scene cover table if err := qb.UpdateCover(ctx, t.Scene.ID, coverImageData); err != nil { @@ -80,7 +80,7 @@ func (t *GenerateCoverTask) Start(ctx context.Context) { } // update the scene with the update date - _, err = qb.UpdatePartial(ctx, t.Scene.ID, updatedScene) + _, err = qb.UpdatePartial(ctx, t.Scene.ID, scenePartial) if err != nil { return fmt.Errorf("error updating scene: %v", err) } diff --git a/internal/manager/task_stash_box_tag.go b/internal/manager/task_stash_box_tag.go index 866c8205cb9..6833f166343 100644 --- a/internal/manager/task_stash_box_tag.go +++ b/internal/manager/task_stash_box_tag.go @@ -138,9 +138,6 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m if t.performer != nil { storedID, _ := strconv.Atoi(*p.StoredID) - existingStashIDs := getStashIDsForPerformer(ctx, storedID) - partial := p.ToPartial(t.box.Endpoint, excluded, existingStashIDs) - image, err := p.GetImage(ctx, excluded) if err != nil { logger.Errorf("Error processing scraped performer image for %s: %v", *p.Name, err) @@ -151,6 +148,13 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m err = txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { qb := instance.Repository.Performer + existingStashIDs, err := qb.GetStashIDs(ctx, storedID) + if err != nil { + return err + } + + partial := p.ToPartial(t.box.Endpoint, excluded, existingStashIDs) + if _, err := qb.UpdatePartial(ctx, t.performer.ID, partial); err != nil { return err } @@ -199,16 +203,6 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m } } -func getStashIDsForPerformer(ctx context.Context, performerID int) []models.StashID { - tempPerformer := &models.Performer{ID: performerID} - - err := tempPerformer.LoadStashIDs(ctx, instance.Repository.Performer) - if err != nil { - return nil - } - return tempPerformer.StashIDs.List() -} - func (t *StashBoxBatchTagTask) stashBoxStudioTag(ctx context.Context) { studio, err := t.findStashBoxStudio(ctx) if err != nil { @@ -292,9 +286,6 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode } } - existingStashIDs := getStashIDsForStudio(ctx, storedID) - partial := s.ToPartial(s.StoredID, t.box.Endpoint, excluded, existingStashIDs) - image, err := s.GetImage(ctx, excluded) if err != nil { logger.Errorf("Error processing scraped studio image for %s: %v", s.Name, err) @@ -305,6 +296,13 @@ func (t *StashBoxBatchTagTask) processMatchedStudio(ctx context.Context, s *mode err = txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { qb := instance.Repository.Studio + existingStashIDs, err := qb.GetStashIDs(ctx, storedID) + if err != nil { + return err + } + + partial := s.ToPartial(s.StoredID, t.box.Endpoint, excluded, existingStashIDs) + if err := studio.ValidateModify(ctx, *partial, qb); err != nil { return err } @@ -400,11 +398,8 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent * } return err } else { - storedID, _ := strconv.Atoi(*parent.StoredID) - // The parent studio matched an existing one and the user has chosen in the UI to link and/or update it - existingStashIDs := getStashIDsForStudio(ctx, storedID) - partial := parent.ToPartial(parent.StoredID, t.box.Endpoint, excluded, existingStashIDs) + storedID, _ := strconv.Atoi(*parent.StoredID) image, err := parent.GetImage(ctx, excluded) if err != nil { @@ -416,7 +411,14 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent * err = txn.WithTxn(ctx, instance.Repository, func(ctx context.Context) error { qb := instance.Repository.Studio - if err := studio.ValidateModify(ctx, *partial, instance.Repository.Studio); err != nil { + existingStashIDs, err := qb.GetStashIDs(ctx, storedID) + if err != nil { + return err + } + + partial := parent.ToPartial(parent.StoredID, t.box.Endpoint, excluded, existingStashIDs) + + if err := studio.ValidateModify(ctx, *partial, qb); err != nil { return err } @@ -440,13 +442,3 @@ func (t *StashBoxBatchTagTask) processParentStudio(ctx context.Context, parent * return err } } - -func getStashIDsForStudio(ctx context.Context, studioID int) []models.StashID { - tempStudio := &models.Studio{ID: studioID} - - err := tempStudio.LoadStashIDs(ctx, instance.Repository.Studio) - if err != nil { - return nil - } - return tempStudio.StashIDs.List() -} diff --git a/pkg/ffmpeg/stream_transcode.go b/pkg/ffmpeg/stream_transcode.go index c5593ab8bf8..8c19af3a606 100644 --- a/pkg/ffmpeg/stream_transcode.go +++ b/pkg/ffmpeg/stream_transcode.go @@ -266,7 +266,7 @@ func (sm *StreamManager) getTranscodeStream(ctx *fsutil.LockContext, options Tra // process killing should be handled by command context _, err := io.Copy(w, stdout) - if err != nil && !errors.Is(err, syscall.EPIPE) { + if err != nil && !errors.Is(err, syscall.EPIPE) && !errors.Is(err, syscall.ECONNRESET) { logger.Errorf("[transcode] error serving transcoded video file: %v", err) } diff --git a/pkg/fsutil/file.go b/pkg/fsutil/file.go index 1bf98266675..0c0eb52715f 100644 --- a/pkg/fsutil/file.go +++ b/pkg/fsutil/file.go @@ -20,7 +20,7 @@ func CopyFile(srcpath, dstpath string) (err error) { return err } - w, err := os.OpenFile(dstpath, os.O_CREATE|os.O_EXCL, 0666) + w, err := os.OpenFile(dstpath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0666) if err != nil { r.Close() // We need to close the input file as the defer below would not be called. return err @@ -59,9 +59,9 @@ func SafeMove(src, dst string) error { err := os.Rename(src, dst) if err != nil { - err = CopyFile(src, dst) - if err != nil { - return err + copyErr := CopyFile(src, dst) + if copyErr != nil { + return fmt.Errorf("copying file during SaveMove failed with: '%w'; renaming file failed previously with: '%v'", copyErr, err) } err = os.Remove(src) diff --git a/pkg/gallery/export.go b/pkg/gallery/export.go index 83f3c31cebc..5412e9a509c 100644 --- a/pkg/gallery/export.go +++ b/pkg/gallery/export.go @@ -14,7 +14,7 @@ import ( func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) { newGalleryJSON := jsonschema.Gallery{ Title: gallery.Title, - URL: gallery.URL, + URLs: gallery.URLs.List(), Details: gallery.Details, CreatedAt: json.JSONTime{Time: gallery.CreatedAt}, UpdatedAt: json.JSONTime{Time: gallery.UpdatedAt}, diff --git a/pkg/gallery/export_test.go b/pkg/gallery/export_test.go index 3a6ffa2ec55..eba08e4e513 100644 --- a/pkg/gallery/export_test.go +++ b/pkg/gallery/export_test.go @@ -59,7 +59,7 @@ func createFullGallery(id int) models.Gallery { Details: details, Rating: &rating, Organized: organized, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), CreatedAt: createTime, UpdatedAt: updateTime, } @@ -85,7 +85,7 @@ func createFullJSONGallery() *jsonschema.Gallery { Details: details, Rating: rating, Organized: organized, - URL: url, + URLs: []string{url}, ZipFiles: []string{path}, CreatedAt: json.JSONTime{ Time: createTime, diff --git a/pkg/gallery/import.go b/pkg/gallery/import.go index 57d151245b5..780b1e63bce 100644 --- a/pkg/gallery/import.go +++ b/pkg/gallery/import.go @@ -65,8 +65,10 @@ func (i *Importer) galleryJSONToGallery(galleryJSON jsonschema.Gallery) models.G if galleryJSON.Details != "" { newGallery.Details = galleryJSON.Details } - if galleryJSON.URL != "" { - newGallery.URL = galleryJSON.URL + if len(galleryJSON.URLs) > 0 { + newGallery.URLs = models.NewRelatedStrings(galleryJSON.URLs) + } else if galleryJSON.URL != "" { + newGallery.URLs = models.NewRelatedStrings([]string{galleryJSON.URL}) } if galleryJSON.Date != "" { d, err := models.ParseDate(galleryJSON.Date) @@ -117,11 +119,10 @@ func (i *Importer) populateStudio(ctx context.Context) error { } func (i *Importer) createStudio(ctx context.Context, name string) (int, error) { - newStudio := &models.Studio{ - Name: name, - } + newStudio := models.NewStudio() + newStudio.Name = name - err := i.StudioWriter.Create(ctx, newStudio) + err := i.StudioWriter.Create(ctx, &newStudio) if err != nil { return 0, err } @@ -177,7 +178,8 @@ func (i *Importer) populatePerformers(ctx context.Context) error { func (i *Importer) createPerformers(ctx context.Context, names []string) ([]*models.Performer, error) { var ret []*models.Performer for _, name := range names { - newPerformer := *models.NewPerformer(name) + newPerformer := models.NewPerformer() + newPerformer.Name = name err := i.PerformerWriter.Create(ctx, &newPerformer) if err != nil { @@ -235,14 +237,15 @@ func (i *Importer) populateTags(ctx context.Context) error { func (i *Importer) createTags(ctx context.Context, names []string) ([]*models.Tag, error) { var ret []*models.Tag for _, name := range names { - newTag := models.NewTag(name) + newTag := models.NewTag() + newTag.Name = name - err := i.TagWriter.Create(ctx, newTag) + err := i.TagWriter.Create(ctx, &newTag) if err != nil { return nil, err } - ret = append(ret, newTag) + ret = append(ret, &newTag) } return ret, nil diff --git a/pkg/gallery/import_test.go b/pkg/gallery/import_test.go index 0997b4a57e2..8263f97870c 100644 --- a/pkg/gallery/import_test.go +++ b/pkg/gallery/import_test.go @@ -66,7 +66,7 @@ func TestImporterPreImport(t *testing.T) { Details: details, Rating: &rating, Organized: organized, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Files: models.NewRelatedFiles([]models.File{}), TagIDs: models.NewRelatedIDs([]int{}), PerformerIDs: models.NewRelatedIDs([]int{}), diff --git a/pkg/gallery/scan.go b/pkg/gallery/scan.go index a8f52e89bb6..f4a9adcc5c5 100644 --- a/pkg/gallery/scan.go +++ b/pkg/gallery/scan.go @@ -5,7 +5,6 @@ import ( "fmt" "path/filepath" "strings" - "time" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" @@ -76,15 +75,11 @@ func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models. } // create a new gallery - now := time.Now() - newGallery := &models.Gallery{ - CreatedAt: now, - UpdatedAt: now, - } + newGallery := models.NewGallery() logger.Infof("%s doesn't exist. Creating new gallery...", f.Base().Path) - if err := h.CreatorUpdater.Create(ctx, newGallery, []models.FileID{baseFile.ID}); err != nil { + if err := h.CreatorUpdater.Create(ctx, &newGallery, []models.FileID{baseFile.ID}); err != nil { return fmt.Errorf("creating new gallery: %w", err) } @@ -92,18 +87,21 @@ func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models. // associate all the images in the zip file with the gallery for _, i := range images { - if _, err := h.ImageFinderUpdater.UpdatePartial(ctx, i.ID, models.ImagePartial{ + imagePartial := models.ImagePartial{ GalleryIDs: &models.UpdateIDs{ IDs: []int{newGallery.ID}, Mode: models.RelationshipUpdateModeAdd, }, - UpdatedAt: models.NewOptionalTime(now), - }); err != nil { + // set UpdatedAt directly instead of using NewImagePartial, to ensure + // that the images have the same UpdatedAt time as the gallery + UpdatedAt: models.NewOptionalTime(newGallery.UpdatedAt), + } + if _, err := h.ImageFinderUpdater.UpdatePartial(ctx, i.ID, imagePartial); err != nil { return fmt.Errorf("adding image %s to gallery: %w", i.Path, err) } } - existing = []*models.Gallery{newGallery} + existing = []*models.Gallery{&newGallery} } if err := h.associateScene(ctx, existing, f); err != nil { diff --git a/pkg/gallery/update.go b/pkg/gallery/update.go index 71d92c5409b..d66da197c81 100644 --- a/pkg/gallery/update.go +++ b/pkg/gallery/update.go @@ -3,7 +3,6 @@ package gallery import ( "context" "fmt" - "time" "github.com/stashapp/stash/pkg/models" ) @@ -15,9 +14,8 @@ type ImageUpdater interface { } func (s *Service) Updated(ctx context.Context, galleryID int) error { - _, err := s.Repository.UpdatePartial(ctx, galleryID, models.GalleryPartial{ - UpdatedAt: models.NewOptionalTime(time.Now()), - }) + galleryPartial := models.NewGalleryPartial() + _, err := s.Repository.UpdatePartial(ctx, galleryID, galleryPartial) return err } @@ -55,21 +53,21 @@ func (s *Service) RemoveImages(ctx context.Context, g *models.Gallery, toRemove } func AddPerformer(ctx context.Context, qb models.GalleryUpdater, o *models.Gallery, performerID int) error { - _, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{ - PerformerIDs: &models.UpdateIDs{ - IDs: []int{performerID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }) + galleryPartial := models.NewGalleryPartial() + galleryPartial.PerformerIDs = &models.UpdateIDs{ + IDs: []int{performerID}, + Mode: models.RelationshipUpdateModeAdd, + } + _, err := qb.UpdatePartial(ctx, o.ID, galleryPartial) return err } func AddTag(ctx context.Context, qb models.GalleryUpdater, o *models.Gallery, tagID int) error { - _, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{tagID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }) + galleryPartial := models.NewGalleryPartial() + galleryPartial.TagIDs = &models.UpdateIDs{ + IDs: []int{tagID}, + Mode: models.RelationshipUpdateModeAdd, + } + _, err := qb.UpdatePartial(ctx, o.ID, galleryPartial) return err } diff --git a/pkg/image/export.go b/pkg/image/export.go index a7c4d8575eb..41eac446fe2 100644 --- a/pkg/image/export.go +++ b/pkg/image/export.go @@ -14,7 +14,7 @@ import ( func ToBasicJSON(image *models.Image) *jsonschema.Image { newImageJSON := jsonschema.Image{ Title: image.Title, - URL: image.URL, + URLs: image.URLs.List(), CreatedAt: json.JSONTime{Time: image.CreatedAt}, UpdatedAt: json.JSONTime{Time: image.UpdatedAt}, } @@ -37,19 +37,6 @@ func ToBasicJSON(image *models.Image) *jsonschema.Image { return &newImageJSON } -// func getImageFileJSON(image *models.Image) *jsonschema.ImageFile { -// ret := &jsonschema.ImageFile{} - -// f := image.PrimaryFile() - -// ret.ModTime = json.JSONTime{Time: f.ModTime} -// ret.Size = f.Size -// ret.Width = f.Width -// ret.Height = f.Height - -// return ret -// } - // GetStudioName returns the name of the provided image's studio. It returns an // empty string if there is no studio assigned to the image. func GetStudioName(ctx context.Context, reader models.StudioGetter, image *models.Image) (string, error) { diff --git a/pkg/image/export_test.go b/pkg/image/export_test.go index 3b64f40cbcd..1a5897271ef 100644 --- a/pkg/image/export_test.go +++ b/pkg/image/export_test.go @@ -53,7 +53,7 @@ func createFullImage(id int) models.Image { OCounter: ocounter, Rating: &rating, Date: &dateObj, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Organized: organized, CreatedAt: createTime, UpdatedAt: updateTime, @@ -66,7 +66,7 @@ func createFullJSONImage() *jsonschema.Image { OCounter: ocounter, Rating: rating, Date: date, - URL: url, + URLs: []string{url}, Organized: organized, Files: []string{path}, CreatedAt: json.JSONTime{ diff --git a/pkg/image/import.go b/pkg/image/import.go index 4ce2287eb7b..8b90fa8a7c6 100644 --- a/pkg/image/import.go +++ b/pkg/image/import.go @@ -62,8 +62,6 @@ func (i *Importer) PreImport(ctx context.Context) error { func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image { newImage := models.Image{ - // Checksum: imageJSON.Checksum, - // Path: i.Path, PerformerIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}), GalleryIDs: models.NewRelatedIDs([]int{}), @@ -81,9 +79,12 @@ func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image { if imageJSON.Rating != 0 { newImage.Rating = &imageJSON.Rating } - if imageJSON.URL != "" { - newImage.URL = imageJSON.URL + if len(imageJSON.URLs) > 0 { + newImage.URLs = models.NewRelatedStrings(imageJSON.URLs) + } else if imageJSON.URL != "" { + newImage.URLs = models.NewRelatedStrings([]string{imageJSON.URL}) } + if imageJSON.Date != "" { d, err := models.ParseDate(imageJSON.Date) if err == nil { @@ -148,11 +149,10 @@ func (i *Importer) populateStudio(ctx context.Context) error { } func (i *Importer) createStudio(ctx context.Context, name string) (int, error) { - newStudio := &models.Studio{ - Name: name, - } + newStudio := models.NewStudio() + newStudio.Name = name - err := i.StudioWriter.Create(ctx, newStudio) + err := i.StudioWriter.Create(ctx, &newStudio) if err != nil { return 0, err } @@ -261,7 +261,8 @@ func (i *Importer) populatePerformers(ctx context.Context) error { func (i *Importer) createPerformers(ctx context.Context, names []string) ([]*models.Performer, error) { var ret []*models.Performer for _, name := range names { - newPerformer := *models.NewPerformer(name) + newPerformer := models.NewPerformer() + newPerformer.Name = name err := i.PerformerWriter.Create(ctx, &newPerformer) if err != nil { @@ -331,10 +332,7 @@ func (i *Importer) Create(ctx context.Context) (*int, error) { fileIDs = append(fileIDs, f.Base().ID) } - err := i.ReaderWriter.Create(ctx, &models.ImageCreateInput{ - Image: &i.image, - FileIDs: fileIDs, - }) + err := i.ReaderWriter.Create(ctx, &i.image, fileIDs) if err != nil { return nil, fmt.Errorf("error creating image: %v", err) } @@ -394,14 +392,15 @@ func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names [] func createTags(ctx context.Context, tagWriter models.TagCreator, names []string) ([]*models.Tag, error) { var ret []*models.Tag for _, name := range names { - newTag := models.NewTag(name) + newTag := models.NewTag() + newTag.Name = name - err := tagWriter.Create(ctx, newTag) + err := tagWriter.Create(ctx, &newTag) if err != nil { return nil, err } - ret = append(ret, newTag) + ret = append(ret, &newTag) } return ret, nil diff --git a/pkg/image/scan.go b/pkg/image/scan.go index d584d0f55fa..9f4aa0d57e9 100644 --- a/pkg/image/scan.go +++ b/pkg/image/scan.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "time" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" @@ -27,7 +26,7 @@ type ScanCreatorUpdater interface { GetFiles(ctx context.Context, relatedID int) ([]models.File, error) GetGalleryIDs(ctx context.Context, relatedID int) ([]int, error) - Create(ctx context.Context, newImage *models.ImageCreateInput) error + Create(ctx context.Context, newImage *models.Image, fileIDs []models.FileID) error UpdatePartial(ctx context.Context, id int, updatedImage models.ImagePartial) (*models.Image, error) AddFileID(ctx context.Context, id int, fileID models.FileID) error } @@ -109,16 +108,12 @@ func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models. } } else { // create a new image - now := time.Now() - newImage := &models.Image{ - CreatedAt: now, - UpdatedAt: now, - GalleryIDs: models.NewRelatedIDs([]int{}), - } + newImage := models.NewImage() + newImage.GalleryIDs = models.NewRelatedIDs([]int{}) logger.Infof("%s doesn't exist. Creating new image...", f.Base().Path) - g, err := h.getGalleryToAssociate(ctx, newImage, f) + g, err := h.getGalleryToAssociate(ctx, &newImage, f) if err != nil { return err } @@ -128,25 +123,23 @@ func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models. logger.Infof("Adding %s to gallery %s", f.Base().Path, g.Path) } - if err := h.CreatorUpdater.Create(ctx, &models.ImageCreateInput{ - Image: newImage, - FileIDs: []models.FileID{imageFile.ID}, - }); err != nil { + if err := h.CreatorUpdater.Create(ctx, &newImage, []models.FileID{imageFile.ID}); err != nil { return fmt.Errorf("creating new image: %w", err) } // update the gallery updated at timestamp if applicable if g != nil { - if _, err := h.GalleryFinder.UpdatePartial(ctx, g.ID, models.GalleryPartial{ - UpdatedAt: models.NewOptionalTime(time.Now()), - }); err != nil { + galleryPartial := models.GalleryPartial{ + UpdatedAt: models.NewOptionalTime(newImage.UpdatedAt), + } + if _, err := h.GalleryFinder.UpdatePartial(ctx, g.ID, galleryPartial); err != nil { return fmt.Errorf("updating gallery updated at timestamp: %w", err) } } h.PluginCache.RegisterPostHooks(ctx, newImage.ID, plugin.ImageCreatePost, nil, nil) - existing = []*models.Image{newImage} + existing = []*models.Image{&newImage} } // remove the old thumbnail if the checksum changed - we'll regenerate it @@ -215,17 +208,20 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models. if changed { // always update updated_at time - if _, err := h.CreatorUpdater.UpdatePartial(ctx, i.ID, models.ImagePartial{ - GalleryIDs: galleryIDs, - UpdatedAt: models.NewOptionalTime(time.Now()), - }); err != nil { + imagePartial := models.NewImagePartial() + imagePartial.GalleryIDs = galleryIDs + + if _, err := h.CreatorUpdater.UpdatePartial(ctx, i.ID, imagePartial); err != nil { return fmt.Errorf("updating image: %w", err) } if g != nil { - if _, err := h.GalleryFinder.UpdatePartial(ctx, g.ID, models.GalleryPartial{ - UpdatedAt: models.NewOptionalTime(time.Now()), - }); err != nil { + galleryPartial := models.GalleryPartial{ + // set UpdatedAt directly instead of using NewGalleryPartial, to ensure + // that the linked gallery has the same UpdatedAt time as this image + UpdatedAt: imagePartial.UpdatedAt, + } + if _, err := h.GalleryFinder.UpdatePartial(ctx, g.ID, galleryPartial); err != nil { return fmt.Errorf("updating gallery updated at timestamp: %w", err) } } @@ -252,16 +248,12 @@ func (h *ScanHandler) getOrCreateFolderBasedGallery(ctx context.Context, f model } // create a new folder-based gallery - now := time.Now() - newGallery := &models.Gallery{ - FolderID: &folderID, - CreatedAt: now, - UpdatedAt: now, - } + newGallery := models.NewGallery() + newGallery.FolderID = &folderID logger.Infof("Creating folder-based gallery for %s", filepath.Dir(f.Base().Path)) - if err := h.GalleryFinder.Create(ctx, newGallery, nil); err != nil { + if err := h.GalleryFinder.Create(ctx, &newGallery, nil); err != nil { return nil, fmt.Errorf("creating folder based gallery: %w", err) } @@ -269,11 +261,11 @@ func (h *ScanHandler) getOrCreateFolderBasedGallery(ctx context.Context, f model // it's possible that there are other images in the folder that // need to be added to the new gallery. Find and add them now. - if err := h.associateFolderImages(ctx, newGallery); err != nil { + if err := h.associateFolderImages(ctx, &newGallery); err != nil { return nil, fmt.Errorf("associating existing folder images: %w", err) } - return newGallery, nil + return &newGallery, nil } func (h *ScanHandler) associateFolderImages(ctx context.Context, g *models.Gallery) error { @@ -285,13 +277,13 @@ func (h *ScanHandler) associateFolderImages(ctx context.Context, g *models.Galle for _, ii := range i { logger.Infof("Adding %s to gallery %s", ii.Path, g.Path) - if _, err := h.CreatorUpdater.UpdatePartial(ctx, ii.ID, models.ImagePartial{ - GalleryIDs: &models.UpdateIDs{ - IDs: []int{g.ID}, - Mode: models.RelationshipUpdateModeAdd, - }, - UpdatedAt: models.NewOptionalTime(time.Now()), - }); err != nil { + imagePartial := models.NewImagePartial() + imagePartial.GalleryIDs = &models.UpdateIDs{ + IDs: []int{g.ID}, + Mode: models.RelationshipUpdateModeAdd, + } + + if _, err := h.CreatorUpdater.UpdatePartial(ctx, ii.ID, imagePartial); err != nil { return fmt.Errorf("updating image: %w", err) } } @@ -311,21 +303,17 @@ func (h *ScanHandler) getOrCreateZipBasedGallery(ctx context.Context, zipFile mo } // create a new zip-based gallery - now := time.Now() - newGallery := &models.Gallery{ - CreatedAt: now, - UpdatedAt: now, - } + newGallery := models.NewGallery() logger.Infof("%s doesn't exist. Creating new gallery...", zipFile.Base().Path) - if err := h.GalleryFinder.Create(ctx, newGallery, []models.FileID{zipFile.Base().ID}); err != nil { + if err := h.GalleryFinder.Create(ctx, &newGallery, []models.FileID{zipFile.Base().ID}); err != nil { return nil, fmt.Errorf("creating zip-based gallery: %w", err) } h.PluginCache.RegisterPostHooks(ctx, newGallery.ID, plugin.GalleryCreatePost, nil, nil) - return newGallery, nil + return &newGallery, nil } func (h *ScanHandler) getOrCreateGallery(ctx context.Context, f models.File) (*models.Gallery, error) { diff --git a/pkg/image/update.go b/pkg/image/update.go index e3a63b53d03..844e2088f71 100644 --- a/pkg/image/update.go +++ b/pkg/image/update.go @@ -7,22 +7,21 @@ import ( ) func AddPerformer(ctx context.Context, qb models.ImageUpdater, i *models.Image, performerID int) error { - _, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{ - PerformerIDs: &models.UpdateIDs{ - IDs: []int{performerID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }) - + imagePartial := models.NewImagePartial() + imagePartial.PerformerIDs = &models.UpdateIDs{ + IDs: []int{performerID}, + Mode: models.RelationshipUpdateModeAdd, + } + _, err := qb.UpdatePartial(ctx, i.ID, imagePartial) return err } func AddTag(ctx context.Context, qb models.ImageUpdater, i *models.Image, tagID int) error { - _, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{tagID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }) + imagePartial := models.NewImagePartial() + imagePartial.TagIDs = &models.UpdateIDs{ + IDs: []int{tagID}, + Mode: models.RelationshipUpdateModeAdd, + } + _, err := qb.UpdatePartial(ctx, i.ID, imagePartial) return err } diff --git a/pkg/models/gallery.go b/pkg/models/gallery.go index d3644d3fd6c..89651dbc41a 100644 --- a/pkg/models/gallery.go +++ b/pkg/models/gallery.go @@ -59,7 +59,7 @@ type GalleryUpdateInput struct { ClientMutationID *string `json:"clientMutationId"` ID string `json:"id"` Title *string `json:"title"` - URL *string `json:"url"` + Urls []string `json:"urls"` Date *string `json:"date"` Details *string `json:"details"` Rating *int `json:"rating"` @@ -70,6 +70,9 @@ type GalleryUpdateInput struct { TagIds []string `json:"tag_ids"` PerformerIds []string `json:"performer_ids"` PrimaryFileID *string `json:"primary_file_id"` + + // deprecated + URL *string `json:"url"` } type GalleryDestroyInput struct { diff --git a/pkg/models/jsonschema/gallery.go b/pkg/models/jsonschema/gallery.go index ca399624ebf..0832cc07a6b 100644 --- a/pkg/models/jsonschema/gallery.go +++ b/pkg/models/jsonschema/gallery.go @@ -21,7 +21,7 @@ type Gallery struct { ZipFiles []string `json:"zip_files,omitempty"` FolderPath string `json:"folder_path,omitempty"` Title string `json:"title,omitempty"` - URL string `json:"url,omitempty"` + URLs []string `json:"urls,omitempty"` Date string `json:"date,omitempty"` Details string `json:"details,omitempty"` Rating int `json:"rating,omitempty"` @@ -32,6 +32,9 @@ type Gallery struct { Tags []string `json:"tags,omitempty"` CreatedAt json.JSONTime `json:"created_at,omitempty"` UpdatedAt json.JSONTime `json:"updated_at,omitempty"` + + // deprecated - for import only + URL string `json:"url,omitempty"` } func (s Gallery) Filename(basename string, hash string) string { diff --git a/pkg/models/jsonschema/image.go b/pkg/models/jsonschema/image.go index 1862ffc8290..7ff0b21621f 100644 --- a/pkg/models/jsonschema/image.go +++ b/pkg/models/jsonschema/image.go @@ -10,10 +10,14 @@ import ( ) type Image struct { - Title string `json:"title,omitempty"` - Studio string `json:"studio,omitempty"` - Rating int `json:"rating,omitempty"` - URL string `json:"url,omitempty"` + Title string `json:"title,omitempty"` + Studio string `json:"studio,omitempty"` + Rating int `json:"rating,omitempty"` + + // deprecated - for import only + URL string `json:"url,omitempty"` + + URLs []string `json:"urls,omitempty"` Date string `json:"date,omitempty"` Organized bool `json:"organized,omitempty"` OCounter int `json:"o_counter,omitempty"` diff --git a/pkg/models/jsonschema/scene.go b/pkg/models/jsonschema/scene.go index 7ebae7a1785..8a081f3b610 100644 --- a/pkg/models/jsonschema/scene.go +++ b/pkg/models/jsonschema/scene.go @@ -42,8 +42,10 @@ type Scene struct { Title string `json:"title,omitempty"` Code string `json:"code,omitempty"` Studio string `json:"studio,omitempty"` + // deprecated - for import only - URL string `json:"url,omitempty"` + URL string `json:"url,omitempty"` + URLs []string `json:"urls,omitempty"` Date string `json:"date,omitempty"` Rating int `json:"rating,omitempty"` diff --git a/pkg/models/mocks/GalleryReaderWriter.go b/pkg/models/mocks/GalleryReaderWriter.go index 2b901466b19..bd1fbf0d2b9 100644 --- a/pkg/models/mocks/GalleryReaderWriter.go +++ b/pkg/models/mocks/GalleryReaderWriter.go @@ -533,6 +533,29 @@ func (_m *GalleryReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([] return r0, r1 } +// GetURLs provides a mock function with given fields: ctx, relatedID +func (_m *GalleryReaderWriter) GetURLs(ctx context.Context, relatedID int) ([]string, error) { + ret := _m.Called(ctx, relatedID) + + var r0 []string + if rf, ok := ret.Get(0).(func(context.Context, int) []string); ok { + r0 = rf(ctx, relatedID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, relatedID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Query provides a mock function with given fields: ctx, galleryFilter, findFilter func (_m *GalleryReaderWriter) Query(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) { ret := _m.Called(ctx, galleryFilter, findFilter) diff --git a/pkg/models/mocks/ImageReaderWriter.go b/pkg/models/mocks/ImageReaderWriter.go index 4924fd51d11..bd651108ab0 100644 --- a/pkg/models/mocks/ImageReaderWriter.go +++ b/pkg/models/mocks/ImageReaderWriter.go @@ -114,13 +114,13 @@ func (_m *ImageReaderWriter) CountByGalleryID(ctx context.Context, galleryID int return r0, r1 } -// Create provides a mock function with given fields: ctx, newImage -func (_m *ImageReaderWriter) Create(ctx context.Context, newImage *models.ImageCreateInput) error { - ret := _m.Called(ctx, newImage) +// Create provides a mock function with given fields: ctx, newImage, fileIDs +func (_m *ImageReaderWriter) Create(ctx context.Context, newImage *models.Image, fileIDs []models.FileID) error { + ret := _m.Called(ctx, newImage, fileIDs) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.ImageCreateInput) error); ok { - r0 = rf(ctx, newImage) + if rf, ok := ret.Get(0).(func(context.Context, *models.Image, []models.FileID) error); ok { + r0 = rf(ctx, newImage, fileIDs) } else { r0 = ret.Error(0) } @@ -462,6 +462,29 @@ func (_m *ImageReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]in return r0, r1 } +// GetURLs provides a mock function with given fields: ctx, relatedID +func (_m *ImageReaderWriter) GetURLs(ctx context.Context, relatedID int) ([]string, error) { + ret := _m.Called(ctx, relatedID) + + var r0 []string + if rf, ok := ret.Get(0).(func(context.Context, int) []string); ok { + r0 = rf(ctx, relatedID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, relatedID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // IncrementOCounter provides a mock function with given fields: ctx, id func (_m *ImageReaderWriter) IncrementOCounter(ctx context.Context, id int) (int, error) { ret := _m.Called(ctx, id) diff --git a/pkg/models/mocks/TagReaderWriter.go b/pkg/models/mocks/TagReaderWriter.go index a061b79b2c9..9b610e49b6e 100644 --- a/pkg/models/mocks/TagReaderWriter.go +++ b/pkg/models/mocks/TagReaderWriter.go @@ -58,6 +58,48 @@ func (_m *TagReaderWriter) Count(ctx context.Context) (int, error) { return r0, r1 } +// CountByChildTagID provides a mock function with given fields: ctx, childID +func (_m *TagReaderWriter) CountByChildTagID(ctx context.Context, childID int) (int, error) { + ret := _m.Called(ctx, childID) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, int) int); ok { + r0 = rf(ctx, childID) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, childID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CountByParentTagID provides a mock function with given fields: ctx, parentID +func (_m *TagReaderWriter) CountByParentTagID(ctx context.Context, parentID int) (int, error) { + ret := _m.Called(ctx, parentID) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, int) int); ok { + r0 = rf(ctx, parentID) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, parentID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Create provides a mock function with given fields: ctx, newTag func (_m *TagReaderWriter) Create(ctx context.Context, newTag *models.Tag) error { ret := _m.Called(ctx, newTag) diff --git a/pkg/models/model_gallery.go b/pkg/models/model_gallery.go index 8f563f06f7b..d35b9a360bb 100644 --- a/pkg/models/model_gallery.go +++ b/pkg/models/model_gallery.go @@ -11,7 +11,6 @@ type Gallery struct { ID int `json:"id"` Title string `json:"title"` - URL string `json:"url"` Date *Date `json:"date"` Details string `json:"details"` // Rating expressed in 1-100 scale @@ -31,9 +30,49 @@ type Gallery struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - SceneIDs RelatedIDs `json:"scene_ids"` - TagIDs RelatedIDs `json:"tag_ids"` - PerformerIDs RelatedIDs `json:"performer_ids"` + URLs RelatedStrings `json:"urls"` + SceneIDs RelatedIDs `json:"scene_ids"` + TagIDs RelatedIDs `json:"tag_ids"` + PerformerIDs RelatedIDs `json:"performer_ids"` +} + +func NewGallery() Gallery { + currentTime := time.Now() + return Gallery{ + CreatedAt: currentTime, + UpdatedAt: currentTime, + } +} + +// GalleryPartial represents part of a Gallery object. It is used to update +// the database entry. Only non-nil fields will be updated. +type GalleryPartial struct { + // Path OptionalString + // Checksum OptionalString + // Zip OptionalBool + Title OptionalString + URLs *UpdateStrings + Date OptionalDate + Details OptionalString + // Rating expressed in 1-100 scale + Rating OptionalInt + Organized OptionalBool + StudioID OptionalInt + // FileModTime OptionalTime + CreatedAt OptionalTime + UpdatedAt OptionalTime + + SceneIDs *UpdateIDs + TagIDs *UpdateIDs + PerformerIDs *UpdateIDs + PrimaryFileID *FileID +} + +func NewGalleryPartial() GalleryPartial { + currentTime := time.Now() + return GalleryPartial{ + UpdatedAt: NewOptionalTime(currentTime), + } } // IsUserCreated returns true if the gallery was created by the user. @@ -42,6 +81,12 @@ func (g *Gallery) IsUserCreated() bool { return g.PrimaryFileID == nil && g.FolderID == nil } +func (g *Gallery) LoadURLs(ctx context.Context, l URLLoader) error { + return g.URLs.load(func() ([]string, error) { + return l.GetURLs(ctx, g.ID) + }) +} + func (g *Gallery) LoadFiles(ctx context.Context, l FileLoader) error { return g.Files.load(func() ([]File, error) { return l.GetFiles(ctx, g.ID) @@ -97,37 +142,6 @@ func (g Gallery) PrimaryChecksum() string { return "" } -// GalleryPartial represents part of a Gallery object. It is used to update -// the database entry. Only non-nil fields will be updated. -type GalleryPartial struct { - // Path OptionalString - // Checksum OptionalString - // Zip OptionalBool - Title OptionalString - URL OptionalString - Date OptionalDate - Details OptionalString - // Rating expressed in 1-100 scale - Rating OptionalInt - Organized OptionalBool - StudioID OptionalInt - // FileModTime OptionalTime - CreatedAt OptionalTime - UpdatedAt OptionalTime - - SceneIDs *UpdateIDs - TagIDs *UpdateIDs - PerformerIDs *UpdateIDs - PrimaryFileID *FileID -} - -func NewGalleryPartial() GalleryPartial { - updatedTime := time.Now() - return GalleryPartial{ - UpdatedAt: NewOptionalTime(updatedTime), - } -} - // GetTitle returns the title of the scene. If the Title field is empty, // then the base filename is returned. func (g Gallery) GetTitle() string { @@ -153,13 +167,3 @@ func (g Gallery) DisplayName() string { } const DefaultGthumbWidth int = 640 - -type Galleries []*Gallery - -func (g *Galleries) Append(o interface{}) { - *g = append(*g, o.(*Gallery)) -} - -func (g *Galleries) New() interface{} { - return &Gallery{} -} diff --git a/pkg/models/model_gallery_chapter.go b/pkg/models/model_gallery_chapter.go index 5c9fc05b2be..6e527106bdd 100644 --- a/pkg/models/model_gallery_chapter.go +++ b/pkg/models/model_gallery_chapter.go @@ -13,6 +13,14 @@ type GalleryChapter struct { UpdatedAt time.Time `json:"updated_at"` } +func NewGalleryChapter() GalleryChapter { + currentTime := time.Now() + return GalleryChapter{ + CreatedAt: currentTime, + UpdatedAt: currentTime, + } +} + // GalleryChapterPartial represents part of a GalleryChapter object. // It is used to update the database entry. type GalleryChapterPartial struct { @@ -24,8 +32,8 @@ type GalleryChapterPartial struct { } func NewGalleryChapterPartial() GalleryChapterPartial { - updatedTime := time.Now() + currentTime := time.Now() return GalleryChapterPartial{ - UpdatedAt: NewOptionalTime(updatedTime), + UpdatedAt: NewOptionalTime(currentTime), } } diff --git a/pkg/models/model_image.go b/pkg/models/model_image.go index 9e0a0389a77..8f3211dc7ac 100644 --- a/pkg/models/model_image.go +++ b/pkg/models/model_image.go @@ -13,12 +13,12 @@ type Image struct { 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"` - 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"` + URLs RelatedStrings `json:"urls"` + Date *Date `json:"date"` // transient - not persisted Files RelatedFiles @@ -36,6 +36,45 @@ type Image struct { PerformerIDs RelatedIDs `json:"performer_ids"` } +func NewImage() Image { + currentTime := time.Now() + return Image{ + CreatedAt: currentTime, + UpdatedAt: currentTime, + } +} + +type ImagePartial struct { + Title OptionalString + // Rating expressed in 1-100 scale + Rating OptionalInt + URLs *UpdateStrings + Date OptionalDate + Organized OptionalBool + OCounter OptionalInt + StudioID OptionalInt + CreatedAt OptionalTime + UpdatedAt OptionalTime + + GalleryIDs *UpdateIDs + TagIDs *UpdateIDs + PerformerIDs *UpdateIDs + PrimaryFileID *FileID +} + +func NewImagePartial() ImagePartial { + currentTime := time.Now() + return ImagePartial{ + UpdatedAt: NewOptionalTime(currentTime), + } +} + +func (i *Image) LoadURLs(ctx context.Context, l URLLoader) error { + return i.URLs.load(func() ([]string, error) { + return l.GetURLs(ctx, i.ID) + }) +} + func (i *Image) LoadFiles(ctx context.Context, l FileLoader) error { return i.Files.load(func() ([]File, error) { return l.GetFiles(ctx, i.ID) @@ -102,43 +141,3 @@ func (i Image) DisplayName() string { return strconv.Itoa(i.ID) } - -type ImageCreateInput struct { - *Image - FileIDs []FileID -} - -type ImagePartial struct { - Title OptionalString - // Rating expressed in 1-100 scale - Rating OptionalInt - URL OptionalString - Date OptionalDate - Organized OptionalBool - OCounter OptionalInt - StudioID OptionalInt - CreatedAt OptionalTime - UpdatedAt OptionalTime - - GalleryIDs *UpdateIDs - TagIDs *UpdateIDs - PerformerIDs *UpdateIDs - PrimaryFileID *FileID -} - -func NewImagePartial() ImagePartial { - updatedTime := time.Now() - return ImagePartial{ - UpdatedAt: NewOptionalTime(updatedTime), - } -} - -type Images []*Image - -func (i *Images) Append(o interface{}) { - *i = append(*i, o.(*Image)) -} - -func (i *Images) New() interface{} { - return &Image{} -} diff --git a/pkg/models/model_joins.go b/pkg/models/model_joins.go index 5fe8b7fa5d9..da70293c3d3 100644 --- a/pkg/models/model_joins.go +++ b/pkg/models/model_joins.go @@ -11,8 +11,8 @@ type MoviesScenes struct { SceneIndex *int `json:"scene_index"` } -func (s MoviesScenes) SceneMovieInput() *SceneMovieInput { - return &SceneMovieInput{ +func (s MoviesScenes) SceneMovieInput() SceneMovieInput { + return SceneMovieInput{ MovieID: strconv.Itoa(s.MovieID), SceneIndex: s.SceneIndex, } @@ -28,12 +28,12 @@ type UpdateMovieIDs struct { Mode RelationshipUpdateMode `json:"mode"` } -func (u *UpdateMovieIDs) SceneMovieInputs() []*SceneMovieInput { +func (u *UpdateMovieIDs) SceneMovieInputs() []SceneMovieInput { if u == nil { return nil } - ret := make([]*SceneMovieInput, len(u.Movies)) + ret := make([]SceneMovieInput, len(u.Movies)) for _, id := range u.Movies { ret = append(ret, id.SceneMovieInput()) } @@ -51,21 +51,7 @@ func (u *UpdateMovieIDs) AddUnique(v MoviesScenes) { u.Movies = append(u.Movies, v) } -func UpdateMovieIDsFromInput(i []*SceneMovieInput) (*UpdateMovieIDs, error) { - ret := &UpdateMovieIDs{ - Mode: RelationshipUpdateModeSet, - } - - var err error - ret.Movies, err = MoviesScenesFromInput(i) - if err != nil { - return nil, err - } - - return ret, nil -} - -func MoviesScenesFromInput(input []*SceneMovieInput) ([]MoviesScenes, error) { +func MoviesScenesFromInput(input []SceneMovieInput) ([]MoviesScenes, error) { ret := make([]MoviesScenes, len(input)) for i, v := range input { diff --git a/pkg/models/model_movie.go b/pkg/models/model_movie.go index cf7f997d887..152f0d3bbb5 100644 --- a/pkg/models/model_movie.go +++ b/pkg/models/model_movie.go @@ -20,6 +20,14 @@ type Movie struct { UpdatedAt time.Time `json:"updated_at"` } +func NewMovie() Movie { + currentTime := time.Now() + return Movie{ + CreatedAt: currentTime, + UpdatedAt: currentTime, + } +} + type MoviePartial struct { Name OptionalString Aliases OptionalString @@ -35,30 +43,11 @@ type MoviePartial struct { UpdatedAt OptionalTime } -var DefaultMovieImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC" - -func NewMovie(name string) *Movie { - currentTime := time.Now() - return &Movie{ - Name: name, - CreatedAt: currentTime, - UpdatedAt: currentTime, - } -} - func NewMoviePartial() MoviePartial { - updatedTime := time.Now() + currentTime := time.Now() return MoviePartial{ - UpdatedAt: NewOptionalTime(updatedTime), + UpdatedAt: NewOptionalTime(currentTime), } } -type Movies []*Movie - -func (m *Movies) Append(o interface{}) { - *m = append(*m, o.(*Movie)) -} - -func (m *Movies) New() interface{} { - return &Movie{} -} +var DefaultMovieImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC" diff --git a/pkg/models/model_performer.go b/pkg/models/model_performer.go index a620f306516..09f92e13c6d 100644 --- a/pkg/models/model_performer.go +++ b/pkg/models/model_performer.go @@ -41,38 +41,12 @@ type Performer struct { StashIDs RelatedStashIDs `json:"stash_ids"` } -func (s *Performer) LoadAliases(ctx context.Context, l AliasLoader) error { - return s.Aliases.load(func() ([]string, error) { - return l.GetAliases(ctx, s.ID) - }) -} - -func (s *Performer) LoadTagIDs(ctx context.Context, l TagIDLoader) error { - return s.TagIDs.load(func() ([]int, error) { - return l.GetTagIDs(ctx, s.ID) - }) -} - -func (s *Performer) LoadStashIDs(ctx context.Context, l StashIDLoader) error { - return s.StashIDs.load(func() ([]StashID, error) { - return l.GetStashIDs(ctx, s.ID) - }) -} - -func (s *Performer) LoadRelationships(ctx context.Context, l PerformerReader) error { - if err := s.LoadAliases(ctx, l); err != nil { - return err - } - - if err := s.LoadTagIDs(ctx, l); err != nil { - return err - } - - if err := s.LoadStashIDs(ctx, l); err != nil { - return err +func NewPerformer() Performer { + currentTime := time.Now() + return Performer{ + CreatedAt: currentTime, + UpdatedAt: currentTime, } - - return nil } // PerformerPartial represents part of a Performer object. It is used to update @@ -112,28 +86,43 @@ type PerformerPartial struct { StashIDs *UpdateStashIDs } -func NewPerformer(name string) *Performer { +func NewPerformerPartial() PerformerPartial { currentTime := time.Now() - return &Performer{ - Name: name, - CreatedAt: currentTime, - UpdatedAt: currentTime, + return PerformerPartial{ + UpdatedAt: NewOptionalTime(currentTime), } } -func NewPerformerPartial() PerformerPartial { - updatedTime := time.Now() - return PerformerPartial{ - UpdatedAt: NewOptionalTime(updatedTime), - } +func (s *Performer) LoadAliases(ctx context.Context, l AliasLoader) error { + return s.Aliases.load(func() ([]string, error) { + return l.GetAliases(ctx, s.ID) + }) } -type Performers []*Performer +func (s *Performer) LoadTagIDs(ctx context.Context, l TagIDLoader) error { + return s.TagIDs.load(func() ([]int, error) { + return l.GetTagIDs(ctx, s.ID) + }) +} -func (p *Performers) Append(o interface{}) { - *p = append(*p, o.(*Performer)) +func (s *Performer) LoadStashIDs(ctx context.Context, l StashIDLoader) error { + return s.StashIDs.load(func() ([]StashID, error) { + return l.GetStashIDs(ctx, s.ID) + }) } -func (p *Performers) New() interface{} { - return &Performer{} +func (s *Performer) LoadRelationships(ctx context.Context, l PerformerReader) error { + if err := s.LoadAliases(ctx, l); err != nil { + return err + } + + if err := s.LoadTagIDs(ctx, l); err != nil { + return err + } + + if err := s.LoadStashIDs(ctx, l); err != nil { + return err + } + + return nil } diff --git a/pkg/models/model_saved_filter.go b/pkg/models/model_saved_filter.go index 51c50be51d1..d680e7c95ef 100644 --- a/pkg/models/model_saved_filter.go +++ b/pkg/models/model_saved_filter.go @@ -67,13 +67,3 @@ type SavedFilter struct { ObjectFilter map[string]interface{} `json:"object_filter"` UIOptions map[string]interface{} `json:"ui_options"` } - -type SavedFilters []*SavedFilter - -func (m *SavedFilters) Append(o interface{}) { - *m = append(*m, o.(*SavedFilter)) -} - -func (m *SavedFilters) New() interface{} { - return &SavedFilter{} -} diff --git a/pkg/models/model_scene.go b/pkg/models/model_scene.go index eadbaab3a34..4cd434eed80 100644 --- a/pkg/models/model_scene.go +++ b/pkg/models/model_scene.go @@ -48,6 +48,50 @@ type Scene struct { StashIDs RelatedStashIDs `json:"stash_ids"` } +func NewScene() Scene { + currentTime := time.Now() + return Scene{ + CreatedAt: currentTime, + UpdatedAt: currentTime, + } +} + +// 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 + // Rating expressed in 1-100 scale + Rating OptionalInt + Organized OptionalBool + OCounter OptionalInt + StudioID OptionalInt + CreatedAt OptionalTime + UpdatedAt OptionalTime + ResumeTime OptionalFloat64 + PlayDuration OptionalFloat64 + PlayCount OptionalInt + LastPlayedAt OptionalTime + + URLs *UpdateStrings + GalleryIDs *UpdateIDs + TagIDs *UpdateIDs + PerformerIDs *UpdateIDs + MovieIDs *UpdateMovieIDs + StashIDs *UpdateStashIDs + PrimaryFileID *FileID +} + +func NewScenePartial() ScenePartial { + currentTime := time.Now() + return ScenePartial{ + UpdatedAt: NewOptionalTime(currentTime), + } +} + func (s *Scene) LoadURLs(ctx context.Context, l URLLoader) error { return s.URLs.load(func() ([]string, error) { return l.GetURLs(ctx, s.ID) @@ -145,77 +189,6 @@ func (s *Scene) LoadRelationships(ctx context.Context, l SceneReader) error { return nil } -// 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 - // Rating expressed in 1-100 scale - Rating OptionalInt - Organized OptionalBool - OCounter OptionalInt - StudioID OptionalInt - CreatedAt OptionalTime - UpdatedAt OptionalTime - ResumeTime OptionalFloat64 - PlayDuration OptionalFloat64 - PlayCount OptionalInt - LastPlayedAt OptionalTime - - URLs *UpdateStrings - GalleryIDs *UpdateIDs - TagIDs *UpdateIDs - PerformerIDs *UpdateIDs - MovieIDs *UpdateMovieIDs - StashIDs *UpdateStashIDs - PrimaryFileID *FileID -} - -func NewScenePartial() ScenePartial { - updatedTime := time.Now() - return ScenePartial{ - UpdatedAt: NewOptionalTime(updatedTime), - } -} - -type SceneMovieInput struct { - MovieID string `json:"movie_id"` - SceneIndex *int `json:"scene_index"` -} - -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 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"` - Urls []string `json:"urls"` - 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"` - ResumeTime *float64 `json:"resume_time"` - PlayDuration *float64 `json:"play_duration"` - PlayCount *int `json:"play_count"` - PrimaryFileID *string `json:"primary_file_id"` -} - // UpdateInput constructs a SceneUpdateInput using the populated fields in the ScenePartial object. func (s ScenePartial) UpdateInput(id int) SceneUpdateInput { var dateStr *string @@ -302,16 +275,6 @@ type SceneFileType struct { Bitrate *int `graphql:"bitrate" json:"bitrate"` } -type Scenes []*Scene - -func (s *Scenes) Append(o interface{}) { - *s = append(*s, o.(*Scene)) -} - -func (s *Scenes) New() interface{} { - return &Scene{} -} - type VideoCaption struct { LanguageCode string `json:"language_code"` Filename string `json:"filename"` diff --git a/pkg/models/model_scene_marker.go b/pkg/models/model_scene_marker.go index 1e9ac611589..df77afecd77 100644 --- a/pkg/models/model_scene_marker.go +++ b/pkg/models/model_scene_marker.go @@ -14,6 +14,14 @@ type SceneMarker struct { UpdatedAt time.Time `json:"updated_at"` } +func NewSceneMarker() SceneMarker { + currentTime := time.Now() + return SceneMarker{ + CreatedAt: currentTime, + UpdatedAt: currentTime, + } +} + // SceneMarkerPartial represents part of a SceneMarker object. // It is used to update the database entry. type SceneMarkerPartial struct { @@ -26,8 +34,8 @@ type SceneMarkerPartial struct { } func NewSceneMarkerPartial() SceneMarkerPartial { - updatedTime := time.Now() + currentTime := time.Now() return SceneMarkerPartial{ - UpdatedAt: NewOptionalTime(updatedTime), + UpdatedAt: NewOptionalTime(currentTime), } } diff --git a/pkg/models/model_scraped_item.go b/pkg/models/model_scraped_item.go index 97d403b10ef..cb383c082e7 100644 --- a/pkg/models/model_scraped_item.go +++ b/pkg/models/model_scraped_item.go @@ -3,7 +3,6 @@ package models import ( "context" "strconv" - "time" "github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stashapp/stash/pkg/utils" @@ -23,17 +22,12 @@ type ScrapedStudio struct { func (ScrapedStudio) IsScrapedContent() {} func (s *ScrapedStudio) ToStudio(endpoint string, excluded map[string]bool) *Studio { - now := time.Now() - // Populate a new studio from the input - newStudio := Studio{ - Name: s.Name, - CreatedAt: now, - UpdatedAt: now, - } + ret := NewStudio() + ret.Name = s.Name if s.RemoteSiteID != nil && endpoint != "" { - newStudio.StashIDs = NewRelatedStashIDs([]StashID{ + ret.StashIDs = NewRelatedStashIDs([]StashID{ { Endpoint: endpoint, StashID: *s.RemoteSiteID, @@ -42,15 +36,15 @@ func (s *ScrapedStudio) ToStudio(endpoint string, excluded map[string]bool) *Stu } if s.URL != nil && !excluded["url"] { - newStudio.URL = *s.URL + ret.URL = *s.URL } if s.Parent != nil && s.Parent.StoredID != nil && !excluded["parent"] && !excluded["parent_studio"] { parentId, _ := strconv.Atoi(*s.Parent.StoredID) - newStudio.ParentID = &parentId + ret.ParentID = &parentId } - return &newStudio + return &ret } func (s *ScrapedStudio) GetImage(ctx context.Context, excluded map[string]bool) ([]byte, error) { @@ -69,17 +63,15 @@ func (s *ScrapedStudio) GetImage(ctx context.Context, excluded map[string]bool) } func (s *ScrapedStudio) ToPartial(id *string, endpoint string, excluded map[string]bool, existingStashIDs []StashID) *StudioPartial { - partial := StudioPartial{ - UpdatedAt: NewOptionalTime(time.Now()), - } - partial.ID, _ = strconv.Atoi(*id) + ret := NewStudioPartial() + ret.ID, _ = strconv.Atoi(*id) if s.Name != "" && !excluded["name"] { - partial.Name = NewOptionalString(s.Name) + ret.Name = NewOptionalString(s.Name) } if s.URL != nil && !excluded["url"] { - partial.URL = NewOptionalString(*s.URL) + ret.URL = NewOptionalString(*s.URL) } if s.Parent != nil && !excluded["parent"] { @@ -87,25 +79,25 @@ func (s *ScrapedStudio) ToPartial(id *string, endpoint string, excluded map[stri parentID, _ := strconv.Atoi(*s.Parent.StoredID) if parentID > 0 { // This is to be set directly as we know it has a value and the translator won't have the field - partial.ParentID = NewOptionalInt(parentID) + ret.ParentID = NewOptionalInt(parentID) } } } else { - partial.ParentID = NewOptionalIntPtr(nil) + ret.ParentID = NewOptionalIntPtr(nil) } if s.RemoteSiteID != nil && endpoint != "" { - partial.StashIDs = &UpdateStashIDs{ + ret.StashIDs = &UpdateStashIDs{ StashIDs: existingStashIDs, Mode: RelationshipUpdateModeSet, } - partial.StashIDs.Set(StashID{ + ret.StashIDs.Set(StashID{ Endpoint: endpoint, StashID: *s.RemoteSiteID, }) } - return &partial + return &ret } // A performer from a scraping operation... @@ -145,7 +137,8 @@ type ScrapedPerformer struct { func (ScrapedPerformer) IsScrapedContent() {} func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool) *Performer { - ret := NewPerformer(*p.Name) + ret := NewPerformer() + ret.Name = *p.Name if p.Aliases != nil && !excluded["aliases"] { ret.Aliases = NewRelatedStrings(stringslice.FromString(*p.Aliases, ",")) @@ -244,7 +237,7 @@ func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool }) } - return ret + return &ret } func (p *ScrapedPerformer) GetImage(ctx context.Context, excluded map[string]bool) ([]byte, error) { @@ -263,10 +256,10 @@ func (p *ScrapedPerformer) GetImage(ctx context.Context, excluded map[string]boo } func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool, existingStashIDs []StashID) PerformerPartial { - partial := NewPerformerPartial() + ret := NewPerformerPartial() if p.Aliases != nil && !excluded["aliases"] { - partial.Aliases = &UpdateStrings{ + ret.Aliases = &UpdateStrings{ Values: stringslice.FromString(*p.Aliases, ","), Mode: RelationshipUpdateModeSet, } @@ -274,88 +267,88 @@ func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool, if p.Birthdate != nil && !excluded["birthdate"] { date, err := ParseDate(*p.Birthdate) if err == nil { - partial.Birthdate = NewOptionalDate(date) + ret.Birthdate = NewOptionalDate(date) } } if p.DeathDate != nil && !excluded["death_date"] { date, err := ParseDate(*p.DeathDate) if err == nil { - partial.DeathDate = NewOptionalDate(date) + ret.DeathDate = NewOptionalDate(date) } } if p.CareerLength != nil && !excluded["career_length"] { - partial.CareerLength = NewOptionalString(*p.CareerLength) + ret.CareerLength = NewOptionalString(*p.CareerLength) } if p.Country != nil && !excluded["country"] { - partial.Country = NewOptionalString(*p.Country) + ret.Country = NewOptionalString(*p.Country) } if p.Ethnicity != nil && !excluded["ethnicity"] { - partial.Ethnicity = NewOptionalString(*p.Ethnicity) + ret.Ethnicity = NewOptionalString(*p.Ethnicity) } if p.EyeColor != nil && !excluded["eye_color"] { - partial.EyeColor = NewOptionalString(*p.EyeColor) + ret.EyeColor = NewOptionalString(*p.EyeColor) } if p.HairColor != nil && !excluded["hair_color"] { - partial.HairColor = NewOptionalString(*p.HairColor) + ret.HairColor = NewOptionalString(*p.HairColor) } if p.FakeTits != nil && !excluded["fake_tits"] { - partial.FakeTits = NewOptionalString(*p.FakeTits) + ret.FakeTits = NewOptionalString(*p.FakeTits) } if p.Gender != nil && !excluded["gender"] { - partial.Gender = NewOptionalString(*p.Gender) + ret.Gender = NewOptionalString(*p.Gender) } if p.Height != nil && !excluded["height"] { h, err := strconv.Atoi(*p.Height) if err == nil { - partial.Height = NewOptionalInt(h) + ret.Height = NewOptionalInt(h) } } if p.Weight != nil && !excluded["weight"] { w, err := strconv.Atoi(*p.Weight) if err == nil { - partial.Weight = NewOptionalInt(w) + ret.Weight = NewOptionalInt(w) } } if p.Instagram != nil && !excluded["instagram"] { - partial.Instagram = NewOptionalString(*p.Instagram) + ret.Instagram = NewOptionalString(*p.Instagram) } if p.Measurements != nil && !excluded["measurements"] { - partial.Measurements = NewOptionalString(*p.Measurements) + ret.Measurements = NewOptionalString(*p.Measurements) } if p.Name != nil && !excluded["name"] { - partial.Name = NewOptionalString(*p.Name) + ret.Name = NewOptionalString(*p.Name) } if p.Disambiguation != nil && !excluded["disambiguation"] { - partial.Disambiguation = NewOptionalString(*p.Disambiguation) + ret.Disambiguation = NewOptionalString(*p.Disambiguation) } if p.Details != nil && !excluded["details"] { - partial.Details = NewOptionalString(*p.Details) + ret.Details = NewOptionalString(*p.Details) } if p.Piercings != nil && !excluded["piercings"] { - partial.Piercings = NewOptionalString(*p.Piercings) + ret.Piercings = NewOptionalString(*p.Piercings) } if p.Tattoos != nil && !excluded["tattoos"] { - partial.Tattoos = NewOptionalString(*p.Tattoos) + ret.Tattoos = NewOptionalString(*p.Tattoos) } if p.Twitter != nil && !excluded["twitter"] { - partial.Twitter = NewOptionalString(*p.Twitter) + ret.Twitter = NewOptionalString(*p.Twitter) } if p.URL != nil && !excluded["url"] { - partial.URL = NewOptionalString(*p.URL) + ret.URL = NewOptionalString(*p.URL) } if p.RemoteSiteID != nil && endpoint != "" { - partial.StashIDs = &UpdateStashIDs{ + ret.StashIDs = &UpdateStashIDs{ StashIDs: existingStashIDs, Mode: RelationshipUpdateModeSet, } - partial.StashIDs.Set(StashID{ + ret.StashIDs.Set(StashID{ Endpoint: endpoint, StashID: *p.RemoteSiteID, }) } - return partial + return ret } type ScrapedTag struct { diff --git a/pkg/models/model_studio.go b/pkg/models/model_studio.go index 9f1deca4974..109535be1b5 100644 --- a/pkg/models/model_studio.go +++ b/pkg/models/model_studio.go @@ -21,28 +21,12 @@ type Studio struct { StashIDs RelatedStashIDs `json:"stash_ids"` } -func (s *Studio) LoadAliases(ctx context.Context, l AliasLoader) error { - return s.Aliases.load(func() ([]string, error) { - return l.GetAliases(ctx, s.ID) - }) -} - -func (s *Studio) LoadStashIDs(ctx context.Context, l StashIDLoader) error { - return s.StashIDs.load(func() ([]StashID, error) { - return l.GetStashIDs(ctx, s.ID) - }) -} - -func (s *Studio) LoadRelationships(ctx context.Context, l PerformerReader) error { - if err := s.LoadAliases(ctx, l); err != nil { - return err +func NewStudio() Studio { + currentTime := time.Now() + return Studio{ + CreatedAt: currentTime, + UpdatedAt: currentTime, } - - if err := s.LoadStashIDs(ctx, l); err != nil { - return err - } - - return nil } // StudioPartial represents part of a Studio object. It is used to update the database entry. @@ -62,12 +46,33 @@ type StudioPartial struct { StashIDs *UpdateStashIDs } -type Studios []*Studio +func NewStudioPartial() StudioPartial { + currentTime := time.Now() + return StudioPartial{ + UpdatedAt: NewOptionalTime(currentTime), + } +} -func (s *Studios) Append(o interface{}) { - *s = append(*s, o.(*Studio)) +func (s *Studio) LoadAliases(ctx context.Context, l AliasLoader) error { + return s.Aliases.load(func() ([]string, error) { + return l.GetAliases(ctx, s.ID) + }) +} + +func (s *Studio) LoadStashIDs(ctx context.Context, l StashIDLoader) error { + return s.StashIDs.load(func() ([]StashID, error) { + return l.GetStashIDs(ctx, s.ID) + }) } -func (s *Studios) New() interface{} { - return &Studio{} +func (s *Studio) LoadRelationships(ctx context.Context, l PerformerReader) error { + if err := s.LoadAliases(ctx, l); err != nil { + return err + } + + if err := s.LoadStashIDs(ctx, l); err != nil { + return err + } + + return nil } diff --git a/pkg/models/model_tag.go b/pkg/models/model_tag.go index e07eee77287..f8c49c5321f 100644 --- a/pkg/models/model_tag.go +++ b/pkg/models/model_tag.go @@ -13,6 +13,14 @@ type Tag struct { UpdatedAt time.Time `json:"updated_at"` } +func NewTag() Tag { + currentTime := time.Now() + return Tag{ + CreatedAt: currentTime, + UpdatedAt: currentTime, + } +} + type TagPartial struct { Name OptionalString Description OptionalString @@ -21,43 +29,14 @@ type TagPartial struct { UpdatedAt OptionalTime } -type TagPath struct { - Tag - Path string `json:"path"` -} - -func NewTag(name string) *Tag { - currentTime := time.Now() - return &Tag{ - Name: name, - CreatedAt: currentTime, - UpdatedAt: currentTime, - } -} - func NewTagPartial() TagPartial { - updatedTime := time.Now() + currentTime := time.Now() return TagPartial{ - UpdatedAt: NewOptionalTime(updatedTime), + UpdatedAt: NewOptionalTime(currentTime), } } -type Tags []*Tag - -func (t *Tags) Append(o interface{}) { - *t = append(*t, o.(*Tag)) -} - -func (t *Tags) New() interface{} { - return &Tag{} -} - -type TagPaths []*TagPath - -func (t *TagPaths) Append(o interface{}) { - *t = append(*t, o.(*TagPath)) -} - -func (t *TagPaths) New() interface{} { - return &TagPath{} +type TagPath struct { + Tag + Path string `json:"path"` } diff --git a/pkg/models/performer.go b/pkg/models/performer.go index 752f1ce08e2..3097c0ebf3b 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -192,3 +192,76 @@ type PerformerFilterType struct { // Filter by updated at UpdatedAt *TimestampCriterionInput `json:"updated_at"` } + +type PerformerCreateInput struct { + Name string `json:"name"` + Disambiguation *string `json:"disambiguation"` + URL *string `json:"url"` + Gender *GenderEnum `json:"gender"` + Birthdate *string `json:"birthdate"` + Ethnicity *string `json:"ethnicity"` + Country *string `json:"country"` + EyeColor *string `json:"eye_color"` + Height *string `json:"height"` + HeightCm *int `json:"height_cm"` + Measurements *string `json:"measurements"` + FakeTits *string `json:"fake_tits"` + PenisLength *float64 `json:"penis_length"` + Circumcised *CircumisedEnum `json:"circumcised"` + CareerLength *string `json:"career_length"` + Tattoos *string `json:"tattoos"` + Piercings *string `json:"piercings"` + Aliases *string `json:"aliases"` + AliasList []string `json:"alias_list"` + Twitter *string `json:"twitter"` + Instagram *string `json:"instagram"` + Favorite *bool `json:"favorite"` + TagIds []string `json:"tag_ids"` + // This should be a URL or a base64 encoded data URL + Image *string `json:"image"` + StashIds []StashID `json:"stash_ids"` + Rating *int `json:"rating"` + Rating100 *int `json:"rating100"` + Details *string `json:"details"` + DeathDate *string `json:"death_date"` + HairColor *string `json:"hair_color"` + Weight *int `json:"weight"` + IgnoreAutoTag *bool `json:"ignore_auto_tag"` +} + +type PerformerUpdateInput struct { + ID string `json:"id"` + Name *string `json:"name"` + Disambiguation *string `json:"disambiguation"` + URL *string `json:"url"` + Gender *GenderEnum `json:"gender"` + Birthdate *string `json:"birthdate"` + Ethnicity *string `json:"ethnicity"` + Country *string `json:"country"` + EyeColor *string `json:"eye_color"` + Height *string `json:"height"` + HeightCm *int `json:"height_cm"` + Measurements *string `json:"measurements"` + FakeTits *string `json:"fake_tits"` + PenisLength *float64 `json:"penis_length"` + Circumcised *CircumisedEnum `json:"circumcised"` + CareerLength *string `json:"career_length"` + Tattoos *string `json:"tattoos"` + Piercings *string `json:"piercings"` + Aliases *string `json:"aliases"` + AliasList []string `json:"alias_list"` + Twitter *string `json:"twitter"` + Instagram *string `json:"instagram"` + Favorite *bool `json:"favorite"` + TagIds []string `json:"tag_ids"` + // This should be a URL or a base64 encoded data URL + Image *string `json:"image"` + StashIds []StashID `json:"stash_ids"` + Rating *int `json:"rating"` + Rating100 *int `json:"rating100"` + Details *string `json:"details"` + DeathDate *string `json:"death_date"` + HairColor *string `json:"hair_color"` + Weight *int `json:"weight"` + IgnoreAutoTag *bool `json:"ignore_auto_tag"` +} diff --git a/pkg/models/relationships.go b/pkg/models/relationships.go index 2e6f07708a0..2c2bc60b10b 100644 --- a/pkg/models/relationships.go +++ b/pkg/models/relationships.go @@ -208,6 +208,19 @@ func (r RelatedStashIDs) List() []StashID { return r.list } +// ForID returns the StashID object for the given endpoint. Returns nil if not found. +func (r *RelatedStashIDs) ForEndpoint(endpoint string) *StashID { + r.mustLoaded() + + for _, v := range r.list { + if v.Endpoint == endpoint { + return &v + } + } + + return nil +} + func (r *RelatedStashIDs) load(fn func() ([]StashID, error)) error { if r.Loaded() { return nil diff --git a/pkg/models/repository_gallery.go b/pkg/models/repository_gallery.go index 64019886cc7..45ad5beb710 100644 --- a/pkg/models/repository_gallery.go +++ b/pkg/models/repository_gallery.go @@ -63,6 +63,7 @@ type GalleryReader interface { GalleryQueryer GalleryCounter + URLLoader FileIDLoader ImageIDLoader SceneIDLoader diff --git a/pkg/models/repository_image.go b/pkg/models/repository_image.go index 5b191b2ab8c..1bf8ba440de 100644 --- a/pkg/models/repository_image.go +++ b/pkg/models/repository_image.go @@ -36,7 +36,7 @@ type ImageCounter interface { // ImageCreator provides methods to create images. type ImageCreator interface { - Create(ctx context.Context, newImage *ImageCreateInput) error + Create(ctx context.Context, newImage *Image, fileIDs []FileID) error } // ImageUpdater provides methods to update images. @@ -63,6 +63,7 @@ type ImageReader interface { ImageQueryer ImageCounter + URLLoader FileIDLoader GalleryIDLoader PerformerIDLoader diff --git a/pkg/models/repository_tag.go b/pkg/models/repository_tag.go index 6351c2bdfa6..ca8f6971bf7 100644 --- a/pkg/models/repository_tag.go +++ b/pkg/models/repository_tag.go @@ -42,6 +42,8 @@ type TagAutoTagQueryer interface { // TagCounter provides methods to count tags. type TagCounter interface { Count(ctx context.Context) (int, error) + CountByParentTagID(ctx context.Context, parentID int) (int, error) + CountByChildTagID(ctx context.Context, childID int) (int, error) } // TagCreator provides methods to create tags. diff --git a/pkg/models/scene.go b/pkg/models/scene.go index e66576f3599..09ac117ad9d 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -113,6 +113,64 @@ type SceneQueryResult struct { resolveErr error } +type SceneMovieInput struct { + MovieID string `json:"movie_id"` + SceneIndex *int `json:"scene_index"` +} + +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"` + Rating *int `json:"rating"` + 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"` + 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"` + // The first id will be assigned as primary. + // Files will be reassigned from existing scenes if applicable. + // Files must not already be primary for another scene. + FileIds []string `json:"file_ids"` +} + +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"` + Urls []string `json:"urls"` + Date *string `json:"date"` + Rating *int `json:"rating"` + 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"` + ResumeTime *float64 `json:"resume_time"` + PlayDuration *float64 `json:"play_duration"` + PlayCount *int `json:"play_count"` + PrimaryFileID *string `json:"primary_file_id"` +} + type SceneDestroyInput struct { ID string `json:"id"` DeleteFile *bool `json:"delete_file"` diff --git a/pkg/models/studio.go b/pkg/models/studio.go index 0973df4e316..2d743db4bb6 100644 --- a/pkg/models/studio.go +++ b/pkg/models/studio.go @@ -35,3 +35,32 @@ type StudioFilterType struct { // Filter by updated at UpdatedAt *TimestampCriterionInput `json:"updated_at"` } + +type StudioCreateInput struct { + Name string `json:"name"` + URL *string `json:"url"` + ParentID *string `json:"parent_id"` + // This should be a URL or a base64 encoded data URL + Image *string `json:"image"` + StashIds []StashID `json:"stash_ids"` + Rating *int `json:"rating"` + Rating100 *int `json:"rating100"` + Details *string `json:"details"` + Aliases []string `json:"aliases"` + IgnoreAutoTag *bool `json:"ignore_auto_tag"` +} + +type StudioUpdateInput struct { + ID string `json:"id"` + Name *string `json:"name"` + URL *string `json:"url"` + ParentID *string `json:"parent_id"` + // This should be a URL or a base64 encoded data URL + Image *string `json:"image"` + StashIds []StashID `json:"stash_ids"` + Rating *int `json:"rating"` + Rating100 *int `json:"rating100"` + Details *string `json:"details"` + Aliases []string `json:"aliases"` + IgnoreAutoTag *bool `json:"ignore_auto_tag"` +} diff --git a/pkg/movie/import.go b/pkg/movie/import.go index e231031e865..8004798ae53 100644 --- a/pkg/movie/import.go +++ b/pkg/movie/import.go @@ -109,11 +109,10 @@ func (i *Importer) populateStudio(ctx context.Context) error { } func (i *Importer) createStudio(ctx context.Context, name string) (int, error) { - newStudio := &models.Studio{ - Name: name, - } + newStudio := models.NewStudio() + newStudio.Name = name - err := i.StudioWriter.Create(ctx, newStudio) + err := i.StudioWriter.Create(ctx, &newStudio) if err != nil { return 0, err } diff --git a/pkg/performer/import.go b/pkg/performer/import.go index 1c3c075a447..9f57d97fe9a 100644 --- a/pkg/performer/import.go +++ b/pkg/performer/import.go @@ -101,14 +101,15 @@ func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names [] func createTags(ctx context.Context, tagWriter models.TagFinderCreator, names []string) ([]*models.Tag, error) { var ret []*models.Tag for _, name := range names { - newTag := models.NewTag(name) + newTag := models.NewTag() + newTag.Name = name - err := tagWriter.Create(ctx, newTag) + err := tagWriter.Create(ctx, &newTag) if err != nil { return nil, err } - ret = append(ret, newTag) + ret = append(ret, &newTag) } return ret, nil diff --git a/pkg/scene/generate/generator.go b/pkg/scene/generate/generator.go index 49568fb2aed..70f6857ea5d 100644 --- a/pkg/scene/generate/generator.go +++ b/pkg/scene/generate/generator.go @@ -97,7 +97,7 @@ func (g Generator) generateFile(lockCtx *fsutil.LockContext, p Paths, pattern st } if err := fsutil.SafeMove(tmpFn, output); err != nil { - return fmt.Errorf("moving %s to %s", tmpFn, output) + return fmt.Errorf("moving %s to %s failed: %w", tmpFn, output, err) } return nil diff --git a/pkg/scene/import.go b/pkg/scene/import.go index e2cfe8abaff..8c67cecdf39 100644 --- a/pkg/scene/import.go +++ b/pkg/scene/import.go @@ -169,11 +169,10 @@ func (i *Importer) populateStudio(ctx context.Context) error { } func (i *Importer) createStudio(ctx context.Context, name string) (int, error) { - newStudio := &models.Studio{ - Name: name, - } + newStudio := models.NewStudio() + newStudio.Name = name - err := i.StudioWriter.Create(ctx, newStudio) + err := i.StudioWriter.Create(ctx, &newStudio) if err != nil { return 0, err } @@ -279,7 +278,8 @@ func (i *Importer) populatePerformers(ctx context.Context) error { func (i *Importer) createPerformers(ctx context.Context, names []string) ([]*models.Performer, error) { var ret []*models.Performer for _, name := range names { - newPerformer := *models.NewPerformer(name) + newPerformer := models.NewPerformer() + newPerformer.Name = name err := i.PerformerWriter.Create(ctx, &newPerformer) if err != nil { @@ -338,9 +338,10 @@ func (i *Importer) populateMovies(ctx context.Context) error { } func (i *Importer) createMovie(ctx context.Context, name string) (int, error) { - newMovie := models.NewMovie(name) + newMovie := models.NewMovie() + newMovie.Name = name - err := i.MovieWriter.Create(ctx, newMovie) + err := i.MovieWriter.Create(ctx, &newMovie) if err != nil { return 0, err } @@ -468,14 +469,15 @@ func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names [] func createTags(ctx context.Context, tagWriter models.TagCreator, names []string) ([]*models.Tag, error) { var ret []*models.Tag for _, name := range names { - newTag := models.NewTag(name) + newTag := models.NewTag() + newTag.Name = name - err := tagWriter.Create(ctx, newTag) + err := tagWriter.Create(ctx, &newTag) if err != nil { return nil, err } - ret = append(ret, newTag) + ret = append(ret, &newTag) } return ret, nil diff --git a/pkg/scene/scan.go b/pkg/scene/scan.go index f16d0d5c61b..821485eb9a6 100644 --- a/pkg/scene/scan.go +++ b/pkg/scene/scan.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "time" "github.com/stashapp/stash/pkg/file/video" "github.com/stashapp/stash/pkg/logger" @@ -100,21 +99,17 @@ func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models. } } else { // create a new scene - now := time.Now() - newScene := &models.Scene{ - CreatedAt: now, - UpdatedAt: now, - } + newScene := models.NewScene() logger.Infof("%s doesn't exist. Creating new scene...", f.Base().Path) - if err := h.CreatorUpdater.Create(ctx, newScene, []models.FileID{videoFile.ID}); err != nil { + if err := h.CreatorUpdater.Create(ctx, &newScene, []models.FileID{videoFile.ID}); err != nil { return fmt.Errorf("creating new scene: %w", err) } h.PluginCache.RegisterPostHooks(ctx, newScene.ID, plugin.SceneCreatePost, nil, nil) - existing = []*models.Scene{newScene} + existing = []*models.Scene{&newScene} } if oldFile != nil { @@ -162,7 +157,8 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models. } // update updated_at time - if _, err := h.CreatorUpdater.UpdatePartial(ctx, s.ID, models.NewScenePartial()); err != nil { + scenePartial := models.NewScenePartial() + if _, err := h.CreatorUpdater.UpdatePartial(ctx, s.ID, scenePartial); err != nil { return fmt.Errorf("updating scene: %w", err) } } diff --git a/pkg/scene/update.go b/pkg/scene/update.go index f0a1a030f83..629fdedad47 100644 --- a/pkg/scene/update.go +++ b/pkg/scene/update.go @@ -74,32 +74,32 @@ func (u UpdateSet) UpdateInput() models.SceneUpdateInput { } func AddPerformer(ctx context.Context, qb models.SceneUpdater, o *models.Scene, performerID int) error { - _, err := qb.UpdatePartial(ctx, o.ID, models.ScenePartial{ - PerformerIDs: &models.UpdateIDs{ - IDs: []int{performerID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }) + scenePartial := models.NewScenePartial() + scenePartial.PerformerIDs = &models.UpdateIDs{ + IDs: []int{performerID}, + Mode: models.RelationshipUpdateModeAdd, + } + _, err := qb.UpdatePartial(ctx, o.ID, scenePartial) return err } func AddTag(ctx context.Context, qb models.SceneUpdater, o *models.Scene, tagID int) error { - _, err := qb.UpdatePartial(ctx, o.ID, models.ScenePartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{tagID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }) + scenePartial := models.NewScenePartial() + scenePartial.TagIDs = &models.UpdateIDs{ + IDs: []int{tagID}, + Mode: models.RelationshipUpdateModeAdd, + } + _, err := qb.UpdatePartial(ctx, o.ID, scenePartial) return err } func AddGallery(ctx context.Context, qb models.SceneUpdater, o *models.Scene, galleryID int) error { - _, err := qb.UpdatePartial(ctx, o.ID, models.ScenePartial{ - TagIDs: &models.UpdateIDs{ - IDs: []int{galleryID}, - Mode: models.RelationshipUpdateModeAdd, - }, - }) + scenePartial := models.NewScenePartial() + scenePartial.TagIDs = &models.UpdateIDs{ + IDs: []int{galleryID}, + Mode: models.RelationshipUpdateModeAdd, + } + _, err := qb.UpdatePartial(ctx, o.ID, scenePartial) return err } diff --git a/pkg/scraper/gallery.go b/pkg/scraper/gallery.go index db2c98755dd..db316409bcf 100644 --- a/pkg/scraper/gallery.go +++ b/pkg/scraper/gallery.go @@ -5,18 +5,24 @@ import "github.com/stashapp/stash/pkg/models" type ScrapedGallery struct { Title *string `json:"title"` Details *string `json:"details"` - URL *string `json:"url"` + URLs []string `json:"urls"` Date *string `json:"date"` Studio *models.ScrapedStudio `json:"studio"` Tags []*models.ScrapedTag `json:"tags"` Performers []*models.ScrapedPerformer `json:"performers"` + + // deprecated + URL *string `json:"url"` } func (ScrapedGallery) IsScrapedContent() {} type ScrapedGalleryInput struct { - Title *string `json:"title"` - Details *string `json:"details"` - URL *string `json:"url"` - Date *string `json:"date"` + Title *string `json:"title"` + Details *string `json:"details"` + URLs []string `json:"urls"` + Date *string `json:"date"` + + // deprecated + URL *string `json:"url"` } diff --git a/pkg/scraper/query_url.go b/pkg/scraper/query_url.go index 49cd08cf717..4bae01c06a8 100644 --- a/pkg/scraper/query_url.go +++ b/pkg/scraper/query_url.go @@ -66,8 +66,8 @@ func queryURLParametersFromGallery(gallery *models.Gallery) queryURLParameters { ret["title"] = gallery.Title } - if gallery.URL != "" { - ret["url"] = gallery.URL + if len(gallery.URLs.List()) > 0 { + ret["url"] = gallery.URLs.List()[0] } return ret diff --git a/pkg/scraper/stash.go b/pkg/scraper/stash.go index da204f347e5..b7f483667a3 100644 --- a/pkg/scraper/stash.go +++ b/pkg/scraper/stash.go @@ -354,11 +354,18 @@ func galleryToUpdateInput(gallery *models.Gallery) models.GalleryUpdateInput { // fallback to file basename if title is empty title := gallery.GetTitle() + var url *string + urls := gallery.URLs.List() + if len(urls) > 0 { + url = &urls[0] + } + return models.GalleryUpdateInput{ ID: strconv.Itoa(gallery.ID), Title: &title, Details: &gallery.Details, - URL: &gallery.URL, + URL: url, + Urls: urls, Date: dateToStringPtr(gallery.Date), } } diff --git a/pkg/sqlite/anonymise.go b/pkg/sqlite/anonymise.go index d8e6d99d6ad..7e4efd70299 100644 --- a/pkg/sqlite/anonymise.go +++ b/pkg/sqlite/anonymise.go @@ -368,7 +368,6 @@ func (db *Anonymiser) anonymiseImages(ctx context.Context) error { query := dialect.From(table).Select( table.Col(idColumn), table.Col("title"), - table.Col("url"), ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) gotSome = false @@ -378,20 +377,17 @@ func (db *Anonymiser) anonymiseImages(ctx context.Context) error { var ( id int title sql.NullString - url sql.NullString ) if err := rows.Scan( &id, &title, - &url, ); err != nil { return err } set := goqu.Record{} db.obfuscateNullString(set, "title", title) - db.obfuscateNullString(set, "url", url) if len(set) > 0 { stmt := dialect.Update(table).Set(set).Where(table.Col(idColumn).Eq(id)) @@ -416,6 +412,10 @@ func (db *Anonymiser) anonymiseImages(ctx context.Context) error { } } + if err := db.anonymiseURLs(ctx, goqu.T(imagesURLsTable), "image_id"); err != nil { + return err + } + return nil } diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index cd87a887c25..6b3b4171dcf 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -33,7 +33,7 @@ const ( dbConnTimeout = 30 ) -var appSchemaVersion uint = 49 +var appSchemaVersion uint = 51 //go:embed migrations/*.sql var migrationsBox embed.FS diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index 7bdf98bd31a..41729c9ad95 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -26,12 +26,13 @@ const ( galleriesImagesTable = "galleries_images" galleriesScenesTable = "scenes_galleries" galleryIDColumn = "gallery_id" + galleriesURLsTable = "gallery_urls" + galleriesURLColumn = "url" ) type galleryRow struct { ID int `db:"id" goqu:"skipinsert"` Title zero.String `db:"title"` - URL zero.String `db:"url"` Date NullDate `db:"date"` Details zero.String `db:"details"` // expressed as 1-100 @@ -46,7 +47,6 @@ type galleryRow struct { func (r *galleryRow) fromGallery(o models.Gallery) { r.ID = o.ID r.Title = zero.StringFrom(o.Title) - r.URL = zero.StringFrom(o.URL) r.Date = NullDateFromDatePtr(o.Date) r.Details = zero.StringFrom(o.Details) r.Rating = intFromPtr(o.Rating) @@ -70,7 +70,6 @@ func (r *galleryQueryRow) resolve() *models.Gallery { ret := &models.Gallery{ ID: r.ID, Title: r.Title.String, - URL: r.URL.String, Date: r.Date.DatePtr(), Details: r.Details.String, Rating: nullIntPtr(r.Rating), @@ -97,7 +96,6 @@ type galleryRowRecord struct { func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) { r.setNullString("title", o.Title) - r.setNullString("url", o.URL) r.setNullDate("date", o.Date) r.setNullString("details", o.Details) r.setNullInt("rating", o.Rating) @@ -178,6 +176,12 @@ func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, f } } + if newObject.URLs.Loaded() { + const startPos = 0 + if err := galleriesURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil { + return err + } + } if newObject.PerformerIDs.Loaded() { if err := galleriesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs.List()); err != nil { return err @@ -212,6 +216,11 @@ func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Galler return err } + if updatedObject.URLs.Loaded() { + if err := galleriesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil { + return err + } + } if updatedObject.PerformerIDs.Loaded() { if err := galleriesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil { return err @@ -257,6 +266,11 @@ func (qb *GalleryStore) UpdatePartial(ctx context.Context, id int, partial model } } + if partial.URLs != nil { + if err := galleriesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil { + return nil, err + } + } if partial.PerformerIDs != nil { if err := galleriesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil { return nil, err @@ -669,7 +683,7 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga 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, galleryURLsCriterionHandler(galleryFilter.URL)) query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil)) query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing)) query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags)) @@ -793,6 +807,18 @@ func (qb *GalleryStore) QueryCount(ctx context.Context, galleryFilter *models.Ga return query.executeCount(ctx) } +func galleryURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc { + h := stringListCriterionHandlerBuilder{ + joinTable: galleriesURLsTable, + stringColumn: galleriesURLColumn, + addJoinTable: func(f *filterBuilder) { + galleriesURLsTableMgr.join(f, "", "galleries.id") + }, + } + + return h.handler(url) +} + func (qb *GalleryStore) galleryPathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if c != nil { @@ -874,6 +900,9 @@ func galleryIsMissingCriterionHandler(qb *GalleryStore, isMissing *string) crite return func(ctx context.Context, f *filterBuilder) { if isMissing != nil && *isMissing != "" { switch *isMissing { + case "url": + galleriesURLsTableMgr.join(f, "", "galleries.id") + f.addWhere("gallery_urls.url IS NULL") case "scenes": f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id") f.addWhere("scenes_join.gallery_id IS NULL") @@ -1107,6 +1136,10 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F query.sortAndPagination += ", COALESCE(galleries.title, galleries.id) COLLATE NATURAL_CI ASC" } +func (qb *GalleryStore) GetURLs(ctx context.Context, galleryID int) ([]string, error) { + return galleriesURLsTableMgr.get(ctx, galleryID) +} + func (qb *GalleryStore) filesRepository() *filesRepository { return &filesRepository{ repository: repository{ diff --git a/pkg/sqlite/gallery_test.go b/pkg/sqlite/gallery_test.go index c8dbe02762f..9b1075ecaed 100644 --- a/pkg/sqlite/gallery_test.go +++ b/pkg/sqlite/gallery_test.go @@ -17,6 +17,11 @@ import ( var invalidID = -1 func loadGalleryRelationships(ctx context.Context, expected models.Gallery, actual *models.Gallery) error { + if expected.URLs.Loaded() { + if err := actual.LoadURLs(ctx, db.Gallery); err != nil { + return err + } + } if expected.SceneIDs.Loaded() { if err := actual.LoadSceneIDs(ctx, db.Gallery); err != nil { return err @@ -72,7 +77,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) { "full", models.Gallery{ Title: title, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Date: &date, Details: details, Rating: &rating, @@ -90,7 +95,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) { "with file", models.Gallery{ Title: title, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Date: &date, Details: details, Rating: &rating, @@ -222,7 +227,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) { &models.Gallery{ ID: galleryIDs[galleryIdxWithScene], Title: title, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Date: &date, Details: details, Rating: &rating, @@ -243,6 +248,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) { "clear nullables", &models.Gallery{ ID: galleryIDs[galleryIdxWithImage], + URLs: models.NewRelatedStrings([]string{}), SceneIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}), PerformerIDs: models.NewRelatedIDs([]int{}), @@ -384,7 +390,7 @@ func clearGalleryPartial() models.GalleryPartial { return models.GalleryPartial{ Title: models.OptionalString{Set: true, Null: true}, Details: models.OptionalString{Set: true, Null: true}, - URL: 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}, @@ -416,9 +422,12 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) { "full", galleryIDs[galleryIdxWithImage], models.GalleryPartial{ - Title: models.NewOptionalString(title), - Details: models.NewOptionalString(details), - URL: models.NewOptionalString(url), + Title: models.NewOptionalString(title), + Details: models.NewOptionalString(details), + URLs: &models.UpdateStrings{ + Values: []string{url}, + Mode: models.RelationshipUpdateModeSet, + }, Date: models.NewOptionalDate(date), Rating: models.NewOptionalInt(rating), Organized: models.NewOptionalBool(true), @@ -443,7 +452,7 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) { ID: galleryIDs[galleryIdxWithImage], Title: title, Details: details, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Date: &date, Rating: &rating, Organized: true, @@ -1653,7 +1662,13 @@ func TestGalleryQueryURL(t *testing.T) { verifyFn := func(g *models.Gallery) { t.Helper() - verifyString(t, g.URL, urlCriterion) + urls := g.URLs.List() + var url string + if len(urls) > 0 { + url = urls[0] + } + + verifyString(t, url, urlCriterion) } verifyGalleryQuery(t, filter, verifyFn) @@ -1683,6 +1698,12 @@ func verifyGalleryQuery(t *testing.T, filter models.GalleryFilterType, verifyFn galleries := queryGallery(ctx, t, sqb, &filter, nil) + for _, g := range galleries { + if err := g.LoadURLs(ctx, sqb); err != nil { + t.Errorf("Error loading gallery URLs: %v", err) + } + } + // assume it should find at least one assert.Greater(t, len(galleries), 0) diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index 35982642695..0ee12f0d947 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -24,27 +24,27 @@ const ( performersImagesTable = "performers_images" imagesTagsTable = "images_tags" imagesFilesTable = "images_files" + imagesURLsTable = "image_urls" + imageURLColumn = "url" ) type imageRow struct { ID int `db:"id" goqu:"skipinsert"` Title zero.String `db:"title"` // expressed as 1-100 - Rating null.Int `db:"rating"` - URL zero.String `db:"url"` - Date NullDate `db:"date"` - Organized bool `db:"organized"` - OCounter int `db:"o_counter"` - StudioID null.Int `db:"studio_id,omitempty"` - CreatedAt Timestamp `db:"created_at"` - UpdatedAt Timestamp `db:"updated_at"` + Rating null.Int `db:"rating"` + Date NullDate `db:"date"` + Organized bool `db:"organized"` + OCounter int `db:"o_counter"` + StudioID null.Int `db:"studio_id,omitempty"` + CreatedAt Timestamp `db:"created_at"` + UpdatedAt Timestamp `db:"updated_at"` } func (r *imageRow) fromImage(i models.Image) { r.ID = i.ID r.Title = zero.StringFrom(i.Title) r.Rating = intFromPtr(i.Rating) - r.URL = zero.StringFrom(i.URL) r.Date = NullDateFromDatePtr(i.Date) r.Organized = i.Organized r.OCounter = i.OCounter @@ -66,7 +66,6 @@ func (r *imageQueryRow) resolve() *models.Image { ID: r.ID, Title: r.Title.String, Rating: nullIntPtr(r.Rating), - URL: r.URL.String, Date: r.Date.DatePtr(), Organized: r.Organized, OCounter: r.OCounter, @@ -93,7 +92,6 @@ type imageRowRecord struct { func (r *imageRowRecord) fromPartial(i models.ImagePartial) { r.setNullString("title", i.Title) r.setNullInt("rating", i.Rating) - r.setNullString("url", i.URL) r.setNullDate("date", i.Date) r.setBool("organized", i.Organized) r.setInt("o_counter", i.OCounter) @@ -160,18 +158,25 @@ func (qb *ImageStore) selectDataset() *goqu.SelectDataset { ) } -func (qb *ImageStore) Create(ctx context.Context, newObject *models.ImageCreateInput) error { +func (qb *ImageStore) Create(ctx context.Context, newObject *models.Image, fileIDs []models.FileID) error { var r imageRow - r.fromImage(*newObject.Image) + r.fromImage(*newObject) id, err := qb.tableMgr.insertID(ctx, r) if err != nil { return err } - if len(newObject.FileIDs) > 0 { + if len(fileIDs) > 0 { const firstPrimary = true - if err := imagesFilesTableMgr.insertJoins(ctx, id, firstPrimary, newObject.FileIDs); err != nil { + if err := imagesFilesTableMgr.insertJoins(ctx, id, firstPrimary, fileIDs); err != nil { + return err + } + } + + if newObject.URLs.Loaded() { + const startPos = 0 + if err := imagesURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil { return err } } @@ -198,7 +203,7 @@ func (qb *ImageStore) Create(ctx context.Context, newObject *models.ImageCreateI return fmt.Errorf("finding after create: %w", err) } - *newObject.Image = *updated + *newObject = *updated return nil } @@ -223,6 +228,12 @@ func (qb *ImageStore) UpdatePartial(ctx context.Context, id int, partial models. return nil, err } } + + if partial.URLs != nil { + if err := imagesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil { + return nil, err + } + } if partial.PerformerIDs != nil { if err := imagesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil { return nil, err @@ -251,6 +262,12 @@ func (qb *ImageStore) Update(ctx context.Context, updatedObject *models.Image) e return err } + if updatedObject.URLs.Loaded() { + if err := imagesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil { + return err + } + } + if updatedObject.PerformerIDs.Loaded() { if err := imagesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil { return err @@ -664,7 +681,7 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", 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, imageURLsCriterionHandler(imageFilter.URL)) query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable)) query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing)) @@ -855,6 +872,18 @@ func imageIsMissingCriterionHandler(qb *ImageStore, isMissing *string) criterion } } +func imageURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc { + h := stringListCriterionHandlerBuilder{ + joinTable: imagesURLsTable, + stringColumn: imageURLColumn, + addJoinTable: func(f *filterBuilder) { + imagesURLsTableMgr.join(f, "", "images.id") + }, + } + + return h.handler(url) +} + func (qb *ImageStore) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder { return multiCriterionHandlerBuilder{ primaryTable: imageTable, @@ -1097,3 +1126,7 @@ func (qb *ImageStore) UpdateTags(ctx context.Context, imageID int, tagIDs []int) // Delete the existing joins and then create new ones return qb.tagsRepository().replace(ctx, imageID, tagIDs) } + +func (qb *ImageStore) GetURLs(ctx context.Context, imageID int) ([]string, error) { + return imagesURLsTableMgr.get(ctx, imageID) +} diff --git a/pkg/sqlite/image_test.go b/pkg/sqlite/image_test.go index 900c0b79456..7735cb5ec43 100644 --- a/pkg/sqlite/image_test.go +++ b/pkg/sqlite/image_test.go @@ -15,6 +15,11 @@ import ( ) func loadImageRelationships(ctx context.Context, expected models.Image, actual *models.Image) error { + if expected.URLs.Loaded() { + if err := actual.LoadURLs(ctx, db.Image); err != nil { + return err + } + } if expected.GalleryIDs.Loaded() { if err := actual.LoadGalleryIDs(ctx, db.Image); err != nil { return err @@ -74,7 +79,7 @@ func Test_imageQueryBuilder_Create(t *testing.T) { Title: title, Rating: &rating, Date: &date, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Organized: true, OCounter: ocounter, StudioID: &studioIDs[studioIdxWithImage], @@ -92,7 +97,7 @@ func Test_imageQueryBuilder_Create(t *testing.T) { Title: title, Rating: &rating, Date: &date, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Organized: true, OCounter: ocounter, StudioID: &studioIDs[studioIdxWithImage], @@ -152,10 +157,7 @@ func Test_imageQueryBuilder_Create(t *testing.T) { } } s := tt.newObject - if err := qb.Create(ctx, &models.ImageCreateInput{ - Image: &s, - FileIDs: fileIDs, - }); (err != nil) != tt.wantErr { + if err := qb.Create(ctx, &s, fileIDs); (err != nil) != tt.wantErr { t.Errorf("imageQueryBuilder.Create() error = %v, wantErr = %v", err, tt.wantErr) } @@ -232,7 +234,7 @@ func Test_imageQueryBuilder_Update(t *testing.T) { ID: imageIDs[imageIdxWithGallery], Title: title, Rating: &rating, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Date: &date, Organized: true, OCounter: ocounter, @@ -381,7 +383,7 @@ func clearImagePartial() models.ImagePartial { return models.ImagePartial{ Title: models.OptionalString{Set: true, Null: true}, Rating: models.OptionalInt{Set: true, Null: true}, - URL: models.OptionalString{Set: true, Null: true}, + URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet}, Date: models.OptionalDate{Set: true, Null: true}, StudioID: models.OptionalInt{Set: true, Null: true}, GalleryIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, @@ -412,9 +414,12 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) { "full", imageIDs[imageIdx1WithGallery], models.ImagePartial{ - Title: models.NewOptionalString(title), - Rating: models.NewOptionalInt(rating), - URL: models.NewOptionalString(url), + Title: models.NewOptionalString(title), + Rating: models.NewOptionalInt(rating), + URLs: &models.UpdateStrings{ + Values: []string{url}, + Mode: models.RelationshipUpdateModeSet, + }, Date: models.NewOptionalDate(date), Organized: models.NewOptionalBool(true), OCounter: models.NewOptionalInt(ocounter), @@ -438,7 +443,7 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) { ID: imageIDs[imageIdx1WithGallery], Title: title, Rating: &rating, - URL: url, + URLs: models.NewRelatedStrings([]string{url}), Date: &date, Organized: true, OCounter: ocounter, @@ -1522,6 +1527,67 @@ func imageQueryQ(ctx context.Context, t *testing.T, sqb models.ImageReader, q st assert.Len(t, images, totalImages) } +func verifyImageQuery(t *testing.T, filter models.ImageFilterType, verifyFn func(ctx context.Context, s *models.Image)) { + t.Helper() + withTxn(func(ctx context.Context) error { + t.Helper() + sqb := db.Image + + images := queryImages(ctx, t, sqb, &filter, nil) + + // assume it should find at least one + assert.Greater(t, len(images), 0) + + for _, image := range images { + verifyFn(ctx, image) + } + + return nil + }) +} + +func TestImageQueryURL(t *testing.T) { + const imageIdx = 1 + imageURL := getImageStringValue(imageIdx, urlField) + urlCriterion := models.StringCriterionInput{ + Value: imageURL, + Modifier: models.CriterionModifierEquals, + } + filter := models.ImageFilterType{ + URL: &urlCriterion, + } + + verifyFn := func(ctx context.Context, o *models.Image) { + t.Helper() + + if err := o.LoadURLs(ctx, db.Image); err != nil { + t.Errorf("Error loading scene URLs: %v", err) + } + + urls := o.URLs.List() + var url string + if len(urls) > 0 { + url = urls[0] + } + + verifyString(t, url, urlCriterion) + } + + verifyImageQuery(t, filter, verifyFn) + urlCriterion.Modifier = models.CriterionModifierNotEquals + verifyImageQuery(t, filter, verifyFn) + urlCriterion.Modifier = models.CriterionModifierMatchesRegex + urlCriterion.Value = "image_.*1_URL" + verifyImageQuery(t, filter, verifyFn) + urlCriterion.Modifier = models.CriterionModifierNotMatchesRegex + verifyImageQuery(t, filter, verifyFn) + urlCriterion.Modifier = models.CriterionModifierIsNull + urlCriterion.Value = "" + verifyImageQuery(t, filter, verifyFn) + urlCriterion.Modifier = models.CriterionModifierNotNull + verifyImageQuery(t, filter, verifyFn) +} + func TestImageQueryPath(t *testing.T) { const imageIdx = 1 imagePath := getFilePath(folderIdxWithImageFiles, getImageBasename(imageIdx)) diff --git a/pkg/sqlite/migrations/48_premigrate.go b/pkg/sqlite/migrations/48_premigrate.go index b16c2258f9d..f0e59620e04 100644 --- a/pkg/sqlite/migrations/48_premigrate.go +++ b/pkg/sqlite/migrations/48_premigrate.go @@ -130,7 +130,7 @@ func (m *schema48PreMigrator) fixStudioNames(ctx context.Context) error { } } - logger.Info("Renaming duplicate studio id %d to %s", id, newName) + logger.Infof("Renaming duplicate studio id %d to %s", id, newName) _, err := m.db.Exec("UPDATE studios SET name = ? WHERE id = ?", newName, id) if err != nil { return err diff --git a/pkg/sqlite/migrations/49_postmigrate.go b/pkg/sqlite/migrations/49_postmigrate.go index 941cf6a8802..c39bc5f3191 100644 --- a/pkg/sqlite/migrations/49_postmigrate.go +++ b/pkg/sqlite/migrations/49_postmigrate.go @@ -133,17 +133,17 @@ func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error { newFindFilter, err := m.getFindFilter(asRawMessage) if err != nil { - return fmt.Errorf("failed to get find filter for saved filter %d: %w", id, err) + return fmt.Errorf("failed to get find filter for saved filter %s : %w", findFilter, err) } objectFilter, err := m.getObjectFilter(mode, asRawMessage) if err != nil { - return fmt.Errorf("failed to get object filter for saved filter %d: %w", id, err) + return fmt.Errorf("failed to get object filter for saved filter %s : %w", findFilter, err) } uiOptions, err := m.getDisplayOptions(asRawMessage) if err != nil { - return fmt.Errorf("failed to get display options for saved filter %d: %w", id, err) + return fmt.Errorf("failed to get display options for saved filter %s : %w", findFilter, err) } _, err = m.db.Exec("UPDATE saved_filters SET find_filter = ?, object_filter = ?, ui_options = ? WHERE id = ?", newFindFilter, objectFilter, uiOptions, id) @@ -252,23 +252,29 @@ func (m *schema49Migrator) convertCriterion(mode models.FilterMode, out map[stri } delete(ret, "type") - // Find out whether the object needs some adjustment/has non-string content attached - // Only adjust if value is present - if v, ok := ret["value"]; ok && v != nil { - var err error - switch { - case arrayContains(migrate49TypeResolution["Boolean"], field): - ret["value"], err = m.adjustCriterionValue(ret["value"], "bool") - case arrayContains(migrate49TypeResolution["Int"], field): - ret["value"], err = m.adjustCriterionValue(ret["value"], "int") - case arrayContains(migrate49TypeResolution["Float"], field): - ret["value"], err = m.adjustCriterionValue(ret["value"], "float64") - case arrayContains(migrate49TypeResolution["Object"], field): - ret["value"], err = m.adjustCriterionValue(ret["value"], "object") - } + // unset the value for IS_NULL or NOT_NULL modifiers + modifier := models.CriterionModifier(ret["modifier"].(string)) + if modifier == models.CriterionModifierIsNull || modifier == models.CriterionModifierNotNull { + delete(ret, "value") + } else { + // Find out whether the object needs some adjustment/has non-string content attached + // Only adjust if value is present + if v, ok := ret["value"]; ok && v != nil { + var err error + switch { + case arrayContains(migrate49TypeResolution["Boolean"], field): + ret["value"], err = m.adjustCriterionValue(ret["value"], "bool") + case arrayContains(migrate49TypeResolution["Int"], field): + ret["value"], err = m.adjustCriterionValue(ret["value"], "int") + case arrayContains(migrate49TypeResolution["Float"], field): + ret["value"], err = m.adjustCriterionValue(ret["value"], "float64") + case arrayContains(migrate49TypeResolution["Object"], field): + ret["value"], err = m.adjustCriterionValue(ret["value"], "object") + } - if err != nil { - return fmt.Errorf("failed to adjust criterion value for %q: %w", field, err) + if err != nil { + return fmt.Errorf("failed to adjust criterion value for %q: %w", field, err) + } } } diff --git a/pkg/sqlite/migrations/50_image_urls.up.sql b/pkg/sqlite/migrations/50_image_urls.up.sql new file mode 100644 index 00000000000..47ff373075b --- /dev/null +++ b/pkg/sqlite/migrations/50_image_urls.up.sql @@ -0,0 +1,70 @@ +PRAGMA foreign_keys=OFF; + +CREATE TABLE `image_urls` ( + `image_id` integer NOT NULL, + `position` integer NOT NULL, + `url` varchar(255) NOT NULL, + foreign key(`image_id`) references `images`(`id`) on delete CASCADE, + PRIMARY KEY(`image_id`, `position`, `url`) +); + +CREATE INDEX `image_urls_url` on `image_urls` (`url`); + +-- drop url +CREATE TABLE "images_new" ( + `id` integer not null primary key autoincrement, + `title` varchar(255), + `rating` tinyint, + `studio_id` integer, + `o_counter` tinyint not null default 0, + `organized` boolean not null default '0', + `created_at` datetime not null, + `updated_at` datetime not null, + `date` date, + foreign key(`studio_id`) references `studios`(`id`) on delete SET NULL +); + +INSERT INTO `images_new` + ( + `id`, + `title`, + `rating`, + `studio_id`, + `o_counter`, + `organized`, + `created_at`, + `updated_at`, + `date` + ) + SELECT + `id`, + `title`, + `rating`, + `studio_id`, + `o_counter`, + `organized`, + `created_at`, + `updated_at`, + `date` + FROM `images`; + +INSERT INTO `image_urls` + ( + `image_id`, + `position`, + `url` + ) + SELECT + `id`, + '0', + `url` + FROM `images` + WHERE `images`.`url` IS NOT NULL AND `images`.`url` != ''; + +DROP INDEX `index_images_on_studio_id`; +DROP TABLE `images`; +ALTER TABLE `images_new` rename to `images`; + +CREATE INDEX `index_images_on_studio_id` on `images` (`studio_id`); + +PRAGMA foreign_keys=ON; diff --git a/pkg/sqlite/migrations/51_gallery_urls.up.sql b/pkg/sqlite/migrations/51_gallery_urls.up.sql new file mode 100644 index 00000000000..b72ee600c15 --- /dev/null +++ b/pkg/sqlite/migrations/51_gallery_urls.up.sql @@ -0,0 +1,76 @@ +PRAGMA foreign_keys=OFF; + +CREATE TABLE `gallery_urls` ( + `gallery_id` integer NOT NULL, + `position` integer NOT NULL, + `url` varchar(255) NOT NULL, + foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE, + PRIMARY KEY(`gallery_id`, `position`, `url`) +); + +CREATE INDEX `gallery_urls_url` on `gallery_urls` (`url`); + +-- drop url +CREATE TABLE `galleries_new` ( + `id` integer not null primary key autoincrement, + `folder_id` integer, + `title` varchar(255), + `date` date, + `details` text, + `studio_id` integer, + `rating` tinyint, + `organized` boolean not null default '0', + `created_at` datetime not null, + `updated_at` datetime not null, + foreign key(`studio_id`) references `studios`(`id`) on delete SET NULL, + foreign key(`folder_id`) references `folders`(`id`) on delete SET NULL +); + +INSERT INTO `galleries_new` + ( + `id`, + `folder_id`, + `title`, + `date`, + `details`, + `studio_id`, + `rating`, + `organized`, + `created_at`, + `updated_at` + ) + SELECT + `id`, + `folder_id`, + `title`, + `date`, + `details`, + `studio_id`, + `rating`, + `organized`, + `created_at`, + `updated_at` + FROM `galleries`; + +INSERT INTO `gallery_urls` + ( + `gallery_id`, + `position`, + `url` + ) + SELECT + `id`, + '0', + `url` + FROM `galleries` + WHERE `galleries`.`url` IS NOT NULL AND `galleries`.`url` != ''; + +DROP INDEX `index_galleries_on_studio_id`; +DROP INDEX `index_galleries_on_folder_id_unique`; +DROP TABLE `galleries`; +ALTER TABLE `galleries_new` rename to `galleries`; + +CREATE INDEX `index_galleries_on_studio_id` on `galleries` (`studio_id`); +CREATE UNIQUE INDEX `index_galleries_on_folder_id_unique` on `galleries` (`folder_id`); + +PRAGMA foreign_keys=ON; diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 215c1740953..e9f1f24dd18 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -1291,6 +1291,9 @@ func sceneIsMissingCriterionHandler(qb *SceneStore, isMissing *string) criterion return func(ctx context.Context, f *filterBuilder) { if isMissing != nil && *isMissing != "" { switch *isMissing { + case "url": + scenesURLsTableMgr.join(f, "", "scenes.id") + f.addWhere("scene_urls.url IS NULL") case "galleries": qb.galleriesRepository().join(f, "galleries_join", "scenes.id") f.addWhere("galleries_join.scene_id IS NULL") diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 737a28e7230..9ca886ac4d2 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -1113,6 +1113,19 @@ func getImageStringValue(index int, field string) string { return fmt.Sprintf("image_%04d_%s", index, field) } +func getImageNullStringPtr(index int, field string) *string { + return getStringPtrFromNullString(getPrefixedNullStringValue("image", index, field)) +} + +func getImageEmptyString(index int, field string) string { + v := getImageNullStringPtr(index, field) + if v == nil { + return "" + } + + return *v +} + func getImageBasename(index int) string { return getImageStringValue(index, pathField) } @@ -1148,10 +1161,12 @@ func makeImage(i int) *models.Image { tids := indexesToIDs(tagIDs, imageTags[i]) return &models.Image{ - Title: title, - Rating: getIntPtr(getRating(i)), - Date: getObjectDate(i), - URL: getImageStringValue(i, urlField), + Title: title, + Rating: getIntPtr(getRating(i)), + Date: getObjectDate(i), + URLs: models.NewRelatedStrings([]string{ + getImageEmptyString(i, urlField), + }), OCounter: getOCounter(i), StudioID: studioID, GalleryIDs: models.NewRelatedIDs(gids), @@ -1177,10 +1192,7 @@ func createImages(ctx context.Context, n int) error { image := makeImage(i) - err := qb.Create(ctx, &models.ImageCreateInput{ - Image: image, - FileIDs: []models.FileID{f.ID}, - }) + err := qb.Create(ctx, image, []models.FileID{f.ID}) if err != nil { return fmt.Errorf("Error creating image %v+: %s", image, err.Error()) @@ -1201,7 +1213,16 @@ func getGalleryNullStringValue(index int, field string) sql.NullString { } func getGalleryNullStringPtr(index int, field string) *string { - return getStringPtr(getPrefixedStringValue("gallery", index, field)) + return getStringPtrFromNullString(getPrefixedNullStringValue("gallery", index, field)) +} + +func getGalleryEmptyString(index int, field string) string { + v := getGalleryNullStringPtr(index, field) + if v == nil { + return "" + } + + return *v } func getGalleryBasename(index int) string { @@ -1233,8 +1254,10 @@ func makeGallery(i int, includeScenes bool) *models.Gallery { tids := indexesToIDs(tagIDs, galleryTags[i]) ret := &models.Gallery{ - Title: getGalleryStringValue(i, titleField), - URL: getGalleryNullStringValue(i, urlField).String, + Title: getGalleryStringValue(i, titleField), + URLs: models.NewRelatedStrings([]string{ + getGalleryEmptyString(i, urlField), + }), Rating: getIntPtr(getRating(i)), Date: getObjectDate(i), StudioID: studioID, diff --git a/pkg/sqlite/tables.go b/pkg/sqlite/tables.go index 69dc1d6a89f..3575030337c 100644 --- a/pkg/sqlite/tables.go +++ b/pkg/sqlite/tables.go @@ -13,11 +13,13 @@ var ( imagesTagsJoinTable = goqu.T(imagesTagsTable) performersImagesJoinTable = goqu.T(performersImagesTable) imagesFilesJoinTable = goqu.T(imagesFilesTable) + imagesURLsJoinTable = goqu.T(imagesURLsTable) galleriesFilesJoinTable = goqu.T(galleriesFilesTable) galleriesTagsJoinTable = goqu.T(galleriesTagsTable) performersGalleriesJoinTable = goqu.T(performersGalleriesTable) galleriesScenesJoinTable = goqu.T(galleriesScenesTable) + galleriesURLsJoinTable = goqu.T(galleriesURLsTable) scenesFilesJoinTable = goqu.T(scenesFilesTable) scenesTagsJoinTable = goqu.T(scenesTagsTable) @@ -70,6 +72,14 @@ var ( }, fkColumn: performersImagesJoinTable.Col(performerIDColumn), } + + imagesURLsTableMgr = &orderedValueTable[string]{ + table: table{ + table: imagesURLsJoinTable, + idColumn: imagesURLsJoinTable.Col(imageIDColumn), + }, + valueColumn: imagesURLsJoinTable.Col(imageURLColumn), + } ) var ( @@ -113,6 +123,14 @@ var ( table: goqu.T(galleriesChaptersTable), idColumn: goqu.T(galleriesChaptersTable).Col(idColumn), } + + galleriesURLsTableMgr = &orderedValueTable[string]{ + table: table{ + table: galleriesURLsJoinTable, + idColumn: galleriesURLsJoinTable.Col(galleryIDColumn), + }, + valueColumn: galleriesURLsJoinTable.Col(galleriesURLColumn), + } ) var ( diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index ce09da4464f..ace5f8346da 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -396,6 +396,20 @@ func (qb *TagStore) FindByChildTagID(ctx context.Context, parentID int) ([]*mode return qb.queryTags(ctx, query, args) } +func (qb *TagStore) CountByParentTagID(ctx context.Context, parentID int) (int, error) { + q := dialect.Select(goqu.COUNT("*")).From(goqu.T("tags")). + InnerJoin(goqu.T("tags_relations"), goqu.On(goqu.I("tags_relations.parent_id").Eq(goqu.I("tags.id")))). + Where(goqu.I("tags_relations.child_id").Eq(goqu.V(parentID))) // Pass the parentID here + return count(ctx, q) +} + +func (qb *TagStore) CountByChildTagID(ctx context.Context, childID int) (int, error) { + q := dialect.Select(goqu.COUNT("*")).From(goqu.T("tags")). + InnerJoin(goqu.T("tags_relations"), goqu.On(goqu.I("tags_relations.child_id").Eq(goqu.I("tags.id")))). + Where(goqu.I("tags_relations.parent_id").Eq(goqu.V(childID))) // Pass the childID here + return count(ctx, q) +} + func (qb *TagStore) Count(ctx context.Context) (int, error) { q := dialect.Select(goqu.COUNT("*")).From(qb.table()) return count(ctx, q) @@ -890,9 +904,9 @@ func (qb *TagStore) queryTags(ctx context.Context, query string, args []interfac return ret, nil } -func (qb *TagStore) queryTagPaths(ctx context.Context, query string, args []interface{}) (models.TagPaths, error) { +func (qb *TagStore) queryTagPaths(ctx context.Context, query string, args []interface{}) ([]*models.TagPath, error) { const single = false - var ret models.TagPaths + var ret []*models.TagPath if err := qb.queryFunc(ctx, query, args, single, func(r *sqlx.Rows) error { var f tagPathRow if err := r.StructScan(&f); err != nil { diff --git a/pkg/studio/import.go b/pkg/studio/import.go index df712daab79..1af5ec5c3e0 100644 --- a/pkg/studio/import.go +++ b/pkg/studio/import.go @@ -77,11 +77,10 @@ func (i *Importer) populateParentStudio(ctx context.Context) error { } func (i *Importer) createParentStudio(ctx context.Context, name string) (int, error) { - newStudio := &models.Studio{ - Name: name, - } + newStudio := models.NewStudio() + newStudio.Name = name - err := i.ReaderWriter.Create(ctx, newStudio) + err := i.ReaderWriter.Create(ctx, &newStudio) if err != nil { return 0, err } diff --git a/pkg/tag/import.go b/pkg/tag/import.go index 368815bbe44..6905d15ad73 100644 --- a/pkg/tag/import.go +++ b/pkg/tag/import.go @@ -151,9 +151,10 @@ func (i *Importer) getParents(ctx context.Context) ([]int, error) { } func (i *Importer) createParent(ctx context.Context, name string) (int, error) { - newTag := models.NewTag(name) + newTag := models.NewTag() + newTag.Name = name - err := i.ReaderWriter.Create(ctx, newTag) + err := i.ReaderWriter.Create(ctx, &newTag) if err != nil { return 0, err } diff --git a/scripts/cross-compile.sh b/scripts/cross-compile.sh index 07d187587bd..2b7e2786fb9 100755 --- a/scripts/cross-compile.sh +++ b/scripts/cross-compile.sh @@ -2,7 +2,7 @@ COMPILER_CONTAINER="stashapp/compiler:7" -BUILD_DATE=`go run -mod=vendor scripts/getDate.go` +BUILD_DATE=`go run scripts/getDate.go` GITHASH=`git rev-parse --short HEAD` STASH_VERSION=`git describe --tags --exclude latest_develop` diff --git a/ui/v2.5/.eslintrc.json b/ui/v2.5/.eslintrc.json index edce4355156..55e4d902dd8 100644 --- a/ui/v2.5/.eslintrc.json +++ b/ui/v2.5/.eslintrc.json @@ -53,10 +53,6 @@ "import/namespace": "off", "import/no-unresolved": "off", "react/display-name": "off", - "react-hooks/exhaustive-deps": [ - "error", - { "additionalHooks": "^(useDebounce)$" } - ], "react/prop-types": "off", "react/style-prop-object": [ "error", diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index 88fe37f2aae..c62b5b7833a 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -5,7 +5,7 @@ import * as GQL from "src/core/generated-graphql"; import { GridCard } from "../Shared/GridCard"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; -import { TagLink } from "../Shared/TagLink"; +import { SceneLink, TagLink } from "../Shared/TagLink"; import { TruncatedText } from "../Shared/TruncatedText"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { PopoverCountButton } from "../Shared/PopoverCountButton"; @@ -31,7 +31,7 @@ export const GalleryCard: React.FC = (props) => { if (props.gallery.scenes.length === 0) return; const popoverContent = props.gallery.scenes.map((scene) => ( - + )); return ( @@ -52,7 +52,7 @@ export const GalleryCard: React.FC = (props) => { if (props.gallery.tags.length <= 0) return; const popoverContent = props.gallery.tags.map((tag) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx index b423b11048c..e007f2f1f0f 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx @@ -30,7 +30,7 @@ export const GalleryAddPanel: React.FC = ({ // if galleries is already present, then we modify it, otherwise add let galleryCriterion = filter.criteria.find((c) => { return c.criterionOption.type === "galleries"; - }) as GalleriesCriterion; + }) as GalleriesCriterion | undefined; if ( galleryCriterion && diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx index 463ced50611..83ffe2bc3d3 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -34,7 +34,7 @@ export const GalleryDetailPanel: React.FC = ({ function renderTags() { if (gallery.tags.length === 0) return; const tags = gallery.tags.map((tag) => ( - + )); return ( <> diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index 1701b5bc7e1..3d40c3c6a78 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -25,7 +25,7 @@ import { } from "src/components/Shared/Select"; import { Icon } from "src/components/Shared/Icon"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; -import { URLField } from "src/components/Shared/URLField"; +import { URLListInput } from "src/components/Shared/URLField"; import { useToast } from "src/hooks/Toast"; import { useFormik } from "formik"; import FormUtils from "src/utils/form"; @@ -42,6 +42,7 @@ import { Performer, PerformerSelect, } from "src/components/Performers/PerformerSelect"; +import { yupDateString, yupUniqueStringList } from "src/utils/yup"; interface IProps { gallery: Partial; @@ -84,20 +85,8 @@ export const GalleryEditPanel: React.FC = ({ const schema = yup.object({ title: titleRequired ? yup.string().required() : yup.string().ensure(), - url: yup.string().ensure(), - date: yup - .string() - .ensure() - .test({ - name: "date", - test: (value) => { - if (!value) return true; - if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false; - if (Number.isNaN(Date.parse(value))) return false; - return true; - }, - message: intl.formatMessage({ id: "validation.date_invalid_form" }), - }), + urls: yupUniqueStringList("urls"), + date: yupDateString(intl), rating100: yup.number().nullable().defined(), studio_id: yup.string().required().nullable(), performer_ids: yup.array(yup.string().required()).defined(), @@ -108,7 +97,7 @@ export const GalleryEditPanel: React.FC = ({ const initialValues = { title: gallery?.title ?? "", - url: gallery?.url ?? "", + urls: gallery?.urls ?? [], date: gallery?.date ?? "", rating100: gallery?.rating100 ?? null, studio_id: gallery?.studio?.id ?? null, @@ -313,8 +302,8 @@ export const GalleryEditPanel: React.FC = ({ formik.setFieldValue("date", galleryData.date); } - if (galleryData.url) { - formik.setFieldValue("url", galleryData.url); + if (galleryData.urls) { + formik.setFieldValue("url", galleryData.urls); } if (galleryData.studio?.stored_id) { @@ -351,13 +340,13 @@ export const GalleryEditPanel: React.FC = ({ } } - async function onScrapeGalleryURL() { - if (!formik.values.url) { + async function onScrapeGalleryURL(url: string) { + if (!url) { return; } setIsLoading(true); try { - const result = await queryScrapeGalleryURL(formik.values.url); + const result = await queryScrapeGalleryURL(url); if (!result || !result.data || !result.data.scrapeGalleryURL) { return; } @@ -392,6 +381,14 @@ export const GalleryEditPanel: React.FC = ({ if (isLoading) return ; + const urlsErrors = Array.isArray(formik.errors.urls) + ? formik.errors.urls[0] + : formik.errors.urls; + const urlsErrorMsg = urlsErrors + ? intl.formatMessage({ id: "validation.urls_must_be_unique" }) + : undefined; + const urlsErrorIdx = urlsErrors?.split(" ").map((e) => parseInt(e)); + return (