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 from books.models import EBook, EBookProgress, EBookHighlights, EBookBookmarks 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//data/), which this same token also unlocks. Writes (progress, highlights, bookmarks, ...) go through those existing endpoints too, to reuse their merge semantics rather than duplicating it here.""" if not request.user.is_authenticated: return JsonResponse({'error': 'authentication required'}, status=401) user = request.user books = list(EBook.objects.filter(user=user).values('id', 'meta_ct', 'meta_iv', 'uploaded_at', 'is_read')) for b in books: b['uploaded_at'] = b['uploaded_at'].isoformat() book_progress = list( EBookProgress.objects.filter(user=user).values('book_id', 'scroll_fraction', 'position_anchor', 'updated_at') ) for p in book_progress: p['updated_at'] = p['updated_at'].isoformat() 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() 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(), '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, })