Files
open-in-mpv/options.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
}