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
api_key
}
pythonPath
}
fragment ConfigInterfaceData on ConfigInterfaceResult {

View file

@ -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 {

View file

@ -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
}

View file

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

View file

@ -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 == "" {

View file

@ -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)

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
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
}

View file

@ -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()

View file

@ -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 {

View file

@ -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
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
GetScraperCDPPath() string
GetScraperCertCheck() bool
GetPythonPath() string
}
func isCDPPathHTTP(c GlobalConfig) bool {

View file

@ -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] "

View file

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

View file

@ -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">

View file

@ -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": {