From 2afa0ed920471ed34b0c89e49529eeab397e3cc0 Mon Sep 17 00:00:00 2001 From: 7dJx1qP <38586902+7dJx1qP@users.noreply.github.com> Date: Thu, 4 Nov 2021 20:47:53 -0400 Subject: [PATCH 1/3] add support for additional performer urls --- graphql/documents/data/performer-slim.graphql | 1 + graphql/documents/data/performer.graphql | 1 + graphql/schema/types/performer.graphql | 3 + pkg/api/resolver_model_performer.go | 11 +++ pkg/api/resolver_mutation_performer.go | 7 ++ pkg/database/database.go | 2 +- .../migrations/29_performers_urls.up.sql | 7 ++ pkg/manager/jsonschema/performer.go | 1 + pkg/models/mocks/PerformerReaderWriter.go | 37 ++++++++ pkg/models/performer.go | 2 + pkg/sqlite/performer.go | 21 +++++ .../PerformerDetailsPanel.tsx | 26 ++++++ .../PerformerDetails/PerformerEditPanel.tsx | 88 ++++++++++++++++++- 13 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 pkg/database/migrations/29_performers_urls.up.sql diff --git a/graphql/documents/data/performer-slim.graphql b/graphql/documents/data/performer-slim.graphql index 1420d15c458..8c42b15fd73 100644 --- a/graphql/documents/data/performer-slim.graphql +++ b/graphql/documents/data/performer-slim.graphql @@ -13,4 +13,5 @@ fragment SlimPerformerData on Performer { stash_id } rating + urls } diff --git a/graphql/documents/data/performer.graphql b/graphql/documents/data/performer.graphql index 34ff0279de1..f88928f2a1b 100644 --- a/graphql/documents/data/performer.graphql +++ b/graphql/documents/data/performer.graphql @@ -37,4 +37,5 @@ fragment PerformerData on Performer { death_date hair_color weight + urls } diff --git a/graphql/schema/types/performer.graphql b/graphql/schema/types/performer.graphql index 8c0c6e396a9..a17b44f9506 100644 --- a/graphql/schema/types/performer.graphql +++ b/graphql/schema/types/performer.graphql @@ -44,6 +44,7 @@ type Performer { updated_at: Time! movie_count: Int movies: [Movie!]! + urls: [String!]! } input PerformerCreateInput { @@ -73,6 +74,7 @@ input PerformerCreateInput { death_date: String hair_color: String weight: Int + urls: [String!] } input PerformerUpdateInput { @@ -103,6 +105,7 @@ input PerformerUpdateInput { death_date: String hair_color: String weight: Int + urls: [String!] } input BulkPerformerUpdateInput { diff --git a/pkg/api/resolver_model_performer.go b/pkg/api/resolver_model_performer.go index ea52873dffa..2ba0aee7426 100644 --- a/pkg/api/resolver_model_performer.go +++ b/pkg/api/resolver_model_performer.go @@ -277,3 +277,14 @@ func (r *performerResolver) MovieCount(ctx context.Context, obj *models.Performe return &res, nil } + +func (r *performerResolver) Urls(ctx context.Context, obj *models.Performer) (ret []string, err error) { + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + ret, err = repo.Performer().GetUrls(obj.ID) + return err + }); err != nil { + return nil, err + } + + return ret, err +} diff --git a/pkg/api/resolver_mutation_performer.go b/pkg/api/resolver_mutation_performer.go index 90e33b78ba1..fb62d0d2420 100644 --- a/pkg/api/resolver_mutation_performer.go +++ b/pkg/api/resolver_mutation_performer.go @@ -275,6 +275,13 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per } } + // Save the urls + if translator.hasField("urls") { + if err := qb.UpdateUrls(performerID, input.Urls); err != nil { + return err + } + } + return nil }); err != nil { return nil, err diff --git a/pkg/database/database.go b/pkg/database/database.go index a23a77aa886..2dadcdb4b48 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -23,7 +23,7 @@ import ( var DB *sqlx.DB var WriteMu sync.Mutex var dbPath string -var appSchemaVersion uint = 28 +var appSchemaVersion uint = 29 var databaseSchemaVersion uint //go:embed migrations/*.sql diff --git a/pkg/database/migrations/29_performers_urls.up.sql b/pkg/database/migrations/29_performers_urls.up.sql new file mode 100644 index 00000000000..b0dfb372dcd --- /dev/null +++ b/pkg/database/migrations/29_performers_urls.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE `performers_urls` ( + `performer_id` integer NOT NULL, + `url` varchar(255) NOT NULL, + foreign key(`performer_id`) references `performers`(`id`) on delete CASCADE +); + +CREATE INDEX `index_performers_urls_on_performer_id` on `performers_urls` (`performer_id`); diff --git a/pkg/manager/jsonschema/performer.go b/pkg/manager/jsonschema/performer.go index 6fee26a18ef..6408ceafc1d 100644 --- a/pkg/manager/jsonschema/performer.go +++ b/pkg/manager/jsonschema/performer.go @@ -36,6 +36,7 @@ type Performer struct { HairColor string `json:"hair_color,omitempty"` Weight int `json:"weight,omitempty"` StashIDs []models.StashID `json:"stash_ids,omitempty"` + Urls []string `json:"urls,omitempty"` } func LoadPerformerFile(filePath string) (*Performer, error) { diff --git a/pkg/models/mocks/PerformerReaderWriter.go b/pkg/models/mocks/PerformerReaderWriter.go index 986074405ba..cca574ed516 100644 --- a/pkg/models/mocks/PerformerReaderWriter.go +++ b/pkg/models/mocks/PerformerReaderWriter.go @@ -381,6 +381,29 @@ func (_m *PerformerReaderWriter) GetTagIDs(performerID int) ([]int, error) { return r0, r1 } +// GetUrls provides a mock function with given fields: performerID +func (_m *PerformerReaderWriter) GetUrls(performerID int) ([]string, error) { + ret := _m.Called(performerID) + + var r0 []string + if rf, ok := ret.Get(0).(func(int) []string); ok { + r0 = rf(performerID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int) error); ok { + r1 = rf(performerID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Query provides a mock function with given fields: performerFilter, findFilter func (_m *PerformerReaderWriter) Query(performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) { ret := _m.Called(performerFilter, findFilter) @@ -521,3 +544,17 @@ func (_m *PerformerReaderWriter) UpdateTags(performerID int, tagIDs []int) error return r0 } + +// UpdateUrls provides a mock function with given fields: performerID, urls +func (_m *PerformerReaderWriter) UpdateUrls(performerID int, urls []string) error { + ret := _m.Called(performerID, urls) + + var r0 error + if rf, ok := ret.Get(0).(func(int, []string) error); ok { + r0 = rf(performerID, urls) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/models/performer.go b/pkg/models/performer.go index ea316be2d05..01724f2c774 100644 --- a/pkg/models/performer.go +++ b/pkg/models/performer.go @@ -19,6 +19,7 @@ type PerformerReader interface { GetImage(performerID int) ([]byte, error) GetStashIDs(performerID int) ([]*StashID, error) GetTagIDs(performerID int) ([]int, error) + GetUrls(performerID int) ([]string, error) } type PerformerWriter interface { @@ -30,6 +31,7 @@ type PerformerWriter interface { DestroyImage(performerID int) error UpdateStashIDs(performerID int, stashIDs []StashID) error UpdateTags(performerID int, tagIDs []int) error + UpdateUrls(performerID int, urls []string) error } type PerformerReaderWriter interface { diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index 71a63c9174b..150ab9fc5a4 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -14,6 +14,8 @@ const performerTable = "performers" const performerIDColumn = "performer_id" const performersTagsTable = "performers_tags" const performersImageTable = "performers_image" // performer cover image +const performersUrlsTable = "performers_urls" +const performersUrlColumn = "url" var countPerformersForTagQuery = ` SELECT tag_id AS id FROM performers_tags @@ -607,3 +609,22 @@ func (qb *performerQueryBuilder) FindByStashIDStatus(hasStashID bool, stashboxEn args := []interface{}{stashboxEndpoint} return qb.queryPerformers(query, args) } + +func (qb *performerQueryBuilder) urlRepository() *stringRepository { + return &stringRepository{ + repository: repository{ + tx: qb.tx, + tableName: performersUrlsTable, + idColumn: performerIDColumn, + }, + stringColumn: performersUrlColumn, + } +} + +func (qb *performerQueryBuilder) GetUrls(performerID int) ([]string, error) { + return qb.urlRepository().get(performerID) +} + +func (qb *performerQueryBuilder) UpdateUrls(performerID int, urls []string) error { + return qb.urlRepository().replace(performerID, urls) +} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index 03223a9c3ba..1ce64fc2f71 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -72,6 +72,31 @@ export const PerformerDetailsPanel: React.FC = ({ ); } + function renderUrls() { + if (!performer.urls.length) { + return; + } + + return ( + <> +
Additional URLs
+
+ +
+ + ); + } + const formatHeight = (height?: string | null) => { if (!height) { return ""; @@ -143,6 +168,7 @@ export const PerformerDetailsPanel: React.FC = ({ /> {renderTagsField()} {renderStashIDs()} + {renderUrls()} ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index 9a803b942a8..524ff09d41e 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -1,5 +1,13 @@ -import React, { useEffect, useState } from "react"; -import { Button, Form, Col, Row, Badge, Dropdown } from "react-bootstrap"; +import React, { useEffect, useRef, useState } from "react"; +import { + Button, + Form, + Col, + Row, + Badge, + Dropdown, + InputGroup, +} from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; @@ -68,6 +76,7 @@ export const PerformerEditPanel: React.FC = ({ const [newTags, setNewTags] = useState(); const [isScraperModalOpen, setIsScraperModalOpen] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + const urlInputRef = useRef(null); // Network state const [isLoading, setIsLoading] = useState(false); @@ -118,6 +127,7 @@ export const PerformerEditPanel: React.FC = ({ death_date: yup.string().optional(), hair_color: yup.string().optional(), weight: yup.number().optional(), + urls: yup.array(yup.string().required()).optional(), }); const initialValues = { @@ -144,6 +154,7 @@ export const PerformerEditPanel: React.FC = ({ death_date: performer.death_date ?? "", hair_color: performer.hair_color ?? "", weight: performer.weight ?? undefined, + urls: performer.urls ?? [], }; type InputValues = typeof initialValues; @@ -817,6 +828,55 @@ export const PerformerEditPanel: React.FC = ({ ); } + const removeUrl = (url: string) => { + formik.setFieldValue( + "urls", + (formik.values.urls ?? []).filter((s) => !(s === url)) + ); + }; + + const addUrl = (url: string) => { + if (urlInputRef.current?.value) { + urlInputRef.current.value = ""; + if (formik.values.urls.indexOf(url) === -1) { + formik.setFieldValue("urls", [...formik.values.urls, url]); + } + } + }; + + function renderUrls() { + if (!formik.values.urls?.length) { + return; + } + + return ( + + + +
    + {formik.values.urls.map((url) => { + return ( +
  • + + + {url} + +
  • + ); + })} +
+ +
+ ); + } + function renderTextField(field: string, title: string, placeholder?: string) { return ( @@ -970,6 +1030,30 @@ export const PerformerEditPanel: React.FC = ({ {renderStashIDs()} + + + + + + + + + + + + + + {renderUrls()} + {renderButtons()} From 549b7b0d2270a9c6f01fad5b4f7d312942fdc64f Mon Sep 17 00:00:00 2001 From: 7dJx1qP <38586902+7dJx1qP@users.noreply.github.com> Date: Thu, 4 Nov 2021 22:44:56 -0400 Subject: [PATCH 2/3] add edit and scrape buttons for additional urls --- .../PerformerDetails/PerformerEditPanel.tsx | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index 524ff09d41e..d840742a032 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -523,8 +523,7 @@ export const PerformerEditPanel: React.FC = ({ } } - async function onScrapePerformerURL() { - const { url } = formik.values; + async function onScrapePerformerURL(url: string) { if (!url) return; setIsLoading(true); try { @@ -835,6 +834,11 @@ export const PerformerEditPanel: React.FC = ({ ); }; + const editUrl = (url: string) => { + urlInputRef!.current!.value = url; + removeUrl(url); + }; + const addUrl = (url: string) => { if (urlInputRef.current?.value) { urlInputRef.current.value = ""; @@ -856,18 +860,43 @@ export const PerformerEditPanel: React.FC = ({
    {formik.values.urls.map((url) => { return ( -
  • +
  • + + {url} + + {urlScrapable(url) ? ( + + ) : ( + "" + )} + - - {url} -
  • ); })} @@ -1005,7 +1034,7 @@ export const PerformerEditPanel: React.FC = ({ From 71f8b40e91d165649f2dcceee5c17d71649db594 Mon Sep 17 00:00:00 2001 From: 7dJx1qP <38586902+7dJx1qP@users.noreply.github.com> Date: Sat, 6 Nov 2021 11:52:33 -0400 Subject: [PATCH 3/3] refactor additional urls edit --- .../PerformerDetails/PerformerEditPanel.tsx | 124 ++++-------------- .../PerformerDetails/PerformerURLInput.tsx | 118 +++++++++++++++++ 2 files changed, 141 insertions(+), 101 deletions(-) create mode 100644 ui/v2.5/src/components/Performers/PerformerDetails/PerformerURLInput.tsx diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index d840742a032..bc169c8c8cd 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -1,13 +1,5 @@ -import React, { useEffect, useRef, useState } from "react"; -import { - Button, - Form, - Col, - Row, - Badge, - Dropdown, - InputGroup, -} from "react-bootstrap"; +import React, { useEffect, useState } from "react"; +import { Button, Form, Col, Row, Badge, Dropdown } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; @@ -45,6 +37,10 @@ import { stashboxDisplayName } from "src/utils/stashbox"; import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import PerformerScrapeModal from "./PerformerScrapeModal"; import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal"; +import { + PerformerURLInput, + IPerformerURLInputInstance, +} from "./PerformerURLInput"; const isScraper = ( scraper: GQL.Scraper | GQL.StashBox @@ -76,7 +72,14 @@ export const PerformerEditPanel: React.FC = ({ const [newTags, setNewTags] = useState(); const [isScraperModalOpen, setIsScraperModalOpen] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); - const urlInputRef = useRef(null); + const [additionalURLs, setAdditionalURLs] = useState< + IPerformerURLInputInstance[] + >( + (performer!.urls ?? []).map((url, i) => ({ + url, + index: i, + })) + ); // Network state const [isLoading, setIsLoading] = useState(false); @@ -827,85 +830,14 @@ export const PerformerEditPanel: React.FC = ({ ); } - const removeUrl = (url: string) => { + const saveAdditionalURLs = (instances: IPerformerURLInputInstance[]) => { + setAdditionalURLs(instances); formik.setFieldValue( "urls", - (formik.values.urls ?? []).filter((s) => !(s === url)) + instances.map((instance) => instance.url ?? "").filter((s) => s !== "") ); }; - const editUrl = (url: string) => { - urlInputRef!.current!.value = url; - removeUrl(url); - }; - - const addUrl = (url: string) => { - if (urlInputRef.current?.value) { - urlInputRef.current.value = ""; - if (formik.values.urls.indexOf(url) === -1) { - formik.setFieldValue("urls", [...formik.values.urls, url]); - } - } - }; - - function renderUrls() { - if (!formik.values.urls?.length) { - return; - } - - return ( - - - -
      - {formik.values.urls.map((url) => { - return ( -
    • - - {url} - - {urlScrapable(url) ? ( - - ) : ( - "" - )} - - -
    • - ); - })} -
    - -
    - ); - } - function renderTextField(field: string, title: string, placeholder?: string) { return ( @@ -1064,24 +996,14 @@ export const PerformerEditPanel: React.FC = ({ - - - - - - + - {renderUrls()} {renderButtons()} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerURLInput.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerURLInput.tsx new file mode 100644 index 00000000000..bbf2f1ac05d --- /dev/null +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerURLInput.tsx @@ -0,0 +1,118 @@ +import React, { useState } from "react"; +import { Button, Form, InputGroup } from "react-bootstrap"; +import { useIntl } from "react-intl"; +import { Icon } from "src/components/Shared"; + +interface IInstanceProps { + instance: IPerformerURLInputInstance; + onSave: (instance: IPerformerURLInputInstance) => void; + onDelete: (id: number) => void; + onScrape: (url: string) => void; + urlScrapable(url: string): boolean; +} + +const Instance: React.FC = ({ + instance, + onSave, + onDelete, + onScrape, + urlScrapable, +}) => { + const intl = useIntl(); + const handleInput = (key: string, value: string) => { + const newObj = { + ...instance, + [key]: value, + }; + onSave(newObj); + }; + + return ( + + + ) => + handleInput("url", e.currentTarget.value.trim()) + } + /> + + + + + + + ); +}; + +interface IPerformerURLInputProps { + urls: IPerformerURLInputInstance[]; + saveURLs: (boxes: IPerformerURLInputInstance[]) => void; + onScrapeClick(url: string): void; + urlScrapable(url: string): boolean; +} + +export interface IPerformerURLInputInstance { + url?: string; + index: number; +} + +export const PerformerURLInput: React.FC = ({ + urls, + saveURLs, + onScrapeClick, + urlScrapable, +}) => { + const intl = useIntl(); + const [index, setIndex] = useState(1000); + + const handleSave = (instance: IPerformerURLInputInstance) => + saveURLs( + urls.map((url) => (url.index === instance.index ? instance : url)) + ); + const handleDelete = (id: number) => + saveURLs(urls.filter((url) => url.index !== id)); + const handleAdd = () => { + saveURLs([...urls, { index }]); + setIndex(index + 1); + }; + + return ( + + {urls.map((instance) => ( + + ))} + + + ); +};