95 lines
3.8 KiB
Python
95 lines
3.8 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, book_rows
|
|
from books.views import _now_ms, progress_state
|
|
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.
|
|
|
|
?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)."""
|
|
if not request.user.is_authenticated:
|
|
return JsonResponse({'error': 'authentication required'}, status=401)
|
|
|
|
user = request.user
|
|
|
|
books = [{k: b[k] for k in ('id', 'meta_ct', 'meta_iv', 'uploaded_at', 'is_read')} for b in book_rows(user)]
|
|
|
|
book_progress = [
|
|
{'book_id': p.book_id, 'updated_at': p.updated_at.isoformat(), **progress_state(p)}
|
|
for p in EBookProgress.objects.filter(user=user)
|
|
]
|
|
|
|
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()
|
|
|
|
if request.GET.get('only') == 'books':
|
|
return JsonResponse({
|
|
'server_time': timezone.now().isoformat(),
|
|
'server_now': _now_ms(),
|
|
'books': books,
|
|
'book_progress': book_progress,
|
|
'book_highlights': book_highlights,
|
|
'book_bookmarks': book_bookmarks,
|
|
})
|
|
|
|
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(),
|
|
'server_now': _now_ms(),
|
|
'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,
|
|
})
|