"""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))