Repository navigation
Load OPDS covers with the catalog's credentials #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -125,6 +125,24 @@ pub async fn download( | |
| Ok((bytes, ct, cd)) | ||
| } | ||
|
|
||
| /// Fetch a cover or thumbnail with the catalog's credentials, as a data: URL. | ||
| /// The webview can't load these itself: an <img> sends no Authorization header, | ||
| /// so catalogs behind a login (Calibre-Web, COPS...) answer 401. | ||
| pub async fn image(auth: &Auth, url: &str) -> Result<String> { | ||
| let resp = check(apply(client(30).get(url).header(ACCEPT, "image/*"), auth).send().await?)?; | ||
| let ct = resp | ||
| .headers() | ||
| .get(reqwest::header::CONTENT_TYPE) | ||
| .and_then(|v| v.to_str().ok()) | ||
| .unwrap_or("") | ||
| .to_string(); | ||
| if !ct.starts_with("image/") { | ||
| return Err(anyhow!("Not an image ({})", ct)); | ||
| } | ||
| let bytes = resp.bytes().await?; | ||
| Ok(format!("data:{};base64,{}", ct, B64.encode(&bytes))) | ||
|
Comment on lines
+142
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Limit image bytes before collecting and encoding them. When an image endpoint sends a very large body, 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /// Parse an OpenSearch Description Document and return the best atom/opds | ||
| /// search URL template. Returns None if no usable template is found. | ||
| /// Expand an OPDS search template. Catalogs advertise either OpenSearch's | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,11 @@ const authKey = (cat) => JSON.stringify(cat.auth ?? null) | |
| export const feed = (cat, url = cat.url) => memo(`f ${authKey(cat)} ${url}`, () => invoke('opds_feed', { url, auth: cat.auth })) | ||
| export const search = (cat, query) => memo(`s ${authKey(cat)} ${cat.url} ${query}`, () => invoke('opds_search', { url: cat.url, query, auth: cat.auth })) | ||
|
|
||
| // An <img> sends no credentials, so covers from a catalog behind a login are fetched | ||
| // by the app (with the catalog's auth) and handed back as data: URLs. | ||
| export const needsAuthedImage = (cat, url) => !!url && (cat.auth?.kind ?? 'none') !== 'none' && /^https?:/i.test(url) | ||
| export const image = (cat, url) => memo(`i ${authKey(cat)} ${url}`, () => invoke('opds_image', { url, auth: cat.auth })) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift Bound the authenticated-image cache. When a reader browses new covers, each data URL remains in 🤖 Prompt for AI Agents |
||
|
|
||
| // Rows worth showing from a feed's navigation: drop page chrome (start, self, html alternates). | ||
| export const navRows = (links) => | ||
| links.filter((l) => !['start', 'self', 'alternate', 'search', 'up'].includes(l.rel) && !/html/.test(l.mime ?? '')) | ||
|
|
@@ -75,12 +80,44 @@ const onProgress = (cb) => { | |
| return ch | ||
| } | ||
|
|
||
| export const download = (cat, entry, acq, progress = () => {}) => | ||
| // A data: URL of the image scaled down to `width` px, as JPEG. | ||
| function shrink(src, width) { | ||
| return new Promise((resolve, reject) => { | ||
| const img = new Image() | ||
| img.onload = () => { | ||
| const canvas = document.createElement('canvas') | ||
| canvas.width = Math.min(width, img.naturalWidth) | ||
| canvas.height = Math.round((img.naturalHeight * canvas.width) / img.naturalWidth) | ||
| canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height) | ||
| resolve(canvas.toDataURL('image/jpeg', 0.85)) | ||
| } | ||
| img.onerror = reject | ||
| img.src = src | ||
| }) | ||
| } | ||
|
|
||
| // The cover kept with a download. The downloads list has no catalog credentials, so a | ||
| // cover behind a login is stored as a small data: URL (the list shows it 40px wide). | ||
| export async function storedCover(cat, url) { | ||
| if (!needsAuthedImage(cat, url)) return url ?? null | ||
| try { | ||
| return await shrink(await image(cat, url), 120) | ||
| } catch { | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| export const download = async (cat, entry, acq, progress = () => {}) => | ||
| invoke('download_book', { | ||
| url: acq.href, | ||
| mime: acq.mime, | ||
| auth: cat.auth, | ||
| meta: { title: entry.title, author: entry.authors[0]?.replace(/^([^,]+), ([^,]+)$/, '$2 $1') ?? null, cover: entry.cover ?? entry.thumbnail ?? null, source: cat.name }, | ||
| meta: { | ||
| title: entry.title, | ||
| author: entry.authors[0]?.replace(/^([^,]+), ([^,]+)$/, '$2 $1') ?? null, | ||
| cover: await storedCover(cat, entry.cover ?? entry.thumbnail), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Do not hold the book download behind a slow cover. If an authenticated cover request stalls, 🤖 Prompt for AI Agents |
||
| source: cat.name, | ||
| }, | ||
| onProgress: onProgress(progress), | ||
| }) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: crosspoint-reader/crosspoint-sync
Length of output: 1743
🏁 Script executed:
Repository: crosspoint-reader/crosspoint-sync
Length of output: 27422
🏁 Script executed:
Repository: crosspoint-reader/crosspoint-sync
Length of output: 42108
Restrict catalog credentials to approved image origins.
OPDS 2 image links resolve absolute URLs without restricting their origin.
needsAuthedImagethen sends any HTTP(S) cover URL toopds_image, which applies the catalog credentials before the initial request. Validate the destination against the catalog origin or an explicit allowlist before applyingauth. Redirect handling does not protect this initial request.🤖 Prompt for AI Agents
Source: Learnings