mirror of
https://github.com/stashapp/stash.git
synced 2026-05-08 12:32:29 +02:00
Split up Tagger component (#1534)
This commit is contained in:
parent
4bdd759dae
commit
723446842f
8 changed files with 1026 additions and 857 deletions
|
|
@ -16,6 +16,10 @@ export type PerformerOperation =
|
|||
| { type: "existing"; data: GQL.PerformerDataFragment }
|
||||
| { type: "skip" };
|
||||
|
||||
export interface IPerformerOperations {
|
||||
[x: string]: PerformerOperation;
|
||||
}
|
||||
|
||||
interface IPerformerResultProps {
|
||||
performer: IStashBoxPerformer;
|
||||
setPerformer: (data: PerformerOperation) => void;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import React, { useState, useReducer } from "react";
|
|||
import cx from "classnames";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { uniq } from "lodash";
|
||||
import { blobToBase64 } from "base64-blob";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
|
|
@ -14,13 +12,7 @@ import {
|
|||
import PerformerResult, { PerformerOperation } from "./PerformerResult";
|
||||
import StudioResult, { StudioOperation } from "./StudioResult";
|
||||
import { IStashBoxScene } from "./utils";
|
||||
import {
|
||||
useCreateTag,
|
||||
useCreatePerformer,
|
||||
useCreateStudio,
|
||||
useUpdatePerformerStashID,
|
||||
useUpdateStudioStashID,
|
||||
} from "./queries";
|
||||
import { useTagScene } from "./taggerService";
|
||||
|
||||
const getDurationStatus = (
|
||||
scene: IStashBoxScene,
|
||||
|
|
@ -140,240 +132,36 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||
);
|
||||
|
||||
const intl = useIntl();
|
||||
const createStudio = useCreateStudio();
|
||||
const createPerformer = useCreatePerformer();
|
||||
const createTag = useCreateTag();
|
||||
const updatePerformerStashID = useUpdatePerformerStashID();
|
||||
const updateStudioStashID = useUpdateStudioStashID();
|
||||
const [updateScene] = GQL.useSceneUpdateMutation({
|
||||
onError: (e) => {
|
||||
const message =
|
||||
e.message === "invalid JPEG format: short Huffman data"
|
||||
? "Failed to save scene due to corrupted cover image"
|
||||
: "Failed to save scene";
|
||||
setError({
|
||||
message,
|
||||
details: e.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { data: allTags } = GQL.useAllTagsForFilterQuery();
|
||||
const tagScene = useTagScene(
|
||||
{
|
||||
tagOperation,
|
||||
setCoverImage,
|
||||
setTags,
|
||||
},
|
||||
setSaveState,
|
||||
setError
|
||||
);
|
||||
|
||||
async function handleSave() {
|
||||
const updatedScene = await tagScene(
|
||||
stashScene,
|
||||
scene,
|
||||
studio,
|
||||
performers,
|
||||
endpoint
|
||||
);
|
||||
|
||||
if (updatedScene) setScene(updatedScene);
|
||||
|
||||
queueFingerprintSubmission(stashScene.id, endpoint);
|
||||
}
|
||||
|
||||
const setPerformer = (
|
||||
performerData: PerformerOperation,
|
||||
performerID: string
|
||||
) => dispatch({ id: performerID, data: performerData });
|
||||
|
||||
const handleSave = async () => {
|
||||
setError({});
|
||||
let performerIDs = [];
|
||||
let studioID = null;
|
||||
|
||||
if (!studio) return;
|
||||
|
||||
if (studio.type === "create") {
|
||||
setSaveState("Creating studio");
|
||||
const newStudio = {
|
||||
name: studio.data.name,
|
||||
stash_ids: [
|
||||
{
|
||||
endpoint,
|
||||
stash_id: scene.studio.stash_id,
|
||||
},
|
||||
],
|
||||
url: studio.data.url,
|
||||
};
|
||||
const studioCreateResult = await createStudio(
|
||||
newStudio,
|
||||
scene.studio.stash_id
|
||||
);
|
||||
|
||||
if (!studioCreateResult?.data?.studioCreate) {
|
||||
setError({
|
||||
message: `Failed to save studio "${newStudio.name}"`,
|
||||
details: studioCreateResult?.errors?.[0].message,
|
||||
});
|
||||
return setSaveState("");
|
||||
}
|
||||
studioID = studioCreateResult.data.studioCreate.id;
|
||||
} else if (studio.type === "update") {
|
||||
setSaveState("Saving studio stashID");
|
||||
const res = await updateStudioStashID(studio.data, [
|
||||
...studio.data.stash_ids,
|
||||
{ stash_id: scene.studio.stash_id, endpoint },
|
||||
]);
|
||||
if (!res?.data?.studioUpdate) {
|
||||
setError({
|
||||
message: `Failed to save stashID to studio "${studio.data.name}"`,
|
||||
details: res?.errors?.[0].message,
|
||||
});
|
||||
return setSaveState("");
|
||||
}
|
||||
studioID = res.data.studioUpdate.id;
|
||||
} else if (studio.type === "existing") {
|
||||
studioID = studio.data.id;
|
||||
} else if (studio.type === "skip") {
|
||||
studioID = stashScene.studio?.id;
|
||||
}
|
||||
|
||||
setSaveState("Saving performers");
|
||||
performerIDs = await Promise.all(
|
||||
Object.keys(performers).map(async (stashID) => {
|
||||
const performer = performers[stashID];
|
||||
if (performer.type === "skip") return "Skip";
|
||||
|
||||
let performerID = performer.data.id;
|
||||
|
||||
if (performer.type === "create") {
|
||||
const imgurl = performer.data.images[0];
|
||||
let imgData = null;
|
||||
if (imgurl) {
|
||||
const img = await fetch(imgurl, {
|
||||
mode: "cors",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (img.status === 200) {
|
||||
const blob = await img.blob();
|
||||
imgData = await blobToBase64(blob);
|
||||
}
|
||||
}
|
||||
|
||||
const performerInput = {
|
||||
name: performer.data.name,
|
||||
gender: performer.data.gender,
|
||||
country: performer.data.country,
|
||||
height: performer.data.height,
|
||||
ethnicity: performer.data.ethnicity,
|
||||
birthdate: performer.data.birthdate,
|
||||
eye_color: performer.data.eye_color,
|
||||
fake_tits: performer.data.fake_tits,
|
||||
measurements: performer.data.measurements,
|
||||
career_length: performer.data.career_length,
|
||||
tattoos: performer.data.tattoos,
|
||||
piercings: performer.data.piercings,
|
||||
twitter: performer.data.twitter,
|
||||
instagram: performer.data.instagram,
|
||||
image: imgData,
|
||||
stash_ids: [
|
||||
{
|
||||
endpoint,
|
||||
stash_id: stashID,
|
||||
},
|
||||
],
|
||||
details: performer.data.details,
|
||||
death_date: performer.data.death_date,
|
||||
hair_color: performer.data.hair_color,
|
||||
weight: Number(performer.data.weight),
|
||||
};
|
||||
|
||||
const res = await createPerformer(performerInput, stashID);
|
||||
if (!res?.data?.performerCreate) {
|
||||
setError({
|
||||
message: `Failed to save performer "${performerInput.name}"`,
|
||||
details: res?.errors?.[0].message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
performerID = res.data?.performerCreate.id;
|
||||
}
|
||||
|
||||
if (performer.type === "update") {
|
||||
const stashIDs = performer.data.stash_ids;
|
||||
await updatePerformerStashID(performer.data.id, [
|
||||
...stashIDs,
|
||||
{ stash_id: stashID, endpoint },
|
||||
]);
|
||||
}
|
||||
|
||||
return performerID;
|
||||
})
|
||||
);
|
||||
|
||||
if (!performerIDs.some((id) => !id)) {
|
||||
setSaveState("Updating scene");
|
||||
const imgurl = scene.images[0];
|
||||
let imgData = null;
|
||||
if (imgurl && setCoverImage) {
|
||||
const img = await fetch(imgurl, {
|
||||
mode: "cors",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (img.status === 200) {
|
||||
const blob = await img.blob();
|
||||
// Sanity check on image size since bad images will fail
|
||||
if (blob.size > 10000) imgData = await blobToBase64(blob);
|
||||
}
|
||||
}
|
||||
|
||||
let updatedTags = stashScene?.tags?.map((t) => t.id) ?? [];
|
||||
if (setTags) {
|
||||
const newTagIDs = tagOperation === "merge" ? updatedTags : [];
|
||||
const tags = scene.tags ?? [];
|
||||
if (tags.length > 0) {
|
||||
const tagDict: Record<string, string> = (allTags?.allTags ?? [])
|
||||
.filter((t) => t.name)
|
||||
.reduce(
|
||||
(dict, t) => ({ ...dict, [t.name.toLowerCase()]: t.id }),
|
||||
{}
|
||||
);
|
||||
const newTags: string[] = [];
|
||||
tags.forEach((tag) => {
|
||||
if (tagDict[tag.name.toLowerCase()])
|
||||
newTagIDs.push(tagDict[tag.name.toLowerCase()]);
|
||||
else newTags.push(tag.name);
|
||||
});
|
||||
|
||||
const createdTags = await Promise.all(
|
||||
newTags.map((tag) => createTag(tag))
|
||||
);
|
||||
createdTags.forEach((createdTag) => {
|
||||
if (createdTag?.data?.tagCreate?.id)
|
||||
newTagIDs.push(createdTag.data.tagCreate.id);
|
||||
});
|
||||
}
|
||||
updatedTags = uniq(newTagIDs);
|
||||
}
|
||||
|
||||
const performer_ids = performerIDs.filter(
|
||||
(id) => id !== "Skip"
|
||||
) as string[];
|
||||
|
||||
const sceneUpdateResult = await updateScene({
|
||||
variables: {
|
||||
input: {
|
||||
id: stashScene.id ?? "",
|
||||
title: scene.title,
|
||||
details: scene.details,
|
||||
date: scene.date,
|
||||
performer_ids:
|
||||
performer_ids.length === 0
|
||||
? stashScene.performers.map((p) => p.id)
|
||||
: performer_ids,
|
||||
studio_id: studioID,
|
||||
cover_image: imgData,
|
||||
url: scene.url,
|
||||
tag_ids: updatedTags,
|
||||
stash_ids: [
|
||||
...(stashScene?.stash_ids ?? []),
|
||||
{
|
||||
endpoint,
|
||||
stash_id: scene.stash_id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (sceneUpdateResult?.data?.sceneUpdate)
|
||||
setScene(sceneUpdateResult.data.sceneUpdate);
|
||||
|
||||
queueFingerprintSubmission(stashScene.id, endpoint);
|
||||
}
|
||||
|
||||
setSaveState("");
|
||||
};
|
||||
|
||||
const classname = cx("row mx-0 mt-2 search-result", {
|
||||
"selected-result": isActive,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,621 +1,18 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Card, Form, InputGroup } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { HashLink } from "react-router-hash-link";
|
||||
import { uniqBy } from "lodash";
|
||||
import { ScenePreview } from "src/components/Scenes/SceneCard";
|
||||
import { useLocalForage } from "src/hooks";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { LoadingIndicator, TruncatedText } from "src/components/Shared";
|
||||
import {
|
||||
stashBoxSceneQuery,
|
||||
stashBoxSceneBatchQuery,
|
||||
useConfiguration,
|
||||
} from "src/core/StashService";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { stashBoxSceneQuery, useConfiguration } from "src/core/StashService";
|
||||
import { Manual } from "src/components/Help/Manual";
|
||||
|
||||
import { SceneQueue } from "src/models/sceneQueue";
|
||||
import StashSearchResult from "./StashSearchResult";
|
||||
import Config from "./Config";
|
||||
import {
|
||||
LOCAL_FORAGE_KEY,
|
||||
ITaggerConfig,
|
||||
ParseMode,
|
||||
initialConfig,
|
||||
} from "./constants";
|
||||
import {
|
||||
parsePath,
|
||||
selectScenes,
|
||||
IStashBoxScene,
|
||||
sortScenesByDuration,
|
||||
} from "./utils";
|
||||
|
||||
const months = [
|
||||
"jan",
|
||||
"feb",
|
||||
"mar",
|
||||
"apr",
|
||||
"may",
|
||||
"jun",
|
||||
"jul",
|
||||
"aug",
|
||||
"sep",
|
||||
"oct",
|
||||
"nov",
|
||||
"dec",
|
||||
];
|
||||
|
||||
const ddmmyyRegex = /\.(\d\d)\.(\d\d)\.(\d\d)\./;
|
||||
const yyyymmddRegex = /(\d{4})[-.](\d{2})[-.](\d{2})/;
|
||||
const mmddyyRegex = /(\d{2})[-.](\d{2})[-.](\d{4})/;
|
||||
const ddMMyyRegex = new RegExp(
|
||||
`(\\d{1,2}).(${months.join("|")})\\.?.(\\d{4})`,
|
||||
"i"
|
||||
);
|
||||
const MMddyyRegex = new RegExp(
|
||||
`(${months.join("|")})\\.?.(\\d{1,2}),?.(\\d{4})`,
|
||||
"i"
|
||||
);
|
||||
const parseDate = (input: string): string => {
|
||||
let output = input;
|
||||
const ddmmyy = output.match(ddmmyyRegex);
|
||||
if (ddmmyy) {
|
||||
output = output.replace(
|
||||
ddmmyy[0],
|
||||
` 20${ddmmyy[1]}-${ddmmyy[2]}-${ddmmyy[3]} `
|
||||
);
|
||||
}
|
||||
const mmddyy = output.match(mmddyyRegex);
|
||||
if (mmddyy) {
|
||||
output = output.replace(
|
||||
mmddyy[0],
|
||||
` ${mmddyy[1]}-${mmddyy[2]}-${mmddyy[3]} `
|
||||
);
|
||||
}
|
||||
const ddMMyy = output.match(ddMMyyRegex);
|
||||
if (ddMMyy) {
|
||||
const month = (months.indexOf(ddMMyy[2].toLowerCase()) + 1)
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
output = output.replace(
|
||||
ddMMyy[0],
|
||||
` ${ddMMyy[3]}-${month}-${ddMMyy[1].padStart(2, "0")} `
|
||||
);
|
||||
}
|
||||
const MMddyy = output.match(MMddyyRegex);
|
||||
if (MMddyy) {
|
||||
const month = (months.indexOf(MMddyy[1].toLowerCase()) + 1)
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
output = output.replace(
|
||||
MMddyy[0],
|
||||
` ${MMddyy[3]}-${month}-${MMddyy[2].padStart(2, "0")} `
|
||||
);
|
||||
}
|
||||
|
||||
const yyyymmdd = output.search(yyyymmddRegex);
|
||||
if (yyyymmdd !== -1)
|
||||
return (
|
||||
output.slice(0, yyyymmdd).replace(/-/g, " ") +
|
||||
output.slice(yyyymmdd, yyyymmdd + 10).replace(/\./g, "-") +
|
||||
output.slice(yyyymmdd + 10).replace(/-/g, " ")
|
||||
);
|
||||
return output.replace(/-/g, " ");
|
||||
};
|
||||
|
||||
function prepareQueryString(
|
||||
scene: Partial<GQL.SlimSceneDataFragment>,
|
||||
paths: string[],
|
||||
filename: string,
|
||||
mode: ParseMode,
|
||||
blacklist: string[]
|
||||
) {
|
||||
if ((mode === "auto" && scene.date && scene.studio) || mode === "metadata") {
|
||||
let str = [
|
||||
scene.date,
|
||||
scene.studio?.name ?? "",
|
||||
(scene?.performers ?? []).map((p) => p.name).join(" "),
|
||||
scene?.title ? scene.title.replace(/[^a-zA-Z0-9 ]+/g, "") : "",
|
||||
]
|
||||
.filter((s) => s !== "")
|
||||
.join(" ");
|
||||
blacklist.forEach((b) => {
|
||||
str = str.replace(new RegExp(b, "gi"), " ");
|
||||
});
|
||||
return str;
|
||||
}
|
||||
let s = "";
|
||||
|
||||
if (mode === "auto" || mode === "filename") {
|
||||
s = filename;
|
||||
} else if (mode === "path") {
|
||||
s = [...paths, filename].join(" ");
|
||||
} else {
|
||||
s = paths[paths.length - 1];
|
||||
}
|
||||
blacklist.forEach((b) => {
|
||||
s = s.replace(new RegExp(b, "gi"), " ");
|
||||
});
|
||||
s = parseDate(s);
|
||||
return s.replace(/\./g, " ");
|
||||
}
|
||||
|
||||
interface ITaggerListProps {
|
||||
scenes: GQL.SlimSceneDataFragment[];
|
||||
queue?: SceneQueue;
|
||||
selectedEndpoint: { endpoint: string; index: number };
|
||||
config: ITaggerConfig;
|
||||
queueFingerprintSubmission: (sceneId: string, endpoint: string) => void;
|
||||
clearSubmissionQueue: (endpoint: string) => void;
|
||||
}
|
||||
|
||||
// Caches fingerprint lookups between page renders
|
||||
let fingerprintCache: Record<string, IStashBoxScene[]> = {};
|
||||
|
||||
const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
scenes,
|
||||
queue,
|
||||
selectedEndpoint,
|
||||
config,
|
||||
queueFingerprintSubmission,
|
||||
clearSubmissionQueue,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [fingerprintError, setFingerprintError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const queryString = useRef<Record<string, string>>({});
|
||||
const inputForm = useRef<HTMLFormElement>(null);
|
||||
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
Record<string, IStashBoxScene[]>
|
||||
>({});
|
||||
const [searchErrors, setSearchErrors] = useState<
|
||||
Record<string, string | undefined>
|
||||
>({});
|
||||
const [selectedResult, setSelectedResult] = useState<
|
||||
Record<string, number>
|
||||
>();
|
||||
const [selectedFingerprintResult, setSelectedFingerprintResult] = useState<
|
||||
Record<string, number>
|
||||
>();
|
||||
const [taggedScenes, setTaggedScenes] = useState<
|
||||
Record<string, Partial<GQL.SlimSceneDataFragment>>
|
||||
>({});
|
||||
const [loadingFingerprints, setLoadingFingerprints] = useState(false);
|
||||
const [fingerprints, setFingerprints] = useState<
|
||||
Record<string, IStashBoxScene[]>
|
||||
>(fingerprintCache);
|
||||
const [hideUnmatched, setHideUnmatched] = useState(false);
|
||||
const fingerprintQueue =
|
||||
config.fingerprintQueue[selectedEndpoint.endpoint] ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
inputForm?.current?.reset();
|
||||
}, [config.mode, config.blacklist]);
|
||||
|
||||
function clearSceneSearchResult(sceneID: string) {
|
||||
// remove sceneID results from the results object
|
||||
const { [sceneID]: _removedResult, ...newSearchResults } = searchResults;
|
||||
const { [sceneID]: _removedError, ...newSearchErrors } = searchErrors;
|
||||
setSearchResults(newSearchResults);
|
||||
setSearchErrors(newSearchErrors);
|
||||
}
|
||||
|
||||
const doBoxSearch = (sceneID: string, searchVal: string) => {
|
||||
clearSceneSearchResult(sceneID);
|
||||
|
||||
stashBoxSceneQuery(searchVal, selectedEndpoint.index)
|
||||
.then((queryData) => {
|
||||
const s = selectScenes(queryData.data?.queryStashBoxScene);
|
||||
setSearchResults({
|
||||
...searchResults,
|
||||
[sceneID]: s,
|
||||
});
|
||||
setSearchErrors({
|
||||
...searchErrors,
|
||||
[sceneID]: undefined,
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
// Destructure to remove existing result
|
||||
const { [sceneID]: unassign, ...results } = searchResults;
|
||||
setSearchResults(results);
|
||||
setSearchErrors({
|
||||
...searchErrors,
|
||||
[sceneID]: "Network Error",
|
||||
});
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
const [
|
||||
submitFingerPrints,
|
||||
{ loading: submittingFingerprints },
|
||||
] = GQL.useSubmitStashBoxFingerprintsMutation({
|
||||
onCompleted: (result) => {
|
||||
setFingerprintError("");
|
||||
if (result.submitStashBoxFingerprints)
|
||||
clearSubmissionQueue(selectedEndpoint.endpoint);
|
||||
},
|
||||
onError: () => {
|
||||
setFingerprintError("Network Error");
|
||||
},
|
||||
});
|
||||
|
||||
const handleFingerprintSubmission = () => {
|
||||
submitFingerPrints({
|
||||
variables: {
|
||||
input: {
|
||||
stash_box_index: selectedEndpoint.index,
|
||||
scene_ids: fingerprintQueue,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTaggedScene = (scene: Partial<GQL.SlimSceneDataFragment>) => {
|
||||
setTaggedScenes({
|
||||
...taggedScenes,
|
||||
[scene.id as string]: scene,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFingerprintSearch = async () => {
|
||||
setLoadingFingerprints(true);
|
||||
const newFingerprints = { ...fingerprints };
|
||||
|
||||
const sceneIDs = scenes
|
||||
.filter((s) => s.stash_ids.length === 0)
|
||||
.map((s) => s.id);
|
||||
|
||||
const results = await stashBoxSceneBatchQuery(
|
||||
sceneIDs,
|
||||
selectedEndpoint.index
|
||||
).catch(() => {
|
||||
setLoadingFingerprints(false);
|
||||
setFingerprintError("Network Error");
|
||||
});
|
||||
|
||||
if (!results) return;
|
||||
|
||||
// clear search errors
|
||||
setSearchErrors({});
|
||||
|
||||
selectScenes(results.data?.queryStashBoxScene).forEach((scene) => {
|
||||
scene.fingerprints?.forEach((f) => {
|
||||
newFingerprints[f.hash] = newFingerprints[f.hash]
|
||||
? [...newFingerprints[f.hash], scene]
|
||||
: [scene];
|
||||
});
|
||||
});
|
||||
|
||||
// Null any ids that are still undefined since it means they weren't found
|
||||
sceneIDs.forEach((id) => {
|
||||
newFingerprints[id] = newFingerprints[id] ?? null;
|
||||
});
|
||||
|
||||
setFingerprints(newFingerprints);
|
||||
fingerprintCache = newFingerprints;
|
||||
setLoadingFingerprints(false);
|
||||
setFingerprintError("");
|
||||
};
|
||||
|
||||
const canFingerprintSearch = () =>
|
||||
scenes.some(
|
||||
(s) => s.stash_ids.length === 0 && fingerprints[s.id] === undefined
|
||||
);
|
||||
|
||||
const getFingerprintCount = () => {
|
||||
return scenes.filter(
|
||||
(s) =>
|
||||
s.stash_ids.length === 0 &&
|
||||
((s.checksum && fingerprints[s.checksum]) ||
|
||||
(s.oshash && fingerprints[s.oshash]) ||
|
||||
(s.phash && fingerprints[s.phash]))
|
||||
).length;
|
||||
};
|
||||
|
||||
const getFingerprintCountMessage = () => {
|
||||
const count = getFingerprintCount();
|
||||
return intl.formatMessage(
|
||||
{ id: "component_tagger.results.fp_found" },
|
||||
{ fpCount: count }
|
||||
);
|
||||
};
|
||||
|
||||
const toggleHideUnmatchedScenes = () => {
|
||||
setHideUnmatched(!hideUnmatched);
|
||||
};
|
||||
|
||||
function generateSceneLink(scene: GQL.SlimSceneDataFragment, index: number) {
|
||||
return queue
|
||||
? queue.makeLink(scene.id, { sceneIndex: index })
|
||||
: `/scenes/${scene.id}`;
|
||||
}
|
||||
|
||||
const renderScenes = () =>
|
||||
scenes.map((scene, index) => {
|
||||
const sceneLink = generateSceneLink(scene, index);
|
||||
const { paths, file, ext } = parsePath(scene.path);
|
||||
const originalDir = scene.path.slice(
|
||||
0,
|
||||
scene.path.length - file.length - ext.length
|
||||
);
|
||||
const defaultQueryString = prepareQueryString(
|
||||
scene,
|
||||
paths,
|
||||
file,
|
||||
config.mode,
|
||||
config.blacklist
|
||||
);
|
||||
|
||||
// Get all scenes matching one of the fingerprints, and return array of unique scenes
|
||||
const fingerprintMatches = uniqBy(
|
||||
[
|
||||
...(fingerprints[scene.checksum ?? ""] ?? []),
|
||||
...(fingerprints[scene.oshash ?? ""] ?? []),
|
||||
...(fingerprints[scene.phash ?? ""] ?? []),
|
||||
].flat(),
|
||||
(f) => f.stash_id
|
||||
);
|
||||
|
||||
const isTagged = taggedScenes[scene.id];
|
||||
const hasStashIDs = scene.stash_ids.length > 0;
|
||||
const width = scene.file.width ? scene.file.width : 0;
|
||||
const height = scene.file.height ? scene.file.height : 0;
|
||||
const isPortrait = height > width;
|
||||
|
||||
let mainContent;
|
||||
if (!isTagged && hasStashIDs) {
|
||||
mainContent = (
|
||||
<div className="text-right">
|
||||
<h5 className="text-bold">
|
||||
<FormattedMessage id="component_tagger.results.match_failed_already_tagged" />
|
||||
</h5>
|
||||
</div>
|
||||
);
|
||||
} else if (!isTagged && !hasStashIDs) {
|
||||
mainContent = (
|
||||
<InputGroup>
|
||||
<InputGroup.Prepend>
|
||||
<InputGroup.Text>
|
||||
<FormattedMessage id="component_tagger.noun_query" />
|
||||
</InputGroup.Text>
|
||||
</InputGroup.Prepend>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
defaultValue={queryString.current[scene.id] || defaultQueryString}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
queryString.current[scene.id] = e.currentTarget.value;
|
||||
}}
|
||||
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||
e.key === "Enter" &&
|
||||
doBoxSearch(
|
||||
scene.id,
|
||||
queryString.current[scene.id] || defaultQueryString
|
||||
)
|
||||
}
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
<Button
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
doBoxSearch(
|
||||
scene.id,
|
||||
queryString.current[scene.id] || defaultQueryString
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormattedMessage id="actions.search" />
|
||||
</Button>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
);
|
||||
} else if (isTagged) {
|
||||
mainContent = (
|
||||
<div className="d-flex flex-column text-right">
|
||||
<h5>
|
||||
<FormattedMessage id="component_tagger.results.match_success" />
|
||||
</h5>
|
||||
<h6>
|
||||
<Link className="bold" to={sceneLink}>
|
||||
{taggedScenes[scene.id].title}
|
||||
</Link>
|
||||
</h6>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let subContent;
|
||||
if (scene.stash_ids.length > 0) {
|
||||
const stashLinks = scene.stash_ids.map((stashID) => {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
className="small d-block"
|
||||
href={`${base}scenes/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
) : (
|
||||
<div className="small">{stashID.stash_id}</div>
|
||||
);
|
||||
|
||||
return link;
|
||||
});
|
||||
subContent = <>{stashLinks}</>;
|
||||
} else if (searchErrors[scene.id]) {
|
||||
subContent = (
|
||||
<div className="text-danger font-weight-bold">
|
||||
{searchErrors[scene.id]}
|
||||
</div>
|
||||
);
|
||||
} else if (searchResults[scene.id]?.length === 0) {
|
||||
subContent = (
|
||||
<div className="text-danger font-weight-bold">
|
||||
<FormattedMessage id="component_tagger.results.match_failed_no_result" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let searchResult;
|
||||
if (fingerprintMatches.length > 0 && !isTagged && !hasStashIDs) {
|
||||
searchResult = sortScenesByDuration(
|
||||
fingerprintMatches,
|
||||
scene.file.duration ?? 0
|
||||
).map((match, i) => (
|
||||
<StashSearchResult
|
||||
showMales={config.showMales}
|
||||
stashScene={scene}
|
||||
isActive={(selectedFingerprintResult?.[scene.id] ?? 0) === i}
|
||||
setActive={() =>
|
||||
setSelectedFingerprintResult({
|
||||
...selectedFingerprintResult,
|
||||
[scene.id]: i,
|
||||
})
|
||||
}
|
||||
setScene={handleTaggedScene}
|
||||
scene={match}
|
||||
setCoverImage={config.setCoverImage}
|
||||
setTags={config.setTags}
|
||||
tagOperation={config.tagOperation}
|
||||
endpoint={selectedEndpoint.endpoint}
|
||||
queueFingerprintSubmission={queueFingerprintSubmission}
|
||||
key={match.stash_id}
|
||||
/>
|
||||
));
|
||||
} else if (
|
||||
searchResults[scene.id]?.length > 0 &&
|
||||
!isTagged &&
|
||||
fingerprintMatches.length === 0
|
||||
) {
|
||||
searchResult = (
|
||||
<ul className="pl-0 mt-3 mb-0">
|
||||
{sortScenesByDuration(
|
||||
searchResults[scene.id],
|
||||
scene.file.duration ?? undefined
|
||||
).map(
|
||||
(sceneResult, i) =>
|
||||
sceneResult && (
|
||||
<StashSearchResult
|
||||
key={sceneResult.stash_id}
|
||||
showMales={config.showMales}
|
||||
stashScene={scene}
|
||||
scene={sceneResult}
|
||||
isActive={(selectedResult?.[scene.id] ?? 0) === i}
|
||||
setActive={() =>
|
||||
setSelectedResult({
|
||||
...selectedResult,
|
||||
[scene.id]: i,
|
||||
})
|
||||
}
|
||||
setCoverImage={config.setCoverImage}
|
||||
tagOperation={config.tagOperation}
|
||||
setTags={config.setTags}
|
||||
setScene={handleTaggedScene}
|
||||
endpoint={selectedEndpoint.endpoint}
|
||||
queueFingerprintSubmission={queueFingerprintSubmission}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return hideUnmatched && fingerprintMatches.length === 0 ? null : (
|
||||
<div key={scene.id} className="mt-3 search-item">
|
||||
<div className="row">
|
||||
<div className="col col-lg-6 overflow-hidden align-items-center d-flex flex-column flex-sm-row">
|
||||
<div className="scene-card mr-3">
|
||||
<Link to={sceneLink}>
|
||||
<ScenePreview
|
||||
image={scene.paths.screenshot ?? undefined}
|
||||
video={scene.paths.preview ?? undefined}
|
||||
isPortrait={isPortrait}
|
||||
soundActive={false}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Link to={sceneLink} className="scene-link overflow-hidden">
|
||||
<TruncatedText
|
||||
text={`${originalDir}\u200B${file}${ext}`}
|
||||
lineCount={2}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-md-6 my-1 align-self-center">
|
||||
{mainContent}
|
||||
<div className="sub-content text-right">{subContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
{searchResult}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="tagger-table">
|
||||
<div className="tagger-table-header d-flex flex-nowrap align-items-center">
|
||||
<b className="ml-auto mr-2 text-danger">{fingerprintError}</b>
|
||||
<div className="mr-2">
|
||||
{(getFingerprintCount() > 0 || hideUnmatched) && (
|
||||
<Button onClick={toggleHideUnmatchedScenes}>
|
||||
<FormattedMessage
|
||||
id="component_tagger.verb_toggle_unmatched"
|
||||
values={{
|
||||
toggle: (
|
||||
<FormattedMessage
|
||||
id={`actions.${hideUnmatched ? "hide" : "show"}`}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
{fingerprintQueue.length > 0 && (
|
||||
<Button
|
||||
onClick={handleFingerprintSubmission}
|
||||
disabled={submittingFingerprints}
|
||||
>
|
||||
{submittingFingerprints ? (
|
||||
<LoadingIndicator message="" inline small />
|
||||
) : (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="component_tagger.verb_submit_fp"
|
||||
values={{ fpCount: fingerprintQueue.length }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleFingerprintSearch}
|
||||
disabled={!canFingerprintSearch() && !loadingFingerprints}
|
||||
>
|
||||
{canFingerprintSearch() && (
|
||||
<span>
|
||||
{intl.formatMessage({ id: "component_tagger.verb_match_fp" })}
|
||||
</span>
|
||||
)}
|
||||
{!canFingerprintSearch() && getFingerprintCountMessage()}
|
||||
{loadingFingerprints && <LoadingIndicator message="" inline small />}
|
||||
</Button>
|
||||
</div>
|
||||
<form ref={inputForm}>{renderScenes()}</form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
import { LOCAL_FORAGE_KEY, ITaggerConfig, initialConfig } from "./constants";
|
||||
import { TaggerList } from "./TaggerList";
|
||||
|
||||
interface ITaggerProps {
|
||||
scenes: GQL.SlimSceneDataFragment[];
|
||||
|
|
@ -631,6 +28,38 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
|||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
|
||||
const clearSubmissionQueue = (endpoint: string) => {
|
||||
if (!config) return;
|
||||
|
||||
setConfig({
|
||||
...config,
|
||||
fingerprintQueue: {
|
||||
...config.fingerprintQueue,
|
||||
[endpoint]: [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const [
|
||||
submitFingerprints,
|
||||
{ loading: submittingFingerprints },
|
||||
] = GQL.useSubmitStashBoxFingerprintsMutation();
|
||||
|
||||
const handleFingerprintSubmission = (endpoint: string) => {
|
||||
if (!config) return;
|
||||
|
||||
return submitFingerprints({
|
||||
variables: {
|
||||
input: {
|
||||
stash_box_index: getEndpointIndex(endpoint),
|
||||
scene_ids: config?.fingerprintQueue[endpoint],
|
||||
},
|
||||
},
|
||||
}).then(() => {
|
||||
clearSubmissionQueue(endpoint);
|
||||
});
|
||||
};
|
||||
|
||||
if (!config) return <LoadingIndicator />;
|
||||
|
||||
const savedEndpointIndex =
|
||||
|
|
@ -645,7 +74,20 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
|||
const selectedEndpoint =
|
||||
stashConfig.data?.configuration.general.stashBoxes[selectedEndpointIndex];
|
||||
|
||||
function getEndpointIndex(endpoint: string) {
|
||||
return (
|
||||
stashConfig.data?.configuration.general.stashBoxes.findIndex(
|
||||
(s) => s.endpoint === endpoint
|
||||
) ?? -1
|
||||
);
|
||||
}
|
||||
|
||||
async function doBoxSearch(searchVal: string) {
|
||||
return (await stashBoxSceneQuery(searchVal, selectedEndpointIndex)).data;
|
||||
}
|
||||
|
||||
const queueFingerprintSubmission = (sceneId: string, endpoint: string) => {
|
||||
if (!config) return;
|
||||
setConfig({
|
||||
...config,
|
||||
fingerprintQueue: {
|
||||
|
|
@ -655,14 +97,16 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const clearSubmissionQueue = (endpoint: string) => {
|
||||
setConfig({
|
||||
...config,
|
||||
fingerprintQueue: {
|
||||
...config.fingerprintQueue,
|
||||
[endpoint]: [],
|
||||
},
|
||||
});
|
||||
const getQueue = (endpoint: string) => {
|
||||
if (!config) return [];
|
||||
return config.fingerprintQueue[endpoint] ?? [];
|
||||
};
|
||||
|
||||
const fingerprintQueue = {
|
||||
queueFingerprintSubmission,
|
||||
getQueue,
|
||||
submitFingerprints: handleFingerprintSubmission,
|
||||
submittingFingerprints,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -708,8 +152,8 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
|||
endpoint: selectedEndpoint.endpoint,
|
||||
index: selectedEndpointIndex,
|
||||
}}
|
||||
queueFingerprintSubmission={queueFingerprintSubmission}
|
||||
clearSubmissionQueue={clearSubmissionQueue}
|
||||
queryScene={doBoxSearch}
|
||||
fingerprintQueue={fingerprintQueue}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
329
ui/v2.5/src/components/Tagger/TaggerList.tsx
Normal file
329
ui/v2.5/src/components/Tagger/TaggerList.tsx
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Card } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { stashBoxSceneBatchQuery } from "src/core/StashService";
|
||||
|
||||
import { SceneQueue } from "src/models/sceneQueue";
|
||||
import { uniqBy } from "lodash";
|
||||
import { ITaggerConfig } from "./constants";
|
||||
import { selectScenes, IStashBoxScene } from "./utils";
|
||||
import { TaggerScene } from "./TaggerScene";
|
||||
|
||||
interface IFingerprintQueue {
|
||||
getQueue: (endpoint: string) => string[];
|
||||
queueFingerprintSubmission: (sceneId: string, endpoint: string) => void;
|
||||
submitFingerprints: (endpoint: string) => Promise<void> | undefined;
|
||||
submittingFingerprints: boolean;
|
||||
}
|
||||
|
||||
interface ITaggerListProps {
|
||||
scenes: GQL.SlimSceneDataFragment[];
|
||||
queue?: SceneQueue;
|
||||
selectedEndpoint: { endpoint: string; index: number };
|
||||
config: ITaggerConfig;
|
||||
queryScene: (searchVal: string) => Promise<GQL.QueryStashBoxSceneQuery>;
|
||||
fingerprintQueue: IFingerprintQueue;
|
||||
}
|
||||
|
||||
// Caches fingerprint lookups between page renders
|
||||
let fingerprintCache: Record<string, IStashBoxScene[]> = {};
|
||||
|
||||
function fingerprintSearchResults(
|
||||
scenes: GQL.SlimSceneDataFragment[],
|
||||
fingerprints: Record<string, IStashBoxScene[]>
|
||||
) {
|
||||
const ret: Record<string, IStashBoxScene[]> = {};
|
||||
|
||||
if (Object.keys(fingerprints).length === 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// perform matching here
|
||||
scenes.forEach((scene) => {
|
||||
// ignore where scene entry is not in results
|
||||
if (
|
||||
(scene.checksum && fingerprints[scene.checksum] !== undefined) ||
|
||||
(scene.oshash && fingerprints[scene.oshash] !== undefined) ||
|
||||
(scene.phash && fingerprints[scene.phash] !== undefined)
|
||||
) {
|
||||
const fingerprintMatches = uniqBy(
|
||||
[
|
||||
...(fingerprints[scene.checksum ?? ""] ?? []),
|
||||
...(fingerprints[scene.oshash ?? ""] ?? []),
|
||||
...(fingerprints[scene.phash ?? ""] ?? []),
|
||||
].flat(),
|
||||
(f) => f.stash_id
|
||||
);
|
||||
|
||||
ret[scene.id] = fingerprintMatches;
|
||||
} else {
|
||||
delete ret[scene.id];
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
scenes,
|
||||
queue,
|
||||
selectedEndpoint,
|
||||
config,
|
||||
queryScene,
|
||||
fingerprintQueue,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [fingerprintError, setFingerprintError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const inputForm = useRef<HTMLFormElement>(null);
|
||||
|
||||
const [searchErrors, setSearchErrors] = useState<
|
||||
Record<string, string | undefined>
|
||||
>({});
|
||||
const [taggedScenes, setTaggedScenes] = useState<
|
||||
Record<string, Partial<GQL.SlimSceneDataFragment>>
|
||||
>({});
|
||||
const [loadingFingerprints, setLoadingFingerprints] = useState(false);
|
||||
const [fingerprints, setFingerprints] = useState<
|
||||
Record<string, IStashBoxScene[]>
|
||||
>(fingerprintCache);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
Record<string, IStashBoxScene[]>
|
||||
>(fingerprintSearchResults(scenes, fingerprints));
|
||||
const [hideUnmatched, setHideUnmatched] = useState(false);
|
||||
const queuedFingerprints = fingerprintQueue.getQueue(
|
||||
selectedEndpoint.endpoint
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
inputForm?.current?.reset();
|
||||
}, [config.mode, config.blacklist]);
|
||||
|
||||
function clearSceneSearchResult(sceneID: string) {
|
||||
// remove sceneID results from the results object
|
||||
const { [sceneID]: _removedResult, ...newSearchResults } = searchResults;
|
||||
const { [sceneID]: _removedError, ...newSearchErrors } = searchErrors;
|
||||
setSearchResults(newSearchResults);
|
||||
setSearchErrors(newSearchErrors);
|
||||
}
|
||||
|
||||
const doSceneQuery = (sceneID: string, searchVal: string) => {
|
||||
clearSceneSearchResult(sceneID);
|
||||
|
||||
queryScene(searchVal)
|
||||
.then((queryData) => {
|
||||
const s = selectScenes(queryData.queryStashBoxScene);
|
||||
setSearchResults({
|
||||
...searchResults,
|
||||
[sceneID]: s,
|
||||
});
|
||||
setSearchErrors({
|
||||
...searchErrors,
|
||||
[sceneID]: undefined,
|
||||
});
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
// Destructure to remove existing result
|
||||
const { [sceneID]: unassign, ...results } = searchResults;
|
||||
setSearchResults(results);
|
||||
setSearchErrors({
|
||||
...searchErrors,
|
||||
[sceneID]: "Network Error",
|
||||
});
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
const handleFingerprintSubmission = () => {
|
||||
fingerprintQueue.submitFingerprints(selectedEndpoint.endpoint);
|
||||
};
|
||||
|
||||
const handleTaggedScene = (scene: Partial<GQL.SlimSceneDataFragment>) => {
|
||||
setTaggedScenes({
|
||||
...taggedScenes,
|
||||
[scene.id as string]: scene,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFingerprintSearch = async () => {
|
||||
setLoadingFingerprints(true);
|
||||
|
||||
setSearchErrors({});
|
||||
setSearchResults({});
|
||||
|
||||
const newFingerprints = { ...fingerprints };
|
||||
|
||||
const filteredScenes = scenes.filter((s) => s.stash_ids.length === 0);
|
||||
const sceneIDs = filteredScenes.map((s) => s.id);
|
||||
|
||||
const results = await stashBoxSceneBatchQuery(
|
||||
sceneIDs,
|
||||
selectedEndpoint.index
|
||||
).catch(() => {
|
||||
setLoadingFingerprints(false);
|
||||
setFingerprintError("Network Error");
|
||||
});
|
||||
|
||||
if (!results) return;
|
||||
|
||||
// clear search errors
|
||||
setSearchErrors({});
|
||||
|
||||
selectScenes(results.data?.queryStashBoxScene).forEach((scene) => {
|
||||
scene.fingerprints?.forEach((f) => {
|
||||
newFingerprints[f.hash] = newFingerprints[f.hash]
|
||||
? [...newFingerprints[f.hash], scene]
|
||||
: [scene];
|
||||
});
|
||||
});
|
||||
|
||||
// Null any ids that are still undefined since it means they weren't found
|
||||
filteredScenes.forEach((scene) => {
|
||||
if (scene.oshash) {
|
||||
newFingerprints[scene.oshash] = newFingerprints[scene.oshash] ?? null;
|
||||
}
|
||||
if (scene.checksum) {
|
||||
newFingerprints[scene.checksum] =
|
||||
newFingerprints[scene.checksum] ?? null;
|
||||
}
|
||||
if (scene.phash) {
|
||||
newFingerprints[scene.phash] = newFingerprints[scene.phash] ?? null;
|
||||
}
|
||||
});
|
||||
|
||||
const newSearchResults = fingerprintSearchResults(scenes, newFingerprints);
|
||||
setSearchResults(newSearchResults);
|
||||
|
||||
setFingerprints(newFingerprints);
|
||||
fingerprintCache = newFingerprints;
|
||||
setLoadingFingerprints(false);
|
||||
setFingerprintError("");
|
||||
};
|
||||
|
||||
const canFingerprintSearch = () =>
|
||||
scenes.some(
|
||||
(s) =>
|
||||
s.stash_ids.length === 0 &&
|
||||
(!s.oshash || fingerprints[s.oshash] === undefined) &&
|
||||
(!s.checksum || fingerprints[s.checksum] === undefined) &&
|
||||
(!s.phash || fingerprints[s.phash] === undefined)
|
||||
);
|
||||
|
||||
const getFingerprintCount = () => {
|
||||
return scenes.filter(
|
||||
(s) =>
|
||||
s.stash_ids.length === 0 &&
|
||||
((s.checksum && fingerprints[s.checksum]) ||
|
||||
(s.oshash && fingerprints[s.oshash]) ||
|
||||
(s.phash && fingerprints[s.phash]))
|
||||
).length;
|
||||
};
|
||||
|
||||
const getFingerprintCountMessage = () => {
|
||||
const count = getFingerprintCount();
|
||||
return intl.formatMessage(
|
||||
{ id: "component_tagger.results.fp_found" },
|
||||
{ fpCount: count }
|
||||
);
|
||||
};
|
||||
|
||||
const toggleHideUnmatchedScenes = () => {
|
||||
setHideUnmatched(!hideUnmatched);
|
||||
};
|
||||
|
||||
function generateSceneLink(scene: GQL.SlimSceneDataFragment, index: number) {
|
||||
return queue
|
||||
? queue.makeLink(scene.id, { sceneIndex: index })
|
||||
: `/scenes/${scene.id}`;
|
||||
}
|
||||
|
||||
const renderScenes = () =>
|
||||
scenes.map((scene, index) => {
|
||||
const sceneLink = generateSceneLink(scene, index);
|
||||
const searchResult = {
|
||||
results: searchResults[scene.id],
|
||||
error: searchErrors[scene.id],
|
||||
};
|
||||
|
||||
return (
|
||||
<TaggerScene
|
||||
key={scene.id}
|
||||
config={config}
|
||||
endpoint={selectedEndpoint.endpoint}
|
||||
queueFingerprintSubmission={
|
||||
fingerprintQueue.queueFingerprintSubmission
|
||||
}
|
||||
scene={scene}
|
||||
url={sceneLink}
|
||||
hideUnmatched={hideUnmatched}
|
||||
loading={loading}
|
||||
taggedScene={taggedScenes[scene.id]}
|
||||
doSceneQuery={(queryString) => doSceneQuery(scene.id, queryString)}
|
||||
tagScene={handleTaggedScene}
|
||||
searchResult={searchResult}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="tagger-table">
|
||||
<div className="tagger-table-header d-flex flex-nowrap align-items-center">
|
||||
<b className="ml-auto mr-2 text-danger">{fingerprintError}</b>
|
||||
<div className="mr-2">
|
||||
{(getFingerprintCount() > 0 || hideUnmatched) && (
|
||||
<Button onClick={toggleHideUnmatchedScenes}>
|
||||
<FormattedMessage
|
||||
id="component_tagger.verb_toggle_unmatched"
|
||||
values={{
|
||||
toggle: (
|
||||
<FormattedMessage
|
||||
id={`actions.${!hideUnmatched ? "hide" : "show"}`}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
{queuedFingerprints.length > 0 && (
|
||||
<Button
|
||||
onClick={handleFingerprintSubmission}
|
||||
disabled={fingerprintQueue.submittingFingerprints}
|
||||
>
|
||||
{fingerprintQueue.submittingFingerprints ? (
|
||||
<LoadingIndicator message="" inline small />
|
||||
) : (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="component_tagger.verb_submit_fp"
|
||||
values={{ fpCount: queuedFingerprints.length }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleFingerprintSearch}
|
||||
disabled={loadingFingerprints}
|
||||
>
|
||||
{canFingerprintSearch() && (
|
||||
<span>
|
||||
{intl.formatMessage({ id: "component_tagger.verb_match_fp" })}
|
||||
</span>
|
||||
)}
|
||||
{!canFingerprintSearch() && getFingerprintCountMessage()}
|
||||
{loadingFingerprints && <LoadingIndicator message="" inline small />}
|
||||
</Button>
|
||||
</div>
|
||||
<form ref={inputForm}>{renderScenes()}</form>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
233
ui/v2.5/src/components/Tagger/TaggerScene.tsx
Normal file
233
ui/v2.5/src/components/Tagger/TaggerScene.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { ScenePreview } from "src/components/Scenes/SceneCard";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import StashSearchResult from "./StashSearchResult";
|
||||
import { ITaggerConfig } from "./constants";
|
||||
import {
|
||||
parsePath,
|
||||
IStashBoxScene,
|
||||
sortScenesByDuration,
|
||||
prepareQueryString,
|
||||
} from "./utils";
|
||||
|
||||
export interface ISearchResult {
|
||||
results?: IStashBoxScene[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ITaggerScene {
|
||||
scene: GQL.SlimSceneDataFragment;
|
||||
url: string;
|
||||
config: ITaggerConfig;
|
||||
searchResult?: ISearchResult;
|
||||
hideUnmatched?: boolean;
|
||||
loading?: boolean;
|
||||
doSceneQuery: (queryString: string) => void;
|
||||
taggedScene?: Partial<GQL.SlimSceneDataFragment>;
|
||||
tagScene: (scene: Partial<GQL.SlimSceneDataFragment>) => void;
|
||||
endpoint: string;
|
||||
queueFingerprintSubmission: (sceneId: string, endpoint: string) => void;
|
||||
}
|
||||
|
||||
export const TaggerScene: React.FC<ITaggerScene> = ({
|
||||
scene,
|
||||
url,
|
||||
config,
|
||||
searchResult,
|
||||
hideUnmatched,
|
||||
loading,
|
||||
doSceneQuery,
|
||||
taggedScene,
|
||||
tagScene,
|
||||
endpoint,
|
||||
queueFingerprintSubmission,
|
||||
}) => {
|
||||
const [selectedResult, setSelectedResult] = useState<number>(0);
|
||||
|
||||
const queryString = useRef<string>("");
|
||||
|
||||
const searchResults = searchResult?.results ?? [];
|
||||
const searchError = searchResult?.error;
|
||||
const emptyResults =
|
||||
searchResult && searchResult.results && searchResult.results.length === 0;
|
||||
|
||||
const { paths, file, ext } = parsePath(scene.path);
|
||||
const originalDir = scene.path.slice(
|
||||
0,
|
||||
scene.path.length - file.length - ext.length
|
||||
);
|
||||
const defaultQueryString = prepareQueryString(
|
||||
scene,
|
||||
paths,
|
||||
file,
|
||||
config.mode,
|
||||
config.blacklist
|
||||
);
|
||||
|
||||
const hasStashIDs = scene.stash_ids.length > 0;
|
||||
const width = scene.file.width ? scene.file.width : 0;
|
||||
const height = scene.file.height ? scene.file.height : 0;
|
||||
const isPortrait = height > width;
|
||||
|
||||
function renderMainContent() {
|
||||
if (!taggedScene && hasStashIDs) {
|
||||
return (
|
||||
<div className="text-right">
|
||||
<h5 className="text-bold">
|
||||
<FormattedMessage id="component_tagger.results.match_failed_already_tagged" />
|
||||
</h5>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!taggedScene && !hasStashIDs) {
|
||||
return (
|
||||
<InputGroup>
|
||||
<InputGroup.Prepend>
|
||||
<InputGroup.Text>
|
||||
<FormattedMessage id="component_tagger.noun_query" />
|
||||
</InputGroup.Text>
|
||||
</InputGroup.Prepend>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
defaultValue={queryString.current || defaultQueryString}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
queryString.current = e.currentTarget.value;
|
||||
}}
|
||||
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||
e.key === "Enter" &&
|
||||
doSceneQuery(queryString.current || defaultQueryString)
|
||||
}
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
<Button
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
doSceneQuery(queryString.current || defaultQueryString)
|
||||
}
|
||||
>
|
||||
<FormattedMessage id="actions.search" />
|
||||
</Button>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (taggedScene) {
|
||||
return (
|
||||
<div className="d-flex flex-column text-right">
|
||||
<h5>
|
||||
<FormattedMessage id="component_tagger.results.match_success" />
|
||||
</h5>
|
||||
<h6>
|
||||
<Link className="bold" to={url}>
|
||||
{taggedScene.title}
|
||||
</Link>
|
||||
</h6>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSubContent() {
|
||||
if (scene.stash_ids.length > 0) {
|
||||
const stashLinks = scene.stash_ids.map((stashID) => {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
key={`${stashID.endpoint}${stashID.stash_id}`}
|
||||
className="small d-block"
|
||||
href={`${base}scenes/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
) : (
|
||||
<div className="small">{stashID.stash_id}</div>
|
||||
);
|
||||
|
||||
return link;
|
||||
});
|
||||
return <>{stashLinks}</>;
|
||||
}
|
||||
|
||||
if (searchError) {
|
||||
return <div className="text-danger font-weight-bold">{searchError}</div>;
|
||||
}
|
||||
|
||||
if (emptyResults) {
|
||||
return (
|
||||
<div className="text-danger font-weight-bold">
|
||||
<FormattedMessage id="component_tagger.results.match_failed_no_result" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSearchResult() {
|
||||
if (searchResults.length > 0 && !taggedScene) {
|
||||
return (
|
||||
<ul className="pl-0 mt-3 mb-0">
|
||||
{sortScenesByDuration(
|
||||
searchResults,
|
||||
scene.file.duration ?? undefined
|
||||
).map(
|
||||
(sceneResult, i) =>
|
||||
sceneResult && (
|
||||
<StashSearchResult
|
||||
key={sceneResult.stash_id}
|
||||
showMales={config.showMales}
|
||||
stashScene={scene}
|
||||
scene={sceneResult}
|
||||
isActive={selectedResult === i}
|
||||
setActive={() => setSelectedResult(i)}
|
||||
setCoverImage={config.setCoverImage}
|
||||
tagOperation={config.tagOperation}
|
||||
setTags={config.setTags}
|
||||
setScene={tagScene}
|
||||
endpoint={endpoint}
|
||||
queueFingerprintSubmission={queueFingerprintSubmission}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return hideUnmatched && emptyResults ? null : (
|
||||
<div key={scene.id} className="mt-3 search-item">
|
||||
<div className="row">
|
||||
<div className="col col-lg-6 overflow-hidden align-items-center d-flex flex-column flex-sm-row">
|
||||
<div className="scene-card mr-3">
|
||||
<Link to={url}>
|
||||
<ScenePreview
|
||||
image={scene.paths.screenshot ?? undefined}
|
||||
video={scene.paths.preview ?? undefined}
|
||||
isPortrait={isPortrait}
|
||||
soundActive={false}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Link to={url} className="scene-link overflow-hidden">
|
||||
<TruncatedText
|
||||
text={`${originalDir}\u200B${file}${ext}`}
|
||||
lineCount={2}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-md-6 my-1 align-self-center">
|
||||
{renderMainContent()}
|
||||
<div className="sub-content text-right">{renderSubContent()}</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderSearchResult()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -179,9 +179,9 @@
|
|||
|
||||
&-item {
|
||||
align-items: center;
|
||||
align-text: left;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
260
ui/v2.5/src/components/Tagger/taggerService.ts
Normal file
260
ui/v2.5/src/components/Tagger/taggerService.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
import { blobToBase64 } from "base64-blob";
|
||||
import { uniq } from "lodash";
|
||||
import {
|
||||
useCreatePerformer,
|
||||
useCreateStudio,
|
||||
useCreateTag,
|
||||
useUpdatePerformerStashID,
|
||||
useUpdateStudioStashID,
|
||||
} from "./queries";
|
||||
import { IPerformerOperations } from "./PerformerResult";
|
||||
import { StudioOperation } from "./StudioResult";
|
||||
import { IStashBoxScene } from "./utils";
|
||||
|
||||
export interface ITagSceneOptions {
|
||||
setCoverImage?: boolean;
|
||||
setTags?: boolean;
|
||||
tagOperation: string;
|
||||
}
|
||||
|
||||
export function useTagScene(
|
||||
options: ITagSceneOptions,
|
||||
setSaveState: (state: string) => void,
|
||||
setError: (err: { message?: string; details?: string }) => void
|
||||
) {
|
||||
const createStudio = useCreateStudio();
|
||||
const createPerformer = useCreatePerformer();
|
||||
const createTag = useCreateTag();
|
||||
const updatePerformerStashID = useUpdatePerformerStashID();
|
||||
const updateStudioStashID = useUpdateStudioStashID();
|
||||
const [updateScene] = GQL.useSceneUpdateMutation({
|
||||
onError: (e) => {
|
||||
const message =
|
||||
e.message === "invalid JPEG format: short Huffman data"
|
||||
? "Failed to save scene due to corrupted cover image"
|
||||
: "Failed to save scene";
|
||||
setError({
|
||||
message,
|
||||
details: e.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { data: allTags } = GQL.useAllTagsForFilterQuery();
|
||||
|
||||
const handleSave = async (
|
||||
stashScene: GQL.SlimSceneDataFragment,
|
||||
scene: IStashBoxScene,
|
||||
studio: StudioOperation | undefined,
|
||||
performers: IPerformerOperations,
|
||||
endpoint: string
|
||||
) => {
|
||||
setError({});
|
||||
let performerIDs = [];
|
||||
let studioID = null;
|
||||
|
||||
if (!studio) return;
|
||||
|
||||
if (studio.type === "create") {
|
||||
setSaveState("Creating studio");
|
||||
const newStudio = {
|
||||
name: studio.data.name,
|
||||
stash_ids: [
|
||||
{
|
||||
endpoint,
|
||||
stash_id: scene.studio.stash_id,
|
||||
},
|
||||
],
|
||||
url: studio.data.url,
|
||||
};
|
||||
const studioCreateResult = await createStudio(
|
||||
newStudio,
|
||||
scene.studio.stash_id
|
||||
);
|
||||
|
||||
if (!studioCreateResult?.data?.studioCreate) {
|
||||
setError({
|
||||
message: `Failed to save studio "${newStudio.name}"`,
|
||||
details: studioCreateResult?.errors?.[0].message,
|
||||
});
|
||||
return setSaveState("");
|
||||
}
|
||||
studioID = studioCreateResult.data.studioCreate.id;
|
||||
} else if (studio.type === "update") {
|
||||
setSaveState("Saving studio stashID");
|
||||
const res = await updateStudioStashID(studio.data, [
|
||||
...studio.data.stash_ids,
|
||||
{ stash_id: scene.studio.stash_id, endpoint },
|
||||
]);
|
||||
if (!res?.data?.studioUpdate) {
|
||||
setError({
|
||||
message: `Failed to save stashID to studio "${studio.data.name}"`,
|
||||
details: res?.errors?.[0].message,
|
||||
});
|
||||
return setSaveState("");
|
||||
}
|
||||
studioID = res.data.studioUpdate.id;
|
||||
} else if (studio.type === "existing") {
|
||||
studioID = studio.data.id;
|
||||
} else if (studio.type === "skip") {
|
||||
studioID = stashScene.studio?.id;
|
||||
}
|
||||
|
||||
setSaveState("Saving performers");
|
||||
performerIDs = await Promise.all(
|
||||
Object.keys(performers).map(async (stashID) => {
|
||||
const performer = performers[stashID];
|
||||
if (performer.type === "skip") return "Skip";
|
||||
|
||||
let performerID = performer.data.id;
|
||||
|
||||
if (performer.type === "create") {
|
||||
const imgurl = performer.data.images[0];
|
||||
let imgData = null;
|
||||
if (imgurl) {
|
||||
const img = await fetch(imgurl, {
|
||||
mode: "cors",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (img.status === 200) {
|
||||
const blob = await img.blob();
|
||||
imgData = await blobToBase64(blob);
|
||||
}
|
||||
}
|
||||
|
||||
const performerInput = {
|
||||
name: performer.data.name,
|
||||
gender: performer.data.gender,
|
||||
country: performer.data.country,
|
||||
height: performer.data.height,
|
||||
ethnicity: performer.data.ethnicity,
|
||||
birthdate: performer.data.birthdate,
|
||||
eye_color: performer.data.eye_color,
|
||||
fake_tits: performer.data.fake_tits,
|
||||
measurements: performer.data.measurements,
|
||||
career_length: performer.data.career_length,
|
||||
tattoos: performer.data.tattoos,
|
||||
piercings: performer.data.piercings,
|
||||
twitter: performer.data.twitter,
|
||||
instagram: performer.data.instagram,
|
||||
image: imgData,
|
||||
stash_ids: [
|
||||
{
|
||||
endpoint,
|
||||
stash_id: stashID,
|
||||
},
|
||||
],
|
||||
details: performer.data.details,
|
||||
death_date: performer.data.death_date,
|
||||
hair_color: performer.data.hair_color,
|
||||
weight: Number(performer.data.weight),
|
||||
};
|
||||
|
||||
const res = await createPerformer(performerInput, stashID);
|
||||
if (!res?.data?.performerCreate) {
|
||||
setError({
|
||||
message: `Failed to save performer "${performerInput.name}"`,
|
||||
details: res?.errors?.[0].message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
performerID = res.data?.performerCreate.id;
|
||||
}
|
||||
|
||||
if (performer.type === "update") {
|
||||
const stashIDs = performer.data.stash_ids;
|
||||
await updatePerformerStashID(performer.data.id, [
|
||||
...stashIDs,
|
||||
{ stash_id: stashID, endpoint },
|
||||
]);
|
||||
}
|
||||
|
||||
return performerID;
|
||||
})
|
||||
);
|
||||
|
||||
if (!performerIDs.some((id) => !id)) {
|
||||
setSaveState("Updating scene");
|
||||
const imgurl = scene.images[0];
|
||||
let imgData = null;
|
||||
if (imgurl && options.setCoverImage) {
|
||||
const img = await fetch(imgurl, {
|
||||
mode: "cors",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (img.status === 200) {
|
||||
const blob = await img.blob();
|
||||
// Sanity check on image size since bad images will fail
|
||||
if (blob.size > 10000) imgData = await blobToBase64(blob);
|
||||
}
|
||||
}
|
||||
|
||||
let updatedTags = stashScene?.tags?.map((t) => t.id) ?? [];
|
||||
if (options.setTags) {
|
||||
const newTagIDs = options.tagOperation === "merge" ? updatedTags : [];
|
||||
const tags = scene.tags ?? [];
|
||||
if (tags.length > 0) {
|
||||
const tagDict: Record<string, string> = (allTags?.allTags ?? [])
|
||||
.filter((t) => t.name)
|
||||
.reduce(
|
||||
(dict, t) => ({ ...dict, [t.name.toLowerCase()]: t.id }),
|
||||
{}
|
||||
);
|
||||
const newTags: string[] = [];
|
||||
tags.forEach((tag) => {
|
||||
if (tagDict[tag.name.toLowerCase()])
|
||||
newTagIDs.push(tagDict[tag.name.toLowerCase()]);
|
||||
else newTags.push(tag.name);
|
||||
});
|
||||
|
||||
const createdTags = await Promise.all(
|
||||
newTags.map((tag) => createTag(tag))
|
||||
);
|
||||
createdTags.forEach((createdTag) => {
|
||||
if (createdTag?.data?.tagCreate?.id)
|
||||
newTagIDs.push(createdTag.data.tagCreate.id);
|
||||
});
|
||||
}
|
||||
updatedTags = uniq(newTagIDs);
|
||||
}
|
||||
|
||||
const performer_ids = performerIDs.filter(
|
||||
(id) => id !== "Skip"
|
||||
) as string[];
|
||||
|
||||
const sceneUpdateResult = await updateScene({
|
||||
variables: {
|
||||
input: {
|
||||
id: stashScene.id ?? "",
|
||||
title: scene.title,
|
||||
details: scene.details,
|
||||
date: scene.date,
|
||||
performer_ids:
|
||||
performer_ids.length === 0
|
||||
? stashScene.performers.map((p) => p.id)
|
||||
: performer_ids,
|
||||
studio_id: studioID,
|
||||
cover_image: imgData,
|
||||
url: scene.url,
|
||||
tag_ids: updatedTags,
|
||||
stash_ids: [
|
||||
...(stashScene?.stash_ids ?? []),
|
||||
{
|
||||
endpoint,
|
||||
stash_id: scene.stash_id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setSaveState("");
|
||||
return sceneUpdateResult?.data?.sceneUpdate;
|
||||
}
|
||||
|
||||
setSaveState("");
|
||||
};
|
||||
|
||||
return handleSave;
|
||||
}
|
||||
|
|
@ -1,5 +1,116 @@
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
import { getCountryByISO } from "src/utils/country";
|
||||
import { ParseMode } from "./constants";
|
||||
|
||||
const months = [
|
||||
"jan",
|
||||
"feb",
|
||||
"mar",
|
||||
"apr",
|
||||
"may",
|
||||
"jun",
|
||||
"jul",
|
||||
"aug",
|
||||
"sep",
|
||||
"oct",
|
||||
"nov",
|
||||
"dec",
|
||||
];
|
||||
|
||||
const ddmmyyRegex = /\.(\d\d)\.(\d\d)\.(\d\d)\./;
|
||||
const yyyymmddRegex = /(\d{4})[-.](\d{2})[-.](\d{2})/;
|
||||
const mmddyyRegex = /(\d{2})[-.](\d{2})[-.](\d{4})/;
|
||||
const ddMMyyRegex = new RegExp(
|
||||
`(\\d{1,2}).(${months.join("|")})\\.?.(\\d{4})`,
|
||||
"i"
|
||||
);
|
||||
const MMddyyRegex = new RegExp(
|
||||
`(${months.join("|")})\\.?.(\\d{1,2}),?.(\\d{4})`,
|
||||
"i"
|
||||
);
|
||||
const parseDate = (input: string): string => {
|
||||
let output = input;
|
||||
const ddmmyy = output.match(ddmmyyRegex);
|
||||
if (ddmmyy) {
|
||||
output = output.replace(
|
||||
ddmmyy[0],
|
||||
` 20${ddmmyy[1]}-${ddmmyy[2]}-${ddmmyy[3]} `
|
||||
);
|
||||
}
|
||||
const mmddyy = output.match(mmddyyRegex);
|
||||
if (mmddyy) {
|
||||
output = output.replace(
|
||||
mmddyy[0],
|
||||
` ${mmddyy[1]}-${mmddyy[2]}-${mmddyy[3]} `
|
||||
);
|
||||
}
|
||||
const ddMMyy = output.match(ddMMyyRegex);
|
||||
if (ddMMyy) {
|
||||
const month = (months.indexOf(ddMMyy[2].toLowerCase()) + 1)
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
output = output.replace(
|
||||
ddMMyy[0],
|
||||
` ${ddMMyy[3]}-${month}-${ddMMyy[1].padStart(2, "0")} `
|
||||
);
|
||||
}
|
||||
const MMddyy = output.match(MMddyyRegex);
|
||||
if (MMddyy) {
|
||||
const month = (months.indexOf(MMddyy[1].toLowerCase()) + 1)
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
output = output.replace(
|
||||
MMddyy[0],
|
||||
` ${MMddyy[3]}-${month}-${MMddyy[2].padStart(2, "0")} `
|
||||
);
|
||||
}
|
||||
|
||||
const yyyymmdd = output.search(yyyymmddRegex);
|
||||
if (yyyymmdd !== -1)
|
||||
return (
|
||||
output.slice(0, yyyymmdd).replace(/-/g, " ") +
|
||||
output.slice(yyyymmdd, yyyymmdd + 10).replace(/\./g, "-") +
|
||||
output.slice(yyyymmdd + 10).replace(/-/g, " ")
|
||||
);
|
||||
return output.replace(/-/g, " ");
|
||||
};
|
||||
|
||||
export function prepareQueryString(
|
||||
scene: Partial<GQL.SlimSceneDataFragment>,
|
||||
paths: string[],
|
||||
filename: string,
|
||||
mode: ParseMode,
|
||||
blacklist: string[]
|
||||
) {
|
||||
if ((mode === "auto" && scene.date && scene.studio) || mode === "metadata") {
|
||||
let str = [
|
||||
scene.date,
|
||||
scene.studio?.name ?? "",
|
||||
(scene?.performers ?? []).map((p) => p.name).join(" "),
|
||||
scene?.title ? scene.title.replace(/[^a-zA-Z0-9 ]+/g, "") : "",
|
||||
]
|
||||
.filter((s) => s !== "")
|
||||
.join(" ");
|
||||
blacklist.forEach((b) => {
|
||||
str = str.replace(new RegExp(b, "gi"), " ");
|
||||
});
|
||||
return str;
|
||||
}
|
||||
let s = "";
|
||||
|
||||
if (mode === "auto" || mode === "filename") {
|
||||
s = filename;
|
||||
} else if (mode === "path") {
|
||||
s = [...paths, filename].join(" ");
|
||||
} else {
|
||||
s = paths[paths.length - 1];
|
||||
}
|
||||
blacklist.forEach((b) => {
|
||||
s = s.replace(new RegExp(b, "gi"), " ");
|
||||
});
|
||||
s = parseDate(s);
|
||||
return s.replace(/\./g, " ");
|
||||
}
|
||||
|
||||
const toTitleCase = (phrase: string) => {
|
||||
return phrase
|
||||
|
|
|
|||
Loading…
Reference in a new issue