Add injected css/javascript to plugins (#3195)

* Add injected css/javascript to plugins
* Manual documentation
This commit is contained in:
WithoutPants 2022-12-05 15:08:22 +11:00 committed by GitHub
parent 87cea80e7b
commit b5b9023b3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 190 additions and 83 deletions

View File

@ -1,10 +1,12 @@
package api
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
@ -31,6 +33,7 @@ import (
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/ui"
)
@ -166,36 +169,8 @@ func Start() error {
}.Routes())
r.Mount("/downloads", downloadsRoutes{}.Routes())
r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
if !c.GetCSSEnabled() {
return
}
// search for custom.css in current directory, then $HOME/.stash
fn := c.GetCSSPath()
exists, _ := fsutil.FileExists(fn)
if !exists {
return
}
http.ServeFile(w, r, fn)
})
r.HandleFunc("/javascript", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
if !c.GetJavascriptEnabled() {
return
}
// search for custom.js in current directory, then $HOME/.stash
fn := c.GetJavascriptPath()
exists, _ := fsutil.FileExists(fn)
if !exists {
return
}
http.ServeFile(w, r, fn)
})
r.HandleFunc("/css", cssHandler(c, pluginCache))
r.HandleFunc("/javascript", javascriptHandler(c, pluginCache))
r.HandleFunc("/customlocales", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if c.GetCustomLocalesEnabled() {
@ -329,6 +304,93 @@ func Start() error {
return nil
}
func copyFile(w io.Writer, path string) (time.Time, error) {
f, err := os.Open(path)
if err != nil {
return time.Time{}, err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return time.Time{}, err
}
_, err = io.Copy(w, f)
return info.ModTime(), err
}
func serveFiles(w http.ResponseWriter, r *http.Request, name string, paths []string) {
buffer := bytes.Buffer{}
latestModTime := time.Time{}
for _, path := range paths {
modTime, err := copyFile(&buffer, path)
if err != nil {
logger.Errorf("error serving file %s: %v", path, err)
} else {
if modTime.After(latestModTime) {
latestModTime = modTime
}
buffer.Write([]byte("\n"))
}
}
bufferReader := bytes.NewReader(buffer.Bytes())
http.ServeContent(w, r, name, latestModTime, bufferReader)
}
func cssHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// concatenate with plugin css files
w.Header().Set("Content-Type", "text/css")
// add plugin css files first
var paths []string
for _, p := range pluginCache.ListPlugins() {
paths = append(paths, p.UI.CSS...)
}
if c.GetCSSEnabled() {
// search for custom.css in current directory, then $HOME/.stash
fn := c.GetCSSPath()
exists, _ := fsutil.FileExists(fn)
if exists {
paths = append(paths, fn)
}
}
serveFiles(w, r, "custom.css", paths)
}
}
func javascriptHandler(c *config.Instance, pluginCache *plugin.Cache) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
// add plugin javascript files first
var paths []string
for _, p := range pluginCache.ListPlugins() {
paths = append(paths, p.UI.Javascript...)
}
if c.GetJavascriptEnabled() {
// search for custom.js in current directory, then $HOME/.stash
fn := c.GetJavascriptPath()
exists, _ := fsutil.FileExists(fn)
if exists {
paths = append(paths, fn)
}
}
serveFiles(w, r, "custom.js", paths)
}
}
func printVersion() {
versionString := githash
if config.IsOfficialBuild() {

View File

@ -56,6 +56,35 @@ type Config struct {
// The hooks configurations for hooks registered by this plugin.
Hooks []*HookConfig `yaml:"hooks"`
// Javascript files that will be injected into the stash UI.
UI UIConfig `yaml:"ui"`
}
type UIConfig struct {
// Javascript files that will be injected into the stash UI.
Javascript []string `yaml:"javascript"`
// CSS files that will be injected into the stash UI.
CSS []string `yaml:"css"`
}
func (c UIConfig) getCSSFiles(parent Config) []string {
ret := make([]string, len(c.CSS))
for i, v := range c.CSS {
ret[i] = filepath.Join(parent.getConfigPath(), v)
}
return ret
}
func (c UIConfig) getJavascriptFiles(parent Config) []string {
ret := make([]string, len(c.Javascript))
for i, v := range c.Javascript {
ret[i] = filepath.Join(parent.getConfigPath(), v)
}
return ret
}
func (c Config) getPluginTasks(includePlugin bool) []*PluginTask {
@ -121,6 +150,10 @@ func (c Config) toPlugin() *Plugin {
Version: c.Version,
Tasks: c.getPluginTasks(false),
Hooks: c.getPluginHooks(false),
UI: PluginUI{
Javascript: c.UI.getJavascriptFiles(c),
CSS: c.UI.getCSSFiles(c),
},
}
}

View File

@ -31,6 +31,15 @@ type Plugin struct {
Version *string `json:"version"`
Tasks []*PluginTask `json:"tasks"`
Hooks []*PluginHook `json:"hooks"`
UI PluginUI `json:"ui"`
}
type PluginUI struct {
// Javascript files that will be injected into the stash UI.
Javascript []string `json:"javascript"`
// CSS files that will be injected into the stash UI.
CSS []string `json:"css"`
}
type ServerConfig interface {

View File

@ -2,6 +2,7 @@
* Performer autotagging does not currently match on performer aliases. This will be addressed when finer control over the matching is implemented.
### ✨ New Features
* Added support for plugins to add injected CSS and Javascript to the UI. ([#3195](https://github.com/stashapp/stash/pull/3195))
* Added disambiguation field to Performers, to differentiate between performers with the same name. ([#3113](https://github.com/stashapp/stash/pull/3113))
### 🎨 Improvements

View File

@ -1,20 +1,20 @@
# Embedded Plugins
# Embedded Plugin Tasks
Embedded plugins are executed within the stash process using a scripting system.
Embedded plugin tasks are executed within the stash process using a scripting system.
## Supported script languages
Stash currently supports Javascript embedded plugins using [otto](https://github.com/robertkrimen/otto).
Stash currently supports Javascript embedded plugin tasks using [otto](https://github.com/robertkrimen/otto).
# Javascript plugins
## Plugin input
The input is provided to Javascript plugins using the `input` global variable, and is an object based on the structure provided in the `Plugin input` section of the [Plugins](/help/Plugins.md) page. Note that the `server_connection` field should not be necessary in most embedded plugins.
The input is provided to Javascript plugin tasks using the `input` global variable, and is an object based on the structure provided in the `Plugin input` section of the [Plugins](/help/Plugins.md) page. Note that the `server_connection` field should not be necessary in most embedded plugins.
## Plugin output
The output of a Javascript plugin is derived from the evaluated value of the script. The output should conform to the structure provided in the `Plugin output` section of the [Plugins](/help/Plugins.md) page.
The output of a Javascript plugin task is derived from the evaluated value of the script. The output should conform to the structure provided in the `Plugin output` section of the [Plugins](/help/Plugins.md) page.
There are a number of ways to return the plugin output:
@ -53,22 +53,6 @@ See the `Javascript API` section below on how to log with Javascript plugins.
# Plugin configuration file format
The basic structure of an embedded plugin configuration file is as follows:
```
name: <plugin name>
description: <optional description of the plugin>
version: <optional version tag>
url: <optional url>
exec:
- <path to script>
interface: [interface type]
tasks:
- ...
```
The `name`, `description`, `version` and `url` fields are displayed on the plugins page.
## exec
For embedded plugins, the `exec` field is a list with the first element being the path to the Javascript file that will be executed. It is expected that the path to the Javascript file is relative to the directory of the plugin configuration file.

View File

@ -1,10 +1,10 @@
# External Plugins
# External Plugin Tasks
External plugins are executed by running an external binary.
External plugin tasks are executed by running an external binary.
## Plugin interfaces
Stash communicates with external plugins using an interface. Stash currently supports RPC and raw interface types.
Stash communicates with external plugin tasks using an interface. Stash currently supports RPC and raw interface types.
### RPC interface
@ -30,27 +30,9 @@ Plugins can log for specific levels or log progress by prefixing the output stri
# Plugin configuration file format
The basic structure of an external plugin configuration file is as follows:
```
name: <plugin name>
description: <optional description of the plugin>
version: <optional version tag>
url: <optional url>
exec:
- <binary name>
- <other args...>
interface: [interface type]
errLog: [one of none trace, debug, info, warning, error]
tasks:
- ...
```
The `name`, `description`, `version` and `url` fields are displayed on the plugins page.
## exec
For external plugins, the `exec` field is a list with the first element being the binary that will be executed, and the subsequent elements are the arguments passed. The execution process will search the path for the binary, then will attempt to find the program in the same directory as the plugin configuration file. The `exe` extension is not necessary on Windows systems.
For external plugin tasks, the `exec` field is a list with the first element being the binary that will be executed, and the subsequent elements are the arguments passed. The execution process will search the path for the binary, then will attempt to find the program in the same directory as the plugin configuration file. The `exe` extension is not necessary on Windows systems.
> **⚠️ Note:** The plugin execution process sets the current working directory to that of the stash process.
@ -75,7 +57,7 @@ exec:
## interface
For external plugins, the `interface` field must be set to one of the following values:
For external plugin tasks, the `interface` field must be set to one of the following values:
* `rpc`
* `raw`
@ -89,7 +71,7 @@ The `errLog` field tells stash what the default log level should be when the plu
# Task configuration
In addition to the standard task configuration, external tags may be configured with an optional `execArgs` field to add extra parameters to the execution arguments for the task.
In addition to the standard task configuration, external tasks may be configured with an optional `execArgs` field to add extra parameters to the execution arguments for the task.
For example:

View File

@ -1,8 +1,12 @@
# Plugins
Stash supports the running tasks via plugins. Plugins can be implemented using embedded Javascript, or by calling an external binary.
Stash supports plugins that can do the following:
- perform custom tasks when triggered by the user from the Tasks page
- perform custom tasks when triggered from specific events
- add custom CSS to the UI
- add custom JavaScript to the UI
Stash also supports triggering of plugin hooks from specific stash operations.
Plugin tasks can be implemented using embedded Javascript, or by calling an external binary.
> **⚠️ Note:** Plugin support is still experimental and is likely to change.
@ -20,13 +24,45 @@ Plugins provide tasks which can be run from the Tasks page.
# Creating plugins
See [External Plugins](/help/ExternalPlugins.md) for details for making external plugins.
## Plugin configuration file format
See [Embedded Plugins](/help/EmbeddedPlugins.md) for details for making embedded plugins.
The basic structure of a plugin configuration file is as follows:
## Plugin input
```
name: <plugin name>
description: <optional description of the plugin>
version: <optional version tag>
url: <optional url>
Plugins may accept an input from the stash server. This input is encoded according to the interface, and has the following structure (presented here in JSON format):
ui:
# optional list of css files to include in the UI
css:
- <path to css file>
# optional list of js files to include in the UI
javascript:
- <path to javascript file>
# the following are used for plugin tasks only
exec:
- ...
interface: [interface type]
errLog: [one of none trace, debug, info, warning, error]
tasks:
- ...
```
The `name`, `description`, `version` and `url` fields are displayed on the plugins page.
The `exec`, `interface`, `errLog` and `tasks` fields are used only for plugins with tasks.
See [External Plugins](/help/ExternalPlugins.md) for details for making plugins with external tasks.
See [Embedded Plugins](/help/EmbeddedPlugins.md) for details for making plugins with embedded tasks.
## Plugin task input
Plugin tasks may accept an input from the stash server. This input is encoded according to the interface, and has the following structure (presented here in JSON format):
```
{
"server_connection": {
@ -57,9 +93,9 @@ Plugins may accept an input from the stash server. This input is encoded accordi
The `server_connection` field contains all the information needed for a plugin to access the parent stash server, if necessary.
## Plugin output
## Plugin task output
Plugin output is expected in the following structure (presented here as JSON format):
Plugin task output is expected in the following structure (presented here as JSON format):
```
{