2026-08-15 16:20:54 +02:00
|
|
|
from django.db.models import Max
|
|
|
|
|
from django.http import JsonResponse
|
|
|
|
|
from django.utils import timezone
|
|
|
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
|
|
2026-10-19 15:37:11 +02:00
|
|
|
from books.models import EBook, EBookProgress, EBookHighlights, EBookBookmarks, book_rows
|
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
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>
2026-10-12 19:31:36 +02:00
|
|
|
from books.views import _now_ms, progress_state
|
2026-08-15 16:20:54 +02:00
|
|
|
from podcasts.models import PodcastFeed, EpisodeProgress, PodcastQueue
|
|
|
|
|
from radio.models import SavedStation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@require_http_methods(['GET'])
|
|
|
|
|
def sync_snapshot(request):
|
|
|
|
|
"""Aggregate read-only snapshot of a user's data for local/native clients
|
|
|
|
|
(see accounts/middleware.py for the Bearer-token auth that makes this
|
|
|
|
|
reachable without a browser session).
|
|
|
|
|
|
|
|
|
|
Deliberately excludes book file bytes and podcast audio — those stay
|
|
|
|
|
large/expensive and are fetched lazily via the existing per-resource
|
|
|
|
|
endpoints (e.g. GET /books/<id>/data/), which this same token also
|
|
|
|
|
unlocks. Writes (progress, highlights, bookmarks, ...) go through those
|
|
|
|
|
existing endpoints too, to reuse their merge semantics rather than
|
2026-10-05 17:50:44 +02:00
|
|
|
duplicating it here.
|
|
|
|
|
|
|
|
|
|
?only=books: just the books' part. The apps' book sync needs nothing else,
|
|
|
|
|
and the podcast part is by far the biggest (every episode ever played:
|
|
|
|
|
megabytes for an account that moved over from AntennaPod)."""
|
2026-08-15 16:20:54 +02:00
|
|
|
if not request.user.is_authenticated:
|
|
|
|
|
return JsonResponse({'error': 'authentication required'}, status=401)
|
|
|
|
|
|
|
|
|
|
user = request.user
|
|
|
|
|
|
2026-10-19 15:37:11 +02:00
|
|
|
books = [{k: b[k] for k in ('id', 'meta_ct', 'meta_iv', 'uploaded_at', 'is_read')} for b in book_rows(user)]
|
2026-08-15 16:20:54 +02:00
|
|
|
|
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
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>
2026-10-12 19:31:36 +02:00
|
|
|
book_progress = [
|
|
|
|
|
{'book_id': p.book_id, 'updated_at': p.updated_at.isoformat(), **progress_state(p)}
|
|
|
|
|
for p in EBookProgress.objects.filter(user=user)
|
|
|
|
|
]
|
2026-08-15 16:20:54 +02:00
|
|
|
|
|
|
|
|
book_highlights = list(EBookHighlights.objects.filter(user=user).values('book_id', 'ct', 'iv', 'updated_at'))
|
|
|
|
|
for h in book_highlights:
|
|
|
|
|
h['updated_at'] = h['updated_at'].isoformat()
|
|
|
|
|
|
|
|
|
|
book_bookmarks = list(EBookBookmarks.objects.filter(user=user).values('book_id', 'ct', 'iv', 'updated_at'))
|
|
|
|
|
for bm in book_bookmarks:
|
|
|
|
|
bm['updated_at'] = bm['updated_at'].isoformat()
|
|
|
|
|
|
2026-10-05 17:50:44 +02:00
|
|
|
if request.GET.get('only') == 'books':
|
|
|
|
|
return JsonResponse({
|
|
|
|
|
'server_time': timezone.now().isoformat(),
|
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
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>
2026-10-12 19:31:36 +02:00
|
|
|
'server_now': _now_ms(),
|
2026-10-05 17:50:44 +02:00
|
|
|
'books': books,
|
|
|
|
|
'book_progress': book_progress,
|
|
|
|
|
'book_highlights': book_highlights,
|
|
|
|
|
'book_bookmarks': book_bookmarks,
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-15 16:20:54 +02:00
|
|
|
podcast_feeds = list(
|
|
|
|
|
PodcastFeed.objects.filter(user=user)
|
|
|
|
|
.annotate(latest_episode_at=Max('episodes__pub_date'))
|
|
|
|
|
.values('id', 'title', 'artwork_url', 'rss_url', 'last_refreshed_at', 'author', 'added_at', 'auto_queue', 'latest_episode_at')
|
|
|
|
|
)
|
|
|
|
|
for f in podcast_feeds:
|
|
|
|
|
for key in ('last_refreshed_at', 'added_at', 'latest_episode_at'):
|
|
|
|
|
if f[key]:
|
|
|
|
|
f[key] = f[key].isoformat()
|
|
|
|
|
|
|
|
|
|
episode_progress = list(
|
|
|
|
|
EpisodeProgress.objects.filter(user=user)
|
|
|
|
|
.values('episode_id', 'position_seconds', 'played', 'dismissed', 'updated_at')
|
|
|
|
|
)
|
|
|
|
|
for ep in episode_progress:
|
|
|
|
|
ep['updated_at'] = ep['updated_at'].isoformat()
|
|
|
|
|
|
|
|
|
|
podcast_queue = list(
|
|
|
|
|
PodcastQueue.objects.filter(user=user).order_by('position').values('episode_id', 'position')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
saved_stations = list(
|
|
|
|
|
SavedStation.objects.filter(user=user)
|
|
|
|
|
.values('id', 'name', 'url', 'bitrate', 'country', 'tags', 'favicon_url', 'is_favorite', 'notes')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return JsonResponse({
|
|
|
|
|
'server_time': timezone.now().isoformat(),
|
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
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>
2026-10-12 19:31:36 +02:00
|
|
|
'server_now': _now_ms(),
|
2026-08-15 16:20:54 +02:00
|
|
|
'books': books,
|
|
|
|
|
'book_progress': book_progress,
|
|
|
|
|
'book_highlights': book_highlights,
|
|
|
|
|
'book_bookmarks': book_bookmarks,
|
|
|
|
|
'podcast_feeds': podcast_feeds,
|
|
|
|
|
'episode_progress': episode_progress,
|
|
|
|
|
'podcast_queue': podcast_queue,
|
|
|
|
|
'saved_stations': saved_stations,
|
|
|
|
|
})
|