Added explicit supported URL schemes matching for all players to reduce protocol abuse, added relevant section to README, added tests for new code
This commit is contained in:
@@ -6,4 +6,10 @@ indent_size = 4
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
|
|
||||||
|
[{Makefile}]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[{*.html,*.js,*.json,*.css}]
|
||||||
|
indent_size = 2
|
11
Makefile
11
Makefile
@@ -1,5 +1,6 @@
|
|||||||
SRC:=config.go ipc.go options.go $(wildcard cmd/open-in-mpv/*)
|
SRC:=config.go ipc.go options.go $(wildcard cmd/open-in-mpv/*)
|
||||||
EXT_SRC:=$(wildcard extension/Chrome/*) extension/Firefox/manifest.json
|
EXT_SRC:=$(wildcard extension/Chrome/*) extension/Firefox/manifest.json
|
||||||
|
SCRIPTS_DIR:=scripts
|
||||||
|
|
||||||
all: build/linux.tar build/mac.tar build/windows.tar build/firefox.zip
|
all: build/linux.tar build/mac.tar build/windows.tar build/firefox.zip
|
||||||
|
|
||||||
@@ -9,8 +10,8 @@ builddir:
|
|||||||
build/linux/open-in-mpv: $(SRC) builddir
|
build/linux/open-in-mpv: $(SRC) builddir
|
||||||
@echo -e "\n# Building for Linux"
|
@echo -e "\n# Building for Linux"
|
||||||
env CGO_ENABLED=0 GOOS=linux GOARCh=amd64 go build -ldflags="-s -w" -o $@ ./cmd/open-in-mpv
|
env CGO_ENABLED=0 GOOS=linux GOARCh=amd64 go build -ldflags="-s -w" -o $@ ./cmd/open-in-mpv
|
||||||
cp scripts/install-protocol.sh $(dir $@)
|
cp $(SCRIPTS_DIR)/install-protocol.sh $(dir $@)
|
||||||
cp scripts/open-in-mpv.desktop $(dir $@)
|
cp $(SCRIPTS_DIR)/open-in-mpv.desktop $(dir $@)
|
||||||
|
|
||||||
build/linux.tar: build/linux/open-in-mpv
|
build/linux.tar: build/linux/open-in-mpv
|
||||||
tar cf $@ -C $(dir $@)linux $(notdir $(wildcard build/linux/*))
|
tar cf $@ -C $(dir $@)linux $(notdir $(wildcard build/linux/*))
|
||||||
@@ -22,7 +23,7 @@ build/mac/open-in-mpv.app: $(SRC) scripts/Info.plist builddir
|
|||||||
@mkdir -p $@/Contents
|
@mkdir -p $@/Contents
|
||||||
env CGO_ENABLED=0 GOOS=darwin GOARCh=amd64 go build -ldflags="-s -w" -o $@/Contents/MacOS/open-in-mpv ./cmd/open-in-mpv
|
env CGO_ENABLED=0 GOOS=darwin GOARCh=amd64 go build -ldflags="-s -w" -o $@/Contents/MacOS/open-in-mpv ./cmd/open-in-mpv
|
||||||
cp config.yml $@/Contents/MacOS/
|
cp config.yml $@/Contents/MacOS/
|
||||||
cp scripts/Info.plist $@/Contents
|
cp $(SCRIPTS_DIR)/Info.plist $@/Contents
|
||||||
|
|
||||||
build/mac.tar: build/mac/open-in-mpv.app
|
build/mac.tar: build/mac/open-in-mpv.app
|
||||||
tar cf $@ -C $(dir $@)/mac open-in-mpv.app
|
tar cf $@ -C $(dir $@)/mac open-in-mpv.app
|
||||||
@@ -30,7 +31,7 @@ build/mac.tar: build/mac/open-in-mpv.app
|
|||||||
build/windows/open-in-mpv.exe: $(SRC) builddir
|
build/windows/open-in-mpv.exe: $(SRC) builddir
|
||||||
@echo -e "\n# Building for Windows"
|
@echo -e "\n# Building for Windows"
|
||||||
env CGO_ENABLED=0 GOOS=windows GOARCh=amd64 go build -ldflags="-s -w -H windowsgui" -o $@ ./cmd/open-in-mpv
|
env CGO_ENABLED=0 GOOS=windows GOARCh=amd64 go build -ldflags="-s -w -H windowsgui" -o $@ ./cmd/open-in-mpv
|
||||||
cp scripts/install-protocol.reg $(dir $@)
|
cp $(SCRIPTS_DIR)/install-protocol.reg $(dir $@)
|
||||||
|
|
||||||
build/windows.tar: build/windows/open-in-mpv.exe
|
build/windows.tar: build/windows/open-in-mpv.exe
|
||||||
tar cf $@ -C $(dir $@)windows $(notdir $(wildcard build/windows/*))
|
tar cf $@ -C $(dir $@)windows $(notdir $(wildcard build/windows/*))
|
||||||
@@ -45,7 +46,7 @@ install: build/linux/open-in-mpv
|
|||||||
cp build/linux/open-in-mpv /usr/bin
|
cp build/linux/open-in-mpv /usr/bin
|
||||||
|
|
||||||
install-protocol:
|
install-protocol:
|
||||||
scripts/install-protocol.sh
|
$(SCRIPTS_DIR)/install-protocol.sh
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
rm /usr/bin/open-in-mpv
|
rm /usr/bin/open-in-mpv
|
||||||
|
75
README.md
75
README.md
@@ -9,15 +9,14 @@
|
|||||||
|
|
||||||
This is a simple web extension (for Chrome and Firefox) which helps open any video in the currently open tab in the [mpv player](https://mpv.io).
|
This is a simple web extension (for Chrome and Firefox) which helps open any video in the currently open tab in the [mpv player](https://mpv.io).
|
||||||
|
|
||||||
The extension itself shares a lot of code with the one from the awesome [iina](https://github.com/iina/iina), while the (bare) backend is written in Go (this is a rewrite from C++).
|
The extension itself shares a lot of code with the one from the awesome [iina](https://github.com/iina/iina), while the (bare) native binary is written in Go (this is a rewrite from C++).
|
||||||
|
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Flag overrides](#flag-overrides)
|
|
||||||
- [Example](#example)
|
|
||||||
- [The `mpv://` protocol](#the-mpv-protocol)
|
- [The `mpv://` protocol](#the-mpv-protocol)
|
||||||
- [Playlist and `enqueue` functionality](#playlist-and-enqueue-functionality)
|
- [Playlist and `enqueue` functionality](#playlist-and-enqueue-functionality)
|
||||||
- [Player support](#player-support)
|
- [Player support](#player-support)
|
||||||
|
- [Supported protocols](#supported-protocols)
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
> Compiled binaries and packed extensions can be found in the [releases page](https://github.com/Baldomo/open-in-mpv/releases).
|
> Compiled binaries and packed extensions can be found in the [releases page](https://github.com/Baldomo/open-in-mpv/releases).
|
||||||
@@ -38,31 +37,33 @@ The configuration file has to be named `config.yml` and can be placed in the sam
|
|||||||
The configuration file has the following structure:
|
The configuration file has the following structure:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
fake: [ open_in_mpv.Player ]
|
fake: # open_in_mpv.Player
|
||||||
name: [ string ]
|
name: # string
|
||||||
executable: [ string ]
|
executable: # string
|
||||||
fullscreen: [ string ]
|
fullscreen: # string
|
||||||
pip: [ string ]
|
pip: # string
|
||||||
enqueue: [ string ]
|
enqueue: # string
|
||||||
new_window: [ string ]
|
new_window: # string
|
||||||
needs_ipc: [ true | false ]
|
needs_ipc: # true | false
|
||||||
flag_overrides: [ map[string]string ]
|
supported_protocols: # []string
|
||||||
|
flag_overrides: # map[string]string
|
||||||
```
|
```
|
||||||
|
|
||||||
> See [the default configuration](config.yml) as an example
|
> See [the default configuration](config.yml) or the [example](#example) below
|
||||||
|
|
||||||
And the `open_in_mpv.Player` object is defined as follows:
|
And the `open_in_mpv.Player` object is defined as follows:
|
||||||
|
|
||||||
| Key | Example value | Description |
|
| Key | Example value | Description |
|
||||||
|---------------|----------------------------------------|--------------|
|
| --------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `name` | `mpv` | Full name of the video player; not used internally |
|
| `name` | `mpv` | Full name of the video player; not used internally |
|
||||||
| `executable` | `mpv` | The player's binary path/name (doesn't need full path if already in `$PATH`) |
|
| `executable` | `mpv` | The player's binary path/name (doesn't need full path if already in `$PATH`) |
|
||||||
| `fullscreen` | `"--fs"` | Flag override to open the player in fullscreen (can be empty) |
|
| `fullscreen` | `"--fs"` | Flag override to open the player in fullscreen (can be empty) |
|
||||||
| `pip` | `"--pip"` | Flag override to open the player in picture-in-picture mode (can be empty) |
|
| `pip` | `"--pip"` | Flag override to open the player in picture-in-picture mode (can be empty) |
|
||||||
| `enqueue` | `"--enqueue"` | Flag override to add the video to the player's queue (can be empty) |
|
| `enqueue` | `"--enqueue"` | Flag override to add the video to the player's queue (can be empty) |
|
||||||
| `new_window` | `"--new-window"` | Flag override to force open a new player window with the video (can be empty) |
|
| `new_window` | `"--new-window"` | Flag override to force open a new player window with the video (can be empty) |
|
||||||
| `needs_ipc` | `false` | Controls whether the player needs IPC communication (only generates mpv-compatible JSON commands, used for enqueing videos) |
|
| `needs_ipc` | `false` | Controls whether the player needs IPC communication (only generates mpv-compatible JSON commands, used for enqueing videos) |
|
||||||
| `flag_overrides` | `"*": "--mpv-options=%s"` | Defines arbitrary text overrides for command line flags (see below) |
|
| `supported_protocols` | `["http", "https"]` | An arbitrary whitelist of protocols the player supports. See the [relevant section](#supported-protocols) |
|
||||||
|
| `flag_overrides` | `"*": "--mpv-%s"` | Defines arbitrary text overrides for command line flags (see below) |
|
||||||
|
|
||||||
#### Flag overrides
|
#### Flag overrides
|
||||||
|
|
||||||
@@ -98,6 +99,11 @@ players:
|
|||||||
enqueue: "--enqueue"
|
enqueue: "--enqueue"
|
||||||
new_window: ""
|
new_window: ""
|
||||||
needs_ipc: true
|
needs_ipc: true
|
||||||
|
supported_protocols:
|
||||||
|
- http
|
||||||
|
- https
|
||||||
|
- ftp
|
||||||
|
- ftps
|
||||||
flag_overrides:
|
flag_overrides:
|
||||||
"*": "--mpv-options=%s"
|
"*": "--mpv-options=%s"
|
||||||
```
|
```
|
||||||
@@ -105,7 +111,7 @@ players:
|
|||||||
### The `mpv://` protocol
|
### The `mpv://` protocol
|
||||||
`open-in-mpv install-protocol` will create a custom `xdg-open` desktop file with a scheme handler for the `mpv://` protocol. This lets `xdg-open` call `open-in-mpv` with an encoded URI, so that it can be parsed and the information can be relayed to `mpv` - this logic follows how `iina` parses and opens custom URIs with the `iina://` protocol on Mac. `install-protocol.sh` has the same functionality.
|
`open-in-mpv install-protocol` will create a custom `xdg-open` desktop file with a scheme handler for the `mpv://` protocol. This lets `xdg-open` call `open-in-mpv` with an encoded URI, so that it can be parsed and the information can be relayed to `mpv` - this logic follows how `iina` parses and opens custom URIs with the `iina://` protocol on Mac. `install-protocol.sh` has the same functionality.
|
||||||
|
|
||||||
Please note that this specification is enforced quite strictly, as the program will error out when:
|
Please note that this specification is enforced quite strictly, as the program will error out when at least one of the following conditions is true:
|
||||||
|
|
||||||
- The protocol is not `mpv://`
|
- The protocol is not `mpv://`
|
||||||
- The method/path is not `/open`
|
- The method/path is not `/open`
|
||||||
@@ -113,14 +119,14 @@ Please note that this specification is enforced quite strictly, as the program w
|
|||||||
|
|
||||||
The table below is a simple documentation of the URL query keys and values used to let the `open-in-mpv` executable what to do.
|
The table below is a simple documentation of the URL query keys and values used to let the `open-in-mpv` executable what to do.
|
||||||
|
|
||||||
| Key | Example value | Description |
|
| Key | Example value | Description |
|
||||||
|---------------|----------------------------------------|-------------|
|
| ------------- | -------------------------------------- | ------------------------------------------------------------------------------ |
|
||||||
| `url` | `https%3A%2F%2Fyoutu.be%2FdQw4w9WgXcQ` | The actual file URL to be played, URL-encoded |
|
| `url` | `https%3A%2F%2Fyoutu.be%2FdQw4w9WgXcQ` | The actual file URL to be played, URL-encoded |
|
||||||
| `full_screen` | `1` | Controls whether the video is played in fullscreen mode |
|
| `full_screen` | `1` | Controls whether the video is played in fullscreen mode |
|
||||||
| `pip` | `1` | Simulates a picture-in-picture mode (only works with mpv for now) |
|
| `pip` | `1` | Simulates a picture-in-picture mode (only works with mpv for now) |
|
||||||
| `enqueue` | `1` | Adds a video to the queue (see below) |
|
| `enqueue` | `1` | Adds a video to the queue (see below) |
|
||||||
| `new_window` | `1` | Forcibly starts a video in a new window even if one is already open |
|
| `new_window` | `1` | Forcibly starts a video in a new window even if one is already open |
|
||||||
| `player` | `celluloid` | Starts any supported video player (see [Player support](#player-support)) |
|
| `player` | `celluloid` | Starts any supported video player (see [Player support](#player-support)) |
|
||||||
| `flags` | `--vo%3Dgpu` | Custom command options and flags to be passed to the video player, URL-encoded |
|
| `flags` | `--vo%3Dgpu` | Custom command options and flags to be passed to the video player, URL-encoded |
|
||||||
|
|
||||||
### Playlist and `enqueue` functionality
|
### Playlist and `enqueue` functionality
|
||||||
@@ -131,4 +137,7 @@ input-ipc-server=/tmp/mpvsocket
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Player support
|
### Player support
|
||||||
Supported players are defined in `config.yml`, where the struct `Player` ([see `config.go`](config.go)) defines supported functionality and command line flag overrides. To request support for a player you're welcome to open a new issue or a pull request or just add your own in your configuration file.
|
Supported players are defined in `config.yml`, where the struct `Player` ([see `config.go`](config.go)) defines supported functionality and command line flag overrides. To request support for a player you're welcome to open a new issue or a pull request or just add your own in your configuration file.
|
||||||
|
|
||||||
|
### Supported protocols
|
||||||
|
Since opening an arbitrary URL with a shell command can cause remote code execution on the host machine (for example by loading arbitrary `.so` files on a player by using special [schemes](https://en.wikipedia.org/wiki/List_of_URI_schemes)), only protocols/[schemes](https://en.wikipedia.org/wiki/List_of_URI_schemes) explicitly specified in the configuration will be processed by the native binary without errors. Defaults to `["http", "https"]` if empty. There is also no special instructions parsing or catch-all values.
|
29
config.go
29
config.go
@@ -28,6 +28,9 @@ type Player struct {
|
|||||||
NewWindow string `yaml:"new_window"`
|
NewWindow string `yaml:"new_window"`
|
||||||
// Controls whether this player needs IPC command to enqueue videos
|
// Controls whether this player needs IPC command to enqueue videos
|
||||||
NeedsIpc bool `yaml:"needs_ipc"`
|
NeedsIpc bool `yaml:"needs_ipc"`
|
||||||
|
// Controls which (video URL) schemes are to be opened by the current
|
||||||
|
// player. There is no match-all, each protocol has to be manuall specified
|
||||||
|
SupportedSchemes []string `yaml:"supported_protocols"`
|
||||||
// Overrides for any generic flag
|
// Overrides for any generic flag
|
||||||
FlagOverrides map[string]string `yaml:"flag_overrides"`
|
FlagOverrides map[string]string `yaml:"flag_overrides"`
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,11 @@ type Config struct {
|
|||||||
Players map[string]Player
|
Players map[string]Player
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var defaultSupportedSchemas = []string{
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
}
|
||||||
|
|
||||||
var defaultConfig = Config{
|
var defaultConfig = Config{
|
||||||
Players: map[string]Player{
|
Players: map[string]Player{
|
||||||
"mpv": {
|
"mpv": {
|
||||||
@@ -52,7 +60,8 @@ var defaultConfig = Config{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tries to load configuration file with fallback
|
// Tries to load configuration file with fallback to a default configuration
|
||||||
|
// object
|
||||||
func LoadConfig() error {
|
func LoadConfig() error {
|
||||||
confDirs := configdir.New("", "open-in-mpv")
|
confDirs := configdir.New("", "open-in-mpv")
|
||||||
confDirs.LocalPath, _ = filepath.Abs(".")
|
confDirs.LocalPath, _ = filepath.Abs(".")
|
||||||
@@ -68,11 +77,25 @@ func LoadConfig() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return yaml.Unmarshal(data, &defaultConfig)
|
err = yaml.Unmarshal(data, &defaultConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the player has no external configuration, use strict defaults
|
||||||
|
for name, player := range defaultConfig.Players {
|
||||||
|
if len(player.SupportedSchemes) == 0 {
|
||||||
|
log.Printf("Player '%s' has no schemas, setting to defaults", player.Name)
|
||||||
|
player.SupportedSchemes = defaultSupportedSchemas
|
||||||
|
defaultConfig.Players[name] = player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns player information for the given name if present, otherwise nil
|
// Returns player information for the given name if present, otherwise nil
|
||||||
func GetPlayerInfo(name string) *Player {
|
func GetPlayerConfig(name string) *Player {
|
||||||
lowerName := strings.ToLower(name)
|
lowerName := strings.ToLower(name)
|
||||||
if p, ok := defaultConfig.Players[lowerName]; ok {
|
if p, ok := defaultConfig.Players[lowerName]; ok {
|
||||||
return &p
|
return &p
|
||||||
|
@@ -1,14 +1,17 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<title>Document</title>
|
<title>Document</title>
|
||||||
<script src="common.js" type="module"></script>
|
<script src="common.js" type="module"></script>
|
||||||
<script src="background.js" type="module"></script>
|
<script src="background.js" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
@@ -3,18 +3,18 @@ import { getOptions, openInMPV, updateBrowserAction } from "./common.js";
|
|||||||
updateBrowserAction();
|
updateBrowserAction();
|
||||||
|
|
||||||
[["page", "pageUrl"], ["link", "linkUrl"], ["video", "srcUrl"], ["audio", "srcUrl"]].forEach(([item, linkType]) => {
|
[["page", "pageUrl"], ["link", "linkUrl"], ["video", "srcUrl"], ["audio", "srcUrl"]].forEach(([item, linkType]) => {
|
||||||
chrome.contextMenus.create({
|
chrome.contextMenus.create({
|
||||||
title: `Open this ${item} in mpv`,
|
title: `Open this ${item} in mpv`,
|
||||||
id: `open${item}inmpv`,
|
id: `open${item}inmpv`,
|
||||||
contexts: [item],
|
contexts: [item],
|
||||||
onclick: (info, tab) => {
|
onclick: (info, tab) => {
|
||||||
getOptions((options) => {
|
getOptions((options) => {
|
||||||
console.log("Got options: ", options);
|
console.log("Got options: ", options);
|
||||||
openInMPV(tab.id, info[linkType], {
|
openInMPV(tab.id, info[linkType], {
|
||||||
mode: options.iconActionOption,
|
mode: options.iconActionOption,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,130 +1,135 @@
|
|||||||
class Option {
|
class Option {
|
||||||
constructor(name, type, defaultValue) {
|
constructor(name, type, defaultValue) {
|
||||||
this.name = name;
|
this.name = name
|
||||||
this.type = type;
|
this.type = type
|
||||||
this.defaultValue = defaultValue;
|
this.defaultValue = defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(value) {
|
setValue(value) {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case "radio":
|
case "radio":
|
||||||
Array.prototype.forEach.call(document.getElementsByName(this.name), (el) => {
|
Array.prototype.forEach.call(document.getElementsByName(this.name), (el) => {
|
||||||
el.checked = el.value === value;
|
el.checked = el.value === value
|
||||||
});
|
})
|
||||||
break;
|
break
|
||||||
case "checkbox":
|
case "checkbox":
|
||||||
document.getElementsByName(this.name).forEach(el => el.checked = value);
|
document.getElementsByName(this.name).forEach(el => el.checked = value)
|
||||||
break;
|
break
|
||||||
case "select":
|
case "select":
|
||||||
document.getElementsByName(this.name).forEach(el => el.value = value);
|
document.getElementsByName(this.name).forEach(el => el.value = value)
|
||||||
break;
|
break
|
||||||
case "text":
|
case "text":
|
||||||
document.getElementsByName(this.name).forEach(el => el.value = value);
|
document.getElementsByName(this.name).forEach(el => el.value = value)
|
||||||
break;
|
break
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getValue() {
|
getValue() {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case "radio":
|
case "radio":
|
||||||
return document.querySelector(`input[name="${this.name}"]:checked`).value;
|
return document.querySelector(`input[name="${this.name}"]:checked`).value
|
||||||
case "checkbox":
|
case "checkbox":
|
||||||
return document.querySelector(`input[name="${this.name}"]`).checked;
|
return document.querySelector(`input[name="${this.name}"]`).checked
|
||||||
case "select":
|
case "select":
|
||||||
return document.querySelector(`select[name="${this.name}"]`).value;
|
return document.querySelector(`select[name="${this.name}"]`).value
|
||||||
case "text":
|
case "text":
|
||||||
return document.querySelector(`input[name="${this.name}"]`).value;
|
return document.querySelector(`input[name="${this.name}"]`).value
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _options = [
|
const _options = [
|
||||||
new Option("iconAction", "radio", "clickOnly"),
|
new Option("iconAction", "radio", "clickOnly"),
|
||||||
new Option("iconActionOption", "radio", "direct"),
|
new Option("iconActionOption", "radio", "direct"),
|
||||||
new Option("mpvPlayer", "select", "mpv"),
|
new Option("mpvPlayer", "select", "mpv"),
|
||||||
new Option("useCustomFlags", "checkbox", false),
|
new Option("useCustomFlags", "checkbox", false),
|
||||||
new Option("customFlags", "text", "")
|
new Option("customFlags", "text", "")
|
||||||
];
|
]
|
||||||
|
|
||||||
export function getOptions(callback) {
|
export function getOptions(callback) {
|
||||||
const getDict = {};
|
const getDict = {}
|
||||||
_options.forEach((item) => {
|
_options.forEach(item => {
|
||||||
getDict[item.name] = item.defaultValue;
|
getDict[item.name] = item.defaultValue
|
||||||
})
|
})
|
||||||
chrome.storage.sync.get(getDict, callback);
|
chrome.storage.sync.get(getDict, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveOptions() {
|
export function saveOptions() {
|
||||||
const saveDict = {};
|
const saveDict = {}
|
||||||
_options.forEach((item) => {
|
_options.forEach(item => {
|
||||||
saveDict[item.name] = item.getValue();
|
saveDict[item.name] = item.getValue()
|
||||||
})
|
})
|
||||||
chrome.storage.sync.set(saveDict);
|
chrome.storage.sync.set(saveDict)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restoreOptions() {
|
export function restoreOptions() {
|
||||||
getOptions((items) => {
|
getOptions((items) => {
|
||||||
_options.forEach((option) => {
|
_options.forEach(option => {
|
||||||
option.setValue(items[option.name]);
|
option.setValue(items[option.name])
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openInMPV(tabId, url, options = {}) {
|
export function openInMPV(tabId, url, options = {}) {
|
||||||
const baseURL = `mpv:///open?`;
|
const baseURL = `mpv:///open?`
|
||||||
|
|
||||||
// Encode video URL
|
// Encode video URL
|
||||||
const params = [`url=${encodeURIComponent(url)}`];
|
const params = [`url=${encodeURIComponent(url)}`]
|
||||||
|
|
||||||
// Add playback options
|
// Add playback options
|
||||||
switch (options.mode) {
|
switch (options.mode) {
|
||||||
case "fullScreen":
|
case "fullScreen":
|
||||||
params.push("full_screen=1"); break;
|
params.push("full_screen=1"); break
|
||||||
case "pip":
|
case "pip":
|
||||||
params.push("pip=1"); break;
|
params.push("pip=1"); break
|
||||||
case "enqueue":
|
case "enqueue":
|
||||||
params.push("enqueue=1"); break;
|
params.push("enqueue=1"); break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new window option
|
// Add new window option
|
||||||
if (options.newWindow) {
|
if (options.newWindow) {
|
||||||
params.push("new_window=1");
|
params.push("new_window=1")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add alternative player and user-defined custom flags
|
// Add alternative player and user-defined custom flags
|
||||||
params.push(`player=${options.mpvPlayer}`);
|
params.push(`player=${options.mpvPlayer}`)
|
||||||
if (options.useCustomFlags && options.customFlags !== "")
|
if (options.useCustomFlags && options.customFlags !== "")
|
||||||
params.push(`flags=${encodeURIComponent(options.customFlags)}`);
|
params.push(`flags=${encodeURIComponent(options.customFlags)}`)
|
||||||
|
|
||||||
const code = `
|
const code = `
|
||||||
var link = document.createElement('a');
|
var link = document.createElement('a')
|
||||||
link.href='${baseURL}${params.join("&")}';
|
link.href='${baseURL}${params.join("&")}'
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link)
|
||||||
link.click();
|
link.click()`
|
||||||
`;
|
console.log(code)
|
||||||
console.log(code);
|
chrome.tabs.executeScript(tabId, { code })
|
||||||
chrome.tabs.executeScript(tabId, { code });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateBrowserAction() {
|
export function updateBrowserAction() {
|
||||||
getOptions((options) => {
|
getOptions(options => {
|
||||||
if (options.iconAction === "clickOnly") {
|
if (options.iconAction === "clickOnly") {
|
||||||
chrome.browserAction.setPopup({ popup: "" });
|
chrome.browserAction.setPopup({ popup: "" })
|
||||||
chrome.browserAction.onClicked.addListener(() => {
|
chrome.browserAction.onClicked.addListener(() => {
|
||||||
// get active window
|
// Get active tab
|
||||||
chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => {
|
chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => {
|
||||||
if (tabs.length === 0) { return; }
|
if (tabs.length === 0)
|
||||||
// TODO: filter url
|
return
|
||||||
const tab = tabs[0];
|
|
||||||
if (tab.id === chrome.tabs.TAB_ID_NONE) { return; }
|
// TODO: filter url
|
||||||
openInMPV(tab.id, tab.url, {
|
const tab = tabs[0]
|
||||||
mode: options.iconActionOption,
|
if (tab.id === chrome.tabs.TAB_ID_NONE)
|
||||||
...options,
|
return
|
||||||
});
|
|
||||||
});
|
openInMPV(tab.id, tab.url, {
|
||||||
});
|
mode: options.iconActionOption,
|
||||||
} else {
|
...options,
|
||||||
chrome.browserAction.setPopup({ popup: "popup.html" });
|
})
|
||||||
}
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.browserAction.setPopup({ popup: "popup.html" })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@@ -1,68 +1,68 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Inter', Arial, sans-serif;
|
font-family: 'Inter', Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 880px;
|
max-width: 880px;
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
background: #f4f4f4;
|
background: #f4f4f4;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 0 16px rgba(0,0,0,.2);
|
box-shadow: 0 0 16px rgba(0, 0, 0, .2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item:not(:last-child) {
|
.item:not(:last-child) {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-top: 1px solid #e4e4e4;
|
border-top: 1px solid #e4e4e4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option:not(:last-child) {
|
.option:not(:last-child) {
|
||||||
border-bottom: 1px solid #e4e4e4;
|
border-bottom: 1px solid #e4e4e4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option .option-title {
|
.option .option-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option .option-details {
|
.option .option-details {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option .option-details:not(:first-child) {
|
.option .option-details:not(:first-child) {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option .option-details:not(:last-child) {
|
.option .option-details:not(:last-child) {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-details > p {
|
.option-details>p {
|
||||||
color: rgb(70, 70, 70);
|
color: rgb(70, 70, 70);
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
border: 1px solid rgb(169, 169, 169);
|
border: 1px solid rgb(169, 169, 169);
|
||||||
margin: 0 0 0.5rem 0;
|
margin: 0 0 0.5rem 0;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
margin: 0.1rem;
|
margin: 0.1rem;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
@@ -1,61 +1,66 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<title>Options</title>
|
<title>Options</title>
|
||||||
<script src="common.js" type="module"></script>
|
<script src="common.js" type="module"></script>
|
||||||
<link rel="stylesheet" href="options.css">
|
<link rel="stylesheet" href="options.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="title">Player options</h2>
|
<h2 class="title">Player options</h2>
|
||||||
<div class="option">
|
<div class="option">
|
||||||
<label class="option-title"> Select the mpv-based player to use</label>
|
<label class="option-title"> Select the mpv-based player to use</label>
|
||||||
<br>
|
<br>
|
||||||
<select name="mpvPlayer">
|
<select name="mpvPlayer">
|
||||||
<option value="mpv">mpv</option>
|
<option value="mpv">mpv</option>
|
||||||
<option value="celluloid">Celluloid</option>
|
<option value="celluloid">Celluloid</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
<div class="option">
|
|
||||||
<label class="option-title"><input type="checkbox" name="useCustomFlags"> Use custom command line flags</label>
|
|
||||||
<div class="option-details" id="customFlagsContainer">
|
|
||||||
<input type="text" name="customFlags">
|
|
||||||
<p> Note: do include hyphens (e.g.<span style="font-family: monospace;">'--fs'</span>)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="option">
|
||||||
<h2 class="title">When clicking on the extension icon</h2>
|
<label class="option-title"><input type="checkbox" name="useCustomFlags"> Use custom command line flags</label>
|
||||||
<div class="option">
|
<div class="option-details" id="customFlagsContainer">
|
||||||
<div class="item">
|
<input type="text" name="customFlags">
|
||||||
<label><input type="radio" name="iconAction" value="clickOnly"> Open current page in mpv</label>
|
<p> Note: do include hyphens (e.g.<span style="font-family: monospace;">'--fs'</span>)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
|
||||||
<label><input type="radio" name="iconAction" value="menu"> Show a menu to select action</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
</div>
|
||||||
<h2 class="title">Default open action</h2>
|
<div class="container">
|
||||||
<div class="option">
|
<h2 class="title">When clicking on the extension icon</h2>
|
||||||
<div class="option-details"><p>Applies to "Open current page in mpv" and the right click context menu</p></div>
|
<div class="option">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label><input type="radio" name="iconActionOption" value="direct"> Open directly</label>
|
<label><input type="radio" name="iconAction" value="clickOnly"> Open current page in mpv</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label><input type="radio" name="iconActionOption" value="fullScreen"> Enter full screen</label>
|
<label><input type="radio" name="iconAction" value="menu"> Show a menu to select action</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
|
||||||
<label><input type="radio" name="iconActionOption" value="pip"> Enter Picture-in-Picture</label>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<label><input type="radio" name="iconActionOption" value="enqueue"> Add to queue</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<script src="options.js" type="module"></script>
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">Default open action</h2>
|
||||||
|
<div class="option">
|
||||||
|
<div class="option-details">
|
||||||
|
<p>Applies to "Open current page in mpv" and the right click context menu</p>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<label><input type="radio" name="iconActionOption" value="direct"> Open directly</label>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<label><input type="radio" name="iconActionOption" value="fullScreen"> Enter full screen</label>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<label><input type="radio" name="iconActionOption" value="pip"> Enter Picture-in-Picture</label>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<label><input type="radio" name="iconActionOption" value="enqueue"> Add to queue</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="options.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
@@ -1,13 +1,11 @@
|
|||||||
import { restoreOptions, saveOptions, updateBrowserAction } from "./common.js";
|
import { restoreOptions, saveOptions, updateBrowserAction } from "./common.js"
|
||||||
|
|
||||||
function listener(el) {
|
const addListener = el => el.addEventListener("change", () => {
|
||||||
el.addEventListener("change", () => {
|
saveOptions()
|
||||||
saveOptions();
|
updateBrowserAction()
|
||||||
updateBrowserAction();
|
})
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", restoreOptions);
|
document.addEventListener("DOMContentLoaded", restoreOptions)
|
||||||
|
|
||||||
Array.prototype.forEach.call(document.getElementsByTagName("input"), listener);
|
Array.prototype.forEach.call(document.getElementsByTagName("input"), addListener)
|
||||||
Array.prototype.forEach.call(document.getElementsByTagName("select"), listener);
|
Array.prototype.forEach.call(document.getElementsByTagName("select"), addListener)
|
||||||
|
@@ -1,34 +1,39 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<title>Document</title>
|
<title>Document</title>
|
||||||
<style>
|
<style>
|
||||||
.menu {
|
.menu {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.menu .menu-item {
|
|
||||||
padding: 0.5rem;
|
.menu .menu-item {
|
||||||
transition: all 0.1s ease-out;
|
padding: 0.5rem;
|
||||||
cursor: pointer;
|
transition: all 0.1s ease-out;
|
||||||
border-radius: 6px;
|
cursor: pointer;
|
||||||
}
|
border-radius: 6px;
|
||||||
.menu .menu-item:hover {
|
}
|
||||||
background: #e4e4e4;
|
|
||||||
}
|
.menu .menu-item:hover {
|
||||||
</style>
|
background: #e4e4e4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<div class="menu-item" id="open-normal">Open in mpv</div>
|
<div class="menu-item" id="open-normal">Open in mpv</div>
|
||||||
<div class="menu-item" id="open-fullScreen">Open in mpv and enter full screen</div>
|
<div class="menu-item" id="open-fullScreen">Open in mpv and enter full screen</div>
|
||||||
<div class="menu-item" id="open-pip">Open in mpv and enter Picture-in-Picture</div>
|
<div class="menu-item" id="open-pip">Open in mpv and enter Picture-in-Picture</div>
|
||||||
<div class="menu-item" id="open-newWindow">Open in a new mpv window</div>
|
<div class="menu-item" id="open-newWindow">Open in a new mpv window</div>
|
||||||
<div class="menu-item" id="open-enqueue">Add to queue (playlist)</div>
|
<div class="menu-item" id="open-enqueue">Add to queue (playlist)</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="./popup.js" type="module"></script>
|
<script src="./popup.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
@@ -1,20 +1,24 @@
|
|||||||
import { openInMPV, getOptions } from "./common.js";
|
import { openInMPV, getOptions } from "./common.js"
|
||||||
|
|
||||||
Array.prototype.forEach.call(document.getElementsByClassName("menu-item"), (item) => {
|
Array.prototype.forEach.call(document.getElementsByClassName("menu-item"), item => {
|
||||||
const mode = item.id.split("-")[1];
|
const mode = item.id.split("-")[1]
|
||||||
item.addEventListener("click", () => {
|
item.addEventListener("click", () => {
|
||||||
getOptions((options) => {
|
getOptions(options => {
|
||||||
chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => {
|
chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => {
|
||||||
if (tabs.length === 0) { return; }
|
if (tabs.length === 0)
|
||||||
const tab = tabs[0];
|
return
|
||||||
if (tab.id === chrome.tabs.TAB_ID_NONE) { return; }
|
|
||||||
console.log(mode)
|
const tab = tabs[0]
|
||||||
openInMPV(tab.id, tab.url, {
|
if (tab.id === chrome.tabs.TAB_ID_NONE)
|
||||||
mode,
|
return
|
||||||
newWindow: mode === "newWindow",
|
|
||||||
...options,
|
console.log(mode)
|
||||||
});
|
openInMPV(tab.id, tab.url, {
|
||||||
});
|
mode,
|
||||||
});
|
newWindow: mode === "newWindow",
|
||||||
});
|
...options,
|
||||||
});
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/Baldomo/open-in-mpv
|
module github.com/Baldomo/open-in-mpv
|
||||||
|
|
||||||
go 1.15
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
|
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
|
||||||
|
102
options.go
102
options.go
@@ -11,14 +11,14 @@ import (
|
|||||||
// URL and acts as a command generator (both CLI and IPC) to spawn and
|
// URL and acts as a command generator (both CLI and IPC) to spawn and
|
||||||
// communicate with an mpv player window.
|
// communicate with an mpv player window.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Flags string
|
|
||||||
Player string
|
|
||||||
Url string
|
|
||||||
Enqueue bool
|
Enqueue bool
|
||||||
|
Flags string
|
||||||
Fullscreen bool
|
Fullscreen bool
|
||||||
|
NeedsIpc bool
|
||||||
NewWindow bool
|
NewWindow bool
|
||||||
Pip bool
|
Pip bool
|
||||||
NeedsIpc bool
|
Player string
|
||||||
|
Url *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility object to marshal an mpv-compatible JSON command. As defined in the
|
// Utility object to marshal an mpv-compatible JSON command. As defined in the
|
||||||
@@ -30,14 +30,14 @@ type enqueueCmd struct {
|
|||||||
// Default constructor for an Option object
|
// Default constructor for an Option object
|
||||||
func NewOptions() Options {
|
func NewOptions() Options {
|
||||||
return Options{
|
return Options{
|
||||||
Flags: "",
|
|
||||||
Player: "mpv",
|
|
||||||
Url: "",
|
|
||||||
Enqueue: false,
|
Enqueue: false,
|
||||||
|
Flags: "",
|
||||||
Fullscreen: false,
|
Fullscreen: false,
|
||||||
|
NeedsIpc: false,
|
||||||
NewWindow: false,
|
NewWindow: false,
|
||||||
Pip: false,
|
Pip: false,
|
||||||
NeedsIpc: false,
|
Player: "mpv",
|
||||||
|
Url: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,29 +60,46 @@ func (o *Options) Parse(uri string) error {
|
|||||||
return fmt.Errorf("Empty or malformed query: %s", u.RawQuery)
|
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"))
|
o.Flags, err = url.QueryUnescape(u.Query().Get("flags"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
o.Url, err = url.QueryUnescape(u.Query().Get("url"))
|
|
||||||
|
// Extract video file URL
|
||||||
|
rawUrl, err := url.QueryUnescape(u.Query().Get("url"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if p, ok := u.Query()["player"]; ok {
|
||||||
o.Player = p[0]
|
o.Player = p[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if GetPlayerInfo(o.Player) == nil {
|
|
||||||
return fmt.Errorf("Unsupported player: %s", o.Player)
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Enqueue = u.Query().Get("enqueue") == "1"
|
o.Enqueue = u.Query().Get("enqueue") == "1"
|
||||||
o.Fullscreen = u.Query().Get("fullscreen") == "1"
|
o.Fullscreen = u.Query().Get("fullscreen") == "1"
|
||||||
o.NewWindow = u.Query().Get("new_window") == "1"
|
o.NewWindow = u.Query().Get("new_window") == "1"
|
||||||
o.Pip = u.Query().Get("pip") == "1"
|
o.Pip = u.Query().Get("pip") == "1"
|
||||||
|
|
||||||
o.NeedsIpc = GetPlayerInfo(o.Player).NeedsIpc
|
o.NeedsIpc = playerConfig.NeedsIpc
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -94,52 +111,66 @@ func (o Options) overrideFlags() string {
|
|||||||
star bool
|
star bool
|
||||||
)
|
)
|
||||||
|
|
||||||
pInfo := GetPlayerInfo(o.Player)
|
playerConfig := GetPlayerConfig(o.Player)
|
||||||
if pInfo == nil {
|
if playerConfig == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
_, star = pInfo.FlagOverrides["*"]
|
// Premature look for star override in configuration
|
||||||
|
_, star = playerConfig.FlagOverrides["*"]
|
||||||
|
|
||||||
for _, flag := range strings.Split(o.Flags, " ") {
|
for _, flag := range strings.Split(o.Flags, " ") {
|
||||||
if star {
|
if star {
|
||||||
|
// Unconditionally replace all flags with the star template
|
||||||
stripped := strings.TrimLeft(flag, "-")
|
stripped := strings.TrimLeft(flag, "-")
|
||||||
replaced := strings.ReplaceAll(pInfo.FlagOverrides["*"], `%s`, stripped)
|
replaced := strings.ReplaceAll(
|
||||||
|
playerConfig.FlagOverrides["*"],
|
||||||
|
`%s`,
|
||||||
|
stripped,
|
||||||
|
)
|
||||||
ret = append(ret, replaced)
|
ret = append(ret, replaced)
|
||||||
} else {
|
continue
|
||||||
if override, ok := pInfo.FlagOverrides[flag]; ok {
|
}
|
||||||
stripped := strings.TrimLeft(flag, "-")
|
|
||||||
ret = append(ret, strings.ReplaceAll(override, `%s`, stripped))
|
// 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, " ")
|
return strings.Join(ret, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds a CLI command used to invoke the player with the appropriate arguments
|
// Builds a CLI command used to invoke the player with the appropriate
|
||||||
|
// arguments
|
||||||
func (o Options) GenerateCommand() []string {
|
func (o Options) GenerateCommand() []string {
|
||||||
var ret []string
|
var ret []string
|
||||||
|
|
||||||
pInfo := GetPlayerInfo(o.Player)
|
playerConfig := GetPlayerConfig(o.Player)
|
||||||
|
|
||||||
if o.Fullscreen {
|
if o.Fullscreen {
|
||||||
ret = append(ret, pInfo.Fullscreen)
|
ret = append(ret, playerConfig.Fullscreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.Pip {
|
if o.Pip {
|
||||||
ret = append(ret, pInfo.Pip)
|
ret = append(ret, playerConfig.Pip)
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.Flags != "" {
|
if o.Flags != "" {
|
||||||
if len(pInfo.FlagOverrides) == 0 {
|
if len(playerConfig.FlagOverrides) == 0 {
|
||||||
ret = append(ret, o.Flags)
|
ret = append(ret, o.Flags)
|
||||||
} else {
|
} else {
|
||||||
ret = append(ret, o.overrideFlags())
|
ret = append(ret, o.overrideFlags())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = append(ret, o.Url)
|
ret = append(ret, o.Url.String())
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@@ -151,7 +182,7 @@ func (o Options) GenerateIPC() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd := enqueueCmd{
|
cmd := enqueueCmd{
|
||||||
[]string{"loadfile", o.Url, "append-play"},
|
[]string{"loadfile", o.Url.String(), "append-play"},
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := json.Marshal(cmd)
|
ret, err := json.Marshal(cmd)
|
||||||
@@ -165,3 +196,14 @@ func (o Options) GenerateIPC() ([]byte, error) {
|
|||||||
|
|
||||||
return ret, nil
|
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
|
||||||
|
}
|
||||||
|
@@ -1,19 +1,21 @@
|
|||||||
package open_in_mpv
|
package open_in_mpv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fakePlayer = Player{
|
var fakePlayer = Player{
|
||||||
Name: "FakePlayer",
|
Name: "FakePlayer",
|
||||||
Executable: "fakeplayer",
|
Executable: "fakeplayer",
|
||||||
Fullscreen: "",
|
Fullscreen: "",
|
||||||
Pip: "",
|
Pip: "",
|
||||||
Enqueue: "",
|
Enqueue: "",
|
||||||
NewWindow: "",
|
NewWindow: "",
|
||||||
NeedsIpc: true,
|
NeedsIpc: true,
|
||||||
FlagOverrides: map[string]string{},
|
SupportedSchemes: []string{"https"},
|
||||||
|
FlagOverrides: map[string]string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUrl(query ...string) string {
|
func testUrl(query ...string) string {
|
||||||
@@ -25,7 +27,7 @@ func testUrl(query ...string) string {
|
|||||||
|
|
||||||
func Test_GenerateCommand(t *testing.T) {
|
func Test_GenerateCommand(t *testing.T) {
|
||||||
o := NewOptions()
|
o := NewOptions()
|
||||||
o.Url = "example.com"
|
o.Url, _ = url.Parse("example.com")
|
||||||
o.Flags = "--vo=gpu"
|
o.Flags = "--vo=gpu"
|
||||||
o.Pip = true
|
o.Pip = true
|
||||||
|
|
||||||
@@ -79,8 +81,42 @@ func Test_overrideFlags_star(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_Parse(t *testing.T) {
|
func Test_Parse(t *testing.T) {
|
||||||
|
fakePlayer.FlagOverrides["*"] = "--bar=%s"
|
||||||
|
defaultConfig.Players["fakeplayer"] = fakePlayer
|
||||||
|
|
||||||
o := NewOptions()
|
o := NewOptions()
|
||||||
_ = o.Parse(testUrl("enqueue=1", "pip=1"))
|
err := o.Parse(testUrl("player=fakeplayer", "enqueue=1", "pip=1"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fakePlayer.SupportedSchemes = []string{}
|
||||||
|
defaultConfig.Players["fakeplayer"] = fakePlayer
|
||||||
|
err = o.Parse(testUrl("player=fakeplayer", "enqueue=1", "pip=1"))
|
||||||
|
if err == nil {
|
||||||
|
t.Logf("%#v", defaultConfig.Players["fakeplayer"])
|
||||||
|
t.Fatal("Err should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
args := o.GenerateCommand()
|
args := o.GenerateCommand()
|
||||||
t.Logf("%s %v", o.Player, args)
|
t.Logf("%s %v", o.Player, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_sliceContains(t *testing.T) {
|
||||||
|
schemas := []string{
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"ftp",
|
||||||
|
"ftps",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceContains("https", schemas) {
|
||||||
|
t.Logf("should return true if element (https) is in slice (%v)", schemas)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
if stringSliceContains("av", schemas) {
|
||||||
|
t.Logf("should return false if element (av) is not in slice (%v)", schemas)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user