From bc8478161982e8ee8a94d75d208e651766f9d600 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Fri, 3 Apr 2026 12:05:40 -0500 Subject: Add modl:// protocol support for mod.pub downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle modl:// links alongside nxm:// for downloading mods from mod.pub. mod.pub uses a different URL format than Nexus — the download URL is embedded as a query parameter: modl://game/?url= - Register x-scheme-handler/modl in desktop files and MIME lists - Accept both nxm and modl schemes in URL parsing (NxmLink, NXMUrl) - Parse modl:// direct download links and route to startDownloadURLs - Make key/expires optional in NxmLink::parse (not used by mod.pub) - Add debug logging for received URLs to aid troubleshooting Co-Authored-By: Claude Opus 4.6 (1M context) --- src/src/main.cpp | 7 ++++++ src/src/nxmhandler_linux.cpp | 51 +++++++++++++++++++++++++++++--------------- src/src/nxmhandler_linux.h | 1 + src/src/shared/util.cpp | 3 ++- 4 files changed, 44 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/src/main.cpp b/src/src/main.cpp index b12ac7e..eed977e 100644 --- a/src/src/main.cpp +++ b/src/src/main.cpp @@ -205,6 +205,13 @@ int run(int argc, char* argv[]) .arg(link.expires) .arg(link.user_id)); }); + + QObject::connect(&nxmHandler, &NxmHandlerLinux::directDownloadReceived, + &app.core(), [&](const QString& url, const QString&) { + QMetaObject::invokeMethod(&app.core(), [&app, url] { + app.core().downloadManager()->startDownloadURLs(QStringList{url}); + }, Qt::QueuedConnection); + }); } #endif diff --git a/src/src/nxmhandler_linux.cpp b/src/src/nxmhandler_linux.cpp index fbf94d7..9441375 100644 --- a/src/src/nxmhandler_linux.cpp +++ b/src/src/nxmhandler_linux.cpp @@ -102,12 +102,15 @@ void updateMimeAppsList(const QString& path, const QString& mimeType, std::optional NxmLink::parse(const QString& url) { const QUrl parsed(url); - if (!parsed.isValid() || parsed.scheme().compare("nxm", Qt::CaseInsensitive) != 0) { + const QString scheme = parsed.scheme().toLower(); + if (!parsed.isValid() || (scheme != "nxm" && scheme != "modl")) { + log::debug("NxmLink::parse rejected scheme: '{}'", url); return {}; } const QString gameDomain = parsed.host().trimmed(); if (gameDomain.isEmpty()) { + log::debug("NxmLink::parse rejected empty host: '{}'", url); return {}; } @@ -115,6 +118,7 @@ std::optional NxmLink::parse(const QString& url) parsed.path().split('/', Qt::SkipEmptyParts); if (parts.size() != 4 || parts[0] != "mods" || parts[2] != "files") { + log::debug("NxmLink::parse rejected path (expected /mods/ID/files/ID): '{}'", url); return {}; } @@ -123,22 +127,15 @@ std::optional NxmLink::parse(const QString& url) const uint64_t modId = parts[1].toULongLong(&modOk); const uint64_t fileId = parts[3].toULongLong(&fileOk); if (!modOk || !fileOk) { + log::debug("NxmLink::parse rejected non-numeric IDs: '{}'", url); return {}; } + // key/expires are required for NXM but optional for modl:// (mod.pub). const QUrlQuery query(parsed); - const QString key = query.queryItemValue("key"); - if (key.isEmpty()) { - return {}; - } - - bool expiresOk = false; - const uint64_t expires = query.queryItemValue("expires").toULongLong(&expiresOk); - if (!expiresOk) { - return {}; - } - - const int userId = query.queryItemValue("user_id").toInt(); + const QString key = query.queryItemValue("key"); + const uint64_t expires = query.queryItemValue("expires").toULongLong(); + const int userId = query.queryItemValue("user_id").toInt(); return NxmLink{gameDomain, modId, fileId, key, expires, userId}; } @@ -229,7 +226,7 @@ void NxmHandlerLinux::registerHandler() const "Type=Application\n" "Name=Mod Organizer 2 NXM Handler\n" "Exec=%1\n" - "MimeType=x-scheme-handler/nxm;\n" + "MimeType=x-scheme-handler/nxm;x-scheme-handler/modl;\n" "NoDisplay=true\n").arg(execLine); if (!writeTextFile(desktopPath, desktop)) { @@ -241,6 +238,10 @@ void NxmHandlerLinux::registerHandler() const "mo2-nxm-handler.desktop"); updateMimeAppsList(appsDir + "/mimeapps.list", "x-scheme-handler/nxm", "mo2-nxm-handler.desktop"); + updateMimeAppsList(configDir + "/mimeapps.list", "x-scheme-handler/modl", + "mo2-nxm-handler.desktop"); + updateMimeAppsList(appsDir + "/mimeapps.list", "x-scheme-handler/modl", + "mo2-nxm-handler.desktop"); const auto result = QProcess::execute("update-desktop-database", QStringList() << appsDir); @@ -302,13 +303,29 @@ void NxmHandlerLinux::processSocketData(QLocalSocket* socket) continue; } + log::info("received link on socket: {}", line); + + // Try NXM-style parse first (nxm:// or modl:// with /mods/ID/files/ID path). const auto link = NxmLink::parse(line); - if (!link) { - log::warn("received invalid nxm url on socket: {}", line); + if (link) { + emit nxmReceived(*link); continue; } - emit nxmReceived(*link); + // modl:// direct download: modl://GAME/?url= + const QUrl parsed(line); + if (parsed.isValid() && parsed.scheme().compare("modl", Qt::CaseInsensitive) == 0) { + const QUrlQuery query(parsed); + const QString downloadUrl = query.queryItemValue("url", QUrl::FullyDecoded); + if (!downloadUrl.isEmpty()) { + const QString gameDomain = parsed.host().trimmed(); + log::info("modl direct download for '{}': {}", gameDomain, downloadUrl); + emit directDownloadReceived(downloadUrl, gameDomain); + continue; + } + } + + log::warn("received unrecognized url on socket: {}", line); } } diff --git a/src/src/nxmhandler_linux.h b/src/src/nxmhandler_linux.h index b6bd675..c4631aa 100644 --- a/src/src/nxmhandler_linux.h +++ b/src/src/nxmhandler_linux.h @@ -41,6 +41,7 @@ public: signals: void nxmReceived(NxmLink link); + void directDownloadReceived(QString url, QString gameDomain); private: void onNewConnection(); diff --git a/src/src/shared/util.cpp b/src/src/shared/util.cpp index 2c982dc..70c0aeb 100644 --- a/src/src/shared/util.cpp +++ b/src/src/shared/util.cpp @@ -510,5 +510,6 @@ void ResetExitFlag() bool isNxmLink(const QString& link) { - return link.startsWith("nxm://", Qt::CaseInsensitive); + return link.startsWith("nxm://", Qt::CaseInsensitive) || + link.startsWith("modl://", Qt::CaseInsensitive); } -- cgit v1.3.1