feat: Add position handling in OPDSEntry and update navigation button behavior in the UI

This commit is contained in:
JB
2025-10-30 08:27:21 -07:00
parent 9acec3c309
commit 13c6b120c9
3 changed files with 45 additions and 12 deletions
+23
View File
@@ -50,6 +50,7 @@ class OPDSLink:
class OPDSEntry: class OPDSEntry:
id: str id: str
title: str title: str
position: Optional[int] = None
authors: List[str] = field(default_factory=list) authors: List[str] = field(default_factory=list)
updated: Optional[str] = None updated: Optional[str] = None
summary: Optional[str] = None summary: Optional[str] = None
@@ -62,6 +63,7 @@ class OPDSEntry:
return { return {
"id": self.id, "id": self.id,
"title": self.title, "title": self.title,
"position": self.position,
"authors": list(self.authors), "authors": list(self.authors),
"updated": self.updated, "updated": self.updated,
"summary": self.summary, "summary": self.summary,
@@ -243,6 +245,7 @@ class CalibreOPDSClient:
def _parse_entry(self, node: ET.Element, base_url: str) -> OPDSEntry: def _parse_entry(self, node: ET.Element, base_url: str) -> OPDSEntry:
entry_id = node.findtext("atom:id", default="", namespaces=NS).strip() entry_id = node.findtext("atom:id", default="", namespaces=NS).strip()
title = node.findtext("atom:title", default="Untitled", namespaces=NS).strip() or "Untitled" title = node.findtext("atom:title", default="Untitled", namespaces=NS).strip() or "Untitled"
position_value = self._extract_position(node)
updated = node.findtext("atom:updated", default=None, namespaces=NS) updated = node.findtext("atom:updated", default=None, namespaces=NS)
summary = node.findtext("atom:summary", default=None, namespaces=NS) or node.findtext( summary = node.findtext("atom:summary", default=None, namespaces=NS) or node.findtext(
"atom:content", default=None, namespaces=NS "atom:content", default=None, namespaces=NS
@@ -272,6 +275,7 @@ class CalibreOPDSClient:
return OPDSEntry( return OPDSEntry(
id=entry_id or title, id=entry_id or title,
title=title, title=title,
position=position_value,
authors=authors, authors=authors,
updated=updated, updated=updated,
summary=cleaned_summary, summary=cleaned_summary,
@@ -281,6 +285,25 @@ class CalibreOPDSClient:
links=list(parsed_links.values()), links=list(parsed_links.values()),
) )
def _extract_position(self, node: ET.Element) -> Optional[int]:
candidates = [
node.findtext("opds:position", default=None, namespaces=NS),
node.findtext("opds:groupPosition", default=None, namespaces=NS),
node.findtext("opds:order", default=None, namespaces=NS),
node.findtext("dc:identifier", default=None, namespaces=NS),
]
for value in candidates:
if value is None:
continue
text = str(value).strip()
if not text:
continue
try:
return int(float(text))
except (TypeError, ValueError):
continue
return None
def _parse_links(self, link_nodes: List[ET.Element], base_url: str) -> Dict[str, OPDSLink]: def _parse_links(self, link_nodes: List[ET.Element], base_url: str) -> Dict[str, OPDSLink]:
results: Dict[str, OPDSLink] = {} results: Dict[str, OPDSLink] = {}
for link in link_nodes: for link in link_nodes:
+1 -1
View File
@@ -2871,7 +2871,7 @@ def calibre_opds_import() -> ResponseReturnValue:
service.store_pending_job(pending) service.store_pending_job(pending)
redirect_url = url_for("web.prepare_job", pending_id=pending.id) redirect_url = url_for("web.prepare_job", pending_id=pending.id, step="book")
return jsonify({ return jsonify({
"pending_id": pending.id, "pending_id": pending.id,
"redirect_url": redirect_url, "redirect_url": redirect_url,
+14 -4
View File
@@ -213,18 +213,23 @@ if (modal && browser) {
]; ];
descriptors.forEach(({ key, label }) => { descriptors.forEach(({ key, label }) => {
const link = resolveRelLink(links, key) || resolveRelLink(links, `/${key}`); const link = resolveRelLink(links, key) || resolveRelLink(links, `/${key}`);
if (!link || !link.href) {
return;
}
const button = document.createElement('button'); const button = document.createElement('button');
button.type = 'button'; button.type = 'button';
button.className = 'button button--ghost'; button.className = 'button button--ghost';
button.textContent = label; button.textContent = label;
const hasLink = Boolean(link && link.href);
if (hasLink) {
button.addEventListener('click', () => { button.addEventListener('click', () => {
const targetQuery = key === 'up' ? '' : state.query; const targetQuery = key === 'up' ? '' : state.query;
const tabId = resolveTabIdForHref(link.href); const tabId = resolveTabIdForHref(link.href);
loadFeed({ href: link.href, query: targetQuery, activeTab: tabId || (targetQuery ? TabIds.SEARCH : TabIds.CUSTOM) }); loadFeed({ href: link.href, query: targetQuery, activeTab: tabId || (targetQuery ? TabIds.SEARCH : TabIds.CUSTOM) });
}); });
} else if (key === 'previous') {
button.disabled = true;
button.setAttribute('aria-disabled', 'true');
} else {
return;
}
navEl.appendChild(button); navEl.appendChild(button);
}); });
navEl.hidden = !navEl.childElementCount; navEl.hidden = !navEl.childElementCount;
@@ -239,7 +244,9 @@ if (modal && browser) {
const title = document.createElement('h3'); const title = document.createElement('h3');
title.className = 'opds-browser__title'; title.className = 'opds-browser__title';
title.textContent = entry.title || 'Untitled'; const positionLabel = Number.isFinite(entry?.position) ? Number(entry.position) : null;
const baseTitle = entry.title || 'Untitled';
title.textContent = positionLabel !== null ? `${positionLabel}. ${baseTitle}` : baseTitle;
header.appendChild(title); header.appendChild(title);
const authors = formatAuthors(entry.authors); const authors = formatAuthors(entry.authors);
@@ -375,6 +382,9 @@ if (modal && browser) {
try { try {
const target = new URL(redirectUrl, window.location.origin); const target = new URL(redirectUrl, window.location.origin);
target.searchParams.set('format', 'json'); target.searchParams.set('format', 'json');
if (!target.searchParams.has('step')) {
target.searchParams.set('step', 'book');
}
await wizard.requestStep(target.toString(), { method: 'GET' }); await wizard.requestStep(target.toString(), { method: 'GET' });
} catch (wizardError) { } catch (wizardError) {
console.error('Unable to open wizard via JSON payload', wizardError); console.error('Unable to open wizard via JSON payload', wizardError);