From 41fbcc738b34a19f66eef3cca8d5dea0b89e07a3 Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sat, 24 Mar 2018 14:13:04 +0100 Subject: [PATCH] Fix and improve Streamlink code Move streamlink code to its own file Fixes #275 Untested on linux, but should work decently there as well. --- chatterino.pro | 6 +- src/util/streamlink.cpp | 182 +++++++++++++++++++++++++++++++++++ src/util/streamlink.hpp | 27 ++++++ src/widgets/qualitypopup.cpp | 29 +++--- src/widgets/qualitypopup.hpp | 7 +- src/widgets/split.cpp | 84 +--------------- 6 files changed, 237 insertions(+), 98 deletions(-) create mode 100644 src/util/streamlink.cpp create mode 100644 src/util/streamlink.hpp diff --git a/chatterino.pro b/chatterino.pro index e73ae9001..9f40c5766 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -175,7 +175,8 @@ SOURCES += \ src/widgets/window.cpp \ src/providers/irc/ircaccount.cpp \ src/providers/irc/ircserver.cpp \ - src/providers/irc/ircchannel2.cpp + src/providers/irc/ircchannel2.cpp \ + src/util/streamlink.cpp HEADERS += \ src/precompiled_header.hpp \ @@ -287,7 +288,8 @@ HEADERS += \ src/providers/irc/abstractircserver.hpp \ src/providers/irc/ircaccount.hpp \ src/providers/irc/ircserver.hpp \ - src/providers/irc/ircchannel2.hpp + src/providers/irc/ircchannel2.hpp \ + src/util/streamlink.hpp RESOURCES += \ resources/resources.qrc diff --git a/src/util/streamlink.cpp b/src/util/streamlink.cpp new file mode 100644 index 000000000..a54190643 --- /dev/null +++ b/src/util/streamlink.cpp @@ -0,0 +1,182 @@ +#include "util/streamlink.hpp" +#include "helpers.hpp" +#include "singletons/settingsmanager.hpp" +#include "widgets/qualitypopup.hpp" + +#include +#include + +#include + +namespace chatterino { +namespace streamlink { + +namespace { + +const char *GetBinaryName() +{ +#ifdef _WIN32 + return "streamlink.exe"; +#else + return "streamlink"; +#endif +} + +const char *GetDefaultBinaryPath() +{ +#ifdef _WIN32 + return "C:\\Program Files (x86)\\Streamlink\\bin\\streamlink.exe"; +#else + return "/usr/bin/streamlink"; +#endif +} + +bool CheckStreamlinkPath(const QString &path) +{ + QFileInfo fileinfo(path); + + if (!fileinfo.exists()) { + return false; + // throw Exception(fS("Streamlink path ({}) is invalid, file does not exist", path)); + } + + if (fileinfo.isDir() || !fileinfo.isExecutable()) { + return false; + } + + return true; +} + +// TODO: Make streamlink binary finder smarter +QString GetStreamlinkBinaryPath() +{ + singletons::SettingManager &settings = singletons::SettingManager::getInstance(); + + QString settingPath = settings.streamlinkPath; + + QStringList paths; + paths << settingPath; + paths << GetDefaultBinaryPath(); +#ifdef _WIN32 + paths << settingPath + "\\" + GetBinaryName(); + paths << settingPath + "\\bin\\" + GetBinaryName(); +#else + paths << "/usr/local/bin/streamlink"; + paths << "/bin/streamlink"; +#endif + + for (const auto &path : paths) { + if (CheckStreamlinkPath(path)) { + return path; + } + } + + throw Exception("Unable to find streamlink binary. Install streamlink or set the binary path " + "in the settings dialog."); +} + +void GetStreamQualities(const QString &channelURL, std::function cb) +{ + QString path = GetStreamlinkBinaryPath(); + + // XXX: Memory leak + QProcess *p = new QProcess(); + + QObject::connect(p, static_cast(&QProcess::finished), [=](int) { + QString lastLine = QString(p->readAllStandardOutput()); + lastLine = lastLine.trimmed().split('\n').last().trimmed(); + if (lastLine.startsWith("Available streams: ")) { + QStringList options; + QStringList split = lastLine.right(lastLine.length() - 19).split(", "); + + for (int i = split.length() - 1; i >= 0; i--) { + QString option = split.at(i); + if (option.endsWith(" (worst)")) { + options << option.left(option.length() - 8); + } else if (option.endsWith(" (best)")) { + options << option.left(option.length() - 7); + } else { + options << option; + } + } + + cb(options); + } + }); + + p->start(path, {channelURL, "--default-stream=KKona"}); +} + +} // namespace + +void OpenStreamlink(const QString &channelURL, const QString &quality, QStringList extraArguments) +{ + singletons::SettingManager &settings = singletons::SettingManager::getInstance(); + + QString path = GetStreamlinkBinaryPath(); + + QStringList arguments; + + QString additionalOptions = settings.streamlinkOpts.getValue(); + if (!additionalOptions.isEmpty()) { + arguments << settings.streamlinkOpts; + } + + arguments.append(extraArguments); + + arguments << channelURL; + + if (!quality.isEmpty()) { + arguments << quality; + } + + QProcess::startDetached(path, arguments); +} + +void Start(const QString &channel) +{ + QString channelURL = "twitch.tv/" + channel; + + singletons::SettingManager &settings = singletons::SettingManager::getInstance(); + + QString preferredQuality = settings.preferredQuality; + preferredQuality = preferredQuality.toLower(); + + if (preferredQuality == "choose") { + GetStreamQualities(channelURL, [=](QStringList qualityOptions) { + widgets::QualityPopup::showDialog(channel, qualityOptions); + }); + + return; + } + + QStringList args; + + // Quality converted from Chatterino format to Streamlink format + QString quality; + // Streamlink qualities to exclude + QString exclude; + + if (preferredQuality == "high") { + exclude = ">720p30"; + quality = "high,best"; + } else if (preferredQuality == "medium") { + exclude = ">540p30"; + quality = "medium,best"; + } else if (preferredQuality == "low") { + exclude = ">360p30"; + quality = "low,best"; + } else if (preferredQuality == "audio only") { + quality = "audio,audio_only"; + } else { + quality = "best"; + } + if (!exclude.isEmpty()) { + args << "--stream-sorting-excludes" << exclude; + } + + OpenStreamlink(channelURL, quality, args); +} + +} // namespace streamlink +} // namespace chatterino diff --git a/src/util/streamlink.hpp b/src/util/streamlink.hpp new file mode 100644 index 000000000..396c09afe --- /dev/null +++ b/src/util/streamlink.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include + +namespace chatterino { +namespace streamlink { + +class Exception : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +// Open streamlink for given channel, quality and extra arguments +// the "Additional arguments" are fetched and added at the beginning of the streamlink call +void OpenStreamlink(const QString &channelURL, const QString &quality, + QStringList extraArguments = QStringList()); + +// Start opening streamlink for the given channel, reading settings like quality from settings +// and opening a quality dialog if the quality is "Choose" +void Start(const QString &channel); + +} // namespace streamlink +} // namespace chatterino diff --git a/src/widgets/qualitypopup.cpp b/src/widgets/qualitypopup.cpp index 73735f3aa..b92797827 100644 --- a/src/widgets/qualitypopup.cpp +++ b/src/widgets/qualitypopup.cpp @@ -1,14 +1,12 @@ #include "qualitypopup.hpp" - -#include +#include "debug/log.hpp" +#include "util/streamlink.hpp" namespace chatterino { namespace widgets { -QualityPopup::QualityPopup(const QString &channel, const QString &path, QStringList options) - : BaseWindow() - , channel(channel) - , path(path) +QualityPopup::QualityPopup(const QString &_channelName, QStringList options) + : channelName(_channelName) { this->ui.okButton.setText("OK"); this->ui.cancelButton.setText("Cancel"); @@ -21,9 +19,7 @@ QualityPopup::QualityPopup(const QString &channel, const QString &path, QStringL this->ui.buttonBox.addButton(&this->ui.okButton, QDialogButtonBox::ButtonRole::AcceptRole); this->ui.buttonBox.addButton(&this->ui.cancelButton, QDialogButtonBox::ButtonRole::RejectRole); - for (int i = 0; i < options.length(); ++i) { - this->ui.selector.addItem(options.at(i)); - } + this->ui.selector.addItems(options); this->ui.vbox.addWidget(&this->ui.selector); this->ui.vbox.addWidget(&this->ui.buttonBox); @@ -31,9 +27,9 @@ QualityPopup::QualityPopup(const QString &channel, const QString &path, QStringL this->setLayout(&this->ui.vbox); } -void QualityPopup::showDialog(const QString &channel, const QString &path, QStringList options) +void QualityPopup::showDialog(const QString &channelName, QStringList options) { - QualityPopup *instance = new QualityPopup(channel, path, options); + QualityPopup *instance = new QualityPopup(channelName, options); instance->setAttribute(Qt::WA_DeleteOnClose, true); @@ -45,9 +41,16 @@ void QualityPopup::showDialog(const QString &channel, const QString &path, QStri void QualityPopup::okButtonClicked() { + QString channelURL = "twitch.tv/" + this->channelName; + singletons::SettingManager &settings = singletons::SettingManager::getInstance(); - QProcess::startDetached(this->path, {"twitch.tv/" + this->channel, - this->ui.selector.currentText(), settings.streamlinkOpts}); + + try { + streamlink::OpenStreamlink(channelURL, this->ui.selector.currentText()); + } catch (const streamlink::Exception &ex) { + debug::Log("Exception caught trying to open streamlink: {}", ex.what()); + } + this->close(); } diff --git a/src/widgets/qualitypopup.hpp b/src/widgets/qualitypopup.hpp index 7875dafe0..ccb57df28 100644 --- a/src/widgets/qualitypopup.hpp +++ b/src/widgets/qualitypopup.hpp @@ -17,8 +17,8 @@ namespace widgets { class QualityPopup : public BaseWindow { public: - QualityPopup(const QString &channel, const QString &path, QStringList options); - static void showDialog(const QString &channel, const QString &path, QStringList options); + QualityPopup(const QString &_channelName, QStringList options); + static void showDialog(const QString &_channelName, QStringList options); private: struct { @@ -29,8 +29,7 @@ private: QPushButton cancelButton; } ui; - QString channel; - QString path; + QString channelName; void okButtonClicked(); void cancelButtonClicked(); diff --git a/src/widgets/split.cpp b/src/widgets/split.cpp index 8911359d8..19550d75a 100644 --- a/src/widgets/split.cpp +++ b/src/widgets/split.cpp @@ -7,6 +7,7 @@ #include "singletons/settingsmanager.hpp" #include "singletons/thememanager.hpp" #include "singletons/windowmanager.hpp" +#include "util/streamlink.hpp" #include "util/urlfetch.hpp" #include "widgets/helper/searchpopup.hpp" #include "widgets/helper/shortcut.hpp" @@ -21,13 +22,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -364,83 +363,10 @@ void Split::doOpenPopupPlayer() void Split::doOpenStreamlink() { - singletons::SettingManager &settings = singletons::SettingManager::getInstance(); - QString preferredQuality = settings.preferredQuality; - preferredQuality = preferredQuality.toLower(); - // TODO(Confuseh): Default streamlink paths - QString path = settings.streamlinkPath; - QString channel = this->channelName.getValue(); - QFileInfo fileinfo = QFileInfo(path); - - if (path.isEmpty()) { - debug::Log("[Split:doOpenStreamlink] No streamlink path selected in Settings"); - return; - } - - if (!fileinfo.exists()) { - debug::Log("[Split:doOpenStreamlink] Streamlink path ({}) is invalid, file does not exist", - path); - return; - } - - if (fileinfo.isDir() || !fileinfo.isExecutable()) { - debug::Log("[Split:doOpenStreamlink] Streamlink path ({}) is invalid, it needs to point to " - "the streamlink executable", - path); - return; - } - - if (preferredQuality != "choose") { - QStringList args = {"twitch.tv/" + channel}; - QString quality = ""; - QString exclude = ""; - if (preferredQuality == "high") { - exclude = ">720p30"; - quality = "high,best"; - } else if (preferredQuality == "medium") { - exclude = ">540p30"; - quality = "medium,best"; - } else if (preferredQuality == "low") { - exclude = ">360p30"; - quality = "low,best"; - } else if (preferredQuality == "audio only") { - quality = "audio,audio_only"; - } else { - quality = "best"; - } - if (quality != "") - args << quality; - if (exclude != "") - args << "--stream-sorting-excludes" << exclude; - args << settings.streamlinkOpts; - QProcess::startDetached(path, args); - } else { - QProcess *p = new QProcess(); - // my god that signal though - QObject::connect(p, static_cast(&QProcess::finished), this, - [path, channel, p](int) { - QString lastLine = QString(p->readAllStandardOutput()); - lastLine = lastLine.trimmed().split('\n').last().trimmed(); - if (lastLine.startsWith("Available streams: ")) { - QStringList options; - QStringList split = - lastLine.right(lastLine.length() - 19).split(", "); - - for (int i = split.length() - 1; i >= 0; i--) { - QString option = split.at(i); - if (option.endsWith(" (worst)")) { - options << option.left(option.length() - 8); - } else if (option.endsWith(" (best)")) { - options << option.left(option.length() - 7); - } else { - options << option; - } - } - - QualityPopup::showDialog(channel, path, options); - } - }); - p->start(path, {"twitch.tv/" + channel, "--default-stream=KKona"}); + try { + streamlink::Start(this->channelName.getValue()); + } catch (const streamlink::Exception &ex) { + debug::Log("Error in doOpenStreamlink: {}", ex.what()); } }