Major refactor of code: removed dependency on libcurl, added url.hpp, moved everything in the oim namespace, updated readme, added more documentation comments
This commit is contained in:
20
Makefile
20
Makefile
@@ -1,13 +1,17 @@
|
||||
INCLUDES = -Isrc/ -I/opt/curl/include
|
||||
LDFLAGS = -L/opt/curl/lib
|
||||
LDLIBS = -lcurl
|
||||
CXXFLAGS = -Wall $(INCLUDES) $(LDFLAGS) $(LDLIBS)
|
||||
INCLUDES = -Isrc/
|
||||
CXXFLAGS_debug = -Wall -DDEBUG -g -rdynamic
|
||||
CXXFLAGS_release = -Wall -fvisibility=hidden -fvisibility-inlines-hidden -std=c++2a -march=x86-64 -mtune=generic -O3 -pipe -fno-plt $(INCLUDES)
|
||||
SRCS = src/curl.hpp \
|
||||
src/mpvopts.hpp \
|
||||
src/main.cpp
|
||||
|
||||
all:
|
||||
$(CXX) $(CXXFLAGS) -march=x86-64 -mtune=generic -O2 -pipe -fno-plt -o open-in-mpv src/main.cpp
|
||||
all: release firefox
|
||||
|
||||
release:
|
||||
$(CXX) $(CXXFLAGS_release) -o open-in-mpv src/main.cpp
|
||||
|
||||
debug:
|
||||
$(CXX) $(CXXFLAGS_debug) -o open-in-mpv src/main.cpp
|
||||
|
||||
install: all
|
||||
cp open-in-mpv /usr/bin
|
||||
@@ -21,4 +25,6 @@ firefox:
|
||||
@rm Firefox/{*.html,*.js,*.png,*.css}
|
||||
|
||||
clean:
|
||||
@rm -f open-in-mpv Firefox.zip Chrome.crx
|
||||
@rm -f open-in-mpv Firefox.zip Chrome.crx
|
||||
|
||||
.PHONY: all release debug install uninstall firefox clean
|
@@ -1,14 +1,13 @@
|
||||
# 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 is a copy of the one from the awesome [iina](https://github.com/iina/iina), while the (bare) backend is written in C++11 (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 C++20 (this is a rewrite from Rust).
|
||||
|
||||
## Installation
|
||||
> Compiled binaries and packed extensions can be found in the [releases page](https://github.com/Baldomo/open-in-mpv/releases).
|
||||
|
||||
This project requires [`libcurl`](https://curl.se/libcurl/). Each distro has its own way of installing the library so I will leave that to your favourite web search engine.
|
||||
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`, just run
|
||||
```sh
|
||||
sudo make install
|
||||
```
|
||||
|
@@ -10,20 +10,30 @@ using std::string;
|
||||
|
||||
const char *DEFAULT_SOCK = "/tmp/mpvsocket";
|
||||
|
||||
class mpvipc {
|
||||
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:
|
||||
mpvipc() : mpvipc(DEFAULT_SOCK) {};
|
||||
mpvipc(const char *sockpath);
|
||||
~mpvipc();
|
||||
ipc() : ipc(DEFAULT_SOCK) {};
|
||||
ipc(const char *sockpath);
|
||||
~ipc();
|
||||
|
||||
bool send(string cmd);
|
||||
};
|
||||
|
||||
mpvipc::mpvipc(const char *sockpath) {
|
||||
/*
|
||||
* Constructor for oim::ipc
|
||||
*/
|
||||
ipc::ipc(const char *sockpath) {
|
||||
this->sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
this->sockaddress.sun_family = AF_UNIX;
|
||||
std::strcpy(this->sockaddress.sun_path, sockpath);
|
||||
@@ -32,12 +42,20 @@ mpvipc::mpvipc(const char *sockpath) {
|
||||
connect(this->sockfd, (const sockaddr*)&this->sockaddress, this->socklen);
|
||||
}
|
||||
|
||||
mpvipc::~mpvipc() {
|
||||
/*
|
||||
* Destructor for oim::ipc
|
||||
*/
|
||||
ipc::~ipc() {
|
||||
close(this->sockfd);
|
||||
}
|
||||
|
||||
bool mpvipc::send(string cmd) {
|
||||
/*
|
||||
* Sends a raw command string to the internal socket at DEFAULT_SOCK
|
||||
*/
|
||||
bool ipc::send(string cmd) {
|
||||
return write(this->sockfd, cmd.c_str(), cmd.length()) != -1;
|
||||
}
|
||||
|
||||
} // namespace name
|
||||
|
||||
#endif
|
@@ -1,5 +1,6 @@
|
||||
#include "mpvopts.hpp"
|
||||
#include "mpvipc.hpp"
|
||||
#include "options.hpp"
|
||||
#include "ipc.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -43,7 +44,7 @@ int main(int argc, char const *argv[]) {
|
||||
return install_protocol();
|
||||
}
|
||||
|
||||
mpvoptions *mo = new mpvoptions();
|
||||
oim::options *mo = new oim::options();
|
||||
try {
|
||||
mo->parse(argv[1]);
|
||||
} catch (string err) {
|
||||
@@ -52,7 +53,7 @@ int main(int argc, char const *argv[]) {
|
||||
}
|
||||
|
||||
if (mo->needs_ipc()) {
|
||||
mpvipc *mipc = new mpvipc();
|
||||
oim::ipc *mipc = new oim::ipc();
|
||||
bool success = mipc->send(mo->build_ipc());
|
||||
if (success) {
|
||||
return 0;
|
||||
|
180
src/mpvopts.hpp
180
src/mpvopts.hpp
@@ -1,180 +0,0 @@
|
||||
#ifndef MPVOPTS_HPP_
|
||||
#define MPVOPTS_HPP_
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
using std::string;
|
||||
|
||||
#include <iostream>
|
||||
|
||||
string url_decode(const string encoded);
|
||||
string query_value(string query, string key);
|
||||
string query_value(string query, string key, string fallback);
|
||||
|
||||
class mpvoptions {
|
||||
private:
|
||||
string url;
|
||||
string flags;
|
||||
string player;
|
||||
bool fullscreen;
|
||||
bool pip;
|
||||
bool enqueue;
|
||||
bool new_window;
|
||||
|
||||
CURLU *curlu;
|
||||
public:
|
||||
mpvoptions();
|
||||
~mpvoptions();
|
||||
|
||||
string build_cmd();
|
||||
string build_ipc();
|
||||
void parse(const char *url);
|
||||
|
||||
bool needs_ipc();
|
||||
};
|
||||
|
||||
mpvoptions::mpvoptions() {
|
||||
this->curlu = curl_url();
|
||||
this->flags = "";
|
||||
this->player = "mpv";
|
||||
this->fullscreen = false;
|
||||
this->pip = false;
|
||||
this->enqueue = false;
|
||||
}
|
||||
|
||||
mpvoptions::~mpvoptions() {
|
||||
curl_url_cleanup(this->curlu);
|
||||
}
|
||||
|
||||
/*
|
||||
* Builds a command used to invoke mpv with the appropriate arguments
|
||||
*/
|
||||
string mpvoptions::build_cmd() {
|
||||
std::ostringstream ret;
|
||||
|
||||
ret << this->player << " ";
|
||||
if (this->fullscreen) ret << "--fs ";
|
||||
if (this->pip) ret << "--ontop --no-border --autofit=384x216 --geometry=98\%:98\% ";
|
||||
if (!this->flags.empty())
|
||||
ret << this->flags << " ";
|
||||
// NOTE: this is not needed for mpv (it always opens a new window), maybe for other players?
|
||||
// if (this->new_window) ret << "--new-window";
|
||||
ret << this->url;
|
||||
|
||||
std::cout << ret.str() << std::endl;
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
string mpvoptions::build_ipc() {
|
||||
std::ostringstream ret;
|
||||
|
||||
if (!this->needs_ipc()) return "";
|
||||
|
||||
// TODO: in the future this will need a serious json serializer for more complicated commands
|
||||
// Syntax: {"command": ["loadfile", "%s", "append-play"]}\n
|
||||
ret << R"({"command": ["loadfile", ")" << this->url << R"(", "append-play"]})" << std::endl;
|
||||
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a URL and populate the current MpvOptions (uses libcurl for parsing)
|
||||
*/
|
||||
void mpvoptions::parse(const char *url) {
|
||||
curl_url_set(this->curlu, CURLUPART_URL, url, CURLU_NO_DEFAULT_PORT | CURLU_NON_SUPPORT_SCHEME | CURLU_NO_AUTHORITY);
|
||||
|
||||
// Check wether the url contains the right scheme or not
|
||||
char *scheme;
|
||||
curl_url_get(this->curlu, CURLUPART_SCHEME, &scheme, 0);
|
||||
if (std::strcmp(scheme, "mpv")) {
|
||||
curl_free(scheme);
|
||||
throw string("Unsupported protocol supplied");
|
||||
}
|
||||
curl_free(scheme);
|
||||
|
||||
// NOTE: libcurl really doesn't like "malformed" url's such as `mpv:///open?xxxxx`
|
||||
// and it messes up parsing path and host: HOST becomes `open` and PATH becomes `/`
|
||||
// so we just check HOST instead of PATH for the correct method
|
||||
char *method;
|
||||
curl_url_get(this->curlu, CURLUPART_HOST, &method, 0);
|
||||
if (std::strcmp(method, "open")) {
|
||||
curl_free(method);
|
||||
throw string("Unsupported method supplied");
|
||||
}
|
||||
curl_free(method);
|
||||
|
||||
// Check wether the url query is empty or not
|
||||
char *query;
|
||||
curl_url_get(this->curlu, CURLUPART_QUERY, &query, 0);
|
||||
if (!query) {
|
||||
curl_free(query);
|
||||
throw string("Empty query");
|
||||
}
|
||||
|
||||
// If the query is not empty, parse it and populate the current object
|
||||
string querystr(query);
|
||||
curl_free(query);
|
||||
this->url = url_decode(query_value(querystr, "url"));
|
||||
this->flags = url_decode(query_value(querystr, "flags"));
|
||||
this->player = query_value(querystr, "player", "mpv");
|
||||
this->fullscreen = query_value(querystr, "fullscreen") == "1";
|
||||
this->pip = query_value(querystr, "pip") == "1";
|
||||
this->enqueue = query_value(querystr, "enqueue") == "1";
|
||||
this->new_window = query_value(querystr, "new_window") == "1";
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks wether or not MpvOptions needs to communicate with mpv via IPC
|
||||
* instead of the command line interface
|
||||
*/
|
||||
bool mpvoptions::needs_ipc() {
|
||||
// For now this is needed only when queuing videos
|
||||
return this->enqueue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Percent-decodes a URL using curl_easy_unescape
|
||||
*/
|
||||
string url_decode(const string encoded) {
|
||||
CURL *curl = curl_easy_init();
|
||||
std::unique_ptr<char, void(*)(char*)> url_decoded(
|
||||
curl_easy_unescape(
|
||||
curl,
|
||||
encoded.c_str(),
|
||||
(int) encoded.length(),
|
||||
nullptr
|
||||
),
|
||||
[](char *ptr) { curl_free(ptr); }
|
||||
);
|
||||
|
||||
return string(url_decoded.get());
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets a value from a query string given a key
|
||||
*/
|
||||
string query_value(string query, 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 query_value(string query, string key, string fallback) {
|
||||
string ret = query_value(query, key);
|
||||
if (ret.empty()) return fallback;
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif
|
125
src/options.hpp
Normal file
125
src/options.hpp
Normal file
@@ -0,0 +1,125 @@
|
||||
#ifndef MPVOPTS_HPP_
|
||||
#define MPVOPTS_HPP_
|
||||
|
||||
#include "url.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using std::string;
|
||||
|
||||
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:
|
||||
string url;
|
||||
string flags;
|
||||
string player;
|
||||
bool fullscreen;
|
||||
bool pip;
|
||||
bool enqueue;
|
||||
bool new_window;
|
||||
|
||||
public:
|
||||
options();
|
||||
|
||||
string build_cmd();
|
||||
string build_ipc();
|
||||
void parse(const char *url);
|
||||
|
||||
bool needs_ipc();
|
||||
};
|
||||
|
||||
/*
|
||||
* Constructor for oim::options
|
||||
*/
|
||||
options::options() {
|
||||
this->url = "";
|
||||
this->flags = "";
|
||||
this->player = "mpv";
|
||||
this->fullscreen = false;
|
||||
this->pip = false;
|
||||
this->enqueue = false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Builds a CLI command used to invoke mpv with the appropriate arguments
|
||||
*/
|
||||
string options::build_cmd() {
|
||||
std::ostringstream ret;
|
||||
|
||||
// TODO: some of these options work only in mpv and not other players
|
||||
// This can be solved by adding a list of some sorts (json/toml/whatever)
|
||||
// containing the flags to use for each functionality and each player
|
||||
ret << this->player << " ";
|
||||
if (this->fullscreen) ret << "--fs ";
|
||||
if (this->pip) ret << "--ontop --no-border --autofit=384x216 --geometry=98\%:98\% ";
|
||||
if (!this->flags.empty())
|
||||
ret << this->flags << " ";
|
||||
// NOTE: this is not needed for mpv (it always opens a new window), maybe for other players?
|
||||
// if (this->new_window) ret << "--new-window";
|
||||
ret << this->url;
|
||||
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
/*
|
||||
* Builds the IPC command needed to enqueue videos in mpv
|
||||
*/
|
||||
string options::build_ipc() {
|
||||
std::ostringstream ret;
|
||||
|
||||
if (!this->needs_ipc()) return "";
|
||||
|
||||
// TODO: 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", ")"
|
||||
<< this->url
|
||||
<< R"(", "append-play"]})" << std::endl;
|
||||
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a URL and populate the current MpvOptions (uses libcurl for parsing)
|
||||
*/
|
||||
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");
|
||||
|
||||
this->url = oim::url_decode(u.query_value("url"));
|
||||
this->flags = oim::url_decode(u.query_value("flags"));
|
||||
this->player = u.query_value("player", "mpv");
|
||||
this->fullscreen = u.query_value("fullscreen") == "1";
|
||||
this->pip = u.query_value("pip") == "1";
|
||||
this->enqueue = u.query_value("enqueue") == "1";
|
||||
this->new_window = u.query_value("new_window") == "1";
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks wether or not oim::options needs to communicate with mpv via IPC
|
||||
* instead of the command line interface
|
||||
*/
|
||||
bool options::needs_ipc() {
|
||||
// For now this is needed only when queuing videos
|
||||
return this->enqueue;
|
||||
}
|
||||
|
||||
} // namespace oim
|
||||
|
||||
#endif
|
148
src/url.hpp
Normal file
148
src/url.hpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#ifndef MPVURL_HPP_
|
||||
#define MPVURL_HPP_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
using std::string;
|
||||
|
||||
/*
|
||||
* 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 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) {
|
||||
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));
|
||||
std::transform(url_s.begin(), prot_i,
|
||||
std::back_inserter(protocol_),
|
||||
std::ptr_fun<int, int>(tolower)); // protocol is icase
|
||||
if (prot_i == url_s.end())
|
||||
return;
|
||||
std::advance(prot_i, prot_end.length());
|
||||
|
||||
string::const_iterator path_i = std::find(prot_i, url_s.end(), '/');
|
||||
host_.reserve(std::distance(prot_i, path_i));
|
||||
std::transform(prot_i, path_i,
|
||||
std::back_inserter(host_),
|
||||
std::ptr_fun<int, int>(tolower)); // host is icase
|
||||
|
||||
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());
|
||||
};
|
||||
|
||||
/* Move constructor for oim::url */
|
||||
url(url &&other) {
|
||||
protocol_ = std::move(other.protocol_);
|
||||
host_ = std::move(other.host_);
|
||||
path_ = std::move(other.path_);
|
||||
query_ = std::move(other.query_);
|
||||
};
|
||||
|
||||
/* 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) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
string ret = query_value(key);
|
||||
if (ret.empty()) return fallback;
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Percent-decodes a URL
|
||||
*/
|
||||
string url_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
|
||||
|
||||
#endif
|
Reference in New Issue
Block a user