Full Go rewrite with WIP suppport for ther systems (Windows custom protocol handler), fixed #4, updated README
144
.clang-format
@@ -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
|
||||
...
|
||||
|
3
.gitignore
vendored
@@ -1,2 +1 @@
|
||||
*.zip
|
||||
*.crx
|
||||
build/
|
44
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
|
46
cmd/open-in-mpv/main.go
Normal file
@@ -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())
|
||||
}
|
82
config.go
Normal file
@@ -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
|
||||
}
|
30
config.yml
Normal file
@@ -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: {}
|
11
config_test.go
Normal file
@@ -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)
|
||||
}
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 917 B |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
8
go.mod
Normal file
@@ -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
|
||||
)
|
5
go.sum
Normal file
@@ -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=
|
BIN
images/badge-chromium.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
1
images/badge-chromium.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="168.96999999999997" height="35" viewBox="0 0 168.96999999999997 35"><rect class="svg__rect" x="0" y="0" width="111.78999999999999" height="35" fill="#396BD7"/><rect class="svg__rect" x="109.78999999999999" y="0" width="59.17999999999999" height="35" fill="#A2C2FA"/><path class="svg__text" d="M13.95 18.19L13.95 18.19L13.95 17.39Q13.95 16.19 14.38 15.27Q14.80 14.35 15.60 13.85Q16.40 13.35 17.45 13.35L17.45 13.35Q18.86 13.35 19.73 14.12Q20.59 14.89 20.73 16.29L20.73 16.29L19.25 16.29Q19.14 15.37 18.71 14.96Q18.28 14.55 17.45 14.55L17.45 14.55Q16.48 14.55 15.97 15.26Q15.45 15.96 15.44 17.33L15.44 17.33L15.44 18.09Q15.44 19.47 15.93 20.20Q16.43 20.92 17.38 20.92L17.38 20.92Q18.25 20.92 18.69 20.53Q19.13 20.14 19.25 19.22L19.25 19.22L20.73 19.22Q20.60 20.59 19.72 21.35Q18.84 22.12 17.38 22.12L17.38 22.12Q16.36 22.12 15.59 21.63Q14.81 21.15 14.39 20.26Q13.97 19.37 13.95 18.19ZM26.52 22L25.04 22L25.04 13.47L26.52 13.47L26.52 17.02L30.34 17.02L30.34 13.47L31.81 13.47L31.81 22L30.34 22L30.34 18.21L26.52 18.21L26.52 22ZM38.04 22L36.55 22L36.55 13.47L39.55 13.47Q41.03 13.47 41.83 14.13Q42.64 14.79 42.64 16.05L42.64 16.05Q42.64 16.90 42.22 17.48Q41.81 18.06 41.07 18.37L41.07 18.37L42.99 21.92L42.99 22L41.40 22L39.69 18.71L38.04 18.71L38.04 22ZM38.04 14.66L38.04 17.52L39.56 17.52Q40.31 17.52 40.73 17.15Q41.15 16.77 41.15 16.11L41.15 16.11Q41.15 15.43 40.76 15.05Q40.37 14.68 39.60 14.66L39.60 14.66L38.04 14.66ZM46.76 18.00L46.76 18.00L46.76 17.52Q46.76 16.28 47.20 15.32Q47.64 14.37 48.45 13.86Q49.26 13.35 50.30 13.35Q51.34 13.35 52.15 13.85Q52.95 14.35 53.39 15.29Q53.83 16.23 53.84 17.48L53.84 17.48L53.84 17.96Q53.84 19.21 53.40 20.16Q52.97 21.10 52.17 21.61Q51.36 22.12 50.31 22.12L50.31 22.12Q49.27 22.12 48.46 21.61Q47.65 21.10 47.21 20.17Q46.77 19.23 46.76 18.00ZM48.24 17.46L48.24 17.96Q48.24 19.36 48.79 20.13Q49.34 20.90 50.31 20.90L50.31 20.90Q51.30 20.90 51.83 20.15Q52.36 19.40 52.36 17.96L52.36 17.96L52.36 17.51Q52.36 16.09 51.82 15.34Q51.28 14.58 50.30 14.58L50.30 14.58Q49.34 14.58 48.80 15.33Q48.25 16.09 48.24 17.46L48.24 17.46ZM59.78 22L58.30 22L58.30 13.47L60.23 13.47L62.69 20.01L65.14 13.47L67.06 13.47L67.06 22L65.58 22L65.58 19.19L65.73 15.43L63.21 22L62.15 22L59.63 15.43L59.78 19.19L59.78 22ZM73.36 22L71.89 22L71.89 13.47L73.36 13.47L73.36 22ZM78.04 19.16L78.04 19.16L78.04 13.47L79.51 13.47L79.51 19.18Q79.51 20.03 79.95 20.48Q80.38 20.93 81.22 20.93L81.22 20.93Q82.94 20.93 82.94 19.13L82.94 19.13L82.94 13.47L84.41 13.47L84.41 19.17Q84.41 20.53 83.54 21.32Q82.67 22.12 81.22 22.12L81.22 22.12Q79.76 22.12 78.90 21.33Q78.04 20.55 78.04 19.16ZM90.46 22L88.98 22L88.98 13.47L90.90 13.47L93.37 20.01L95.82 13.47L97.74 13.47L97.74 22L96.26 22L96.26 19.19L96.41 15.43L93.89 22L92.83 22L90.31 15.43L90.46 19.19L90.46 22Z" fill="#FFFFFF"/><path class="svg__text" d="M123.55 17.80L123.55 17.80Q123.55 16.54 124.15 15.54Q124.75 14.55 125.80 13.99Q126.85 13.43 128.17 13.43L128.17 13.43Q129.32 13.43 130.24 13.84Q131.17 14.25 131.78 15.02L131.78 15.02L130.27 16.39Q129.46 15.40 128.29 15.40L128.29 15.40Q127.60 15.40 127.07 15.70Q126.54 16 126.24 16.54Q125.95 17.09 125.95 17.80L125.95 17.80Q125.95 18.51 126.24 19.05Q126.54 19.60 127.07 19.90Q127.60 20.20 128.29 20.20L128.29 20.20Q129.46 20.20 130.27 19.22L130.27 19.22L131.78 20.58Q131.17 21.35 130.25 21.76Q129.32 22.17 128.17 22.17L128.17 22.17Q126.85 22.17 125.80 21.61Q124.75 21.05 124.15 20.05Q123.55 19.06 123.55 17.80ZM138.70 22L136.32 22L136.32 13.60L140.16 13.60Q141.30 13.60 142.14 13.98Q142.98 14.35 143.44 15.06Q143.89 15.76 143.89 16.71L143.89 16.71Q143.89 17.62 143.47 18.30Q143.04 18.98 142.25 19.36L142.25 19.36L144.06 22L141.51 22L139.99 19.77L138.70 19.77L138.70 22ZM138.70 15.47L138.70 17.93L140.01 17.93Q140.75 17.93 141.12 17.61Q141.49 17.29 141.49 16.71L141.49 16.71Q141.49 16.12 141.12 15.79Q140.75 15.47 140.01 15.47L140.01 15.47L138.70 15.47ZM150.53 22L147.82 22L150.88 17.75L147.95 13.60L150.63 13.60L152.31 16.02L153.96 13.60L156.53 13.60L153.60 17.66L156.73 22L153.99 22L152.25 19.40L150.53 22Z" fill="#FFFFFF" x="122.78999999999999"/></svg>
|
After Width: | Height: | Size: 4.0 KiB |
BIN
images/badge-firefox.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
1
images/badge-firefox.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="143.44" height="35" viewBox="0 0 143.44 35"><rect class="svg__rect" x="0" y="0" width="91.55" height="35" fill="#FF505F"/><rect class="svg__rect" x="89.55" y="0" width="53.890000000000015" height="35" fill="#FF7139"/><path class="svg__text" d="M15.70 22L14.22 22L14.22 13.47L19.64 13.47L19.64 14.66L15.70 14.66L15.70 17.20L19.13 17.20L19.13 18.38L15.70 18.38L15.70 22ZM25.36 22L23.89 22L23.89 13.47L25.36 13.47L25.36 22ZM31.65 22L30.17 22L30.17 13.47L33.17 13.47Q34.65 13.47 35.45 14.13Q36.25 14.79 36.25 16.05L36.25 16.05Q36.25 16.90 35.84 17.48Q35.43 18.06 34.69 18.37L34.69 18.37L36.61 21.92L36.61 22L35.02 22L33.31 18.71L31.65 18.71L31.65 22ZM31.65 14.66L31.65 17.52L33.18 17.52Q33.93 17.52 34.35 17.15Q34.77 16.77 34.77 16.11L34.77 16.11Q34.77 15.43 34.38 15.05Q33.99 14.68 33.22 14.66L33.22 14.66L31.65 14.66ZM46.23 22L40.65 22L40.65 13.47L46.19 13.47L46.19 14.66L42.13 14.66L42.13 17.02L45.64 17.02L45.64 18.19L42.13 18.19L42.13 20.82L46.23 20.82L46.23 22ZM51.90 22L50.42 22L50.42 13.47L55.84 13.47L55.84 14.66L51.90 14.66L51.90 17.20L55.34 17.20L55.34 18.38L51.90 18.38L51.90 22ZM59.73 18.00L59.73 18.00L59.73 17.52Q59.73 16.28 60.18 15.32Q60.62 14.37 61.42 13.86Q62.23 13.35 63.27 13.35Q64.31 13.35 65.12 13.85Q65.93 14.35 66.37 15.29Q66.81 16.23 66.81 17.48L66.81 17.48L66.81 17.96Q66.81 19.21 66.38 20.16Q65.94 21.10 65.14 21.61Q64.33 22.12 63.28 22.12L63.28 22.12Q62.25 22.12 61.43 21.61Q60.62 21.10 60.18 20.17Q59.74 19.23 59.73 18.00ZM61.22 17.46L61.22 17.96Q61.22 19.36 61.76 20.13Q62.31 20.90 63.28 20.90L63.28 20.90Q64.27 20.90 64.80 20.15Q65.33 19.40 65.33 17.96L65.33 17.96L65.33 17.51Q65.33 16.09 64.79 15.34Q64.26 14.58 63.27 14.58L63.27 14.58Q62.31 14.58 61.77 15.33Q61.23 16.09 61.22 17.46L61.22 17.46ZM72.37 22L70.65 22L73.29 17.70L70.71 13.47L72.42 13.47L74.21 16.55L76.00 13.47L77.72 13.47L75.14 17.70L77.77 22L76.05 22L74.21 18.87L72.37 22Z" fill="#FFFFFF"/><path class="svg__text" d="M105.59 22L102.88 22L105.94 17.75L103.01 13.60L105.68 13.60L107.36 16.02L109.02 13.60L111.59 13.60L108.66 17.66L111.78 22L109.05 22L107.31 19.40L105.59 22ZM118.44 22L116.07 22L116.07 13.60L119.91 13.60Q121.05 13.60 121.89 13.98Q122.73 14.35 123.19 15.06Q123.64 15.76 123.64 16.71L123.64 16.71Q123.64 17.66 123.19 18.35Q122.73 19.05 121.89 19.42Q121.05 19.80 119.91 19.80L119.91 19.80L118.44 19.80L118.44 22ZM118.44 15.47L118.44 17.93L119.76 17.93Q120.50 17.93 120.87 17.61Q121.24 17.29 121.24 16.71L121.24 16.71Q121.24 16.12 120.87 15.80Q120.50 15.47 119.76 15.47L119.76 15.47L118.44 15.47ZM130.77 22L128.39 22L128.39 13.60L130.77 13.60L130.77 22Z" fill="#FFFFFF" x="102.55"/></svg>
|
After Width: | Height: | Size: 2.6 KiB |
47
ipc.go
Normal file
@@ -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)
|
||||
}
|
167
options.go
Normal file
@@ -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
|
||||
}
|
86
options_test.go
Normal file
@@ -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)
|
||||
}
|
12
scripts/install-protocol.reg
Normal file
@@ -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\""
|
58
src/ipc.hpp
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
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
|
70
src/main.cpp
@@ -1,70 +0,0 @@
|
||||
#include "ipc.hpp"
|
||||
#include "options.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
}
|
197
src/options.hpp
@@ -1,197 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "players.hpp"
|
||||
#include "url.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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
|
@@ -1,85 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
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<string, string> flag_overrides;
|
||||
};
|
||||
|
||||
unordered_map<string, player> 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
|
160
src/url.hpp
@@ -1,160 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
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<std::byte>(value - '0');
|
||||
}
|
||||
|
||||
if ((value >= 'a') && (value <= 'f')) {
|
||||
return static_cast<std::byte>(value + '\x0a' - 'a');
|
||||
}
|
||||
|
||||
if ((value >= 'A') && (value <= 'F')) {
|
||||
return static_cast<std::byte>(value + '\x0a' - 'A');
|
||||
}
|
||||
|
||||
return static_cast<std::byte>(' ');
|
||||
}
|
||||
|
||||
} // 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<char>(
|
||||
(0x10u * std::to_integer<unsigned int>(b1)) +
|
||||
std::to_integer<unsigned int>(b2));
|
||||
ret += parsed;
|
||||
} else {
|
||||
ret += *i;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace oim
|