aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-03 12:05:40 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-03 12:05:40 -0500
commitbc8478161982e8ee8a94d75d208e651766f9d600 (patch)
treed703e56502c9813e3ffbb393c3fffebca03e6516
parentd2eec897b2ec1022744a993135e3595c8fea005b (diff)
Add modl:// protocol support for mod.pub downloads
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=<encoded-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) <noreply@anthropic.com>
-rw-r--r--data/icons/com.fluorine.manager.desktop2
-rw-r--r--libs/uibase/src/nxmurl.cpp2
-rw-r--r--src/src/main.cpp7
-rw-r--r--src/src/nxmhandler_linux.cpp51
-rw-r--r--src/src/nxmhandler_linux.h1
-rw-r--r--src/src/shared/util.cpp3
6 files changed, 46 insertions, 20 deletions
diff --git a/data/icons/com.fluorine.manager.desktop b/data/icons/com.fluorine.manager.desktop
index ff62c76..5d85cf8 100644
--- a/data/icons/com.fluorine.manager.desktop
+++ b/data/icons/com.fluorine.manager.desktop
@@ -8,7 +8,7 @@ Icon=com.fluorine.manager
Terminal=false
Categories=Game;Utility;
Keywords=mod;organizer;modding;skyrim;fallout;
-MimeType=x-scheme-handler/nxm;
+MimeType=x-scheme-handler/nxm;x-scheme-handler/modl;
Actions=create-portable;list-instances;
[Desktop Action create-portable]
diff --git a/libs/uibase/src/nxmurl.cpp b/libs/uibase/src/nxmurl.cpp
index d09cdde..33bb22f 100644
--- a/libs/uibase/src/nxmurl.cpp
+++ b/libs/uibase/src/nxmurl.cpp
@@ -29,7 +29,7 @@ NXMUrl::NXMUrl(const QString& url)
{
QUrl nxm(url);
QUrlQuery query(nxm);
- QRegularExpression exp("nxm://[a-z0-9]+/mods/(\\d+)/files/(\\d+)",
+ QRegularExpression exp("(?:nxm|modl)://[a-z0-9]+/mods/(\\d+)/files/(\\d+)",
QRegularExpression::CaseInsensitiveOption);
auto match = exp.match(url);
if (!match.hasMatch()) {
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> 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> 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> 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=<encoded-download-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);
}