Full Go rewrite with WIP suppport for ther systems (Windows custom protocol handler), fixed #4, updated README

This commit is contained in:
Baldomo
2021-07-28 14:09:20 +02:00
parent 6218f0de7a
commit 74987a4ba9
40 changed files with 603 additions and 749 deletions

View File

@@ -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
View File

@@ -1,2 +1 @@
*.zip build/
*.crx

View File

@@ -1,35 +1,31 @@
INCLUDES = -Isrc/ SRC:=config.go ipc.go options.go
CXXFLAGS_debug = -Wall -DDEBUG -g -rdynamic -std=c++2a $(INCLUDES) EXT_SRC:=$(wildcard extension/Chrome/*) extension/Firefox/manifest.json
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
all: release firefox all: build/open-in-mpv
release: $(SRCS) build/open-in-mpv: $(SRC)
$(CXX) $(CXXFLAGS_release) -o open-in-mpv src/main.cpp @mkdir -p build
go build -ldflags="-s -w" -o build/open-in-mpv ./cmd/open-in-mpv
debug: $(SRCS) build/Firefox.zip: $(EXT_SRC)
$(CXX) $(CXXFLAGS_debug) -o open-in-mpv src/main.cpp @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 install: build/open-in-mpv
cp open-in-mpv /usr/bin cp build/open-in-mpv /usr/bin
install-protocol:
scripts/install-protocol.sh
uninstall: uninstall:
rm /usr/bin/open-in-mpv 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: clean:
@rm -f open-in-mpv Firefox.zip Chrome.crx rm -rf build/*
fmt: test:
clang-format -i src/*.{hpp,cpp} go test ./...
.PHONY: all release debug install uninstall firefox clean fmt .PHONY: all install install-protocol uninstall clean test

File diff suppressed because one or more lines are too long

46
cmd/open-in-mpv/main.go Normal file
View 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
View 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
View 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
View 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)
}

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 917 B

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

8
go.mod Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

1
images/badge-firefox.svg Normal file
View 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
View 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
View 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
View 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)
}

View 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\""

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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