135 lines
4.4 KiB
Python
135 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Scan Calibre-web OPDS catalog for all books and current read status."""
|
|
import json, os, xml.etree.ElementTree as ET
|
|
import urllib.request
|
|
import base64
|
|
|
|
BASE = "http://192.168.68.190:8083"
|
|
AUTH = base64.b64encode(b"andy:Nimbly-Rumble-Unlucky9").decode()
|
|
NS = {"atom": "http://www.w3.org/2005/Atom"}
|
|
|
|
def opds_fetch(path):
|
|
req = urllib.request.Request(f"{BASE}{path}")
|
|
req.add_header("Authorization", f"Basic {AUTH}")
|
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
return resp.read().decode()
|
|
|
|
# Get all letter pages
|
|
root_xml = opds_fetch("/opds/books")
|
|
root = ET.fromstring(root_xml)
|
|
letters = []
|
|
for entry in root.findall("atom:entry", NS):
|
|
link = entry.find("atom:link", NS)
|
|
if link is not None:
|
|
href = link.get("href", "")
|
|
if href:
|
|
letters.append(href)
|
|
|
|
# Fetch each letter page
|
|
all_books = []
|
|
seen_titles = set()
|
|
for letter_path in letters:
|
|
xml_data = opds_fetch(letter_path)
|
|
feed = ET.fromstring(xml_data)
|
|
for entry in feed.findall("atom:entry", NS):
|
|
title_el = entry.find("atom:title", NS)
|
|
title = title_el.text.strip() if title_el is not None and title_el.text else "Unknown"
|
|
|
|
author_el = entry.find("atom:author", NS)
|
|
author = "Unknown"
|
|
if author_el is not None:
|
|
name_el = author_el.find("atom:name", NS)
|
|
if name_el is not None and name_el.text:
|
|
author = name_el.text.strip()
|
|
|
|
key = f"{title}|{author}"
|
|
if key in seen_titles:
|
|
continue
|
|
seen_titles.add(key)
|
|
|
|
categories = []
|
|
for cat in entry.findall("atom:category", NS):
|
|
label = cat.get("label", "")
|
|
if label:
|
|
categories.append(label)
|
|
|
|
pub_el = entry.find("atom:published", NS)
|
|
pub_year = pub_el.text[:4] if pub_el is not None and pub_el.text else ""
|
|
|
|
all_books.append({
|
|
"title": title,
|
|
"author": author,
|
|
"pub_year": pub_year,
|
|
"categories": categories
|
|
})
|
|
|
|
# Also scan read books list via session (need to login first)
|
|
# Use web scraping approach
|
|
import http.cookiejar, urllib.parse
|
|
|
|
cj = http.cookiejar.CookieJar()
|
|
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
|
|
|
|
# Get login page + CSRF
|
|
login_resp = opener.open(f"{BASE}/login")
|
|
login_html = login_resp.read().decode()
|
|
csrf_match = __import__('re').search(r'csrf_token" value="([^"]+)"', login_html)
|
|
csrf_token = csrf_match.group(1) if csrf_match else ""
|
|
|
|
# Login
|
|
login_data = urllib.parse.urlencode({
|
|
"csrf_token": csrf_token,
|
|
"username": "andy",
|
|
"password": "Nimbly-Rumble-Unlucky9",
|
|
"remember_me": "true",
|
|
"submit": ""
|
|
}).encode()
|
|
opener.open(f"{BASE}/login", data=login_data)
|
|
|
|
# Get read books page
|
|
read_resp = opener.open(f"{BASE}/read/stored")
|
|
read_html = read_resp.read().decode()
|
|
|
|
# Extract read book titles - look for links with checkmark
|
|
read_titles = []
|
|
for line in read_html.split('\n'):
|
|
# Find book links in read books section
|
|
# Pattern: href="/book/XX" and the title after
|
|
pass
|
|
|
|
# Simpler: just get the page and extract
|
|
import re
|
|
# Find "Read Books (N)" heading
|
|
read_match = re.search(r'Read Books \((\d+)\)', read_html)
|
|
read_count = int(read_match.group(1)) if read_match else 0
|
|
|
|
# Find book titles that come after "Read Books" heading
|
|
# The HTML has a pattern: links to /book/ID with the title as link text
|
|
book_links = re.findall(r'href="/book/\d+">([^<]+)', read_html)
|
|
#print(f"All book links: {book_links}")
|
|
|
|
# For read books, they appear in the /read/stored page specifically
|
|
book_links_in_read = re.findall(r'href="/book/(\d+)">([^<]+)', read_html)
|
|
print(f"Read count from page: {read_count}")
|
|
print(f"Read books detected: {len(book_links_in_read)}")
|
|
read_books = []
|
|
for bid, btitle in book_links_in_read:
|
|
read_books.append({"id": bid, "title": btitle.strip()})
|
|
print(f" - {btitle.strip()} (id={bid})")
|
|
|
|
# Generate output
|
|
output = {
|
|
"total_books_in_library": len(all_books),
|
|
"read_count": read_count,
|
|
"read_books": read_books,
|
|
"library": sorted(all_books, key=lambda x: (x["author"], x["title"]))
|
|
}
|
|
|
|
print(f"\nTotal books in library: {len(all_books)}")
|
|
print(f"Books read: {read_count}")
|
|
|
|
# Save to file
|
|
out_path = "/root/.hermes/reading_library.json"
|
|
with open(out_path, "w") as f:
|
|
json.dump(output, f, indent=2)
|
|
print(f"\nSaved to {out_path}") |