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:
Baldomo
2020-12-25 22:31:41 +01:00
parent 98de7307d1
commit c6db44ea7c
7 changed files with 318 additions and 201 deletions

View File

@@ -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
@@ -22,3 +26,5 @@ firefox:
clean:
@rm -f open-in-mpv Firefox.zip Chrome.crx
.PHONY: all release debug install uninstall firefox clean

View File

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

View File

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

View File

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

View File

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