mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Config for stash-box instances (#748)
This commit is contained in:
@@ -22,6 +22,11 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||||||
excludes
|
excludes
|
||||||
scraperUserAgent
|
scraperUserAgent
|
||||||
scraperCDPPath
|
scraperCDPPath
|
||||||
|
stashBoxes {
|
||||||
|
name
|
||||||
|
endpoint
|
||||||
|
api_key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ input ConfigGeneralInput {
|
|||||||
scraperUserAgent: String
|
scraperUserAgent: String
|
||||||
"""Scraper CDP path. Path to chrome executable or remote address"""
|
"""Scraper CDP path. Path to chrome executable or remote address"""
|
||||||
scraperCDPPath: String
|
scraperCDPPath: String
|
||||||
|
"""Stash-box instances used for tagging"""
|
||||||
|
stashBoxes: [StashBoxInput!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigGeneralResult {
|
type ConfigGeneralResult {
|
||||||
@@ -118,6 +120,8 @@ type ConfigGeneralResult {
|
|||||||
scraperUserAgent: String
|
scraperUserAgent: String
|
||||||
"""Scraper CDP path. Path to chrome executable or remote address"""
|
"""Scraper CDP path. Path to chrome executable or remote address"""
|
||||||
scraperCDPPath: String
|
scraperCDPPath: String
|
||||||
|
"""Stash-box instances used for tagging"""
|
||||||
|
stashBoxes: [StashBox!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
input ConfigInterfaceInput {
|
input ConfigInterfaceInput {
|
||||||
|
|||||||
11
graphql/schema/types/stash-box.graphql
Normal file
11
graphql/schema/types/stash-box.graphql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
type StashBox {
|
||||||
|
endpoint: String!
|
||||||
|
api_key: String!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input StashBoxInput {
|
||||||
|
endpoint: String!
|
||||||
|
api_key: String!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
@@ -130,6 +130,13 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
|||||||
refreshScraperCache = true
|
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 {
|
if err := config.Write(); err != nil {
|
||||||
return makeConfigGeneralResult(), err
|
return makeConfigGeneralResult(), err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
|||||||
Excludes: config.GetExcludes(),
|
Excludes: config.GetExcludes(),
|
||||||
ScraperUserAgent: &scraperUserAgent,
|
ScraperUserAgent: &scraperUserAgent,
|
||||||
ScraperCDPPath: &scraperCDPPath,
|
ScraperCDPPath: &scraperCDPPath,
|
||||||
|
StashBoxes: config.GetStashBoxes(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
@@ -67,6 +68,9 @@ const ScrapersPath = "scrapers_path"
|
|||||||
const ScraperUserAgent = "scraper_user_agent"
|
const ScraperUserAgent = "scraper_user_agent"
|
||||||
const ScraperCDPPath = "scraper_cdp_path"
|
const ScraperCDPPath = "scraper_cdp_path"
|
||||||
|
|
||||||
|
// stash-box options
|
||||||
|
const StashBoxes = "stash_boxes"
|
||||||
|
|
||||||
// plugin options
|
// plugin options
|
||||||
const PluginsPath = "plugins_path"
|
const PluginsPath = "plugins_path"
|
||||||
|
|
||||||
@@ -198,6 +202,12 @@ func GetScraperCDPPath() string {
|
|||||||
return viper.GetString(ScraperCDPPath)
|
return viper.GetString(ScraperCDPPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetStashBoxes() []*models.StashBox {
|
||||||
|
var boxes []*models.StashBox
|
||||||
|
_ = viper.UnmarshalKey(StashBoxes, &boxes)
|
||||||
|
return boxes
|
||||||
|
}
|
||||||
|
|
||||||
func GetDefaultPluginsPath() string {
|
func GetDefaultPluginsPath() string {
|
||||||
// default to the same directory as the config file
|
// default to the same directory as the config file
|
||||||
fn := filepath.Join(GetConfigPath(), "plugins")
|
fn := filepath.Join(GetConfigPath(), "plugins")
|
||||||
@@ -332,6 +342,21 @@ func ValidateCredentials(username string, password string) bool {
|
|||||||
return username == authUser && err == nil
|
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.
|
// GetMaxSessionAge gets the maximum age for session cookies, in seconds.
|
||||||
// Session cookie expiry times are refreshed every request.
|
// Session cookie expiry times are refreshed every request.
|
||||||
func GetMaxSessionAge() int {
|
func GetMaxSessionAge() int {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { useConfiguration, useConfigureGeneral } from "src/core/StashService";
|
|||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Icon, LoadingIndicator } from "src/components/Shared";
|
import { Icon, LoadingIndicator } from "src/components/Shared";
|
||||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||||
|
import StashBoxConfiguration, {
|
||||||
|
IStashBoxInstance,
|
||||||
|
} from "./StashBoxConfiguration";
|
||||||
|
|
||||||
export const SettingsConfigurationPanel: React.FC = () => {
|
export const SettingsConfigurationPanel: React.FC = () => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
@@ -54,6 +57,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
const [scraperCDPPath, setScraperCDPPath] = useState<string | undefined>(
|
const [scraperCDPPath, setScraperCDPPath] = useState<string | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
const [stashBoxes, setStashBoxes] = useState<IStashBoxInstance[]>([]);
|
||||||
|
|
||||||
const { data, error, loading } = useConfiguration();
|
const { data, error, loading } = useConfiguration();
|
||||||
|
|
||||||
@@ -82,6 +86,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
excludes,
|
excludes,
|
||||||
scraperUserAgent,
|
scraperUserAgent,
|
||||||
scraperCDPPath,
|
scraperCDPPath,
|
||||||
|
stashBoxes: stashBoxes.map(
|
||||||
|
(b) =>
|
||||||
|
({
|
||||||
|
name: b?.name ?? "",
|
||||||
|
api_key: b?.api_key ?? "",
|
||||||
|
endpoint: b?.endpoint ?? "",
|
||||||
|
} as GQL.StashBoxInput)
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -114,6 +126,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
setExcludes(conf.general.excludes);
|
setExcludes(conf.general.excludes);
|
||||||
setScraperUserAgent(conf.general.scraperUserAgent ?? undefined);
|
setScraperUserAgent(conf.general.scraperUserAgent ?? undefined);
|
||||||
setScraperCDPPath(conf.general.scraperCDPPath ?? 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]);
|
}, [data, error]);
|
||||||
|
|
||||||
@@ -547,6 +567,11 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<Form.Group>
|
||||||
|
<h4>Stash-box integration</h4>
|
||||||
|
<StashBoxConfiguration boxes={stashBoxes} saveBoxes={setStashBoxes} />
|
||||||
|
</Form.Group>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
|
|||||||
137
ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx
Normal file
137
ui/v2.5/src/components/Settings/StashBoxConfiguration.tsx
Normal file
@@ -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<IInstanceProps> = ({
|
||||||
|
instance,
|
||||||
|
onSave,
|
||||||
|
onDelete,
|
||||||
|
isMulti,
|
||||||
|
}) => {
|
||||||
|
const handleInput = (key: string, value: string) => {
|
||||||
|
const newObj = {
|
||||||
|
...instance,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
onSave(newObj);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group className="row no-gutters">
|
||||||
|
<InputGroup className="col">
|
||||||
|
<Form.Control
|
||||||
|
placeholder="Name"
|
||||||
|
className="text-input col-3 stash-box-name"
|
||||||
|
value={instance?.name}
|
||||||
|
isValid={!isMulti || (instance?.name?.length ?? 0) > 0}
|
||||||
|
onInput={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInput("name", e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Control
|
||||||
|
placeholder="GraphQL endpoint"
|
||||||
|
className="text-input col-3 stash-box-endpoint"
|
||||||
|
value={instance?.endpoint}
|
||||||
|
isValid={(instance?.endpoint?.length ?? 0) > 0}
|
||||||
|
onInput={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInput("endpoint", e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Control
|
||||||
|
placeholder="API key"
|
||||||
|
className="text-input col-3 stash-box-apikey"
|
||||||
|
value={instance?.api_key}
|
||||||
|
isValid={(instance?.api_key?.length ?? 0) > 0}
|
||||||
|
onInput={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
handleInput("api_key", e.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputGroup.Append>
|
||||||
|
<Button
|
||||||
|
className=""
|
||||||
|
variant="danger"
|
||||||
|
title="Delete"
|
||||||
|
onClick={() => onDelete(instance.index)}
|
||||||
|
>
|
||||||
|
<Icon icon="minus" />
|
||||||
|
</Button>
|
||||||
|
</InputGroup.Append>
|
||||||
|
</InputGroup>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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<IStashBoxConfigurationProps> = ({
|
||||||
|
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 (
|
||||||
|
<Form.Group>
|
||||||
|
<h6>Stash-box Endpoints</h6>
|
||||||
|
{boxes.length > 0 && (
|
||||||
|
<div className="row no-gutters">
|
||||||
|
<h6 className="col-3 ml-1">Name</h6>
|
||||||
|
<h6 className="col-3 ml-1">Endpoint</h6>
|
||||||
|
<h6 className="col-3 ml-1">API Key</h6>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{boxes.map((instance) => (
|
||||||
|
<Instance
|
||||||
|
instance={instance}
|
||||||
|
onSave={handleSave}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
key={instance.index}
|
||||||
|
isMulti={boxes.length > 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
className="minimal"
|
||||||
|
title="Add stash-box instance"
|
||||||
|
onClick={handleAdd}
|
||||||
|
>
|
||||||
|
<Icon icon="plus" />
|
||||||
|
</Button>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Stash-box facilitates automated tagging of scenes and performers based
|
||||||
|
on fingerprints and filenames.
|
||||||
|
<br />
|
||||||
|
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.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StashBoxConfiguration;
|
||||||
Reference in New Issue
Block a user