From c0ee09684152ca7d65377938c6d0f2a6b29cc65a Mon Sep 17 00:00:00 2001 From: Brandon Philips Date: Mon, 20 Apr 2026 13:35:17 -0700 Subject: [PATCH] fix: relative opds paths and query param with copyparty (#1535) ## Summary * **What is the goal of this PR?** This PR fixes bugs when using Copyparty as an OPDS server. https://github.com/9001/copyparty?tab=readme-ov-file#opds-feeds OPDS uses a query parameter `?opds` to differentiate between HTML requests and OPDS requests for the same path. It also uses relative paths in the responses instead of full paths. Crosspoint didn't handle these two cases. * **What changes are included?** Fixes to the two issues above. ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). Here is some example XML from my Copyparty instance: https://gist.github.com/philips/9ecec29dfb69ed0591b032f16e799675 ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO >**_ Partially. I used Claude code to write the fix. I am not a strong C++ programmer. But, I manually compiled and tested. --------- Co-authored-by: Claude Sonnet 4.6 --- src/activities/browser/OpdsBookBrowserActivity.cpp | 11 ++++++++--- src/util/UrlUtils.cpp | 13 +++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index e4c6f9fd9..e78e9373a 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -225,7 +225,10 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) { navigationHistory.push_back(currentPath); - currentPath = entry.href; + // Resolve to a full URL so sub-sub-navigation retains parent path context + const std::string feedUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, currentPath); + currentPath = UrlUtils::buildUrl(feedUrl, entry.href); + state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); entries.clear(); @@ -255,10 +258,12 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { downloadProgress = downloadTotal = 0; requestUpdate(true); - std::string downloadUrl = - (book.href.find("http") == 0) ? book.href : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); + // Build full download URL relative to the current feed, not the root server URL + const std::string feedUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, currentPath); + std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href); std::string filename = "/" + StringUtils::sanitizeFilename(book.title + (book.author.empty() ? "" : " - " + book.author)) + ".epub"; + LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str()); const auto result = HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) { diff --git a/src/util/UrlUtils.cpp b/src/util/UrlUtils.cpp index a2156552b..34e24914e 100644 --- a/src/util/UrlUtils.cpp +++ b/src/util/UrlUtils.cpp @@ -37,11 +37,16 @@ std::string buildUrl(const std::string& serverUrl, const std::string& path) { // Absolute path - use just the host return extractHost(urlWithProtocol) + path; } - // Relative path - append to server URL - if (urlWithProtocol.back() == '/') { - return urlWithProtocol + path; + // Relative path - strip query string from base before appending + std::string base = urlWithProtocol; + const size_t queryPos = base.find('?'); + if (queryPos != std::string::npos) { + base.resize(queryPos); } - return urlWithProtocol + "/" + path; + if (base.back() == '/') { + return base + path; + } + return base + "/" + path; } } // namespace UrlUtils