docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop. - Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht. Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben. /books/ mit X-Server-Now, /api/sync/ mit server_now. - Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert. Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat. Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle". - Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste Stelle, Liste und Snapshot). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
67 lines
2.8 KiB
Python
67 lines
2.8 KiB
Python
from django.db import models
|
|
from django.contrib.auth.models import User
|
|
|
|
|
|
class EBook(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ebooks')
|
|
meta_ct = models.TextField() # base64 AES-GCM ciphertext of {title, author, filename}
|
|
meta_iv = models.CharField(max_length=32) # hex IV for metadata
|
|
data_ct = models.TextField() # base64 AES-GCM ciphertext of raw EPUB bytes
|
|
data_iv = models.CharField(max_length=32) # hex IV for EPUB data
|
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
is_read = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
ordering = ['uploaded_at']
|
|
|
|
def __str__(self):
|
|
return f"EBook #{self.pk} (user={self.user_id})"
|
|
|
|
|
|
class EBookProgress(models.Model):
|
|
"""Where to continue a book: the place it was last read, on any device
|
|
(docs/progress-sync.md), plus the furthest place ever reached."""
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ebook_progress')
|
|
book = models.ForeignKey(EBook, on_delete=models.CASCADE, related_name='progress')
|
|
scroll_fraction = models.FloatField(default=0.0)
|
|
position_anchor = models.CharField(max_length=30, blank=True, default='')
|
|
# When the reader was at that place (ms since epoch, server clock); null before 2026-10-12.
|
|
read_at = models.BigIntegerField(null=True, blank=True)
|
|
device = models.CharField(max_length=40, blank=True, default='')
|
|
furthest_fraction = models.FloatField(default=0.0)
|
|
furthest_anchor = models.CharField(max_length=30, blank=True, default='')
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
unique_together = ('user', 'book')
|
|
|
|
def __str__(self):
|
|
return f"Progress book={self.book_id} user={self.user_id} {self.scroll_fraction:.2f}"
|
|
|
|
|
|
class EBookHighlights(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ebook_highlights')
|
|
book = models.ForeignKey(EBook, on_delete=models.CASCADE, related_name='highlights')
|
|
ct = models.TextField() # base64 AES-GCM ciphertext of JSON array
|
|
iv = models.CharField(max_length=32) # hex IV
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
unique_together = ('user', 'book')
|
|
|
|
def __str__(self):
|
|
return f"Highlights book={self.book_id} user={self.user_id}"
|
|
|
|
|
|
class EBookBookmarks(models.Model):
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ebook_bookmarks')
|
|
book = models.ForeignKey(EBook, on_delete=models.CASCADE, related_name='bookmarks')
|
|
ct = models.TextField()
|
|
iv = models.CharField(max_length=32)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
unique_together = ('user', 'book')
|
|
|
|
def __str__(self):
|
|
return f"Bookmarks book={self.book_id} user={self.user_id}"
|