Various bugfixes for scene tagger (#1014)

* Tagger fixes
* Validate stash-box endpoint pattern
This commit is contained in:
InfiniteTF 2020-12-28 03:28:29 +01:00 committed by GitHub
parent 5c10712aab
commit d50238cf41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 185 additions and 105 deletions

View File

@ -7,6 +7,7 @@ import (
"errors"
"io/ioutil"
"path/filepath"
"regexp"
"github.com/spf13/viper"
@ -427,11 +428,18 @@ func ValidateCredentials(username string, password string) bool {
func ValidateStashBoxes(boxes []*models.StashBoxInput) error {
isMulti := len(boxes) > 1
re, err := regexp.Compile("^http.*graphql$")
if err != nil {
return errors.New("Failure to generate regular expression")
}
for _, box := range boxes {
if box.APIKey == "" {
return errors.New("Stash-box API Key cannot be blank")
} else if box.Endpoint == "" {
return errors.New("Stash-box Endpoint cannot be blank")
} else if !re.Match([]byte(box.Endpoint)) {
return errors.New("Stash-box Endpoint is invalid")
} else if isMulti && box.Name == "" {
return errors.New("Stash-box Name cannot be blank")
}

View File

@ -20,3 +20,4 @@
### 🐛 Bug fixes
* Corrected file sizes on 32bit platforms
* Fixed login redirect to remember the current page.
* Fixed scene tagger config saving

View File

@ -41,7 +41,7 @@ const Instance: React.FC<IInstanceProps> = ({
value={instance?.endpoint}
isValid={(instance?.endpoint?.length ?? 0) > 0}
onInput={(e: React.ChangeEvent<HTMLInputElement>) =>
handleInput("endpoint", e.currentTarget.value)
handleInput("endpoint", e.currentTarget.value.trim())
}
/>
<Form.Control
@ -50,7 +50,7 @@ const Instance: React.FC<IInstanceProps> = ({
value={instance?.api_key}
isValid={(instance?.api_key?.length ?? 0) > 0}
onInput={(e: React.ChangeEvent<HTMLInputElement>) =>
handleInput("api_key", e.currentTarget.value)
handleInput("api_key", e.currentTarget.value.trim())
}
/>
<InputGroup.Append>

View File

@ -16,7 +16,7 @@
}
&.inline {
display: inline-block;
display: inline;
height: auto;
margin-left: 0.5rem;
}

View File

@ -1,4 +1,4 @@
import React, { Dispatch, useEffect, useState } from "react";
import React, { Dispatch, useRef } from "react";
import {
Badge,
Button,
@ -8,51 +8,9 @@ import {
InputGroup,
} from "react-bootstrap";
import { Icon } from "src/components/Shared";
import localForage from "localforage";
import { useConfiguration } from "src/core/StashService";
const DEFAULT_BLACKLIST = [
"\\sXXX\\s",
"1080p",
"720p",
"2160p",
"KTR",
"RARBG",
"\\scom\\s",
"\\[",
"\\]",
];
export const initialConfig: ITaggerConfig = {
blacklist: DEFAULT_BLACKLIST,
showMales: false,
mode: "auto",
setCoverImage: true,
setTags: false,
tagOperation: "merge",
fingerprintQueue: {},
};
export type ParseMode = "auto" | "filename" | "dir" | "path" | "metadata";
const ModeDesc = {
auto: "Uses metadata if present, or filename",
metadata: "Only uses metadata",
filename: "Only uses filename",
dir: "Only uses parent directory of video file",
path: "Uses entire file path",
};
export interface ITaggerConfig {
blacklist: string[];
showMales: boolean;
mode: ParseMode;
setCoverImage: boolean;
setTags: boolean;
tagOperation: string;
selectedEndpoint?: string;
fingerprintQueue: Record<string, string[]>;
}
import { ITaggerConfig, ParseMode, ModeDesc } from "./constants";
interface IConfigProps {
show: boolean;
@ -62,26 +20,7 @@ interface IConfigProps {
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
const stashConfig = useConfiguration();
const [blacklistInput, setBlacklistInput] = useState<string>("");
useEffect(() => {
localForage.getItem<ITaggerConfig>("tagger").then((data) => {
setConfig({
blacklist: data?.blacklist ?? DEFAULT_BLACKLIST,
showMales: data?.showMales ?? false,
mode: data?.mode ?? "auto",
setCoverImage: data?.setCoverImage ?? true,
setTags: data?.setTags ?? false,
tagOperation: data?.tagOperation ?? "merge",
selectedEndpoint: data?.selectedEndpoint,
fingerprintQueue: data?.fingerprintQueue ?? {},
});
});
}, [setConfig]);
useEffect(() => {
localForage.setItem("tagger", config);
}, [config]);
const blacklistRef = useRef<HTMLInputElement | null>(null);
const handleInstanceSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedEndpoint = e.currentTarget.value;
@ -102,11 +41,14 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
};
const handleBlacklistAddition = () => {
if (!blacklistRef.current) return;
const input = blacklistRef.current.value;
setConfig({
...config,
blacklist: [...config.blacklist, blacklistInput],
blacklist: [...config.blacklist, input],
});
setBlacklistInput("");
blacklistRef.current.value = "";
};
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
@ -204,13 +146,7 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
<div className="col-md-6">
<h5>Blacklist</h5>
<InputGroup>
<Form.Control
className="text-input"
value={blacklistInput}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setBlacklistInput(e.currentTarget.value)
}
/>
<Form.Control className="text-input" ref={blacklistRef} />
<InputGroup.Append>
<Button onClick={handleBlacklistAddition}>Add</Button>
</InputGroup.Append>

View File

@ -104,8 +104,18 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
const updatePerformerStashID = useUpdatePerformerStashID();
const updateStudioStashID = useUpdateStudioStashID();
const [updateScene] = GQL.useSceneUpdateMutation({
onError: (errors) => errors,
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 setPerformer = (
@ -302,12 +312,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
},
});
if (!sceneUpdateResult?.data?.sceneUpdate) {
setError({
message: "Failed to save scene",
details: sceneUpdateResult?.errors?.[0].message,
});
} else if (sceneUpdateResult.data?.sceneUpdate)
if (sceneUpdateResult?.data?.sceneUpdate)
setScene(sceneUpdateResult.data.sceneUpdate);
queueFingerprintSubmission(stashScene.id, endpoint);

View File

@ -3,6 +3,7 @@ import { Button, Card, Form, InputGroup } from "react-bootstrap";
import { Link } from "react-router-dom";
import { HashLink } from "react-router-hash-link";
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";
@ -14,7 +15,13 @@ import {
import { Manual } from "src/components/Help/Manual";
import StashSearchResult from "./StashSearchResult";
import Config, { ITaggerConfig, initialConfig, ParseMode } from "./Config";
import Config from "./Config";
import {
LOCAL_FORAGE_KEY,
ITaggerConfig,
ParseMode,
initialConfig,
} from "./constants";
import {
parsePath,
selectScenes,
@ -22,7 +29,79 @@ import {
sortScenesByDuration,
} from "./utils";
const dateRegex = /\.(\d\d)\.(\d\d)\.(\d\d)\./;
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],
` 20${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) +
output.slice(yyyymmdd + 10).replace(/-/g, " ")
);
return output.replace(/-/g, " ");
};
function prepareQueryString(
scene: Partial<GQL.SlimSceneDataFragment>,
paths: string[],
@ -45,6 +124,7 @@ function prepareQueryString(
return str;
}
let s = "";
if (mode === "auto" || mode === "filename") {
s = filename;
} else if (mode === "path") {
@ -55,11 +135,7 @@ function prepareQueryString(
blacklist.forEach((b) => {
s = s.replace(new RegExp(b, "i"), "");
});
const date = s.match(dateRegex);
s = s.replace(/-/g, " ");
if (date) {
s = s.replace(date[0], ` 20${date[1]}-${date[2]}-${date[3]} `);
}
s = parseDate(s);
return s.replace(/\./g, " ");
}
@ -248,6 +324,9 @@ const TaggerList: React.FC<ITaggerListProps> = ({
} else if (!isTagged && !hasStashIDs) {
mainContent = (
<InputGroup>
<InputGroup.Prepend>
<InputGroup.Text>Query</InputGroup.Text>
</InputGroup.Prepend>
<Form.Control
className="text-input"
value={modifiedQuery || defaultQueryString}
@ -417,12 +496,6 @@ const TaggerList: React.FC<ITaggerListProps> = ({
return (
<Card className="tagger-table">
<div className="tagger-table-header d-flex flex-nowrap align-items-center">
<div className="col-md-6 pl-0">
<b>Scene</b>
</div>
<div className="col-md-2">
<b>Query</b>
</div>
<b className="ml-auto mr-2 text-danger">{fingerprintError}</b>
<div className="mr-2">
{fingerprintQueue.length > 0 && (
@ -460,10 +533,15 @@ interface ITaggerProps {
export const Tagger: React.FC<ITaggerProps> = ({ scenes }) => {
const stashConfig = useConfiguration();
const [config, setConfig] = useState<ITaggerConfig>(initialConfig);
const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>(
LOCAL_FORAGE_KEY,
initialConfig
);
const [showConfig, setShowConfig] = useState(false);
const [showManual, setShowManual] = useState(false);
if (!config) return <LoadingIndicator />;
const savedEndpointIndex =
stashConfig.data?.configuration.general.stashBoxes.findIndex(
(s) => s.endpoint === config.selectedEndpoint
@ -503,7 +581,7 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes }) => {
onClose={() => setShowManual(false)}
defaultActiveTab="Tagger.md"
/>
<div className="tagger-container row mx-md-auto">
<div className="tagger-container mx-md-auto">
{selectedEndpointIndex !== -1 && selectedEndpoint ? (
<>
<div className="row mb-2 no-gutters">

View File

@ -0,0 +1,42 @@
export const LOCAL_FORAGE_KEY = "tagger";
export const DEFAULT_BLACKLIST = [
"\\sXXX\\s",
"1080p",
"720p",
"2160p",
"KTR",
"RARBG",
"\\scom\\s",
"\\[",
"\\]",
];
export const initialConfig: ITaggerConfig = {
blacklist: DEFAULT_BLACKLIST,
showMales: false,
mode: "auto",
setCoverImage: true,
setTags: false,
tagOperation: "merge",
fingerprintQueue: {},
};
export type ParseMode = "auto" | "filename" | "dir" | "path" | "metadata";
export const ModeDesc = {
auto: "Uses metadata if present, or filename",
metadata: "Only uses metadata",
filename: "Only uses filename",
dir: "Only uses parent directory of video file",
path: "Uses entire file path",
};
export interface ITaggerConfig {
blacklist: string[];
showMales: boolean;
mode: ParseMode;
setCoverImage: boolean;
setTags: boolean;
tagOperation: string;
selectedEndpoint?: string;
fingerprintQueue: Record<string, string[]>;
}

View File

@ -27,8 +27,9 @@ interface ILocalForage<T> {
const Loading: Record<string, boolean> = {};
const Cache: Record<string, {}> = {};
function useLocalForage<T>(
key: string
export function useLocalForage<T>(
key: string,
defaultValue: T = {} as T
): [ILocalForage<T>, Dispatch<SetStateAction<T>>] {
const [error, setError] = React.useState(null);
const [data, setData] = React.useState<T>(Cache[key] as T);
@ -43,27 +44,32 @@ function useLocalForage<T>(
setData(parsed);
Cache[key] = parsed;
} else {
setData({} as T);
Cache[key] = {};
setData(defaultValue);
Cache[key] = defaultValue;
}
setError(null);
} catch (err) {
setError(err);
Cache[key] = defaultValue;
} finally {
Loading[key] = false;
setLoading(false);
}
}
if (!loading && !Cache[key]) {
Loading[key] = true;
setLoading(true);
runAsync();
}
}, [loading, data, key]);
}, [loading, key, defaultValue]);
useEffect(() => {
if (!_.isEqual(Cache[key], data)) {
Cache[key] = _.merge(Cache[key], data);
Cache[key] = _.merge({
...Cache[key],
...data,
});
localForage.setItem(key, JSON.stringify(Cache[key]));
}
});

View File

@ -1,5 +1,9 @@
export { default as useToast } from "./Toast";
export { useInterfaceLocalForage, useChangelogStorage } from "./LocalForage";
export {
useInterfaceLocalForage,
useChangelogStorage,
useLocalForage,
} from "./LocalForage";
export {
useScenesList,
useSceneMarkersList,