From de881769950bf34ed8af6ba8786dcfe378452f6d Mon Sep 17 00:00:00 2001 From: Anselm Busse Date: Wed, 10 Mar 2021 17:43:30 +0100 Subject: [PATCH] Add a 'list' option for the CLI This commit addresses Issue #448 by adding a command line option that allows the listing of all the Apps reported by the remote host as a CSV. --- app/app.pro | 2 + app/cli/commandlineparser.cpp | 46 ++++++++ app/cli/commandlineparser.h | 15 +++ app/cli/listapps.cpp | 190 ++++++++++++++++++++++++++++++++++ app/cli/listapps.h | 40 +++++++ app/gui/CliListAppsSegue.qml | 57 ++++++++++ app/main.cpp | 10 ++ app/qml.qrc | 1 + 8 files changed, 361 insertions(+) create mode 100644 app/cli/listapps.cpp create mode 100644 app/cli/listapps.h create mode 100644 app/gui/CliListAppsSegue.qml diff --git a/app/app.pro b/app/app.pro index ba94f3c8..c72ea990 100644 --- a/app/app.pro +++ b/app/app.pro @@ -143,6 +143,7 @@ SOURCES += \ backend/boxartmanager.cpp \ backend/richpresencemanager.cpp \ cli/commandlineparser.cpp \ + cli/listapps.cpp \ cli/quitstream.cpp \ cli/startstream.cpp \ settings/compatfetcher.cpp \ @@ -184,6 +185,7 @@ HEADERS += \ backend/boxartmanager.h \ backend/richpresencemanager.h \ cli/commandlineparser.h \ + cli/listapps.h \ cli/quitstream.h \ cli/startstream.h \ settings/streamingpreferences.h \ diff --git a/app/cli/commandlineparser.cpp b/app/cli/commandlineparser.cpp index 977a163d..d2c2ac7b 100644 --- a/app/cli/commandlineparser.cpp +++ b/app/cli/commandlineparser.cpp @@ -164,6 +164,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin "Starts Moonlight normally if no arguments are given.\n" "\n" "Available actions:\n" + " list List the available apps as CSV\n" " quit Quit the currently running app\n" " stream Start streaming an app\n" " pair Pair a new host\n" @@ -196,6 +197,8 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin return StreamRequested; } else if (action == "pair") { return PairRequested; + } else if (action == "list") { + return ListRequested; } } @@ -523,3 +526,46 @@ QString StreamCommandLineParser::getAppName() const { return m_AppName; } + +ListCommandLineParser::ListCommandLineParser() +{ +} + +ListCommandLineParser::~ListCommandLineParser() +{ +} + +void ListCommandLineParser::parse(const QStringList &args) +{ + CommandLineParser parser; + parser.setupCommonOptions(); + parser.setApplicationDescription( + "\n" + "List the available apps on the given host as CSV:\n" + "\tName, ID, HDR Support, App Collection Game, Hidden, Direct Launch, Path to Boxart" + ); + parser.addPositionalArgument("list", "list available apps"); + parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", ""); + + if (!parser.parse(args)) { + parser.showError(parser.errorText()); + } + + parser.handleUnknownOptions(); + + // This method will not return and terminates the process if --version or + // --help is specified + parser.handleHelpAndVersionOptions(); + + // Verify that host has been provided + auto posArgs = parser.positionalArguments(); + if (posArgs.length() < 2) { + parser.showError("Host not provided"); + } + m_Host = parser.positionalArguments().at(1); +} + +QString ListCommandLineParser::getHost() const +{ + return m_Host; +} diff --git a/app/cli/commandlineparser.h b/app/cli/commandlineparser.h index cb1d19ee..d003d7fb 100644 --- a/app/cli/commandlineparser.h +++ b/app/cli/commandlineparser.h @@ -13,6 +13,7 @@ public: StreamRequested, QuitRequested, PairRequested, + ListRequested, }; GlobalCommandLineParser(); @@ -72,3 +73,17 @@ private: QMap m_VideoDecoderMap; QMap m_CaptureSysKeysModeMap; }; + +class ListCommandLineParser +{ +public: + ListCommandLineParser(); + virtual ~ListCommandLineParser(); + + void parse(const QStringList &args); + + QString getHost() const; + +private: + QString m_Host; +}; diff --git a/app/cli/listapps.cpp b/app/cli/listapps.cpp new file mode 100644 index 00000000..ee5e621c --- /dev/null +++ b/app/cli/listapps.cpp @@ -0,0 +1,190 @@ +#include "listapps.h" + +#include "backend/boxartmanager.h" +#include "backend/computermanager.h" +#include "backend/computerseeker.h" +#include "streaming/session.h" + +#include +#include + +#define COMPUTER_SEEK_TIMEOUT 10000 +#define APP_SEEK_TIMEOUT 10000 + +namespace CliListApps +{ + +enum State { + StateInit, + StateSeekComputer, + StateListApp, + StateSeekApp, + StateFailure, +}; + +class Event +{ +public: + enum Type { + ComputerFound, + ComputerUpdated, + ComputerSeekTimedout, + Executed, + }; + + Event(Type type) + : type(type), computerManager(nullptr), computer(nullptr) {} + + Type type; + ComputerManager *computerManager; + NvComputer *computer; + QString errorMessage; +}; + +class LauncherPrivate +{ + Q_DECLARE_PUBLIC(Launcher) + +public: + LauncherPrivate(Launcher *q) : q_ptr(q) {} + + void handleEvent(Event event) + { + Q_Q(Launcher); + NvApp app; + + switch (event.type) { + // Occurs when CliListAppsSegue becomes visible and the UI calls launcher's execute() + case Event::Executed: + if (m_State == StateInit) { + m_State = StateSeekComputer; + m_ComputerManager = event.computerManager; + + m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q); + q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound, + q, &Launcher::onComputerFound); + q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout, + q, &Launcher::onComputerSeekTimeout); + m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT); + + q->connect(m_ComputerManager, &ComputerManager::computerStateChanged, + q, &Launcher::onComputerUpdated); + + m_BoxArtManager = new BoxArtManager(q); + + emit q->searchingComputer(); + } + break; + // Occurs when computer search timed out + case Event::ComputerSeekTimedout: + if (m_State == StateSeekComputer) { + m_State = StateFailure; + emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName)); + } + break; + // Occurs when searched computer is found + case Event::ComputerFound: + if (m_State == StateSeekComputer) { + if (event.computer->pairState == NvComputer::PS_PAIRED) { + m_State = StateSeekApp; + m_Computer = event.computer; + m_TimeoutTimer->start(APP_SEEK_TIMEOUT); + emit q->searchingApps(); + } else { + m_State = StateFailure; + QString msg = QObject::tr("Computer %1 has not been paired. " + "Please open Moonlight to pair before retrieving games list.") + .arg(event.computer->name); + emit q->failed(msg); + } + } + break; + // Occurs when a computer is updated + case Event::ComputerUpdated: + if (m_State == StateSeekApp) { + for (int i = 0; i < m_Computer->appList.length(); i++) { + printApp(m_Computer->appList[i]); + } + + QCoreApplication::exit(0); + } + break; + } + } + + void printApp(NvApp app) const + { + fprintf(stdout, "%s,%d,%s,%s,%s,%s,%s\n", qPrintable(app.name), + app.id, + app.hdrSupported ? "true" : "false", + app.isAppCollectorGame ? "true" : "false", + app.hidden ? "true" : "false", + app.directLaunch ? "true" : "false", + qPrintable(m_BoxArtManager->loadBoxArt(m_Computer, app).toDisplayString())); + } + + Launcher *q_ptr; + ComputerManager *m_ComputerManager; + QString m_ComputerName; + ComputerSeeker *m_ComputerSeeker; + BoxArtManager *m_BoxArtManager; + NvComputer *m_Computer; + State m_State; + QTimer *m_TimeoutTimer; +}; + +Launcher::Launcher(QString computer, QObject *parent) + : QObject(parent), + m_DPtr(new LauncherPrivate(this)) +{ + Q_D(Launcher); + d->m_ComputerName = computer; + d->m_State = StateInit; + d->m_TimeoutTimer = new QTimer(this); + d->m_TimeoutTimer->setSingleShot(true); + connect(d->m_TimeoutTimer, &QTimer::timeout, + this, &Launcher::onComputerSeekTimeout); +} + +Launcher::~Launcher() +{ +} + +void Launcher::execute(ComputerManager *manager) +{ + Q_D(Launcher); + Event event(Event::Executed); + event.computerManager = manager; + d->handleEvent(event); +} + +bool Launcher::isExecuted() const +{ + Q_D(const Launcher); + return d->m_State != StateInit; +} + +void Launcher::onComputerFound(NvComputer *computer) +{ + Q_D(Launcher); + Event event(Event::ComputerFound); + event.computer = computer; + d->handleEvent(event); +} + +void Launcher::onComputerSeekTimeout() +{ + Q_D(Launcher); + Event event(Event::ComputerSeekTimedout); + d->handleEvent(event); +} + +void Launcher::onComputerUpdated(NvComputer *computer) +{ + Q_D(Launcher); + Event event(Event::ComputerUpdated); + event.computer = computer; + d->handleEvent(event); +} + +} diff --git a/app/cli/listapps.h b/app/cli/listapps.h new file mode 100644 index 00000000..84181246 --- /dev/null +++ b/app/cli/listapps.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +class ComputerManager; +class NvComputer; + +namespace CliListApps +{ + +class LauncherPrivate; + +class Launcher : public QObject +{ + Q_OBJECT + Q_DECLARE_PRIVATE_D(m_DPtr, Launcher) + +public: + explicit Launcher(QString computer, QObject *parent = nullptr); + ~Launcher(); + + Q_INVOKABLE void execute(ComputerManager *manager); + Q_INVOKABLE bool isExecuted() const; + +signals: + void searchingComputer(); + void searchingApps(); + void failed(QString text); + +private slots: + void onComputerFound(NvComputer *computer); + void onComputerUpdated(NvComputer *computer); + void onComputerSeekTimeout(); + +private: + QScopedPointer m_DPtr; +}; + +} diff --git a/app/gui/CliListAppsSegue.qml b/app/gui/CliListAppsSegue.qml new file mode 100644 index 00000000..ccbe860c --- /dev/null +++ b/app/gui/CliListAppsSegue.qml @@ -0,0 +1,57 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.2 + +import ComputerManager 1.0 +import Session 1.0 + +Item { + function onSearchingComputer() { + stageLabel.text = qsTr("Establishing connection to PC...") + } + + function onSearchingApps() { + stageLabel.text = qsTr("Searching for Apps...") + } + + function onFailure(message) { + errorDialog.text = message + errorDialog.open() + } + + StackView.onActivated: { + if (!launcher.isExecuted()) { + toolBar.visible = false + launcher.searchingComputer.connect(onSearchingComputer) + launcher.searchingComputer.connect(onSearchingApps) + launcher.failed.connect(onFailure) + launcher.execute(ComputerManager) + } + } + + Row { + anchors.centerIn: parent + spacing: 5 + + BusyIndicator { + id: stageSpinner + } + + Label { + id: stageLabel + height: stageSpinner.height + text: stageText + font.pointSize: 20 + verticalAlignment: Text.AlignVCenter + + wrapMode: Text.Wrap + } + } + + ErrorMessageDialog { + id: errorDialog + + onClosed: { + Qt.quit() + } + } +} diff --git a/app/main.cpp b/app/main.cpp index dd778389..ab0d00a6 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -28,6 +28,7 @@ #include #endif +#include "cli/listapps.h" #include "cli/quitstream.h" #include "cli/startstream.h" #include "cli/pair.h" @@ -620,6 +621,15 @@ int main(int argc, char *argv[]) engine.rootContext()->setContextProperty("launcher", launcher); break; } + case GlobalCommandLineParser::ListRequested: + { + initialView = "qrc:/gui/CliListAppsSegue.qml"; + ListCommandLineParser listParser; + listParser.parse(app.arguments()); + auto launcher = new CliListApps::Launcher(listParser.getHost(), &app); + engine.rootContext()->setContextProperty("launcher", launcher); + break; + } } engine.rootContext()->setContextProperty("initialView", initialView); diff --git a/app/qml.qrc b/app/qml.qrc index 5c2c369f..12b68622 100644 --- a/app/qml.qrc +++ b/app/qml.qrc @@ -10,6 +10,7 @@ gui/NavigableToolButton.qml gui/NavigableItemDelegate.qml gui/NavigableMenuItem.qml + gui/CliListAppsSegue.qml gui/CliQuitStreamSegue.qml gui/CliStartStreamSegue.qml gui/AutoResizingComboBox.qml