diff --git a/Makefile b/Makefile index 5567578..c298f90 100644 --- a/Makefile +++ b/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 \ No newline at end of file + @rm -f open-in-mpv Firefox.zip Chrome.crx + +.PHONY: all release debug install uninstall firefox clean \ No newline at end of file diff --git a/README.md b/README.md index 7aeeab3..77fce38 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/src/mpvipc.hpp b/src/ipc.hpp similarity index 57% rename from src/mpvipc.hpp rename to src/ipc.hpp index 137a447..4ec14e0 100644 --- a/src/mpvipc.hpp +++ b/src/ipc.hpp @@ -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 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index b5dfb9a..146079a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ -#include "mpvopts.hpp" -#include "mpvipc.hpp" +#include "options.hpp" +#include "ipc.hpp" + #include #include #include @@ -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; diff --git a/src/mpvopts.hpp b/src/mpvopts.hpp deleted file mode 100644 index ec03da0..0000000 --- a/src/mpvopts.hpp +++ /dev/null @@ -1,180 +0,0 @@ -#ifndef MPVOPTS_HPP_ -#define MPVOPTS_HPP_ - -#include -#include -#include -#include -#include -using std::string; - -#include - -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 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 \ No newline at end of file diff --git a/src/options.hpp b/src/options.hpp new file mode 100644 index 0000000..31e6eed --- /dev/null +++ b/src/options.hpp @@ -0,0 +1,125 @@ +#ifndef MPVOPTS_HPP_ +#define MPVOPTS_HPP_ + +#include "url.hpp" + +#include +#include +#include + +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 \ No newline at end of file diff --git a/src/url.hpp b/src/url.hpp new file mode 100644 index 0000000..72708b7 --- /dev/null +++ b/src/url.hpp @@ -0,0 +1,148 @@ +#ifndef MPVURL_HPP_ +#define MPVURL_HPP_ + +#include +#include +#include +#include +#include + +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(value - '0'); + } + + if ((value >= 'a') && (value <= 'f')) { + return static_cast(value + '\x0a' - 'a'); + } + + if ((value >= 'A') && (value <= 'F')) { + return static_cast(value + '\x0a' - 'A'); + } + + return static_cast(' '); +} + +namespace 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(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(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( + (0x10u * std::to_integer(b1)) + std::to_integer(b2) + ); + ret += parsed; + } else { + ret += *i; + } + } + + return ret; +} + +} // namespace oim + +#endif \ No newline at end of file