mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Python path setting (#2409)
* Add python package * Add python path backend config * Add python path to system settings page * Apply python path to script scrapers and plugins
This commit is contained in:
parent
329b611348
commit
0cd9a0a474
17 changed files with 147 additions and 53 deletions
|
|
@ -44,6 +44,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||
endpoint
|
||||
api_key
|
||||
}
|
||||
pythonPath
|
||||
}
|
||||
|
||||
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ input ConfigGeneralInput {
|
|||
scraperCertCheck: Boolean @deprecated(reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead")
|
||||
"""Stash-box instances used for tagging"""
|
||||
stashBoxes: [StashBoxInput!]
|
||||
"""Python path - resolved using path if unset"""
|
||||
pythonPath: String
|
||||
}
|
||||
|
||||
type ConfigGeneralResult {
|
||||
|
|
@ -188,6 +190,8 @@ type ConfigGeneralResult {
|
|||
scraperCertCheck: Boolean! @deprecated(reason: "use ConfigResult.scraping instead")
|
||||
"""Stash-box instances used for tagging"""
|
||||
stashBoxes: [StashBox!]!
|
||||
"""Python path - resolved using path if unset"""
|
||||
pythonPath: String!
|
||||
}
|
||||
|
||||
input ConfigDisableDropdownCreateInput {
|
||||
|
|
|
|||
|
|
@ -265,6 +265,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
|||
c.Set(config.StashBoxes, input.StashBoxes)
|
||||
}
|
||||
|
||||
if input.PythonPath != nil {
|
||||
c.Set(config.PythonPath, input.PythonPath)
|
||||
}
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
|||
ScraperCertCheck: config.GetScraperCertCheck(),
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
PythonPath: config.GetPythonPath(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ const (
|
|||
// stash-box options
|
||||
StashBoxes = "stash_boxes"
|
||||
|
||||
PythonPath = "python_path"
|
||||
|
||||
// plugin options
|
||||
PluginsPath = "plugins_path"
|
||||
|
||||
|
|
@ -624,6 +626,10 @@ func (i *Instance) GetPluginsPath() string {
|
|||
return i.getString(PluginsPath)
|
||||
}
|
||||
|
||||
func (i *Instance) GetPythonPath() string {
|
||||
return i.getString(PythonPath)
|
||||
}
|
||||
|
||||
func (i *Instance) GetHost() string {
|
||||
ret := i.getString(Host)
|
||||
if ret == "" {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ func TestConcurrentConfigAccess(t *testing.T) {
|
|||
i.SetChecksumDefaultValues(i.GetVideoFileNamingAlgorithm(), i.IsCalculateMD5())
|
||||
i.Set(AutostartVideoOnPlaySelected, i.GetAutostartVideoOnPlaySelected())
|
||||
i.Set(ContinuePlaylistDefault, i.GetContinuePlaylistDefault())
|
||||
i.Set(PythonPath, i.GetPythonPath())
|
||||
}
|
||||
wg.Done()
|
||||
}(k)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
// Package exec provides functions that wrap os/exec functions. These functions prevent external commands from opening windows on the Windows platform.
|
||||
package exec
|
||||
|
||||
import "os/exec"
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Command wraps the exec.Command function, preventing Windows from opening a window when starting.
|
||||
func Command(name string, arg ...string) *exec.Cmd {
|
||||
|
|
@ -9,3 +12,10 @@ func Command(name string, arg ...string) *exec.Cmd {
|
|||
hideExecShell(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
// CommandContext wraps the exec.CommandContext function, preventing Windows from opening a window when starting.
|
||||
func CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
||||
ret := exec.CommandContext(ctx, name, arg...)
|
||||
hideExecShell(ret)
|
||||
return ret
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ type ServerConfig interface {
|
|||
GetConfigPath() string
|
||||
HasTLSConfig() bool
|
||||
GetPluginsPath() string
|
||||
GetPythonPath() string
|
||||
}
|
||||
|
||||
// Cache stores plugin details.
|
||||
|
|
@ -172,6 +173,7 @@ func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName st
|
|||
input: buildPluginInput(plugin, operation, serverConnection, args),
|
||||
progress: progress,
|
||||
gqlHandler: c.gqlHandler,
|
||||
serverConfig: c.config,
|
||||
}
|
||||
return task.createTask(), nil
|
||||
}
|
||||
|
|
@ -220,6 +222,7 @@ func (c Cache) executePostHooks(ctx context.Context, hookType HookTriggerEnum, h
|
|||
operation: &h.OperationConfig,
|
||||
input: pluginInput,
|
||||
gqlHandler: c.gqlHandler,
|
||||
serverConfig: c.config,
|
||||
}
|
||||
|
||||
task := pt.createTask()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
stashExec "github.com/stashapp/stash/pkg/exec"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/plugin/common"
|
||||
"github.com/stashapp/stash/pkg/python"
|
||||
)
|
||||
|
||||
type rawTaskBuilder struct{}
|
||||
|
|
@ -30,19 +32,6 @@ type rawPluginTask struct {
|
|||
done chan bool
|
||||
}
|
||||
|
||||
func FindPythonExecutable() (string, error) {
|
||||
_, err := exec.LookPath("python3")
|
||||
|
||||
if err != nil {
|
||||
_, err = exec.LookPath("python")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "python", nil
|
||||
}
|
||||
return "python3", nil
|
||||
}
|
||||
|
||||
func (t *rawPluginTask) Start() error {
|
||||
if t.started {
|
||||
return errors.New("task already started")
|
||||
|
|
@ -53,14 +42,26 @@ func (t *rawPluginTask) Start() error {
|
|||
return fmt.Errorf("empty exec value in operation %s", t.operation.Name)
|
||||
}
|
||||
|
||||
if command[0] == "python" || command[0] == "python3" {
|
||||
executable, err := FindPythonExecutable()
|
||||
if err == nil {
|
||||
command[0] = executable
|
||||
}
|
||||
var cmd *exec.Cmd
|
||||
if python.IsPythonCommand(command[0]) {
|
||||
pythonPath := t.serverConfig.GetPythonPath()
|
||||
var p *python.Python
|
||||
if pythonPath != "" {
|
||||
p = python.New(pythonPath)
|
||||
} else {
|
||||
p, _ = python.Resolve()
|
||||
}
|
||||
|
||||
cmd := stashExec.Command(command[0], command[1:]...)
|
||||
if p != nil {
|
||||
cmd = p.Command(context.TODO(), command[1:])
|
||||
}
|
||||
|
||||
// if could not find python, just use the command args as-is
|
||||
}
|
||||
|
||||
if cmd == nil {
|
||||
cmd = stashExec.Command(command[0], command[1:]...)
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ type pluginTask struct {
|
|||
operation *OperationConfig
|
||||
input common.PluginInput
|
||||
gqlHandler http.Handler
|
||||
serverConfig ServerConfig
|
||||
|
||||
progress chan float64
|
||||
result *common.PluginOutput
|
||||
|
|
|
|||
44
pkg/python/exec.go
Normal file
44
pkg/python/exec.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
package python
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
|
||||
stashExec "github.com/stashapp/stash/pkg/exec"
|
||||
)
|
||||
|
||||
type Python string
|
||||
|
||||
func (p *Python) Command(ctx context.Context, args []string) *exec.Cmd {
|
||||
return stashExec.CommandContext(ctx, string(*p), args...)
|
||||
}
|
||||
|
||||
// New returns a new Python instance at the given path.
|
||||
func New(path string) *Python {
|
||||
ret := Python(path)
|
||||
return &ret
|
||||
}
|
||||
|
||||
// Resolve tries to find the python executable in the system.
|
||||
// It first checks for python3, then python.
|
||||
// Returns nil and an exec.ErrNotFound error if not found.
|
||||
func Resolve() (*Python, error) {
|
||||
_, err := exec.LookPath("python3")
|
||||
|
||||
if err != nil {
|
||||
_, err = exec.LookPath("python")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := Python("python")
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
ret := Python("python3")
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
// IsPythonCommand returns true if arg is "python" or "python3"
|
||||
func IsPythonCommand(arg string) bool {
|
||||
return arg == "python" || arg == "python3"
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ type GlobalConfig interface {
|
|||
GetScrapersPath() string
|
||||
GetScraperCDPPath() string
|
||||
GetScraperCertCheck() bool
|
||||
GetPythonPath() string
|
||||
}
|
||||
|
||||
func isCDPPathHTTP(c GlobalConfig) bool {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
stashExec "github.com/stashapp/stash/pkg/exec"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/python"
|
||||
)
|
||||
|
||||
var ErrScraperScript = errors.New("scraper script error")
|
||||
|
|
@ -34,14 +35,27 @@ func newScriptScraper(scraper scraperTypeConfig, config config, globalConfig Glo
|
|||
func (s *scriptScraper) runScraperScript(inString string, out interface{}) error {
|
||||
command := s.scraper.Script
|
||||
|
||||
if command[0] == "python" || command[0] == "python3" {
|
||||
executable, err := findPythonExecutable()
|
||||
if err == nil {
|
||||
command[0] = executable
|
||||
}
|
||||
var cmd *exec.Cmd
|
||||
if python.IsPythonCommand(command[0]) {
|
||||
pythonPath := s.globalConfig.GetPythonPath()
|
||||
var p *python.Python
|
||||
if pythonPath != "" {
|
||||
p = python.New(pythonPath)
|
||||
} else {
|
||||
p, _ = python.Resolve()
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
cmd = p.Command(context.TODO(), command[1:])
|
||||
}
|
||||
|
||||
// if could not find python, just use the command args as-is
|
||||
}
|
||||
|
||||
if cmd == nil {
|
||||
cmd = stashExec.Command(command[0], command[1:]...)
|
||||
}
|
||||
|
||||
cmd := stashExec.Command(command[0], command[1:]...)
|
||||
cmd.Dir = filepath.Dir(s.config.path)
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
|
|
@ -220,22 +234,6 @@ func (s *scriptScraper) scrapeGalleryByGallery(ctx context.Context, gallery *mod
|
|||
return ret, err
|
||||
}
|
||||
|
||||
func findPythonExecutable() (string, error) {
|
||||
_, err := exec.LookPath("python3")
|
||||
|
||||
if err != nil {
|
||||
_, err = exec.LookPath("python")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "python", nil
|
||||
}
|
||||
|
||||
return "python3", nil
|
||||
}
|
||||
|
||||
func handleScraperStderr(name string, scraperOutputReader io.ReadCloser) {
|
||||
const scraperPrefix = "[Scrape / %s] "
|
||||
|
||||
|
|
|
|||
|
|
@ -830,6 +830,10 @@ func (mockGlobalConfig) GetScraperExcludeTagPatterns() []string {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (mockGlobalConfig) GetPythonPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestSubScrape(t *testing.T) {
|
||||
retHTML := `
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
### ✨ New Features
|
||||
* Add python location in System Settings for script scrapers and plugins. ([#2409](https://github.com/stashapp/stash/pull/2409))
|
||||
|
||||
### 🎨 Improvements
|
||||
* Maintain lightbox settings and add lightbox settings to Interface settings page. ([#2406](https://github.com/stashapp/stash/pull/2406))
|
||||
* Image lightbox now transitions to next/previous image when scrolling in pan-Y mode. ([#2403](https://github.com/stashapp/stash/pull/2403))
|
||||
|
|
|
|||
|
|
@ -147,6 +147,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
value={general.customPerformerImageLocation ?? undefined}
|
||||
onChange={(v) => saveGeneral({ customPerformerImageLocation: v })}
|
||||
/>
|
||||
|
||||
<StringSetting
|
||||
id="python-path"
|
||||
headingID="config.general.python_path.heading"
|
||||
subHeadingID="config.general.python_path.description"
|
||||
value={general.pythonPath ?? undefined}
|
||||
onChange={(v) => saveGeneral({ pythonPath: v })}
|
||||
/>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection headingID="config.general.hashing">
|
||||
|
|
|
|||
|
|
@ -279,6 +279,10 @@
|
|||
"number_of_parallel_task_for_scan_generation_head": "Number of parallel task for scan/generation",
|
||||
"parallel_scan_head": "Parallel Scan/Generation",
|
||||
"preview_generation": "Preview Generation",
|
||||
"python_path": {
|
||||
"description": "Location of python executable. Used for script scrapers and plugins. If blank, python will be resolved from the environment",
|
||||
"heading": "Python Path"
|
||||
},
|
||||
"scraper_user_agent": "Scraper User Agent",
|
||||
"scraper_user_agent_desc": "User-Agent string used during scrape http requests",
|
||||
"scrapers_path": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue