Files
hermes-config/scripts/scan_calibre_library.py

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}")