mirror of https://github.com/stashapp/stash.git
Various bugfixes for scene tagger (#1014)
* Tagger fixes * Validate stash-box endpoint pattern
This commit is contained in:
parent
5c10712aab
commit
d50238cf41
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
}
|
||||
|
||||
&.inline {
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
height: auto;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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[]>;
|
||||
}
|
|
@ -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]));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
export { default as useToast } from "./Toast";
|
||||
export { useInterfaceLocalForage, useChangelogStorage } from "./LocalForage";
|
||||
export {
|
||||
useInterfaceLocalForage,
|
||||
useChangelogStorage,
|
||||
useLocalForage,
|
||||
} from "./LocalForage";
|
||||
export {
|
||||
useScenesList,
|
||||
useSceneMarkersList,
|
||||
|
|
Loading…
Reference in New Issue