2018-06-26 14:09:39 +02:00
|
|
|
#include "util/StreamLink.hpp"
|
2018-04-27 22:11:19 +02:00
|
|
|
|
2018-06-26 14:09:39 +02:00
|
|
|
#include "Application.hpp"
|
2022-01-15 18:20:06 +01:00
|
|
|
#include "providers/irc/IrcMessageBuilder.hpp"
|
2018-06-28 19:46:45 +02:00
|
|
|
#include "singletons/Settings.hpp"
|
2022-01-15 18:20:06 +01:00
|
|
|
#include "singletons/WindowManager.hpp"
|
2018-11-23 17:51:55 +01:00
|
|
|
#include "util/Helpers.hpp"
|
2021-03-06 19:56:36 +01:00
|
|
|
#include "util/SplitCommand.hpp"
|
2022-01-15 18:20:06 +01:00
|
|
|
#include "widgets/Window.hpp"
|
2018-06-26 15:11:45 +02:00
|
|
|
#include "widgets/dialogs/QualityPopup.hpp"
|
2022-01-15 18:20:06 +01:00
|
|
|
#include "widgets/splits/Split.hpp"
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-05-06 16:33:00 +02:00
|
|
|
#include <QErrorMessage>
|
2018-03-24 14:13:04 +01:00
|
|
|
#include <QFileInfo>
|
|
|
|
#include <QProcess>
|
2020-11-21 16:20:10 +01:00
|
|
|
#include "common/QLogging.hpp"
|
2021-08-21 13:00:01 +02:00
|
|
|
#include "common/Version.hpp"
|
2018-03-24 14:13:04 +01:00
|
|
|
|
|
|
|
#include <functional>
|
|
|
|
|
|
|
|
namespace chatterino {
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
2018-08-15 22:46:20 +02:00
|
|
|
const char *getBinaryName()
|
|
|
|
{
|
2018-03-24 14:13:04 +01:00
|
|
|
#ifdef _WIN32
|
2018-08-15 22:46:20 +02:00
|
|
|
return "streamlink.exe";
|
2018-03-24 14:13:04 +01:00
|
|
|
#else
|
2018-08-15 22:46:20 +02:00
|
|
|
return "streamlink";
|
2018-03-24 14:13:04 +01:00
|
|
|
#endif
|
2018-08-15 22:46:20 +02:00
|
|
|
}
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-08-15 22:46:20 +02:00
|
|
|
const char *getDefaultBinaryPath()
|
|
|
|
{
|
2018-03-24 14:13:04 +01:00
|
|
|
#ifdef _WIN32
|
2018-08-15 22:46:20 +02:00
|
|
|
return "C:\\Program Files (x86)\\Streamlink\\bin\\streamlink.exe";
|
2018-03-24 14:13:04 +01:00
|
|
|
#else
|
2018-08-15 22:46:20 +02:00
|
|
|
return "/usr/bin/streamlink";
|
2018-03-24 14:13:04 +01:00
|
|
|
#endif
|
2018-05-06 16:33:00 +02:00
|
|
|
}
|
|
|
|
|
2018-08-15 22:46:20 +02:00
|
|
|
bool checkStreamlinkPath(const QString &path)
|
|
|
|
{
|
|
|
|
QFileInfo fileinfo(path);
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (!fileinfo.exists())
|
|
|
|
{
|
2018-08-15 22:46:20 +02:00
|
|
|
return false;
|
|
|
|
// throw Exception(fS("Streamlink path ({}) is invalid, file does
|
|
|
|
// not exist", path));
|
|
|
|
}
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-08-15 22:46:20 +02:00
|
|
|
return fileinfo.isExecutable();
|
|
|
|
}
|
2018-05-06 16:33:00 +02:00
|
|
|
|
2018-08-15 22:46:20 +02:00
|
|
|
void showStreamlinkNotFoundError()
|
|
|
|
{
|
|
|
|
static QErrorMessage *msg = new QErrorMessage;
|
2020-10-31 15:12:42 +01:00
|
|
|
msg->setWindowTitle("Chatterino - streamlink not found");
|
2018-08-15 22:46:20 +02:00
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (getSettings()->streamlinkUseCustomPath)
|
|
|
|
{
|
2021-03-13 20:14:47 +01:00
|
|
|
msg->showMessage("Unable to find Streamlink executable\nMake sure "
|
|
|
|
"your custom path is pointing to the DIRECTORY "
|
|
|
|
"where the streamlink executable is located");
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-08-15 22:46:20 +02:00
|
|
|
msg->showMessage(
|
|
|
|
"Unable to find Streamlink executable.\nIf you have Streamlink "
|
|
|
|
"installed, you might need to enable the custom path option");
|
2018-05-06 16:33:00 +02:00
|
|
|
}
|
2018-08-15 22:46:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
QProcess *createStreamlinkProcess()
|
|
|
|
{
|
|
|
|
auto p = new QProcess;
|
2021-08-21 13:00:01 +02:00
|
|
|
|
|
|
|
const QString path = [] {
|
|
|
|
if (getSettings()->streamlinkUseCustomPath)
|
|
|
|
{
|
|
|
|
return getSettings()->streamlinkPath + "/" + getBinaryName();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return QString{getBinaryName()};
|
|
|
|
}
|
|
|
|
}();
|
|
|
|
|
|
|
|
if (Version::instance().isFlatpak())
|
|
|
|
{
|
|
|
|
p->setProgram("flatpak-spawn");
|
|
|
|
p->setArguments({"--host", path});
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
p->setProgram(path);
|
|
|
|
}
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-08-15 22:46:20 +02:00
|
|
|
QObject::connect(p, &QProcess::errorOccurred, [=](auto err) {
|
2018-10-21 13:43:02 +02:00
|
|
|
if (err == QProcess::FailedToStart)
|
|
|
|
{
|
2018-08-15 22:46:20 +02:00
|
|
|
showStreamlinkNotFoundError();
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-01-17 20:19:10 +01:00
|
|
|
qCWarning(chatterinoStreamlink) << "Error occurred" << err;
|
2018-08-15 22:46:20 +02:00
|
|
|
}
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-08-15 22:46:20 +02:00
|
|
|
p->deleteLater();
|
|
|
|
});
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-08-15 22:46:20 +02:00
|
|
|
QObject::connect(
|
2021-03-13 20:14:47 +01:00
|
|
|
p,
|
|
|
|
static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(
|
|
|
|
&QProcess::finished),
|
|
|
|
[=](int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) {
|
2020-11-08 12:02:19 +01:00
|
|
|
p->deleteLater();
|
2018-08-15 22:46:20 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return p;
|
|
|
|
}
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-05-06 16:33:00 +02:00
|
|
|
} // namespace
|
|
|
|
|
2018-08-06 21:17:03 +02:00
|
|
|
void getStreamQualities(const QString &channelURL,
|
|
|
|
std::function<void(QStringList)> cb)
|
2018-03-24 14:13:04 +01:00
|
|
|
{
|
2018-05-06 16:33:00 +02:00
|
|
|
auto p = createStreamlinkProcess();
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-08-06 21:17:03 +02:00
|
|
|
QObject::connect(
|
2021-03-13 20:14:47 +01:00
|
|
|
p,
|
|
|
|
static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(
|
|
|
|
&QProcess::finished),
|
|
|
|
[=](int exitCode, QProcess::ExitStatus /*exitStatus*/) {
|
|
|
|
if (exitCode != 0)
|
2018-10-21 13:43:02 +02:00
|
|
|
{
|
2021-03-13 20:14:47 +01:00
|
|
|
qCWarning(chatterinoStreamlink) << "Got error code" << exitCode;
|
2018-08-06 21:17:03 +02:00
|
|
|
// return;
|
2018-03-24 14:13:04 +01:00
|
|
|
}
|
2018-08-06 21:17:03 +02:00
|
|
|
QString lastLine = QString(p->readAllStandardOutput());
|
|
|
|
lastLine = lastLine.trimmed().split('\n').last().trimmed();
|
2018-10-21 13:43:02 +02:00
|
|
|
if (lastLine.startsWith("Available streams: "))
|
|
|
|
{
|
2018-08-06 21:17:03 +02:00
|
|
|
QStringList options;
|
|
|
|
QStringList split =
|
|
|
|
lastLine.right(lastLine.length() - 19).split(", ");
|
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
for (int i = split.length() - 1; i >= 0; i--)
|
|
|
|
{
|
2018-08-06 21:17:03 +02:00
|
|
|
QString option = split.at(i);
|
2018-10-21 13:43:02 +02:00
|
|
|
if (option == "best)")
|
|
|
|
{
|
2018-08-14 17:36:20 +02:00
|
|
|
// As it turns out, sometimes, one quality option can
|
|
|
|
// be the best and worst quality at the same time.
|
|
|
|
// Since we start loop from the end, we can check
|
|
|
|
// that and act accordingly
|
|
|
|
option = split.at(--i);
|
|
|
|
// "900p60 (worst"
|
|
|
|
options << option.left(option.length() - 7);
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else if (option.endsWith(" (worst)"))
|
|
|
|
{
|
2018-08-06 21:17:03 +02:00
|
|
|
options << option.left(option.length() - 8);
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else if (option.endsWith(" (best)"))
|
|
|
|
{
|
2018-08-06 21:17:03 +02:00
|
|
|
options << option.left(option.length() - 7);
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-08-06 21:17:03 +02:00
|
|
|
options << option;
|
|
|
|
}
|
|
|
|
}
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-08-06 21:17:03 +02:00
|
|
|
cb(options);
|
|
|
|
}
|
|
|
|
});
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2021-08-21 13:00:01 +02:00
|
|
|
p->setArguments(p->arguments() +
|
|
|
|
QStringList{channelURL, "--default-stream=KKona"});
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-05-06 16:33:00 +02:00
|
|
|
p->start();
|
|
|
|
}
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2018-08-06 21:17:03 +02:00
|
|
|
void openStreamlink(const QString &channelURL, const QString &quality,
|
|
|
|
QStringList extraArguments)
|
2018-03-24 14:13:04 +01:00
|
|
|
{
|
2021-08-21 13:00:01 +02:00
|
|
|
auto proc = createStreamlinkProcess();
|
|
|
|
auto arguments = proc->arguments()
|
|
|
|
<< extraArguments << channelURL << quality;
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2021-03-06 19:56:36 +01:00
|
|
|
// Remove empty arguments before appending additional streamlink options
|
|
|
|
// as the options might purposely contain empty arguments
|
|
|
|
arguments.removeAll(QString());
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2021-03-06 19:56:36 +01:00
|
|
|
QString additionalOptions = getSettings()->streamlinkOpts.getValue();
|
|
|
|
arguments << splitCommand(additionalOptions);
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2021-08-21 13:00:01 +02:00
|
|
|
proc->setArguments(std::move(arguments));
|
|
|
|
bool res = proc->startDetached();
|
2018-05-06 16:33:00 +02:00
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (!res)
|
|
|
|
{
|
2018-05-06 16:33:00 +02:00
|
|
|
showStreamlinkNotFoundError();
|
|
|
|
}
|
2018-03-24 14:13:04 +01:00
|
|
|
}
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
void openStreamlinkForChannel(const QString &channel)
|
2018-03-24 14:13:04 +01:00
|
|
|
{
|
2022-01-15 18:20:06 +01:00
|
|
|
static const QString INFO_TEMPLATE("Opening %1 in Streamlink ...");
|
|
|
|
|
|
|
|
auto *currentPage = dynamic_cast<SplitContainer *>(
|
|
|
|
getApp()->windows->getMainWindow().getNotebook().getSelectedPage());
|
|
|
|
if (currentPage != nullptr)
|
|
|
|
{
|
|
|
|
if (auto currentSplit = currentPage->getSelectedSplit();
|
|
|
|
currentSplit != nullptr)
|
|
|
|
{
|
|
|
|
currentSplit->getChannel()->addMessage(
|
|
|
|
makeSystemMessage(INFO_TEMPLATE.arg(channel)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-27 22:11:19 +02:00
|
|
|
QString channelURL = "twitch.tv/" + channel;
|
2018-03-24 14:13:04 +01:00
|
|
|
|
2019-11-16 11:58:13 +01:00
|
|
|
QString preferredQuality = getSettings()->preferredQuality.getValue();
|
2018-03-24 14:13:04 +01:00
|
|
|
preferredQuality = preferredQuality.toLower();
|
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (preferredQuality == "choose")
|
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
getStreamQualities(channelURL, [=](QStringList qualityOptions) {
|
2021-08-15 15:59:52 +02:00
|
|
|
QualityPopup::showDialog(channelURL, qualityOptions);
|
2018-03-24 14:13:04 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
QStringList args;
|
|
|
|
|
|
|
|
// Quality converted from Chatterino format to Streamlink format
|
|
|
|
QString quality;
|
|
|
|
// Streamlink qualities to exclude
|
|
|
|
QString exclude;
|
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (preferredQuality == "high")
|
|
|
|
{
|
2018-03-24 14:13:04 +01:00
|
|
|
exclude = ">720p30";
|
|
|
|
quality = "high,best";
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else if (preferredQuality == "medium")
|
|
|
|
{
|
2018-03-24 14:13:04 +01:00
|
|
|
exclude = ">540p30";
|
|
|
|
quality = "medium,best";
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else if (preferredQuality == "low")
|
|
|
|
{
|
2018-03-24 14:13:04 +01:00
|
|
|
exclude = ">360p30";
|
|
|
|
quality = "low,best";
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else if (preferredQuality == "audio only")
|
|
|
|
{
|
2018-03-24 14:13:04 +01:00
|
|
|
quality = "audio,audio_only";
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-03-24 14:13:04 +01:00
|
|
|
quality = "best";
|
|
|
|
}
|
2018-10-21 13:43:02 +02:00
|
|
|
if (!exclude.isEmpty())
|
|
|
|
{
|
2018-03-24 14:13:04 +01:00
|
|
|
args << "--stream-sorting-excludes" << exclude;
|
|
|
|
}
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
openStreamlink(channelURL, quality, args);
|
2018-03-24 14:13:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace chatterino
|