diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index ada4e99fb..384ba85ee 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -22,6 +22,11 @@ fragment ConfigGeneralData on ConfigGeneralResult { excludes scraperUserAgent scraperCDPPath + stashBoxes { + name + endpoint + api_key + } } fragment ConfigInterfaceData on ConfigInterfaceResult { diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 0803ca9d1..94831d1a5 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -69,6 +69,8 @@ input ConfigGeneralInput { scraperUserAgent: String """Scraper CDP path. Path to chrome executable or remote address""" scraperCDPPath: String + """Stash-box instances used for tagging""" + stashBoxes: [StashBoxInput!]! } type ConfigGeneralResult { @@ -118,6 +120,8 @@ type ConfigGeneralResult { scraperUserAgent: String """Scraper CDP path. Path to chrome executable or remote address""" scraperCDPPath: String + """Stash-box instances used for tagging""" + stashBoxes: [StashBox!]! } input ConfigInterfaceInput { diff --git a/graphql/schema/types/stash-box.graphql b/graphql/schema/types/stash-box.graphql new file mode 100644 index 000000000..0ca3686ee --- /dev/null +++ b/graphql/schema/types/stash-box.graphql @@ -0,0 +1,11 @@ +type StashBox { + endpoint: String! + api_key: String! + name: String! +} + +input StashBoxInput { + endpoint: String! + api_key: String! + name: String! +} diff --git a/pkg/api/resolver_mutation_configure.go b/pkg/api/resolver_mutation_configure.go index edeb7f011..31faeab01 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -130,6 +130,13 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co refreshScraperCache = true } + if input.StashBoxes != nil { + if err := config.ValidateStashBoxes(input.StashBoxes); err != nil { + return nil, err + } + config.Set(config.StashBoxes, input.StashBoxes) + } + if err := config.Write(); err != nil { return makeConfigGeneralResult(), err } diff --git a/pkg/api/resolver_query_configuration.go b/pkg/api/resolver_query_configuration.go index d8e0bbeaf..190439fa0 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -66,6 +66,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult { Excludes: config.GetExcludes(), ScraperUserAgent: &scraperUserAgent, ScraperCDPPath: &scraperCDPPath, + StashBoxes: config.GetStashBoxes(), } } diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index 383febe7a..a5a746f44 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -3,6 +3,7 @@ package config import ( "golang.org/x/crypto/bcrypt" + "errors" "io/ioutil" "path/filepath" @@ -67,6 +68,9 @@ const ScrapersPath = "scrapers_path" const ScraperUserAgent = "scraper_user_agent" const ScraperCDPPath = "scraper_cdp_path" +// stash-box options +const StashBoxes = "stash_boxes" + // plugin options const PluginsPath = "plugins_path" @@ -198,6 +202,12 @@ func GetScraperCDPPath() string { return viper.GetString(ScraperCDPPath) } +func GetStashBoxes() []*models.StashBox { + var boxes []*models.StashBox + _ = viper.UnmarshalKey(StashBoxes, &boxes) + return boxes +} + func GetDefaultPluginsPath() string { // default to the same directory as the config file fn := filepath.Join(GetConfigPath(), "plugins") @@ -332,6 +342,21 @@ func ValidateCredentials(username string, password string) bool { return username == authUser && err == nil } +func ValidateStashBoxes(boxes []*models.StashBoxInput) error { + isMulti := len(boxes) > 1 + + 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 isMulti && box.Name == "" { + return errors.New("Stash-box Name cannot be blank") + } + } + return nil +} + // GetMaxSessionAge gets the maximum age for session cookies, in seconds. // Session cookie expiry times are refreshed every request. func GetMaxSessionAge() int { diff --git a/ui/v2.5/src/components/Settings/SettingsConfigurationPanel.tsx b/ui/v2.5/src/components/Settings/SettingsConfigurationPanel.tsx index 2ec4ae622..34d181da7 100644 --- a/ui/v2.5/src/components/Settings/SettingsConfigurationPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsConfigurationPanel.tsx @@ -5,6 +5,9 @@ import { useConfiguration, useConfigureGeneral } from "src/core/StashService"; import { useToast } from "src/hooks"; import { Icon, LoadingIndicator } from "src/components/Shared"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; +import StashBoxConfiguration, { + IStashBoxInstance, +} from "./StashBoxConfiguration"; export const SettingsConfigurationPanel: React.FC = () => { const Toast = useToast(); @@ -54,6 +57,7 @@ export const SettingsConfigurationPanel: React.FC = () => { const [scraperCDPPath, setScraperCDPPath] = useState( undefined ); + const [stashBoxes, setStashBoxes] = useState([]); const { data, error, loading } = useConfiguration(); @@ -82,6 +86,14 @@ export const SettingsConfigurationPanel: React.FC = () => { excludes, scraperUserAgent, scraperCDPPath, + stashBoxes: stashBoxes.map( + (b) => + ({ + name: b?.name ?? "", + api_key: b?.api_key ?? "", + endpoint: b?.endpoint ?? "", + } as GQL.StashBoxInput) + ), }); useEffect(() => { @@ -114,6 +126,14 @@ export const SettingsConfigurationPanel: React.FC = () => { setExcludes(conf.general.excludes); setScraperUserAgent(conf.general.scraperUserAgent ?? undefined); setScraperCDPPath(conf.general.scraperCDPPath ?? undefined); + setStashBoxes( + conf.general.stashBoxes.map((box, i) => ({ + name: box?.name ?? undefined, + endpoint: box.endpoint, + api_key: box.api_key, + index: i, + })) ?? [] + ); } }, [data, error]); @@ -547,6 +567,11 @@ export const SettingsConfigurationPanel: React.FC = () => { +
+ +

Stash-box integration

+ +

diff --git a/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx b/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx new file mode 100644 index 000000000..32ee4c3f2 --- /dev/null +++ b/ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx @@ -0,0 +1,137 @@ +import React, { useState } from "react"; +import { Button, Form, InputGroup } from "react-bootstrap"; +import { Icon } from "src/components/Shared"; + +interface IInstanceProps { + instance: IStashBoxInstance; + onSave: (instance: IStashBoxInstance) => void; + onDelete: (id: number) => void; + isMulti: boolean; +} + +const Instance: React.FC = ({ + instance, + onSave, + onDelete, + isMulti, +}) => { + const handleInput = (key: string, value: string) => { + const newObj = { + ...instance, + [key]: value, + }; + onSave(newObj); + }; + + return ( + + + 0} + onInput={(e: React.ChangeEvent) => + handleInput("name", e.currentTarget.value) + } + /> + 0} + onInput={(e: React.ChangeEvent) => + handleInput("endpoint", e.currentTarget.value) + } + /> + 0} + onInput={(e: React.ChangeEvent) => + handleInput("api_key", e.currentTarget.value) + } + /> + + + + + + ); +}; + +interface IStashBoxConfigurationProps { + boxes: IStashBoxInstance[]; + saveBoxes: (boxes: IStashBoxInstance[]) => void; +} + +export interface IStashBoxInstance { + name?: string; + endpoint?: string; + api_key?: string; + index: number; +} + +export const StashBoxConfiguration: React.FC = ({ + boxes, + saveBoxes, +}) => { + const [index, setIndex] = useState(1000); + + const handleSave = (instance: IStashBoxInstance) => + saveBoxes( + boxes.map((box) => (box.index === instance.index ? instance : box)) + ); + const handleDelete = (id: number) => + saveBoxes(boxes.filter((box) => box.index !== id)); + const handleAdd = () => { + saveBoxes([...boxes, { index }]); + setIndex(index + 1); + }; + + return ( + +
Stash-box Endpoints
+ {boxes.length > 0 && ( +
+
Name
+
Endpoint
+
API Key
+
+ )} + {boxes.map((instance) => ( + 1} + /> + ))} + + + Stash-box facilitates automated tagging of scenes and performers based + on fingerprints and filenames. +
+ Endpoint and API key can be found on your account page on the stash-box + instance. Names are required when more than one instance is added. +
+
+ ); +}; + +export default StashBoxConfiguration;