210 lines
4.4 KiB
Go
210 lines
4.4 KiB
Go
package open_in_mpv
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// The Options struct defines a model for the data contained in the mpv://
|
|
// URL and acts as a command generator (both CLI and IPC) to spawn and
|
|
// communicate with an mpv player window.
|
|
type Options struct {
|
|
Enqueue bool
|
|
Flags string
|
|
Fullscreen bool
|
|
NeedsIpc bool
|
|
NewWindow bool
|
|
Pip bool
|
|
Player string
|
|
Url *url.URL
|
|
}
|
|
|
|
// Utility object to marshal an mpv-compatible JSON command. As defined in the
|
|
// documentation, a valid IPC command is a JSON array of strings
|
|
type enqueueCmd struct {
|
|
Command []string `json:"command,omitempty"`
|
|
}
|
|
|
|
// Default constructor for an Option object
|
|
func NewOptions() Options {
|
|
return Options{
|
|
Enqueue: false,
|
|
Flags: "",
|
|
Fullscreen: false,
|
|
NeedsIpc: false,
|
|
NewWindow: false,
|
|
Pip: false,
|
|
Player: "mpv",
|
|
Url: nil,
|
|
}
|
|
}
|
|
|
|
// Parse a mpv:// URL and populate the current Options
|
|
func (o *Options) Parse(uri string) error {
|
|
u, err := url.Parse(uri)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if u.Scheme != "mpv" {
|
|
return fmt.Errorf("Unsupported protocol: %s", u.Scheme)
|
|
}
|
|
|
|
if u.Path != "/open" {
|
|
return fmt.Errorf("Unsupported method: %s", u.Path)
|
|
}
|
|
|
|
if len(u.RawQuery) < 2 {
|
|
return fmt.Errorf("Empty or malformed query: %s", u.RawQuery)
|
|
}
|
|
|
|
playerConfig := GetPlayerConfig(o.Player)
|
|
if playerConfig == nil {
|
|
return fmt.Errorf("Unsupported player: %s", o.Player)
|
|
}
|
|
|
|
// Extract player command line flags
|
|
o.Flags, err = url.QueryUnescape(u.Query().Get("flags"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Extract video file URL
|
|
rawUrl, err := url.QueryUnescape(u.Query().Get("url"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Parse the unprocessed URL
|
|
o.Url, err = url.Parse(rawUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Validate the raw URL scheme against the configured ones
|
|
if !stringSliceContains(o.Url.Scheme, playerConfig.SupportedSchemes) {
|
|
return fmt.Errorf(
|
|
"Unsupported schema for player '%s': %s. Did you forget to add it in the configuration?",
|
|
playerConfig.Name,
|
|
o.Url.Scheme,
|
|
)
|
|
}
|
|
|
|
if p, ok := u.Query()["player"]; ok {
|
|
o.Player = p[0]
|
|
}
|
|
|
|
o.Enqueue = u.Query().Get("enqueue") == "1"
|
|
o.Fullscreen = u.Query().Get("fullscreen") == "1"
|
|
o.NewWindow = u.Query().Get("new_window") == "1"
|
|
o.Pip = u.Query().Get("pip") == "1"
|
|
|
|
o.NeedsIpc = playerConfig.NeedsIpc
|
|
|
|
return nil
|
|
}
|
|
|
|
// Parses flag overrides and returns the final flags
|
|
func (o Options) overrideFlags() string {
|
|
var (
|
|
ret []string
|
|
star bool
|
|
)
|
|
|
|
playerConfig := GetPlayerConfig(o.Player)
|
|
if playerConfig == nil {
|
|
return ""
|
|
}
|
|
|
|
// Premature look for star override in configuration
|
|
_, star = playerConfig.FlagOverrides["*"]
|
|
|
|
for _, flag := range strings.Split(o.Flags, " ") {
|
|
if star {
|
|
// Unconditionally replace all flags with the star template
|
|
stripped := strings.TrimLeft(flag, "-")
|
|
replaced := strings.ReplaceAll(
|
|
playerConfig.FlagOverrides["*"],
|
|
`%s`,
|
|
stripped,
|
|
)
|
|
ret = append(ret, replaced)
|
|
continue
|
|
}
|
|
|
|
// Otherwise, iterate over all templates for the current flag and
|
|
// do the necessary string replacements
|
|
if override, ok := playerConfig.FlagOverrides[flag]; ok {
|
|
stripped := strings.TrimLeft(flag, "-")
|
|
ret = append(ret, strings.ReplaceAll(
|
|
override,
|
|
`%s`,
|
|
stripped,
|
|
))
|
|
}
|
|
}
|
|
|
|
return strings.Join(ret, " ")
|
|
}
|
|
|
|
// Builds a CLI command used to invoke the player with the appropriate
|
|
// arguments
|
|
func (o Options) GenerateCommand() []string {
|
|
var ret []string
|
|
|
|
playerConfig := GetPlayerConfig(o.Player)
|
|
|
|
if o.Fullscreen {
|
|
ret = append(ret, playerConfig.Fullscreen)
|
|
}
|
|
|
|
if o.Pip {
|
|
ret = append(ret, playerConfig.Pip)
|
|
}
|
|
|
|
if o.Flags != "" {
|
|
if len(playerConfig.FlagOverrides) == 0 {
|
|
ret = append(ret, o.Flags)
|
|
} else {
|
|
ret = append(ret, o.overrideFlags())
|
|
}
|
|
}
|
|
|
|
ret = append(ret, o.Url.String())
|
|
|
|
return ret
|
|
}
|
|
|
|
// Builds the IPC command needed to enqueue videos if the player requires it
|
|
func (o Options) GenerateIPC() ([]byte, error) {
|
|
if !o.NeedsIpc {
|
|
return []byte{}, fmt.Errorf("This player does not need IPC")
|
|
}
|
|
|
|
cmd := enqueueCmd{
|
|
[]string{"loadfile", o.Url.String(), "append-play"},
|
|
}
|
|
|
|
ret, err := json.Marshal(cmd)
|
|
if err != nil {
|
|
return []byte{}, err
|
|
}
|
|
|
|
if ret[len(ret)-1] != '\n' {
|
|
ret = append(ret, '\n')
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// Simple linear search for value in slice of strings
|
|
func stringSliceContains(value string, v []string) bool {
|
|
for _, elem := range v {
|
|
if elem == value {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|