first streaming mp3 zip
authorRadek Czajka <rczajka@rczajka.pl>
Tue, 16 Jun 2026 11:21:03 +0000 (13:21 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Tue, 16 Jun 2026 11:21:03 +0000 (13:21 +0200)
requirements/requirements.txt
src/catalogue/urls.py
src/catalogue/views.py

index d434c96..56887b9 100644 (file)
@@ -41,6 +41,8 @@ Pillow==9.5.0
 mutagen==1.47
 sorl-thumbnail==12.10.0
 
+zipstream-ng==1.9.2
+
 # home-brewed & dependencies
 librarian==26.5
 
index 91c66a2..724a783 100644 (file)
@@ -51,6 +51,7 @@ urlpatterns = [
     path('zip/epub.zip', views.download_zip, {'file_format': 'epub', 'slug': None}, 'download_zip_epub'),
     path('zip/mobi.zip', views.download_zip, {'file_format': 'mobi', 'slug': None}, 'download_zip_mobi'),
     path('zip/mp3/<slug:slug>.zip', views.download_zip, {'media_format': 'mp3'}, 'download_zip_mp3'),
+    path('zip/<slug:slug>_mp3.zip', views.stream_zip, {'media_format': 'mp3'}),
     path('zip/ogg/<slug:slug>.zip', views.download_zip, {'media_format': 'ogg'}, 'download_zip_ogg'),
 
     # Public interface. Do not change this URLs.
index e754e05..4873106 100644 (file)
@@ -5,11 +5,13 @@ from collections import OrderedDict
 import random
 import re
 from urllib.parse import quote_plus
+from slugify import slugify
+from zipstream import ZipStream
 
 from django.conf import settings
 from django.template.loader import render_to_string
 from django.shortcuts import get_object_or_404, render, redirect
-from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect
+from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponsePermanentRedirect, StreamingHttpResponse
 from django.urls import reverse
 from django.db.models import Q, QuerySet
 from django.contrib.auth.decorators import login_required, user_passes_test
@@ -518,6 +520,31 @@ def download_zip(request, file_format=None, media_format=None, slug=None):
     return HttpResponseRedirect(quote_plus(settings.MEDIA_URL + url, safe='/?='))
 
 
+def stream_zip(request, media_format=None, slug=None):
+    book = get_object_or_404(Book, slug=slug)
+    def iterate_audiobooks(book, names):
+        for bm in book.media.filter(type=media_format).order_by('index'):
+            yield (
+                bm.file.path,
+                names + (slugify(bm.part_name),) if bm.part_name else names
+            )
+        for child in book.get_children():
+            yield from iterate_audiobooks(child, names + (slugify(child.title),))
+
+    zs = ZipStream()
+
+    for i, (file_path, names) in enumerate(iterate_audiobooks(book, ())):
+        index = i + 1
+        part_name = '_'.join(names)
+        ext = file_path.rsplit('.', 1)[-1]
+        zip_name = f'{book.slug}_{index:03d}_{part_name}'[:240] + '.' + ext
+        zs.add_path(file_path, zip_name)
+
+    response = StreamingHttpResponse(zs, content_type='application/zip')
+    response['Content-Disposition'] = f'attachment; filename={slug}_{media_format}.zip'
+    return response
+
+
 class CustomPDFFormView(AjaxableFormView):
     form_class = forms.CustomPDFForm
     title = gettext_lazy('Stwórz własny PDF')