83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
|
|
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/<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
|
||
|
|
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,
|
||
|
|
})
|