117 lines
4.3 KiB
Python
117 lines
4.3 KiB
Python
|
|
"""Folders of podcasts and audiobooks (docs/library.md): like tags, any number per
|
||
|
|
subscription (PodcastFeed.folders), shown as filter chips in their order
|
||
|
|
(PodcastSettings.folder_order). Renaming onto an existing name merges; deleting
|
||
|
|
a folder leaves its subscriptions where they are."""
|
||
|
|
import json
|
||
|
|
|
||
|
|
from django.http import JsonResponse
|
||
|
|
from django.views.decorators.csrf import csrf_exempt
|
||
|
|
from django.views.decorators.http import require_http_methods
|
||
|
|
|
||
|
|
from .models import PodcastFeed, PodcastSettings
|
||
|
|
|
||
|
|
MAX_FOLDERS = 20
|
||
|
|
MAX_NAME = 100
|
||
|
|
|
||
|
|
|
||
|
|
def clean_name(value):
|
||
|
|
return ' '.join(str(value or '').split())[:MAX_NAME]
|
||
|
|
|
||
|
|
|
||
|
|
def clean_list(value):
|
||
|
|
"""A list of folder names: trimmed, without empties and doubles (first spelling wins)."""
|
||
|
|
if not isinstance(value, list):
|
||
|
|
raise ValueError
|
||
|
|
out, seen = [], set()
|
||
|
|
for v in value:
|
||
|
|
if not isinstance(v, str):
|
||
|
|
raise ValueError
|
||
|
|
name = clean_name(v)
|
||
|
|
if name and name.casefold() not in seen:
|
||
|
|
seen.add(name.casefold())
|
||
|
|
out.append(name)
|
||
|
|
return out[:MAX_FOLDERS]
|
||
|
|
|
||
|
|
|
||
|
|
def set_folders(feed, names):
|
||
|
|
"""The feed's folders, and `folder` (the first) for older clients."""
|
||
|
|
feed.folders = names
|
||
|
|
feed.folder = names[0] if names else ''
|
||
|
|
|
||
|
|
|
||
|
|
def _body(request):
|
||
|
|
try:
|
||
|
|
body = json.loads(request.body or b'{}')
|
||
|
|
except (json.JSONDecodeError, ValueError):
|
||
|
|
return None
|
||
|
|
return body if isinstance(body, dict) else None
|
||
|
|
|
||
|
|
|
||
|
|
def _data(user):
|
||
|
|
counts = {}
|
||
|
|
for names in PodcastFeed.objects.filter(user=user).values_list('folders', flat=True):
|
||
|
|
for n in names or []:
|
||
|
|
counts[n] = counts.get(n, 0) + 1
|
||
|
|
order = [n for n in PodcastSettings.for_user(user).folder_order if n in counts]
|
||
|
|
rest = sorted((n for n in counts if n not in order), key=str.casefold)
|
||
|
|
return {'order': order + rest, 'folders': [{'name': n, 'count': counts[n]} for n in order + rest]}
|
||
|
|
|
||
|
|
|
||
|
|
@csrf_exempt
|
||
|
|
@require_http_methods(['GET', 'POST'])
|
||
|
|
def folders(request):
|
||
|
|
"""GET {order, folders: [{name, count}]}; POST {order: [names]} sets the chip order."""
|
||
|
|
if not request.user.is_authenticated:
|
||
|
|
return JsonResponse({'error': 'authentication required'}, status=401)
|
||
|
|
if request.method == 'POST':
|
||
|
|
body = _body(request)
|
||
|
|
try:
|
||
|
|
order = clean_list((body or {}).get('order'))
|
||
|
|
except ValueError:
|
||
|
|
return JsonResponse({'error': 'order: a list of names'}, status=400)
|
||
|
|
s = PodcastSettings.for_user(request.user)
|
||
|
|
s.folder_order = order
|
||
|
|
s.save(update_fields=['folder_order'])
|
||
|
|
return JsonResponse(_data(request.user))
|
||
|
|
|
||
|
|
|
||
|
|
def _rewrite(user, fn):
|
||
|
|
for feed in PodcastFeed.objects.filter(user=user).only('id', 'folders', 'folder'):
|
||
|
|
names = fn(list(feed.folders or []))
|
||
|
|
if names != (feed.folders or []):
|
||
|
|
set_folders(feed, clean_list(names))
|
||
|
|
feed.save(update_fields=['folders', 'folder'])
|
||
|
|
|
||
|
|
|
||
|
|
@csrf_exempt
|
||
|
|
@require_http_methods(['POST'])
|
||
|
|
def rename_folder(request):
|
||
|
|
"""{old, new}: every subscription in `old` is in `new` instead (merged if it exists)."""
|
||
|
|
if not request.user.is_authenticated:
|
||
|
|
return JsonResponse({'error': 'authentication required'}, status=401)
|
||
|
|
body = _body(request) or {}
|
||
|
|
old, new = clean_name(body.get('old')), clean_name(body.get('new'))
|
||
|
|
if not old or not new:
|
||
|
|
return JsonResponse({'error': 'old and new required'}, status=400)
|
||
|
|
_rewrite(request.user, lambda names: [new if n == old else n for n in names])
|
||
|
|
s = PodcastSettings.for_user(request.user)
|
||
|
|
s.folder_order = clean_list([new if n == old else n for n in s.folder_order])
|
||
|
|
s.save(update_fields=['folder_order'])
|
||
|
|
return JsonResponse(_data(request.user))
|
||
|
|
|
||
|
|
|
||
|
|
@csrf_exempt
|
||
|
|
@require_http_methods(['POST'])
|
||
|
|
def delete_folder(request):
|
||
|
|
"""{name}: the folder goes, its subscriptions stay."""
|
||
|
|
if not request.user.is_authenticated:
|
||
|
|
return JsonResponse({'error': 'authentication required'}, status=401)
|
||
|
|
name = clean_name((_body(request) or {}).get('name'))
|
||
|
|
if not name:
|
||
|
|
return JsonResponse({'error': 'name required'}, status=400)
|
||
|
|
_rewrite(request.user, lambda names: [n for n in names if n != name])
|
||
|
|
s = PodcastSettings.for_user(request.user)
|
||
|
|
s.folder_order = [n for n in s.folder_order if n != name]
|
||
|
|
s.save(update_fields=['folder_order'])
|
||
|
|
return JsonResponse(_data(request.user))
|