A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Brandon Philips
Claude Sonnet 4.6
and committed by
GitHub
c0ee0968 1a145fe0

+17 -7
+8 -3
src/activities/browser/OpdsBookBrowserActivity.cpp
··· 225 225 226 226 void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) { 227 227 navigationHistory.push_back(currentPath); 228 - currentPath = entry.href; 228 + // Resolve to a full URL so sub-sub-navigation retains parent path context 229 + const std::string feedUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, currentPath); 230 + currentPath = UrlUtils::buildUrl(feedUrl, entry.href); 231 + 229 232 state = BrowserState::LOADING; 230 233 statusMessage = tr(STR_LOADING); 231 234 entries.clear(); ··· 255 258 downloadProgress = downloadTotal = 0; 256 259 requestUpdate(true); 257 260 258 - std::string downloadUrl = 259 - (book.href.find("http") == 0) ? book.href : UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); 261 + // Build full download URL relative to the current feed, not the root server URL 262 + const std::string feedUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, currentPath); 263 + std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href); 260 264 std::string filename = 261 265 "/" + StringUtils::sanitizeFilename(book.title + (book.author.empty() ? "" : " - " + book.author)) + ".epub"; 266 + LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str()); 262 267 263 268 const auto result = 264 269 HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
+9 -4
src/util/UrlUtils.cpp
··· 37 37 // Absolute path - use just the host 38 38 return extractHost(urlWithProtocol) + path; 39 39 } 40 - // Relative path - append to server URL 41 - if (urlWithProtocol.back() == '/') { 42 - return urlWithProtocol + path; 40 + // Relative path - strip query string from base before appending 41 + std::string base = urlWithProtocol; 42 + const size_t queryPos = base.find('?'); 43 + if (queryPos != std::string::npos) { 44 + base.resize(queryPos); 45 + } 46 + if (base.back() == '/') { 47 + return base + path; 43 48 } 44 - return urlWithProtocol + "/" + path; 49 + return base + "/" + path; 45 50 } 46 51 47 52 } // namespace UrlUtils