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:
WithoutPants 2022-03-24 09:22:41 +11:00 committed by GitHub
parent 329b611348
commit 0cd9a0a474
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 147 additions and 53 deletions

View file

@ -44,6 +44,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
endpoint endpoint
api_key api_key
} }
pythonPath
} }
fragment ConfigInterfaceData on ConfigInterfaceResult { fragment ConfigInterfaceData on ConfigInterfaceResult {

View file

@ -107,6 +107,8 @@ input ConfigGeneralInput {
scraperCertCheck: Boolean @deprecated(reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead") scraperCertCheck: Boolean @deprecated(reason: "use mutation ConfigureScraping(input: ConfigScrapingInput) instead")
"""Stash-box instances used for tagging""" """Stash-box instances used for tagging"""
stashBoxes: [StashBoxInput!] stashBoxes: [StashBoxInput!]
"""Python path - resolved using path if unset"""
pythonPath: String
} }
type ConfigGeneralResult { type ConfigGeneralResult {
@ -188,6 +190,8 @@ type ConfigGeneralResult {
scraperCertCheck: Boolean! @deprecated(reason: "use ConfigResult.scraping instead") scraperCertCheck: Boolean! @deprecated(reason: "use ConfigResult.scraping instead")
"""Stash-box instances used for tagging""" """Stash-box instances used for tagging"""
stashBoxes: [StashBox!]! stashBoxes: [StashBox!]!
"""Python path - resolved using path if unset"""
pythonPath: String!
} }
input ConfigDisableDropdownCreateInput { input ConfigDisableDropdownCreateInput {

View file

@ -265,6 +265,10 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
c.Set(config.StashBoxes, input.StashBoxes) c.Set(config.StashBoxes, input.StashBoxes)
} }
if input.PythonPath != nil {
c.Set(config.PythonPath, input.PythonPath)
}
if err := c.Write(); err != nil { if err := c.Write(); err != nil {
return makeConfigGeneralResult(), err return makeConfigGeneralResult(), err
} }

View file

@ -120,6 +120,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
ScraperCertCheck: config.GetScraperCertCheck(), ScraperCertCheck: config.GetScraperCertCheck(),
ScraperCDPPath: &scraperCDPPath, ScraperCDPPath: &scraperCDPPath,
StashBoxes: config.GetStashBoxes(), StashBoxes: config.GetStashBoxes(),
PythonPath: config.GetPythonPath(),
} }
} }

View file

@ -105,6 +105,8 @@ const (
// stash-box options // stash-box options
StashBoxes = "stash_boxes" StashBoxes = "stash_boxes"
PythonPath = "python_path"
// plugin options // plugin options
PluginsPath = "plugins_path" PluginsPath = "plugins_path"
@ -624,6 +626,10 @@ func (i *Instance) GetPluginsPath() string {
return i.getString(PluginsPath) return i.getString(PluginsPath)
} }
func (i *Instance) GetPythonPath() string {
return i.getString(PythonPath)
}
func (i *Instance) GetHost() string { func (i *Instance) GetHost() string {
ret := i.getString(Host) ret := i.getString(Host)
if ret == "" { if ret == "" {

View file

@ -108,6 +108,7 @@ func TestConcurrentConfigAccess(t *testing.T) {
i.SetChecksumDefaultValues(i.GetVideoFileNamingAlgorithm(), i.IsCalculateMD5()) i.SetChecksumDefaultValues(i.GetVideoFileNamingAlgorithm(), i.IsCalculateMD5())
i.Set(AutostartVideoOnPlaySelected, i.GetAutostartVideoOnPlaySelected()) i.Set(AutostartVideoOnPlaySelected, i.GetAutostartVideoOnPlaySelected())
i.Set(ContinuePlaylistDefault, i.GetContinuePlaylistDefault()) i.Set(ContinuePlaylistDefault, i.GetContinuePlaylistDefault())
i.Set(PythonPath, i.GetPythonPath())
} }
wg.Done() wg.Done()
}(k) }(k)

View file

@ -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 provides functions that wrap os/exec functions. These functions prevent external commands from opening windows on the Windows platform.
package exec package exec
import "os/exec" import (
"context"
"os/exec"
)
// Command wraps the exec.Command function, preventing Windows from opening a window when starting. // Command wraps the exec.Command function, preventing Windows from opening a window when starting.
func Command(name string, arg ...string) *exec.Cmd { func Command(name string, arg ...string) *exec.Cmd {
@ -9,3 +12,10 @@ func Command(name string, arg ...string) *exec.Cmd {
hideExecShell(ret) hideExecShell(ret)
return 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
}

View file

@ -28,6 +28,7 @@ type ServerConfig interface {
GetConfigPath() string GetConfigPath() string
HasTLSConfig() bool HasTLSConfig() bool
GetPluginsPath() string GetPluginsPath() string
GetPythonPath() string
} }
// Cache stores plugin details. // 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), input: buildPluginInput(plugin, operation, serverConnection, args),
progress: progress, progress: progress,
gqlHandler: c.gqlHandler, gqlHandler: c.gqlHandler,
serverConfig: c.config,
} }
return task.createTask(), nil return task.createTask(), nil
} }
@ -220,6 +222,7 @@ func (c Cache) executePostHooks(ctx context.Context, hookType HookTriggerEnum, h
operation: &h.OperationConfig, operation: &h.OperationConfig,
input: pluginInput, input: pluginInput,
gqlHandler: c.gqlHandler, gqlHandler: c.gqlHandler,
serverConfig: c.config,
} }
task := pt.createTask() task := pt.createTask()

View file

@ -1,6 +1,7 @@
package plugin package plugin
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -11,6 +12,7 @@ import (
stashExec "github.com/stashapp/stash/pkg/exec" stashExec "github.com/stashapp/stash/pkg/exec"
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/plugin/common" "github.com/stashapp/stash/pkg/plugin/common"
"github.com/stashapp/stash/pkg/python"
) )
type rawTaskBuilder struct{} type rawTaskBuilder struct{}
@ -30,19 +32,6 @@ type rawPluginTask struct {
done chan bool 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 { func (t *rawPluginTask) Start() error {
if t.started { if t.started {
return errors.New("task already 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) return fmt.Errorf("empty exec value in operation %s", t.operation.Name)
} }
if command[0] == "python" || command[0] == "python3" { var cmd *exec.Cmd
executable, err := FindPythonExecutable() if python.IsPythonCommand(command[0]) {
if err == nil { pythonPath := t.serverConfig.GetPythonPath()
command[0] = executable 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() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {

View file

@ -34,6 +34,7 @@ type pluginTask struct {
operation *OperationConfig operation *OperationConfig
input common.PluginInput input common.PluginInput
gqlHandler http.Handler gqlHandler http.Handler
serverConfig ServerConfig
progress chan float64 progress chan float64
result *common.PluginOutput result *common.PluginOutput

44
pkg/python/exec.go Normal file
View 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"
}

View file

@ -36,6 +36,7 @@ type GlobalConfig interface {
GetScrapersPath() string GetScrapersPath() string
GetScraperCDPPath() string GetScraperCDPPath() string
GetScraperCertCheck() bool GetScraperCertCheck() bool
GetPythonPath() string
} }
func isCDPPathHTTP(c GlobalConfig) bool { func isCDPPathHTTP(c GlobalConfig) bool {

View file

@ -13,6 +13,7 @@ import (
stashExec "github.com/stashapp/stash/pkg/exec" stashExec "github.com/stashapp/stash/pkg/exec"
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/python"
) )
var ErrScraperScript = errors.New("scraper script error") 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 { func (s *scriptScraper) runScraperScript(inString string, out interface{}) error {
command := s.scraper.Script command := s.scraper.Script
if command[0] == "python" || command[0] == "python3" { var cmd *exec.Cmd
executable, err := findPythonExecutable() if python.IsPythonCommand(command[0]) {
if err == nil { pythonPath := s.globalConfig.GetPythonPath()
command[0] = executable 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) cmd.Dir = filepath.Dir(s.config.path)
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
@ -220,22 +234,6 @@ func (s *scriptScraper) scrapeGalleryByGallery(ctx context.Context, gallery *mod
return ret, err 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) { func handleScraperStderr(name string, scraperOutputReader io.ReadCloser) {
const scraperPrefix = "[Scrape / %s] " const scraperPrefix = "[Scrape / %s] "

View file

@ -830,6 +830,10 @@ func (mockGlobalConfig) GetScraperExcludeTagPatterns() []string {
return nil return nil
} }
func (mockGlobalConfig) GetPythonPath() string {
return ""
}
func TestSubScrape(t *testing.T) { func TestSubScrape(t *testing.T) {
retHTML := ` retHTML := `
<div> <div>

View file

@ -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 ### 🎨 Improvements
* Maintain lightbox settings and add lightbox settings to Interface settings page. ([#2406](https://github.com/stashapp/stash/pull/2406)) * 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)) * Image lightbox now transitions to next/previous image when scrolling in pan-Y mode. ([#2403](https://github.com/stashapp/stash/pull/2403))

View file

@ -147,6 +147,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
value={general.customPerformerImageLocation ?? undefined} value={general.customPerformerImageLocation ?? undefined}
onChange={(v) => saveGeneral({ customPerformerImageLocation: v })} 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>
<SettingSection headingID="config.general.hashing"> <SettingSection headingID="config.general.hashing">

View file

@ -279,6 +279,10 @@
"number_of_parallel_task_for_scan_generation_head": "Number of parallel task for scan/generation", "number_of_parallel_task_for_scan_generation_head": "Number of parallel task for scan/generation",
"parallel_scan_head": "Parallel Scan/Generation", "parallel_scan_head": "Parallel Scan/Generation",
"preview_generation": "Preview 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": "Scraper User Agent",
"scraper_user_agent_desc": "User-Agent string used during scrape http requests", "scraper_user_agent_desc": "User-Agent string used during scrape http requests",
"scrapers_path": { "scrapers_path": {