fix: Include pending_id in redirect URL and form data when available

This commit is contained in:
JB
2025-11-29 12:47:51 -08:00
parent f2ba8f692c
commit 59c8568348
3 changed files with 123 additions and 0 deletions
+113
View File
@@ -1111,6 +1111,96 @@ class CalibreOPDSClient:
filtered = [e for s, e in scored_entries]
return dataclasses.replace(feed, entries=filtered)
def _estimate_letter_position(self, letter: str) -> float:
"""Estimate the relative position (0.0-1.0) of a letter in an alphabetical list."""
if letter == "#":
return 0.0
if not letter or not letter.isalpha():
return 0.0
# Approximate cumulative distribution of starting letters in English book titles
# This is a heuristic to jump closer to the target
weights = {
'A': 0.00, 'B': 0.08, 'C': 0.15, 'D': 0.22, 'E': 0.28,
'F': 0.33, 'G': 0.38, 'H': 0.43, 'I': 0.49, 'J': 0.53,
'K': 0.55, 'L': 0.58, 'M': 0.63, 'N': 0.68, 'O': 0.71,
'P': 0.75, 'Q': 0.80, 'R': 0.81, 'S': 0.85, 'T': 0.92,
'U': 0.97, 'V': 0.98, 'W': 0.99, 'X': 0.995, 'Y': 0.997, 'Z': 0.999
}
return weights.get(letter.upper(), 0.0)
def _attempt_smart_jump(self, feed: OPDSFeed, letter: str) -> Optional[OPDSFeed]:
"""
Attempt to jump to a page closer to the target letter by analyzing pagination links.
Returns a new OPDSFeed if a jump was successful, or None.
"""
first_link = self._resolve_link(feed.links, "first")
last_link = self._resolve_link(feed.links, "last")
next_link = self._resolve_link(feed.links, "next")
if not (first_link and last_link and next_link):
return None
# Try to extract offsets from URLs to determine page size and total items
# Common Calibre pattern: .../offset/0, .../offset/50
def extract_offset(href: str) -> Optional[int]:
match = re.search(r"/(\d+)/?$", href)
if match:
return int(match.group(1))
# Try query param
parsed = urlparse(href)
qs = dict(pair.split('=') for pair in parsed.query.split('&') if '=' in pair)
if 'offset' in qs:
try:
return int(qs['offset'])
except ValueError:
pass
return None
start_offset = extract_offset(first_link.href)
next_offset = extract_offset(next_link.href)
last_offset = extract_offset(last_link.href)
if start_offset is None or next_offset is None or last_offset is None:
return None
page_size = next_offset - start_offset
if page_size <= 0:
return None
# Estimate total items (last_offset is the start of the last page)
# We assume the last page is roughly half full for estimation
total_items = last_offset + (page_size // 2)
target_ratio = self._estimate_letter_position(letter)
# Aim slightly early (subtract 1-2 pages worth) to ensure we don't miss the start
target_offset = int(total_items * target_ratio)
target_offset = max(0, target_offset - (page_size * 2))
# Round to nearest page boundary
target_offset = (target_offset // page_size) * page_size
# If the jump is too small (e.g. we are already near the start), don't bother
if target_offset < (page_size * 3):
return None
# Construct the new URL
# We assume the URL structure is consistent and we can just replace the offset
# This is risky but works for standard Calibre OPDS
base_href = first_link.href
if str(start_offset) in base_href:
# Path based replacement
# Replace the last occurrence of the offset
parts = base_href.rsplit(str(start_offset), 1)
if len(parts) == 2:
new_href = f"{parts[0]}{target_offset}{parts[1]}"
try:
return self.fetch_feed(new_href)
except Exception:
return None
return None
def browse_letter(
self,
letter: str,
@@ -1164,6 +1254,29 @@ class CalibreOPDSClient:
seen_ids.add(entry_id)
collected.append(entry)
# Check the first page for navigation links before attempting any jumps
# This handles the case where "By Title" has "A", "B", "C" folders
has_nav_links = False
for entry in base_feed.entries:
if self._has_navigation_link(entry):
has_nav_links = True
if letter_matches(entry, mode):
for link in entry.links:
if self._is_navigation_link(link):
href = (link.href or "").strip()
if href:
letter_href = href
break
if letter_href:
break
# If we didn't find a direct link, and it looks like a flat list (no nav links matching criteria),
# try to jump closer to the target letter if we are in a sorted mode
if not letter_href and mode in {"title", "author", "series"}:
jump_feed = self._attempt_smart_jump(base_feed, key)
if jump_feed:
base_feed = jump_feed
for page in self._iter_paginated_feeds(base_feed, max_pages=max_pages):
for entry in page.entries:
if not letter_matches(entry, mode):
+4
View File
@@ -799,6 +799,9 @@ if (modal && browser) {
if (wizard?.requestStep) {
try {
const target = new URL(redirectUrl, window.location.origin);
if (payload.pending_id && !target.searchParams.has('pending_id')) {
target.searchParams.set('pending_id', payload.pending_id);
}
target.searchParams.set('format', 'json');
if (!target.searchParams.has('step')) {
target.searchParams.set('step', 'book');
@@ -812,6 +815,7 @@ if (modal && browser) {
window.location.assign(redirectUrl);
}
}
} catch (error) {
setStatus(error instanceof Error ? error.message : 'Unable to queue this book.', 'error');
} finally {
+6
View File
@@ -330,6 +330,12 @@ const submitWizardForm = async (form, submitter) => {
if (submitter && submitter.name && !formData.has(submitter.name)) {
formData.append(submitter.name, submitter.value ?? "");
}
// Ensure pending_id is included if available in modal state but missing from form
if (!formData.get("pending_id") && modal && modal.dataset.pendingId) {
formData.set("pending_id", modal.dataset.pendingId);
}
const allowValidation = !submitter?.hasAttribute("formnovalidate") && !form.noValidate;
if (allowValidation && typeof form.reportValidity === "function" && !form.reportValidity()) {
return;