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 <noreply@anthropic.com>
This commit is contained in:
Brandon Philips
2026-04-20 15:35:17 -05:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 1a145fe085
commit c0ee096841
2 changed files with 17 additions and 7 deletions
@@ -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) {
+9 -4
View File
@@ -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