diff --git a/.clang-format b/.clang-format
deleted file mode 100644
index 6d77dfa..0000000
--- a/.clang-format
+++ /dev/null
@@ -1,144 +0,0 @@
----
-Language: Cpp
-# BasedOnStyle: LLVM
-AccessModifierOffset: -2
-AlignAfterOpenBracket: AlwaysBreak
-AlignConsecutiveMacros: true
-AlignConsecutiveAssignments: false
-AlignConsecutiveBitFields: false
-AlignConsecutiveDeclarations: false
-AlignEscapedNewlines: Left
-AlignOperands: Align
-AlignTrailingComments: true
-AllowAllArgumentsOnNextLine: true
-AllowAllConstructorInitializersOnNextLine: true
-AllowAllParametersOfDeclarationOnNextLine: true
-AllowShortEnumsOnASingleLine: true
-AllowShortBlocksOnASingleLine: Never
-AllowShortCaseLabelsOnASingleLine: false
-AllowShortFunctionsOnASingleLine: All
-AllowShortLambdasOnASingleLine: All
-AllowShortIfStatementsOnASingleLine: Never
-AllowShortLoopsOnASingleLine: false
-AlwaysBreakAfterDefinitionReturnType: None
-AlwaysBreakAfterReturnType: None
-AlwaysBreakBeforeMultilineStrings: false
-AlwaysBreakTemplateDeclarations: MultiLine
-BinPackArguments: true
-BinPackParameters: true
-BraceWrapping:
- AfterCaseLabel: false
- AfterClass: false
- AfterControlStatement: false
- AfterEnum: false
- AfterFunction: false
- AfterNamespace: false
- AfterObjCDeclaration: false
- AfterStruct: false
- AfterUnion: false
- AfterExternBlock: false
- BeforeCatch: false
- BeforeElse: false
- BeforeLambdaBody: false
- BeforeWhile: false
- IndentBraces: false
- SplitEmptyFunction: true
- SplitEmptyRecord: true
- SplitEmptyNamespace: true
-BreakBeforeBinaryOperators: None
-BreakBeforeBraces: Attach
-BreakBeforeInheritanceComma: false
-BreakInheritanceList: AfterColon
-BreakBeforeTernaryOperators: false
-BreakConstructorInitializersBeforeComma: false
-BreakConstructorInitializers: AfterColon
-BreakAfterJavaFieldAnnotations: false
-BreakStringLiterals: true
-ColumnLimit: 80
-CommentPragmas: '^ IWYU pragma:'
-CompactNamespaces: false
-ConstructorInitializerAllOnOneLineOrOnePerLine: false
-ConstructorInitializerIndentWidth: 4
-ContinuationIndentWidth: 4
-Cpp11BracedListStyle: false
-DeriveLineEnding: true
-DerivePointerAlignment: false
-DisableFormat: false
-ExperimentalAutoDetectBinPacking: false
-FixNamespaceComments: true
-ForEachMacros:
- - foreach
- - Q_FOREACH
- - BOOST_FOREACH
-IncludeBlocks: Regroup
-IncludeCategories:
- - Regex: '^"(llvm|llvm-c|clang|clang-c)/'
- Priority: 2
- SortPriority: 0
- - Regex: '^(<|"(gtest|gmock|isl|json)/)'
- Priority: 3
- SortPriority: 0
- - Regex: '.*'
- Priority: 1
- SortPriority: 0
-IncludeIsMainRegex: '(Test)?$'
-IncludeIsMainSourceRegex: ''
-IndentCaseLabels: false
-IndentCaseBlocks: false
-IndentGotoLabels: true
-IndentPPDirectives: None
-IndentExternBlock: AfterExternBlock
-IndentWidth: 4
-IndentWrappedFunctionNames: false
-InsertTrailingCommas: None
-JavaScriptQuotes: Leave
-JavaScriptWrapImports: true
-KeepEmptyLinesAtTheStartOfBlocks: true
-MacroBlockBegin: ''
-MacroBlockEnd: ''
-MaxEmptyLinesToKeep: 1
-NamespaceIndentation: None
-PenaltyBreakAssignment: 2
-PenaltyBreakBeforeFirstCallParameter: 19
-PenaltyBreakComment: 300
-PenaltyBreakFirstLessLess: 120
-PenaltyBreakString: 1000
-PenaltyBreakTemplateDeclaration: 10
-PenaltyExcessCharacter: 1000000
-PenaltyReturnTypeOnItsOwnLine: 60
-PointerAlignment: Right
-ReflowComments: true
-SortIncludes: true
-SortUsingDeclarations: true
-SpaceAfterCStyleCast: false
-SpaceAfterLogicalNot: false
-SpaceAfterTemplateKeyword: true
-SpaceBeforeAssignmentOperators: true
-SpaceBeforeCpp11BracedList: false
-SpaceBeforeCtorInitializerColon: true
-SpaceBeforeInheritanceColon: true
-SpaceBeforeParens: ControlStatements
-SpaceBeforeRangeBasedForLoopColon: true
-SpaceInEmptyBlock: false
-SpaceInEmptyParentheses: false
-SpacesBeforeTrailingComments: 1
-SpacesInAngles: false
-SpacesInConditionalStatement: false
-SpacesInContainerLiterals: true
-SpacesInCStyleCastParentheses: false
-SpacesInParentheses: false
-SpacesInSquareBrackets: false
-SpaceBeforeSquareBrackets: false
-Standard: Latest
-StatementMacros:
- - Q_UNUSED
- - QT_REQUIRE_VERSION
-TabWidth: 8
-UseCRLF: false
-UseTab: Never
-WhitespaceSensitiveMacros:
- - STRINGIZE
- - PP_STRINGIZE
- - BOOST_PP_STRINGIZE
-...
-
diff --git a/.gitignore b/.gitignore
index 678dcbf..d163863 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
-*.zip
-*.crx
+build/
\ No newline at end of file
diff --git a/Makefile b/Makefile
index f6c677a..030a621 100644
--- a/Makefile
+++ b/Makefile
@@ -1,35 +1,31 @@
-INCLUDES = -Isrc/
-CXXFLAGS_debug = -Wall -DDEBUG -g -rdynamic -std=c++2a $(INCLUDES)
-CXXFLAGS_release = -Wall -fvisibility=hidden -fvisibility-inlines-hidden -std=c++2a -march=x86-64 -mtune=generic -O3 -pipe -fno-plt $(INCLUDES)
-SRCS = src/ipc.hpp \
- src/options.hpp \
- src/players.hpp \
- src/url.hpp \
- src/main.cpp
+SRC:=config.go ipc.go options.go
+EXT_SRC:=$(wildcard extension/Chrome/*) extension/Firefox/manifest.json
-all: release firefox
+all: build/open-in-mpv
-release: $(SRCS)
- $(CXX) $(CXXFLAGS_release) -o open-in-mpv src/main.cpp
+build/open-in-mpv: $(SRC)
+ @mkdir -p build
+ go build -ldflags="-s -w" -o build/open-in-mpv ./cmd/open-in-mpv
-debug: $(SRCS)
- $(CXX) $(CXXFLAGS_debug) -o open-in-mpv src/main.cpp
+build/Firefox.zip: $(EXT_SRC)
+ @mkdir -p build
+ cp -t extension/Firefox extension/Chrome/{*.html,*.js,*.png,*.css}
+ zip -r build/Firefox.zip extension/Firefox/
+ @rm extension/Firefox/{*.html,*.js,*.png,*.css}
-install: release
- cp open-in-mpv /usr/bin
+install: build/open-in-mpv
+ cp build/open-in-mpv /usr/bin
+
+install-protocol:
+ scripts/install-protocol.sh
uninstall:
rm /usr/bin/open-in-mpv
-firefox:
- cp -t Firefox Chrome/{*.html,*.js,*.png,*.css}
- pushd Firefox && zip ../Firefox.zip * && popd
- @rm Firefox/{*.html,*.js,*.png,*.css}
-
clean:
- @rm -f open-in-mpv Firefox.zip Chrome.crx
+ rm -rf build/*
-fmt:
- clang-format -i src/*.{hpp,cpp}
+test:
+ go test ./...
-.PHONY: all release debug install uninstall firefox clean fmt
+.PHONY: all install install-protocol uninstall clean test
\ No newline at end of file
diff --git a/README.md b/README.md
index 957e0e3..9d9d5a1 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,20 @@
open-in-mpv
-
-
+
+
---
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 C++20 (this is a rewrite from Rust).
+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++).
- [Installation](#installation)
+- [Configuration](#configuration)
+ - [Flag overrides](#flag-overrides)
+ - [Example](#example)
- [The `mpv://` protocol](#the-mpv-protocol)
- [Playlist and `enqueue` functionality](#playlist-and-enqueue-functionality)
- [Player support](#player-support)
@@ -19,10 +22,84 @@ The extension itself shares a lot of code with the one from the awesome [iina](h
### Installation
> Compiled binaries and packed extensions can be found in the [releases page](https://github.com/Baldomo/open-in-mpv/releases).
-This project does not require any external library to run or compile release builds. To build and install `open-in-mpv`, just run
+To build and install `open-in-mpv` and the `mpv://` protocol handler, just run
-```sh
-sudo make install
+```
+$ sudo make install
+$ make install-protocol
+```
+
+### Configuration
+The configuration file has to be named `config.yml` and can be placed in the same folder as the executable or in:
+- Windows: `C:\Users\\AppData\Roaming\open-in-mpv\`
+- Linux: `~/.config/open-in-mpv/`
+- Mac: `~/Library/Application Support/open-in-mpv/`
+
+The configuration file has the following structure:
+
+```yaml
+fake: [ open_in_mpv.Player ]
+ name: [ string ]
+ executable: [ string ]
+ fullscreen: [ string ]
+ pip: [ string ]
+ enqueue: [ string ]
+ new_window: [ string ]
+ needs_ipc: [ true | false ]
+ flag_overrides: [ map[string]string ]
+```
+
+> See [the default configuration](config.yml) as an example
+
+And the `open_in_mpv.Player` object is defined as follows:
+
+| Key | Example value | Description |
+|---------------|----------------------------------------|--------------|
+| `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`) |
+| `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) |
+| `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) |
+| `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) |
+
+#### Flag overrides
+
+The configuration for a player can contain overrides for command line flags defined in the web extension's configuration page, so not to make a different player crash/not start because the flags are not accepted. With the correct overrides any kind of flag passed in [the URL](#the-mpv-protocol) by the extension will either be ignored or replaced with a pattern/prefix/suffix so that it becomes valid for the player in use.
+
+- `"*"`: matches anything and will take precedence over any other override
+ - e.g. the pair `"*": ""` will void all flags
+- `"flag"`: matches the flag `--flag`
+ - e.g. the pair `"--foo": "--bar"` will replace `--foo` with `--bar`
+- `"%s"`: is replaced with the original flag without any leading dash
+ - e.g. the pair `"--foo": "--%s-bar"` will replace `--foo` with `--foo-bar`
+
+> Note: command line options with parameters such as `--foo=bar` are considered as a whole, single flag
+
+Celluloid, for example, expects all mpv-related command line flags to be prefixed with `--mpv-`, so its configuration will contain the following overrides:
+
+```yaml
+flag_overrides:
+ "*": "--mpv-%s"
+```
+
+#### Example
+
+This is a full example of a fictitious player definition in the configuration file:
+
+```yaml
+players:
+ fake:
+ name: fake-player
+ executable: fakeplayer
+ fullscreen: "--fs"
+ pip: "--ontop --no-border --autofit=384x216 --geometry=98\\%:98\\%"
+ enqueue: "--enqueue"
+ new_window: ""
+ needs_ipc: true
+ flag_overrides:
+ "*": "--mpv-options=%s"
```
### The `mpv://` protocol
@@ -37,7 +114,7 @@ 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.
| Key | Example value | Description |
-|---------------|----------------------------------------|---------------|
+|---------------|----------------------------------------|-------------|
| `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 |
| `pip` | `1` | Simulates a picture-in-picture mode (only works with mpv for now) |
@@ -47,11 +124,11 @@ The table below is a simple documentation of the URL query keys and values used
| `flags` | `--vo%3Dgpu` | Custom command options and flags to be passed to the video player, URL-encoded |
### Playlist and `enqueue` functionality
-For `enqueue` to work properly with any mpv-based player (provided it supports mpv's IPC), the player has to read commands from a socket. This can be achieved by adding the following line to the video player's configuration (usually `.config/mpv/mpv.conf` for mpv).
+For `enqueue` to work properly with any mpv-based player (provided it supports mpv's IPC), the player has to read commands from a socket. This can be achieved by adding the following line to the video player's configuration (usually `$HOME/.config/mpv/mpv.conf` for mpv).
```conf
input-ipc-server=/tmp/mpvsocket
```
### Player support
-Supported players are defined in `src/players.hpp`, where the struct `player` 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.
\ No newline at end of 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.
\ No newline at end of file
diff --git a/cmd/open-in-mpv/main.go b/cmd/open-in-mpv/main.go
new file mode 100644
index 0000000..b76206a
--- /dev/null
+++ b/cmd/open-in-mpv/main.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+
+ oim "github.com/Baldomo/open-in-mpv"
+)
+
+func must(err error) {
+ if err == nil {
+ return
+ }
+
+ log.Fatal(err.Error())
+}
+
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Println("This program is not supposed to be called from the command line!")
+ os.Exit(1)
+ }
+
+ must(oim.LoadConfig())
+
+ opts := oim.NewOptions()
+ must(opts.Parse(os.Args[1]))
+
+ if opts.NeedsIpc {
+ cmd, err := opts.GenerateIPC()
+ must(err)
+ err = oim.SendBytes(cmd)
+ if err == nil {
+ os.Exit(0)
+ }
+ log.Println("Error writing to socket, opening new instance")
+ }
+
+ args := opts.GenerateCommand()
+ player := exec.Command(opts.Player, args...)
+ log.Println(player.String())
+ must(player.Start())
+ // must(player.Wait())
+}
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..e9ad3ce
--- /dev/null
+++ b/config.go
@@ -0,0 +1,82 @@
+package open_in_mpv
+
+import (
+ "log"
+ "path/filepath"
+ "strings"
+
+ "github.com/shibukawa/configdir"
+ "gopkg.in/yaml.v2"
+)
+
+// The Player struct contains useful informations for an mpv-based player,
+// such as binary name and fullscreen/pip/enqueue/new_window flags overrides for
+// use in GenerateIPC(). A way to override any generic flags is also
+// provided through the map FlagOverrides.
+type Player struct {
+ // Full name of the player
+ Name string
+ // Executable path/name (if already in $PATH) for the player
+ Executable string
+ // Flag override for fullscreen
+ Fullscreen string
+ // Flag override for picture-in-picture mode
+ Pip string
+ // Flag override for enqueuing videos
+ Enqueue string
+ // Flag override for forcing new window
+ NewWindow string `yaml:"new_window"`
+ // Controls whether this player needs IPC command to enqueue videos
+ NeedsIpc bool `yaml:"needs_ipc"`
+ // Overrides for any generic flag
+ FlagOverrides map[string]string `yaml:"flag_overrides"`
+}
+
+// Top-level configuration object, maps a player by name to its Player object
+type Config struct {
+ Players map[string]Player
+}
+
+var defaultConfig = Config{
+ Players: map[string]Player{
+ "mpv": {
+ Name: "mpv",
+ Executable: "mpv",
+ Fullscreen: "--fs",
+ Pip: `--ontop --no-border --autofit=384x216 --geometry=98%:98%`,
+ Enqueue: "",
+ NewWindow: "",
+ NeedsIpc: true,
+ FlagOverrides: map[string]string{},
+ },
+ },
+}
+
+// Tries to load configuration file with fallback
+func LoadConfig() error {
+ confDirs := configdir.New("", "open-in-mpv")
+ confDirs.LocalPath, _ = filepath.Abs(".")
+ confFolder := confDirs.QueryFolderContainsFile("config.yml")
+
+ if confFolder == nil {
+ log.Println("No config file found, using default")
+ return nil
+ }
+
+ data, err := confFolder.ReadFile("config.yml")
+ if err != nil {
+ return err
+ }
+
+ return yaml.Unmarshal(data, &defaultConfig)
+}
+
+// Returns player information for the given name if present, otherwise nil
+func GetPlayerInfo(name string) *Player {
+ lowerName := strings.ToLower(name)
+ if p, ok := defaultConfig.Players[lowerName]; ok {
+ return &p
+ }
+
+ return nil
+}
diff --git a/config.yml b/config.yml
new file mode 100644
index 0000000..3306361
--- /dev/null
+++ b/config.yml
@@ -0,0 +1,30 @@
+---
+players:
+ mpv:
+ name: mpv
+ executable: mpv
+ fullscreen: "--fs"
+ pip: "--ontop --no-border --autofit=384x216 --geometry=98\\%:98\\%"
+ enqueue: ""
+ new_window: ""
+ needs_ipc: true
+ flag_overrides: {}
+ celluloid:
+ name: Celluloid
+ executable: celluloid
+ fullscreen: ""
+ pip: ""
+ enqueue: "--enqueue"
+ new_window: "--new-window"
+ needs_ipc: false
+ flag_overrides:
+ "*": "--mpv-%s"
+ mpvnet:
+ name: mpv.net
+ executable: mpvnet.exe
+ fullscreen: "--fs"
+ pip: "--ontop --no-border --autofit=384x216 --geometry=98\\%:98\\%"
+ enqueue: "--queue"
+ new_window: ""
+ needs_ipc: false
+ flag_overrides: {}
\ No newline at end of file
diff --git a/config_test.go b/config_test.go
new file mode 100644
index 0000000..33ae5fa
--- /dev/null
+++ b/config_test.go
@@ -0,0 +1,11 @@
+package open_in_mpv
+
+import "testing"
+
+func Test_LoadConfig(t *testing.T) {
+ err := LoadConfig()
+ if err != nil {
+ t.Error(err)
+ }
+ t.Logf("%#v\n", defaultConfig)
+}
diff --git a/Chrome/README.md b/extension/Chrome/README.md
similarity index 100%
rename from Chrome/README.md
rename to extension/Chrome/README.md
diff --git a/Chrome/background.html b/extension/Chrome/background.html
similarity index 100%
rename from Chrome/background.html
rename to extension/Chrome/background.html
diff --git a/Chrome/background.js b/extension/Chrome/background.js
similarity index 100%
rename from Chrome/background.js
rename to extension/Chrome/background.js
diff --git a/Chrome/common.js b/extension/Chrome/common.js
similarity index 100%
rename from Chrome/common.js
rename to extension/Chrome/common.js
diff --git a/Chrome/icon.png b/extension/Chrome/icon.png
similarity index 100%
rename from Chrome/icon.png
rename to extension/Chrome/icon.png
diff --git a/Chrome/icon.svg b/extension/Chrome/icon.svg
similarity index 100%
rename from Chrome/icon.svg
rename to extension/Chrome/icon.svg
diff --git a/Chrome/icon128.png b/extension/Chrome/icon128.png
similarity index 100%
rename from Chrome/icon128.png
rename to extension/Chrome/icon128.png
diff --git a/Chrome/icon16.png b/extension/Chrome/icon16.png
similarity index 100%
rename from Chrome/icon16.png
rename to extension/Chrome/icon16.png
diff --git a/Chrome/icon48.png b/extension/Chrome/icon48.png
similarity index 100%
rename from Chrome/icon48.png
rename to extension/Chrome/icon48.png
diff --git a/Chrome/manifest.json b/extension/Chrome/manifest.json
similarity index 100%
rename from Chrome/manifest.json
rename to extension/Chrome/manifest.json
diff --git a/Chrome/options.css b/extension/Chrome/options.css
similarity index 100%
rename from Chrome/options.css
rename to extension/Chrome/options.css
diff --git a/Chrome/options.html b/extension/Chrome/options.html
similarity index 100%
rename from Chrome/options.html
rename to extension/Chrome/options.html
diff --git a/Chrome/options.js b/extension/Chrome/options.js
similarity index 100%
rename from Chrome/options.js
rename to extension/Chrome/options.js
diff --git a/Chrome/popup.html b/extension/Chrome/popup.html
similarity index 100%
rename from Chrome/popup.html
rename to extension/Chrome/popup.html
diff --git a/Chrome/popup.js b/extension/Chrome/popup.js
similarity index 100%
rename from Chrome/popup.js
rename to extension/Chrome/popup.js
diff --git a/Firefox/manifest.json b/extension/Firefox/manifest.json
similarity index 100%
rename from Firefox/manifest.json
rename to extension/Firefox/manifest.json
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..afadc10
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,8 @@
+module github.com/Baldomo/open-in-mpv
+
+go 1.15
+
+require (
+ github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
+ gopkg.in/yaml.v2 v2.4.0
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..6be7e62
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,5 @@
+github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
+github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/images/badge-chromium.png b/images/badge-chromium.png
new file mode 100644
index 0000000..350835d
Binary files /dev/null and b/images/badge-chromium.png differ
diff --git a/images/badge-chromium.svg b/images/badge-chromium.svg
new file mode 100644
index 0000000..a97ec3a
--- /dev/null
+++ b/images/badge-chromium.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/badge-firefox.png b/images/badge-firefox.png
new file mode 100644
index 0000000..7618372
Binary files /dev/null and b/images/badge-firefox.png differ
diff --git a/images/badge-firefox.svg b/images/badge-firefox.svg
new file mode 100644
index 0000000..471a0ec
--- /dev/null
+++ b/images/badge-firefox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ipc.go b/ipc.go
new file mode 100644
index 0000000..fba70cd
--- /dev/null
+++ b/ipc.go
@@ -0,0 +1,47 @@
+package open_in_mpv
+
+import "net"
+
+const defaultSocket = "/tmp/mpvsocket"
+
+var defaultIpc = Ipc{
+ SocketAddress: defaultSocket,
+}
+
+// Defines an IPC connection with a UNIX socket
+type Ipc struct {
+ conn net.Conn
+ SocketAddress string
+}
+
+// Send a byte-encoded command to the specified UNIX socket
+func (i *Ipc) Send(cmd []byte) error {
+ var err error
+ i.conn, err = net.Dial("unix", i.SocketAddress)
+ if err != nil {
+ return err
+ }
+ defer i.conn.Close()
+
+ // The command has to be newline terminated
+ if cmd[len(cmd)-1] != '\n' {
+ cmd = append(cmd, '\n')
+ }
+
+ _, err = i.conn.Write(cmd)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Generic public send string command for the default connection
+func SendString(cmd string) error {
+ return defaultIpc.Send([]byte(cmd))
+}
+
+// Generic public send byte-encoded string command for the default connection
+func SendBytes(cmd []byte) error {
+ return defaultIpc.Send(cmd)
+}
diff --git a/options.go b/options.go
new file mode 100644
index 0000000..dfdd70f
--- /dev/null
+++ b/options.go
@@ -0,0 +1,167 @@
+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 {
+ Flags string
+ Player string
+ Url string
+ Enqueue bool
+ Fullscreen bool
+ NewWindow bool
+ Pip bool
+ NeedsIpc bool
+}
+
+// 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{
+ Flags: "",
+ Player: "mpv",
+ Url: "",
+ Enqueue: false,
+ Fullscreen: false,
+ NewWindow: false,
+ Pip: false,
+ NeedsIpc: false,
+ }
+}
+
+// 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)
+ }
+
+ o.Flags, err = url.QueryUnescape(u.Query().Get("flags"))
+ if err != nil {
+ return err
+ }
+ o.Url, err = url.QueryUnescape(u.Query().Get("url"))
+ if err != nil {
+ return err
+ }
+
+ if p, ok := u.Query()["player"]; ok {
+ 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.Fullscreen = u.Query().Get("fullscreen") == "1"
+ o.NewWindow = u.Query().Get("new_window") == "1"
+ o.Pip = u.Query().Get("pip") == "1"
+
+ o.NeedsIpc = GetPlayerInfo(o.Player).NeedsIpc
+
+ return nil
+}
+
+// Parses flag overrides and returns the final flags
+func (o Options) overrideFlags() string {
+ var (
+ ret []string
+ star bool
+ )
+
+ pInfo := GetPlayerInfo(o.Player)
+ if pInfo == nil {
+ return ""
+ }
+
+ _, star = pInfo.FlagOverrides["*"]
+
+ for _, flag := range strings.Split(o.Flags, " ") {
+ if star {
+ stripped := strings.TrimLeft(flag, "-")
+ replaced := strings.ReplaceAll(pInfo.FlagOverrides["*"], `%s`, stripped)
+ ret = append(ret, replaced)
+ } else {
+ if override, ok := pInfo.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
+
+ pInfo := GetPlayerInfo(o.Player)
+
+ if o.Fullscreen {
+ ret = append(ret, pInfo.Fullscreen)
+ }
+
+ if o.Pip {
+ ret = append(ret, pInfo.Pip)
+ }
+
+ if o.Flags != "" {
+ if len(pInfo.FlagOverrides) == 0 {
+ ret = append(ret, o.Flags)
+ } else {
+ ret = append(ret, o.overrideFlags())
+ }
+ }
+
+ ret = append(ret, o.Url)
+
+ 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, "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
+}
diff --git a/options_test.go b/options_test.go
new file mode 100644
index 0000000..bce82df
--- /dev/null
+++ b/options_test.go
@@ -0,0 +1,86 @@
+package open_in_mpv
+
+import (
+ "strings"
+ "testing"
+)
+
+var fakePlayer = Player{
+ Name: "FakePlayer",
+ Executable: "fakeplayer",
+ Fullscreen: "",
+ Pip: "",
+ Enqueue: "",
+ NewWindow: "",
+ NeedsIpc: true,
+ FlagOverrides: map[string]string{},
+}
+
+func testUrl(query ...string) string {
+ elems := []string{
+ `mpv:///open?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdQw4w9WgXcQ`,
+ }
+ return strings.Join(append(elems, query...), "&")
+}
+
+func Test_GenerateCommand(t *testing.T) {
+ o := NewOptions()
+ o.Url = "example.com"
+ o.Flags = "--vo=gpu"
+ o.Pip = true
+
+ args := o.GenerateCommand()
+ t.Logf("%s %v", o.Player, args)
+}
+
+func Test_GenerateIPC(t *testing.T) {
+ o := NewOptions()
+ _ = o.Parse(testUrl("enqueue=1", "pip=1"))
+ ipc, _ := o.GenerateIPC()
+ t.Log(string(ipc))
+}
+
+func Test_overrideFlags_single(t *testing.T) {
+ fakePlayer.FlagOverrides["--foo"] = "--bar=%s"
+ defaultConfig.Players["fakeplayer"] = fakePlayer
+
+ o := NewOptions()
+ err := o.Parse(testUrl("player=fakeplayer", "flags=--foo"))
+ if err != nil {
+ t.Error(err)
+ }
+
+ result := o.overrideFlags()
+
+ if result != "--bar=foo" {
+ t.Fail()
+ t.Log(result)
+ t.Logf("%#v\n", o)
+ }
+}
+
+func Test_overrideFlags_star(t *testing.T) {
+ fakePlayer.FlagOverrides["*"] = "--bar=%s"
+ defaultConfig.Players["fakeplayer"] = fakePlayer
+
+ o := NewOptions()
+ err := o.Parse(testUrl("player=fakeplayer", "flags=--foo%20--baz"))
+ if err != nil {
+ t.Error(err)
+ }
+
+ result := o.overrideFlags()
+
+ if result != "--bar=foo --bar=baz" {
+ t.Fail()
+ t.Log(result)
+ t.Logf("%#v\n", o)
+ }
+}
+
+func Test_Parse(t *testing.T) {
+ o := NewOptions()
+ _ = o.Parse(testUrl("enqueue=1", "pip=1"))
+ args := o.GenerateCommand()
+ t.Logf("%s %v", o.Player, args)
+}
diff --git a/scripts/install-protocol.reg b/scripts/install-protocol.reg
new file mode 100644
index 0000000..e2667ff
--- /dev/null
+++ b/scripts/install-protocol.reg
@@ -0,0 +1,12 @@
+Windows Registry Editor Version 5.00
+
+[HKEY_CLASSES_ROOT\mpv]
+"URL Protocol"=""
+@="URL:mpv"
+
+[HKEY_CLASSES_ROOT\mpv\shell]
+
+[HKEY_CLASSES_ROOT\mpv\shell\open]
+
+[HKEY_CLASSES_ROOT\mpv\shell\open\command]
+@="\"C:\\Program Files\\open-in-mpv\\open-in-mpv.exe\" \"%1\""
diff --git a/install-protocol.sh b/scripts/install-protocol.sh
similarity index 100%
rename from install-protocol.sh
rename to scripts/install-protocol.sh
diff --git a/src/ipc.hpp b/src/ipc.hpp
deleted file mode 100644
index 2fc233f..0000000
--- a/src/ipc.hpp
+++ /dev/null
@@ -1,58 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-
-using std::string;
-
-const char *DEFAULT_SOCK = "/tmp/mpvsocket";
-
-namespace oim {
-
-/*
- * The class oim::ipc provides easy communication and basic socket management
- * for any running mpv instance configured to receive commands over a JSON-IPC
- * server/socket.
- */
-class ipc {
- private:
- sockaddr_un sockaddress_;
- int sockfd_;
- int socklen_;
-
- public:
- ipc() : ipc(DEFAULT_SOCK){};
-
- /*
- * Constructor for oim::ipc
- */
- ipc(const char *sockpath);
-
- /*
- * Destructor for oim::ipc
- */
- ~ipc() { close(sockfd_); };
-
- /*
- * Sends a raw command string to the internal socket at DEFAULT_SOCK
- */
- bool send(string cmd);
-};
-
-ipc::ipc(const char *sockpath) {
- sockfd_ = socket(AF_UNIX, SOCK_STREAM, 0);
- sockaddress_.sun_family = AF_UNIX;
- std::strcpy(sockaddress_.sun_path, sockpath);
- socklen_ = sizeof(sockaddress_);
-
- connect(sockfd_, (const sockaddr *)&sockaddress_, socklen_);
-}
-
-bool ipc::send(string cmd) {
- return write(sockfd_, cmd.c_str(), cmd.length()) != -1;
-}
-
-} // namespace oim
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
deleted file mode 100644
index 955796c..0000000
--- a/src/main.cpp
+++ /dev/null
@@ -1,70 +0,0 @@
-#include "ipc.hpp"
-#include "options.hpp"
-
-#include
-#include
-#include
-#include
-
-using std::string;
-
-const char *help[2] = {
- "This program is not supposed to be called from the command line!",
- "Call with 'install-protocol' to instal the xdg-compatible protocol file "
- "in ~/.local/share/applications/"
-};
-
-bool install_protocol() {
- const char *protocol_file = R"([Desktop Entry]
-Name=open-in-mpv
-Exec=open-in-mpv %u
-Type=Application
-Terminal=false
-NoDisplay=true
-MimeType=x-scheme-handler/mpv
-)";
-
- const char *homedir = std::getenv("HOME");
- if (!homedir)
- return false;
-
- std::ofstream protfile(
- string(homedir) + "/.local/share/applications/open-in-mpv.desktop");
- protfile << protocol_file;
- protfile.flush();
- protfile.close();
-
- return true;
-}
-
-int main(int argc, char const *argv[]) {
- if (argc == 1) {
- std::cout << help[0] << std::endl << help[1] << std::endl;
- return 0;
- };
-
- if (string(argv[1]) == "install-protocol")
- return install_protocol() ? 0 : 1;
-
- oim::options *mo = new oim::options();
- try {
- mo->parse(argv[1]);
- } catch (string err) {
- std::cout << err << std::endl;
- return 1;
- }
-
- if (mo->needs_ipc()) {
- oim::ipc *mipc = new oim::ipc();
- bool success = mipc->send(mo->build_ipc());
- if (success) {
- return 0;
- }
-
- std::cerr << "Error writing to socket, opening new instance"
- << std::endl;
- }
- std::system(mo->build_cmd().c_str());
-
- return 0;
-}
diff --git a/src/options.hpp b/src/options.hpp
deleted file mode 100644
index d71093a..0000000
--- a/src/options.hpp
+++ /dev/null
@@ -1,197 +0,0 @@
-#pragma once
-
-#include "players.hpp"
-#include "url.hpp"
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-using std::string;
-
-namespace {
-// Replace all occurrences of `from` with `to` in `str`
-string replace_all(string str, const string &from, const string &to) {
- size_t start_pos = 0;
- while ((start_pos = str.find(from, start_pos)) != string::npos) {
- str.replace(start_pos, from.length(), to);
- // Handles case where 'to' is a substring of 'from'
- start_pos += to.length();
- }
- return str;
-}
-
-// Remove all characters equals to `c` at the beginning of the string
-string lstrip(string str, char c) {
- while (str.at(0) == c)
- str.replace(0, 1, "");
- return str;
-}
-} // namespace
-
-namespace oim {
-
-/*
- * The class oim::options 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.
- */
-class options {
- private:
- player *player_info_;
- string url_;
- string flags_;
- string player_;
- bool fullscreen_;
- bool pip_;
- bool enqueue_;
- bool new_window_;
-
- /*
- * Parses flag overrides and returns the final flags
- */
- string override_flags();
-
- public:
- /*
- * Constructor for oim::options
- */
- options();
-
- /*
- * Builds a CLI command used to invoke mpv with the appropriate arguments
- */
- string build_cmd();
-
- /*
- * Builds the IPC command needed to enqueue videos in mpv
- */
- string build_ipc();
-
- /*
- * Parse a URL and populate the current oim::options
- */
- void parse(const char *url);
-
- /*
- * Checks wether or not oim::options needs to communicate with mpv via IPC
- * instead of the command line interface
- */
- bool needs_ipc();
-};
-
-options::options() {
- url_ = "";
- flags_ = "";
- player_ = "mpv";
- fullscreen_ = false;
- pip_ = false;
- enqueue_ = false;
- new_window_ = false;
-}
-
-string options::build_cmd() {
- std::ostringstream ret;
-
- if (player_info_ == nullptr) {
- return "";
- }
-
- ret << player_info_->executable << " ";
- if (fullscreen_ && !player_info_->fullscreen.empty())
- ret << player_info_->fullscreen << " ";
- if (pip_ && !player_info_->pip.empty())
- ret << player_info_->pip << " ";
- if (!flags_.empty()) {
- if (!player_info_->flag_overrides.empty())
- ret << override_flags() << " ";
- else
- ret << flags_ << " ";
- }
- ret << url_;
-
- return ret.str();
-}
-
-string options::build_ipc() {
- std::ostringstream ret;
-
- if (!needs_ipc())
- return "";
-
- // In the future this may need a more serious json serializer for
- // more complicated commands
- // Syntax: {"command": ["loadfile", "%s", "append-play"]}\n
- ret << R"({"command": ["loadfile", ")" << url_ << R"(", "append-play"]})"
- << std::endl;
-
- return ret.str();
-}
-
-string options::override_flags() {
- // Return immediatly in case there are no overrides
- if (player_info_->flag_overrides.empty())
- return flags_;
-
- bool star = false;
- std::ostringstream ret;
-
- // Check whether there's a global override
- auto star_pair = player_info_->flag_overrides.find("*");
- if (star_pair != player_info_->flag_overrides.end())
- star = true;
-
- // Turn flags_ into a stream to tokenize somewhat idiomatically
- auto flagstream = std::istringstream{ flags_ };
- string tmp;
-
- while (flagstream >> tmp) {
- if (star) {
- // Remove all dashes at the beginning of the flag
- string stripped = ::lstrip(tmp, '-');
- ret << ::replace_all((*star_pair).second, "%s", stripped);
- } else {
- // Search for the flag currently being processed
- auto fo = player_info_->flag_overrides.find(tmp);
- if (fo == player_info_->flag_overrides.end())
- continue;
-
- ret << ::replace_all((*fo).second, "%s", tmp);
- }
- }
-
- return ret.str();
-}
-
-void options::parse(const char *url_s) {
- oim::url u(url_s);
-
- if (u.protocol() != "mpv")
- throw string("Unsupported protocol supplied: ") + u.protocol();
-
- if (u.path() != "/open")
- throw string("Unsupported method supplied: ") + u.path();
-
- if (u.query().empty())
- throw string("Empty query");
-
- url_ = percent_decode(u.query_value("url"));
- flags_ = percent_decode(u.query_value("flags"));
- player_ = u.query_value("player", "mpv");
-
- player_info_ = get_player_info(player_);
- if (player_info_ == nullptr)
- throw string("Unsupported player: ") + player_;
-
- fullscreen_ = u.query_value("full_screen") == "1";
- pip_ = u.query_value("pip") == "1";
- enqueue_ = u.query_value("enqueue") == "1";
- new_window_ = u.query_value("new_window") == "1";
-}
-
-bool options::needs_ipc() { return player_info_->needs_ipc; }
-
-} // namespace oim
\ No newline at end of file
diff --git a/src/players.hpp b/src/players.hpp
deleted file mode 100644
index 24409f7..0000000
--- a/src/players.hpp
+++ /dev/null
@@ -1,85 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-
-using std::string;
-using std::unordered_map;
-
-namespace oim {
-
-/*
- * Struct `oim::player` contains useful informations for an mpv-based player,
- * such as binary name and fullscreen/pip/enqueue/new_window flags overrides for
- * use in `options::build_ipc()`. A way to override any generic flags is also
- * provided through the map `flag_overrides`.
- */
-struct player {
- string name;
- string executable;
-
- string fullscreen;
- string pip;
- string enqueue;
- string new_window;
-
- bool needs_ipc;
-
- /*
- * Overrides for any extra command line flag
- *
- * Override syntax:
- * `"*"`: matches anything and will take precedence over any other
- * override
- * e.g. the pair `{"*", ""}` will void all flags
- * `"flag"`: matches the flag `--flag`
- * e.g. the pair `{"--foo", "--bar"}` will replace `--foo` with
- * `--bar`
- * `"%s"`: is replaced with the original flag without the leading `--`
- * e.g. the pair `{"--foo", "--%s-bar"}` will replace `--foo` with
- * `--foo-bar`
- *
- * Note: command line options with parameters such as `--foo=bar` are
- * considered a flags as a whole
- */
- unordered_map flag_overrides;
-};
-
-unordered_map player_info = {
- { "mpv",
- { .name = "mpv",
- .executable = "mpv",
- .fullscreen = "--fs",
- .pip = "--ontop --no-border --autofit=384x216 --geometry=98\%:98\%",
- .enqueue = "",
- .new_window = "",
- .needs_ipc = true,
- .flag_overrides = {} } },
- { "celluloid",
- { .name = "Celluloid",
- .executable = "celluloid",
- .fullscreen = "",
- .pip = "",
- .enqueue = "--enqueue",
- .new_window = "--new-window",
- .needs_ipc = false,
- .flag_overrides = { { "*", "--mpv-options=%s" } } } },
-};
-
-player *get_player_info(string name) {
- if (name.empty())
- return &player_info["mpv"];
-
- string lower_name(name);
- std::transform(name.begin(), name.end(), lower_name.begin(), ::tolower);
-
- auto info = player_info.find(lower_name);
- if (info == player_info.end()) {
- return nullptr;
- }
-
- return &(*info).second;
-}
-
-} // namespace oim
diff --git a/src/url.hpp b/src/url.hpp
deleted file mode 100644
index bc89ca2..0000000
--- a/src/url.hpp
+++ /dev/null
@@ -1,160 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-
-using std::string;
-
-namespace {
-/*
- * Converts a single character to a percent-decodable byte representation
- * Taken from
- * https://github.com/cpp-netlib/url/blob/main/include/skyr/v1/percent_encoding/percent_decode_range.hpp
- */
-inline std::byte alnum_to_hex(char value) {
- if ((value >= '0') && (value <= '9')) {
- return static_cast(value - '0');
- }
-
- if ((value >= 'a') && (value <= 'f')) {
- return static_cast(value + '\x0a' - 'a');
- }
-
- if ((value >= 'A') && (value <= 'F')) {
- return static_cast(value + '\x0a' - 'A');
- }
-
- return static_cast(' ');
-}
-
-} // namespace
-
-namespace oim {
-
-/*
- * The class oim::url contains utility methods to parse a URL string and
- * access its fields by name (e.g. parses protocol, host, query etc.). Simple
- * query value searching by key is also provided by url::query_value(string).
- */
-class url {
- private:
- string protocol_, host_, path_, query_;
-
- public:
- /* Constructor with C-style string URL */
- url(const char *url_s) : url(string(url_s)){};
-
- /* Constructor with C++ std::string URL */
- url(const string &url_s);
-
- /* Move constructor for oim::url */
- url(url &&other);
-
- /* Accessor for the URL's protocol string */
- string protocol() { return protocol_; }
-
- /* Accessor for the URL's host string */
- string host() { return host_; }
-
- /* Accessor for the URL's whole path */
- string path() { return path_; }
-
- /* Accessor for the URL's whole query string */
- string query() { return query_; }
-
- /*
- * Gets a value from a query string given a key
- */
- string query_value(string key);
-
- /*
- * Gets a value from a query string given a key (overload with optional
- * fallback if value isn't found)
- */
- string query_value(string key, string fallback);
-};
-
-url::url(const string &url_s) {
- const string prot_end("://");
- string::const_iterator prot_i = std::search(
- url_s.begin(), url_s.end(), prot_end.begin(), prot_end.end());
- protocol_.reserve(std::distance(url_s.begin(), prot_i));
- // The protocol is case insensitive
- std::transform(
- url_s.begin(), prot_i, std::back_inserter(protocol_), ::tolower);
- if (prot_i == url_s.end())
- return;
- std::advance(prot_i, prot_end.length());
-
- // The path starts with '/'
- string::const_iterator path_i = std::find(prot_i, url_s.end(), '/');
- host_.reserve(std::distance(prot_i, path_i));
- // The host is also case insensitive
- std::transform(prot_i, path_i, std::back_inserter(host_), ::tolower);
-
- // Everything else is query
- string::const_iterator query_i = std::find(path_i, url_s.end(), '?');
- path_.assign(path_i, query_i);
- if (query_i != url_s.end())
- ++query_i;
- query_.assign(query_i, url_s.end());
-}
-
-url::url(url &&other) {
- protocol_ = std::move(other.protocol_);
- host_ = std::move(other.host_);
- path_ = std::move(other.path_);
- query_ = std::move(other.query_);
-}
-
-string url::query_value(string key) {
- // Find the beginning of the last occurrence of `key` in `query`
- auto pos = query_.rfind(key + "=");
- if (pos == string::npos)
- return "";
-
- // Offset calculation (beginning of the value string associated with
- // `key`):
- // pos: positione of the first character of `key`
- // key.length(): self explanatory
- // 1: length of character '='
- int offset = pos + key.length() + 1;
- // Return a string starting from the offset and with appropriate length
- // (difference between the position of the first '&' char after the
- // value and `offset`)
- return query_.substr(offset, query_.find('&', pos) - offset);
-}
-
-string url::query_value(string key, string fallback) {
- string ret = query_value(key);
- if (ret.empty())
- return fallback;
- return ret;
-}
-
-/*
- * Percent-decodes a URL
- */
-string percent_decode(const string encoded) {
- string ret = "";
- for (auto i = encoded.begin(); i < encoded.end(); i++) {
- if (*i == '%') {
- std::byte b1 = ::alnum_to_hex(*++i);
- std::byte b2 = ::alnum_to_hex(*++i);
-
- char parsed = static_cast(
- (0x10u * std::to_integer(b1)) +
- std::to_integer(b2));
- ret += parsed;
- } else {
- ret += *i;
- }
- }
-
- return ret;
-}
-
-} // namespace oim
\ No newline at end of file