From ab17992ebb8ea5d95bb1584d25484879bcacaadd Mon Sep 17 00:00:00 2001 From: haraldpdl Date: Sun, 19 Apr 2026 10:51:45 +0200 Subject: [PATCH] fix(opds): atom:updated reflects last modification, not date added Books.atom_timestamp returned Books.timestamp (date added), which is set at import and never changes. OPDS clients use atom:updated to decide whether a book has changed on the server, so cover swaps, metadata edits, and any other post-import change were invisible to sync clients; they would keep serving the stale cover and title until a manual refresh. Atom RFC 4287 defines updated as "the most recent instant in time when an entry or feed was modified", and Calibre already tracks that field as last_modified, bumping it on every metadata and cover edit. Switching the property to return last_modified (with a fallback to timestamp when last_modified is NULL) aligns Calibre-Web's behaviour with the Atom contract. This change only affects the OPDS feed's atom:updated element. Kobo sync uses its own last_modified comparison path, so it is unaffected. --- cps/db.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cps/db.py b/cps/db.py index 74b25c89..08cdba90 100644 --- a/cps/db.py +++ b/cps/db.py @@ -449,7 +449,16 @@ class Books(Base): @property def atom_timestamp(self): - return self.timestamp.strftime('%Y-%m-%dT%H:%M:%S+00:00') or '' + # OPDS atom:updated is defined as "the most recent instant in time + # when the entry was modified". Books.timestamp is the date added and + # never changes after import, so metadata and cover edits were + # invisible to OPDS sync clients. Use last_modified, which Calibre + # updates on every metadata or cover change; fall back to timestamp + # only if last_modified happens to be missing. + t = self.last_modified or self.timestamp + if t is None: + return '' + return t.strftime('%Y-%m-%dT%H:%M:%S+00:00') or '' class CustomColumns(Base):