Add gallery scraping (#862)

This commit is contained in:
SpedNSFW
2020-10-21 09:24:32 +11:00
committed by GitHub
parent 872bb70f6e
commit 147d0067f5
19 changed files with 1296 additions and 1 deletions

View File

@@ -2,16 +2,23 @@ import React, { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { Button, Form, Col, Row } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import { useGalleryCreate, useGalleryUpdate } from "src/core/StashService";
import {
queryScrapeGalleryURL,
useGalleryCreate,
useGalleryUpdate,
useListGalleryScrapers,
} from "src/core/StashService";
import {
PerformerSelect,
TagSelect,
StudioSelect,
Icon,
LoadingIndicator,
} from "src/components/Shared";
import { useToast } from "src/hooks";
import { FormUtils, EditableTextUtils } from "src/utils";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
interface IProps {
isVisible: boolean;
@@ -42,6 +49,13 @@ export const GalleryEditPanel: React.FC<
const [performerIds, setPerformerIds] = useState<string[]>();
const [tagIds, setTagIds] = useState<string[]>();
const Scrapers = useListGalleryScrapers();
const [
scrapedGallery,
setScrapedGallery,
] = useState<GQL.ScrapedGallery | null>();
// Network state
const [isLoading, setIsLoading] = useState(true);
@@ -148,10 +162,121 @@ export const GalleryEditPanel: React.FC<
setIsLoading(false);
}
function onScrapeDialogClosed(gallery?: GQL.ScrapedGalleryDataFragment) {
if (gallery) {
updateGalleryFromScrapedGallery(gallery);
}
setScrapedGallery(undefined);
}
function maybeRenderScrapeDialog() {
if (!scrapedGallery) {
return;
}
const currentGallery = getGalleryInput();
return (
<GalleryScrapeDialog
gallery={currentGallery}
scraped={scrapedGallery}
onClose={(gallery) => {
onScrapeDialogClosed(gallery);
}}
/>
);
}
function urlScrapable(scrapedUrl: string): boolean {
return (Scrapers?.data?.listGalleryScrapers ?? []).some((s) =>
(s?.gallery?.urls ?? []).some((u) => scrapedUrl.includes(u))
);
}
function updateGalleryFromScrapedGallery(
gallery: GQL.ScrapedGalleryDataFragment
) {
if (gallery.title) {
setTitle(gallery.title);
}
if (gallery.details) {
setDetails(gallery.details);
}
if (gallery.date) {
setDate(gallery.date);
}
if (gallery.url) {
setUrl(gallery.url);
}
if (gallery.studio && gallery.studio.stored_id) {
setStudioId(gallery.studio.stored_id);
}
if (gallery.performers && gallery.performers.length > 0) {
const idPerfs = gallery.performers.filter((p) => {
return p.stored_id !== undefined && p.stored_id !== null;
});
if (idPerfs.length > 0) {
const newIds = idPerfs.map((p) => p.stored_id);
setPerformerIds(newIds as string[]);
}
}
if (gallery?.tags?.length) {
const idTags = gallery.tags.filter((p) => {
return p.stored_id !== undefined && p.stored_id !== null;
});
if (idTags.length > 0) {
const newIds = idTags.map((p) => p.stored_id);
setTagIds(newIds as string[]);
}
}
}
async function onScrapeGalleryURL() {
if (!url) {
return;
}
setIsLoading(true);
try {
const result = await queryScrapeGalleryURL(url);
if (!result || !result.data || !result.data.scrapeGalleryURL) {
return;
}
setScrapedGallery(result.data.scrapeGalleryURL);
} catch (e) {
Toast.error(e);
} finally {
setIsLoading(false);
}
}
function maybeRenderScrapeButton() {
if (!url || !urlScrapable(url)) {
return undefined;
}
return (
<Button
className="minimal scrape-url-button"
onClick={onScrapeGalleryURL}
title="Scrape"
>
<Icon className="fa-fw" icon="file-download" />
</Button>
);
}
if (isLoading) return <LoadingIndicator />;
return (
<div id="gallery-edit-details">
{maybeRenderScrapeDialog()}
<div className="form-container row px-3 pt-3">
<div className="col edit-buttons mb-3 pl-0">
<Button className="edit-button" variant="primary" onClick={onSave}>
@@ -177,6 +302,9 @@ export const GalleryEditPanel: React.FC<
<Form.Group controlId="url" as={Row}>
<Col xs={3} className="pr-0 url-label">
<Form.Label className="col-form-label">URL</Form.Label>
<div className="float-right scrape-button-container">
{maybeRenderScrapeButton()}
</div>
</Col>
<Col xs={9}>
{EditableTextUtils.renderInputGroup({

View File

@@ -0,0 +1,451 @@
import React, { useState } from "react";
import { StudioSelect, PerformerSelect } from "src/components/Shared";
import * as GQL from "src/core/generated-graphql";
import { TagSelect } from "src/components/Shared/Select";
import {
ScrapeDialog,
ScrapeDialogRow,
ScrapeResult,
ScrapedInputGroupRow,
ScrapedTextAreaRow,
} from "src/components/Shared/ScrapeDialog";
import _ from "lodash";
import {
useStudioCreate,
usePerformerCreate,
useTagCreate,
} from "src/core/StashService";
import { useToast } from "src/hooks";
function renderScrapedStudio(
result: ScrapeResult<string>,
isNew?: boolean,
onChange?: (value: string) => void
) {
const resultValue = isNew ? result.newValue : result.originalValue;
const value = resultValue ? [resultValue] : [];
return (
<StudioSelect
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChange) {
onChange(items[0]?.id);
}
}}
ids={value}
/>
);
}
function renderScrapedStudioRow(
result: ScrapeResult<string>,
onChange: (value: ScrapeResult<string>) => void,
newStudio?: GQL.ScrapedSceneStudio,
onCreateNew?: (value: GQL.ScrapedSceneStudio) => void
) {
return (
<ScrapeDialogRow
title="Studio"
result={result}
renderOriginalField={() => renderScrapedStudio(result)}
renderNewField={() =>
renderScrapedStudio(result, true, (value) =>
onChange(result.cloneWithValue(value))
)
}
onChange={onChange}
newValues={newStudio ? [newStudio] : undefined}
onCreateNew={onCreateNew}
/>
);
}
function renderScrapedPerformers(
result: ScrapeResult<string[]>,
isNew?: boolean,
onChange?: (value: string[]) => void
) {
const resultValue = isNew ? result.newValue : result.originalValue;
const value = resultValue ?? [];
return (
<PerformerSelect
isMulti
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChange) {
onChange(items.map((i) => i.id));
}
}}
ids={value}
/>
);
}
function renderScrapedPerformersRow(
result: ScrapeResult<string[]>,
onChange: (value: ScrapeResult<string[]>) => void,
newPerformers: GQL.ScrapedScenePerformer[],
onCreateNew?: (value: GQL.ScrapedScenePerformer) => void
) {
return (
<ScrapeDialogRow
title="Performers"
result={result}
renderOriginalField={() => renderScrapedPerformers(result)}
renderNewField={() =>
renderScrapedPerformers(result, true, (value) =>
onChange(result.cloneWithValue(value))
)
}
onChange={onChange}
newValues={newPerformers}
onCreateNew={onCreateNew}
/>
);
}
function renderScrapedTags(
result: ScrapeResult<string[]>,
isNew?: boolean,
onChange?: (value: string[]) => void
) {
const resultValue = isNew ? result.newValue : result.originalValue;
const value = resultValue ?? [];
return (
<TagSelect
isMulti
className="form-control react-select"
isDisabled={!isNew}
onSelect={(items) => {
if (onChange) {
onChange(items.map((i) => i.id));
}
}}
ids={value}
/>
);
}
function renderScrapedTagsRow(
result: ScrapeResult<string[]>,
onChange: (value: ScrapeResult<string[]>) => void,
newTags: GQL.ScrapedSceneTag[],
onCreateNew?: (value: GQL.ScrapedSceneTag) => void
) {
return (
<ScrapeDialogRow
title="Tags"
result={result}
renderOriginalField={() => renderScrapedTags(result)}
renderNewField={() =>
renderScrapedTags(result, true, (value) =>
onChange(result.cloneWithValue(value))
)
}
newValues={newTags}
onChange={onChange}
onCreateNew={onCreateNew}
/>
);
}
interface IGalleryScrapeDialogProps {
gallery: Partial<GQL.GalleryUpdateInput>;
scraped: GQL.ScrapedGallery;
onClose: (scrapedGallery?: GQL.ScrapedGallery) => void;
}
interface IHasStoredID {
stored_id?: string | null;
}
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
props: IGalleryScrapeDialogProps
) => {
const [title, setTitle] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(props.gallery.title, props.scraped.title)
);
const [url, setURL] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(props.gallery.url, props.scraped.url)
);
const [date, setDate] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(props.gallery.date, props.scraped.date)
);
const [studio, setStudio] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(
props.gallery.studio_id,
props.scraped.studio?.stored_id
)
);
const [newStudio, setNewStudio] = useState<
GQL.ScrapedSceneStudio | undefined
>(
props.scraped.studio && !props.scraped.studio.stored_id
? props.scraped.studio
: undefined
);
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<ScrapeResult<string[]>>(
new ScrapeResult<string[]>(
sortIdList(props.gallery.performer_ids),
mapStoredIdObjects(props.scraped.performers ?? undefined)
)
);
const [newPerformers, setNewPerformers] = useState<
GQL.ScrapedScenePerformer[]
>(props.scraped.performers?.filter((t) => !t.stored_id) ?? []);
const [tags, setTags] = useState<ScrapeResult<string[]>>(
new ScrapeResult<string[]>(
sortIdList(props.gallery.tag_ids),
mapStoredIdObjects(props.scraped.tags ?? undefined)
)
);
const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>(
props.scraped.tags?.filter((t) => !t.stored_id) ?? []
);
const [details, setDetails] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(props.gallery.details, props.scraped.details)
);
const [createStudio] = useStudioCreate({ name: "" });
const [createPerformer] = usePerformerCreate();
const [createTag] = useTagCreate({ name: "" });
const Toast = useToast();
// don't show the dialog if nothing was scraped
if (
[title, url, date, studio, performers, tags, details].every(
(r) => !r.scraped
)
) {
props.onClose();
return <></>;
}
async function createNewStudio(toCreate: GQL.ScrapedSceneStudio) {
try {
const result = await createStudio({
variables: {
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: (
<span>
Created studio: <b>{toCreate.name}</b>
</span>
),
});
} catch (e) {
Toast.error(e);
}
}
async function createNewPerformer(toCreate: GQL.ScrapedScenePerformer) {
let performerInput: GQL.PerformerCreateInput = {};
try {
performerInput = Object.assign(performerInput, toCreate);
const result = await createPerformer({
variables: performerInput,
});
// add the new performer to the new performers value
const performerClone = performers.cloneWithValue(performers.newValue);
if (!performerClone.newValue) {
performerClone.newValue = [];
}
performerClone.newValue.push(result.data!.performerCreate!.id);
setPerformers(performerClone);
// remove the performer from the list
const newPerformersClone = newPerformers.concat();
const pIndex = newPerformersClone.indexOf(toCreate);
newPerformersClone.splice(pIndex, 1);
setNewPerformers(newPerformersClone);
Toast.success({
content: (
<span>
Created performer: <b>{toCreate.name}</b>
</span>
),
});
} catch (e) {
Toast.error(e);
}
}
async function createNewTag(toCreate: GQL.ScrapedSceneTag) {
let tagInput: GQL.TagCreateInput = { name: "" };
try {
tagInput = Object.assign(tagInput, toCreate);
const result = await createTag({
variables: tagInput,
});
// add the new tag to the new tags value
const tagClone = tags.cloneWithValue(tags.newValue);
if (!tagClone.newValue) {
tagClone.newValue = [];
}
tagClone.newValue.push(result.data!.tagCreate!.id);
setTags(tagClone);
// remove the tag from the list
const newTagsClone = newTags.concat();
const pIndex = newTagsClone.indexOf(toCreate);
newTagsClone.splice(pIndex, 1);
setNewTags(newTagsClone);
Toast.success({
content: (
<span>
Created tag: <b>{toCreate.name}</b>
</span>
),
});
} catch (e) {
Toast.error(e);
}
}
function makeNewScrapedItem(): GQL.ScrapedGalleryDataFragment {
const newStudioValue = studio.getNewValue();
return {
title: title.getNewValue(),
url: url.getNewValue(),
date: date.getNewValue(),
studio: newStudioValue
? {
stored_id: newStudioValue,
name: "",
}
: undefined,
performers: performers.getNewValue()?.map((p) => {
return {
stored_id: p,
name: "",
};
}),
tags: tags.getNewValue()?.map((m) => {
return {
stored_id: m,
name: "",
};
}),
details: details.getNewValue(),
};
}
function renderScrapeRows() {
return (
<>
<ScrapedInputGroupRow
title="Title"
result={title}
onChange={(value) => setTitle(value)}
/>
<ScrapedInputGroupRow
title="URL"
result={url}
onChange={(value) => setURL(value)}
/>
<ScrapedInputGroupRow
title="Date"
placeholder="YYYY-MM-DD"
result={date}
onChange={(value) => setDate(value)}
/>
{renderScrapedStudioRow(
studio,
(value) => setStudio(value),
newStudio,
createNewStudio
)}
{renderScrapedPerformersRow(
performers,
(value) => setPerformers(value),
newPerformers,
createNewPerformer
)}
{renderScrapedTagsRow(
tags,
(value) => setTags(value),
newTags,
createNewTag
)}
<ScrapedTextAreaRow
title="Details"
result={details}
onChange={(value) => setDetails(value)}
/>
</>
);
}
return (
<ScrapeDialog
title="Gallery Scrape Results"
renderScrapeRows={renderScrapeRows}
onClose={(apply) => {
props.onClose(apply ? makeNewScrapedItem() : undefined);
}}
/>
);
};