import React, { useMemo, useState } from "react"; import * as GQL from "src/core/generated-graphql"; import { MovieSelect, TagSelect, StudioSelect, PerformerSelect, } from "src/components/Shared/Select"; import { ScrapeDialog, ScrapeDialogRow, ScrapeResult, ScrapedInputGroupRow, ScrapedTextAreaRow, ScrapedImageRow, IHasName, ScrapedStringListRow, } from "src/components/Shared/ScrapeDialog"; import clone from "lodash-es/clone"; import { useStudioCreate, usePerformerCreate, useMovieCreate, useTagCreate, makePerformerCreateInput, } from "src/core/StashService"; import { useToast } from "src/hooks/Toast"; import DurationUtils from "src/utils/duration"; import { useIntl } from "react-intl"; import { uniq } from "lodash-es"; interface IScrapedStudioRow { title: string; result: ScrapeResult; onChange: (value: ScrapeResult) => void; newStudio?: GQL.ScrapedStudio; onCreateNew?: (value: GQL.ScrapedStudio) => void; } export const ScrapedStudioRow: React.FC = ({ title, result, onChange, newStudio, onCreateNew, }) => { function renderScrapedStudio( scrapeResult: ScrapeResult, isNew?: boolean, onChangeFn?: (value: string) => void ) { const resultValue = isNew ? scrapeResult.newValue : scrapeResult.originalValue; const value = resultValue ? [resultValue] : []; return ( { if (onChangeFn) { onChangeFn(items[0]?.id); } }} ids={value} /> ); } return ( renderScrapedStudio(result)} renderNewField={() => renderScrapedStudio(result, true, (value) => onChange(result.cloneWithValue(value)) ) } onChange={onChange} newValues={newStudio ? [newStudio] : undefined} onCreateNew={() => { if (onCreateNew && newStudio) onCreateNew(newStudio); }} /> ); }; interface IScrapedObjectsRow { title: string; result: ScrapeResult; onChange: (value: ScrapeResult) => void; newObjects?: T[]; onCreateNew?: (value: T) => void; renderObjects: ( result: ScrapeResult, isNew?: boolean, onChange?: (value: string[]) => void ) => JSX.Element; } export const ScrapedObjectsRow = ( props: IScrapedObjectsRow ) => { const { title, result, onChange, newObjects, onCreateNew, renderObjects } = props; return ( renderObjects(result)} renderNewField={() => renderObjects(result, true, (value) => onChange(result.cloneWithValue(value)) ) } onChange={onChange} newValues={newObjects} onCreateNew={(i) => { if (onCreateNew) onCreateNew(newObjects![i]); }} /> ); }; type IScrapedObjectRowImpl = Omit, "renderObjects">; export const ScrapedPerformersRow: React.FC< IScrapedObjectRowImpl > = ({ title, result, onChange, newObjects, onCreateNew }) => { const performersCopy = useMemo(() => { return ( newObjects?.map((p) => { const name: string = p.name ?? ""; return { ...p, name }; }) ?? [] ); }, [newObjects]); type PerformerType = GQL.ScrapedPerformer & { name: string; }; function renderScrapedPerformers( scrapeResult: ScrapeResult, isNew?: boolean, onChangeFn?: (value: string[]) => void ) { const resultValue = isNew ? scrapeResult.newValue : scrapeResult.originalValue; const value = resultValue ?? []; return ( { if (onChangeFn) { onChangeFn(items.map((i) => i.id)); } }} ids={value} /> ); } return ( title={title} result={result} renderObjects={renderScrapedPerformers} onChange={onChange} newObjects={performersCopy} onCreateNew={onCreateNew} /> ); }; export const ScrapedMoviesRow: React.FC< IScrapedObjectRowImpl > = ({ title, result, onChange, newObjects, onCreateNew }) => { const moviesCopy = useMemo(() => { return ( newObjects?.map((p) => { const name: string = p.name ?? ""; return { ...p, name }; }) ?? [] ); }, [newObjects]); type MovieType = GQL.ScrapedMovie & { name: string; }; function renderScrapedMovies( scrapeResult: ScrapeResult, isNew?: boolean, onChangeFn?: (value: string[]) => void ) { const resultValue = isNew ? scrapeResult.newValue : scrapeResult.originalValue; const value = resultValue ?? []; return ( { if (onChangeFn) { onChangeFn(items.map((i) => i.id)); } }} ids={value} /> ); } return ( title={title} result={result} renderObjects={renderScrapedMovies} onChange={onChange} newObjects={moviesCopy} onCreateNew={onCreateNew} /> ); }; export const ScrapedTagsRow: React.FC< IScrapedObjectRowImpl > = ({ title, result, onChange, newObjects, onCreateNew }) => { function renderScrapedTags( scrapeResult: ScrapeResult, isNew?: boolean, onChangeFn?: (value: string[]) => void ) { const resultValue = isNew ? scrapeResult.newValue : scrapeResult.originalValue; const value = resultValue ?? []; return ( { if (onChangeFn) { onChangeFn(items.map((i) => i.id)); } }} ids={value} /> ); } return ( title={title} result={result} renderObjects={renderScrapedTags} onChange={onChange} newObjects={newObjects} onCreateNew={onCreateNew} /> ); }; interface ISceneScrapeDialogProps { scene: Partial; scraped: GQL.ScrapedScene; endpoint?: string; onClose: (scrapedScene?: GQL.ScrapedScene) => void; } interface IHasStoredID { stored_id?: string | null; } export const SceneScrapeDialog: React.FC = ({ scene, scraped, onClose, endpoint, }) => { const [title, setTitle] = useState>( new ScrapeResult(scene.title, scraped.title) ); const [code, setCode] = useState>( new ScrapeResult(scene.code, scraped.code) ); const [urls, setURLs] = useState>( new ScrapeResult( scene.urls, scraped.urls ? uniq((scene.urls ?? []).concat(scraped.urls ?? [])) : undefined ) ); const [date, setDate] = useState>( new ScrapeResult(scene.date, scraped.date) ); const [director, setDirector] = useState>( new ScrapeResult(scene.director, scraped.director) ); const [studio, setStudio] = useState>( new ScrapeResult(scene.studio_id, scraped.studio?.stored_id) ); const [newStudio, setNewStudio] = useState( scraped.studio && !scraped.studio.stored_id ? scraped.studio : undefined ); const [stashID, setStashID] = useState( new ScrapeResult( scene.stash_ids?.find((s) => s.endpoint === endpoint)?.stash_id, scraped.remote_site_id ) ); function mapStoredIdObjects( scrapedObjects?: IHasStoredID[] ): string[] | undefined { if (!scrapedObjects) { return undefined; } const ret = scrapedObjects .map((p) => p.stored_id) .filter((p) => { return p !== undefined && p !== null; }) as string[]; if (ret.length === 0) { return undefined; } // sort by id numerically ret.sort((a, b) => { return parseInt(a, 10) - parseInt(b, 10); }); return ret; } function sortIdList(idList?: string[] | null) { if (!idList) { return; } const ret = clone(idList); // sort by id numerically ret.sort((a, b) => { return parseInt(a, 10) - parseInt(b, 10); }); return ret; } const [performers, setPerformers] = useState>( new ScrapeResult( sortIdList(scene.performer_ids), mapStoredIdObjects(scraped.performers ?? undefined) ) ); const [newPerformers, setNewPerformers] = useState( scraped.performers?.filter((t) => !t.stored_id) ?? [] ); const [movies, setMovies] = useState>( new ScrapeResult( sortIdList(scene.movies?.map((p) => p.movie_id)), mapStoredIdObjects(scraped.movies ?? undefined) ) ); const [newMovies, setNewMovies] = useState( scraped.movies?.filter((t) => !t.stored_id) ?? [] ); const [tags, setTags] = useState>( new ScrapeResult( sortIdList(scene.tag_ids), mapStoredIdObjects(scraped.tags ?? undefined) ) ); const [newTags, setNewTags] = useState( scraped.tags?.filter((t) => !t.stored_id) ?? [] ); const [details, setDetails] = useState>( new ScrapeResult(scene.details, scraped.details) ); const [image, setImage] = useState>( new ScrapeResult(scene.cover_image, scraped.image) ); const [createStudio] = useStudioCreate(); const [createPerformer] = usePerformerCreate(); const [createMovie] = useMovieCreate(); const [createTag] = useTagCreate(); const intl = useIntl(); const Toast = useToast(); // don't show the dialog if nothing was scraped if ( [ title, code, urls, date, director, studio, performers, movies, tags, details, image, stashID, ].every((r) => !r.scraped) && newTags.length === 0 && newPerformers.length === 0 && newMovies.length === 0 && !newStudio ) { onClose(); return <>; } async function createNewStudio(toCreate: GQL.ScrapedStudio) { try { const result = await createStudio({ variables: { input: { name: toCreate.name, url: toCreate.url, }, }, }); // set the new studio as the value setStudio(studio.cloneWithValue(result.data!.studioCreate!.id)); setNewStudio(undefined); Toast.success({ content: ( Created studio: {toCreate.name} ), }); } catch (e) { Toast.error(e); } } async function createNewPerformer(toCreate: GQL.ScrapedPerformer) { const input = makePerformerCreateInput(toCreate); try { const result = await createPerformer({ variables: { input }, }); const newValue = [...(performers.newValue ?? [])]; if (result.data?.performerCreate) newValue.push(result.data.performerCreate.id); // add the new performer to the new performers value const performerClone = performers.cloneWithValue(newValue); setPerformers(performerClone); // remove the performer from the list const newPerformersClone = newPerformers.concat(); const pIndex = newPerformersClone.findIndex( (p) => p.name === toCreate.name ); if (pIndex === -1) throw new Error("Could not find performer to remove"); newPerformersClone.splice(pIndex, 1); setNewPerformers(newPerformersClone); Toast.success({ content: ( Created performer: {toCreate.name} ), }); } catch (e) { Toast.error(e); } } async function createNewMovie(toCreate: GQL.ScrapedMovie) { let movieInput: GQL.MovieCreateInput = { name: "" }; try { movieInput = Object.assign(movieInput, toCreate); // #788 - convert duration and rating to the correct type movieInput.duration = DurationUtils.stringToSeconds( toCreate.duration ?? undefined ); if (!movieInput.duration) { movieInput.duration = undefined; } movieInput.rating = parseInt(toCreate.rating ?? "0", 10); if (!movieInput.rating || Number.isNaN(movieInput.rating)) { movieInput.rating = undefined; } const result = await createMovie({ variables: { input: movieInput }, }); // add the new movie to the new movies value const movieClone = movies.cloneWithValue(movies.newValue); if (!movieClone.newValue) { movieClone.newValue = []; } movieClone.newValue.push(result.data!.movieCreate!.id); setMovies(movieClone); // remove the movie from the list const newMoviesClone = newMovies.concat(); const pIndex = newMoviesClone.findIndex((p) => p.name === toCreate.name); if (pIndex === -1) throw new Error("Could not find movie to remove"); newMoviesClone.splice(pIndex, 1); setNewMovies(newMoviesClone); Toast.success({ content: ( Created movie: {toCreate.name} ), }); } catch (e) { Toast.error(e); } } async function createNewTag(toCreate: GQL.ScrapedTag) { const tagInput: GQL.TagCreateInput = { name: toCreate.name ?? "" }; try { const result = await createTag({ variables: { input: tagInput, }, }); const newValue = [...(tags.newValue ?? [])]; if (result.data?.tagCreate) newValue.push(result.data.tagCreate.id); // add the new tag to the new tags value const tagClone = tags.cloneWithValue(newValue); setTags(tagClone); // remove the tag from the list const newTagsClone = newTags.concat(); const pIndex = newTagsClone.indexOf(toCreate); if (pIndex === -1) throw new Error("Could not find tag to remove"); newTagsClone.splice(pIndex, 1); setNewTags(newTagsClone); Toast.success({ content: ( Created tag: {toCreate.name} ), }); } catch (e) { Toast.error(e); } } function makeNewScrapedItem(): GQL.ScrapedSceneDataFragment { const newStudioValue = studio.getNewValue(); return { title: title.getNewValue(), code: code.getNewValue(), urls: urls.getNewValue(), date: date.getNewValue(), director: director.getNewValue(), studio: newStudioValue ? { stored_id: newStudioValue, name: "", } : undefined, performers: performers.getNewValue()?.map((p) => { return { stored_id: p, name: "", }; }), movies: movies.getNewValue()?.map((m) => { return { stored_id: m, name: "", }; }), tags: tags.getNewValue()?.map((m) => { return { stored_id: m, name: "", }; }), details: details.getNewValue(), image: image.getNewValue(), remote_site_id: stashID.getNewValue(), }; } function renderScrapeRows() { return ( <> setTitle(value)} /> setCode(value)} /> setURLs(value)} /> setDate(value)} /> setDirector(value)} /> setStudio(value)} newStudio={newStudio} onCreateNew={createNewStudio} /> setPerformers(value)} newObjects={newPerformers} onCreateNew={createNewPerformer} /> setMovies(value)} newObjects={newMovies} onCreateNew={createNewMovie} /> setTags(value)} newObjects={newTags} onCreateNew={createNewTag} /> setDetails(value)} /> setStashID(value)} /> setImage(value)} /> ); } return ( { onClose(apply ? makeNewScrapedItem() : undefined); }} /> ); }; export default SceneScrapeDialog;