From d50238cf41a74b12b195673539a3d9a8f4eae8c6 Mon Sep 17 00:00:00 2001 From: InfiniteTF Date: Mon, 28 Dec 2020 03:28:29 +0100 Subject: [PATCH] Various bugfixes for scene tagger (#1014) * Tagger fixes * Validate stash-box endpoint pattern --- pkg/manager/config/config.go | 8 ++ .../src/components/Changelog/versions/v050.md | 1 + .../Settings/StashBoxConfiguration.tsx | 4 +- ui/v2.5/src/components/Shared/styles.scss | 2 +- ui/v2.5/src/components/Tagger/Config.tsx | 82 ++----------- .../components/Tagger/StashSearchResult.tsx | 19 +-- ui/v2.5/src/components/Tagger/Tagger.tsx | 108 +++++++++++++++--- ui/v2.5/src/components/Tagger/constants.ts | 42 +++++++ ui/v2.5/src/hooks/LocalForage.ts | 18 ++- ui/v2.5/src/hooks/index.ts | 6 +- 10 files changed, 185 insertions(+), 105 deletions(-) create mode 100644 ui/v2.5/src/components/Tagger/constants.ts diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index 1a6fe405a..622500bec 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -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") } diff --git a/ui/v2.5/src/components/Changelog/versions/v050.md b/ui/v2.5/src/components/Changelog/versions/v050.md index f79c0a872..d01bf1814 100644 --- a/ui/v2.5/src/components/Changelog/versions/v050.md +++ b/ui/v2.5/src/components/Changelog/versions/v050.md @@ -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 diff --git a/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx b/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx index 32ee4c3f2..54cd96468 100644 --- a/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx +++ b/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx @@ -41,7 +41,7 @@ const Instance: React.FC = ({ value={instance?.endpoint} isValid={(instance?.endpoint?.length ?? 0) > 0} onInput={(e: React.ChangeEvent) => - handleInput("endpoint", e.currentTarget.value) + handleInput("endpoint", e.currentTarget.value.trim()) } /> = ({ value={instance?.api_key} isValid={(instance?.api_key?.length ?? 0) > 0} onInput={(e: React.ChangeEvent) => - handleInput("api_key", e.currentTarget.value) + handleInput("api_key", e.currentTarget.value.trim()) } /> diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index 02bacdef5..2e1efc0ff 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -16,7 +16,7 @@ } &.inline { - display: inline-block; + display: inline; height: auto; margin-left: 0.5rem; } diff --git a/ui/v2.5/src/components/Tagger/Config.tsx b/ui/v2.5/src/components/Tagger/Config.tsx index f07c08d1f..f320567af 100644 --- a/ui/v2.5/src/components/Tagger/Config.tsx +++ b/ui/v2.5/src/components/Tagger/Config.tsx @@ -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; -} +import { ITaggerConfig, ParseMode, ModeDesc } from "./constants"; interface IConfigProps { show: boolean; @@ -62,26 +20,7 @@ interface IConfigProps { const Config: React.FC = ({ show, config, setConfig }) => { const stashConfig = useConfiguration(); - const [blacklistInput, setBlacklistInput] = useState(""); - - useEffect(() => { - localForage.getItem("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(null); const handleInstanceSelect = (e: React.ChangeEvent) => { const selectedEndpoint = e.currentTarget.value; @@ -102,11 +41,14 @@ const Config: React.FC = ({ 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 = ({ show, config, setConfig }) => {
Blacklist
- ) => - setBlacklistInput(e.currentTarget.value) - } - /> + diff --git a/ui/v2.5/src/components/Tagger/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/StashSearchResult.tsx index 7803db37a..62aa9a277 100755 --- a/ui/v2.5/src/components/Tagger/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/StashSearchResult.tsx @@ -104,8 +104,18 @@ const StashSearchResult: React.FC = ({ 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 = ({ }, }); - 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); diff --git a/ui/v2.5/src/components/Tagger/Tagger.tsx b/ui/v2.5/src/components/Tagger/Tagger.tsx index fd9354d16..2fdfd5d8f 100755 --- a/ui/v2.5/src/components/Tagger/Tagger.tsx +++ b/ui/v2.5/src/components/Tagger/Tagger.tsx @@ -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, 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 = ({ } else if (!isTagged && !hasStashIDs) { mainContent = ( + + Query + = ({ return (
-
- Scene -
-
- Query -
{fingerprintError}
{fingerprintQueue.length > 0 && ( @@ -460,10 +533,15 @@ interface ITaggerProps { export const Tagger: React.FC = ({ scenes }) => { const stashConfig = useConfiguration(); - const [config, setConfig] = useState(initialConfig); + const [{ data: config }, setConfig] = useLocalForage( + LOCAL_FORAGE_KEY, + initialConfig + ); const [showConfig, setShowConfig] = useState(false); const [showManual, setShowManual] = useState(false); + if (!config) return ; + const savedEndpointIndex = stashConfig.data?.configuration.general.stashBoxes.findIndex( (s) => s.endpoint === config.selectedEndpoint @@ -503,7 +581,7 @@ export const Tagger: React.FC = ({ scenes }) => { onClose={() => setShowManual(false)} defaultActiveTab="Tagger.md" /> -
+
{selectedEndpointIndex !== -1 && selectedEndpoint ? ( <>
diff --git a/ui/v2.5/src/components/Tagger/constants.ts b/ui/v2.5/src/components/Tagger/constants.ts new file mode 100644 index 000000000..d065f9867 --- /dev/null +++ b/ui/v2.5/src/components/Tagger/constants.ts @@ -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; +} diff --git a/ui/v2.5/src/hooks/LocalForage.ts b/ui/v2.5/src/hooks/LocalForage.ts index 43763e2ad..6f0340a58 100644 --- a/ui/v2.5/src/hooks/LocalForage.ts +++ b/ui/v2.5/src/hooks/LocalForage.ts @@ -27,8 +27,9 @@ interface ILocalForage { const Loading: Record = {}; const Cache: Record = {}; -function useLocalForage( - key: string +export function useLocalForage( + key: string, + defaultValue: T = {} as T ): [ILocalForage, Dispatch>] { const [error, setError] = React.useState(null); const [data, setData] = React.useState(Cache[key] as T); @@ -43,27 +44,32 @@ function useLocalForage( 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])); } }); diff --git a/ui/v2.5/src/hooks/index.ts b/ui/v2.5/src/hooks/index.ts index 15ddf6fea..55f5ddeba 100644 --- a/ui/v2.5/src/hooks/index.ts +++ b/ui/v2.5/src/hooks/index.ts @@ -1,5 +1,9 @@ export { default as useToast } from "./Toast"; -export { useInterfaceLocalForage, useChangelogStorage } from "./LocalForage"; +export { + useInterfaceLocalForage, + useChangelogStorage, + useLocalForage, +} from "./LocalForage"; export { useScenesList, useSceneMarkersList,