Serve partner audiobooks with isbns
authorRadek Czajka <rczajka@rczajka.pl>
Fri, 24 Apr 2026 10:50:48 +0000 (12:50 +0200)
committerRadek Czajka <rczajka@rczajka.pl>
Fri, 24 Apr 2026 10:50:48 +0000 (12:50 +0200)
src/catalogue/forms.py
src/catalogue/migrations/0056_book_isbn_mp3.py [new file with mode: 0644]
src/catalogue/models/book.py
src/partners/admin.py
src/partners/api/urls.py
src/partners/api/views.py
src/partners/migrations/0003_audiopricelevel.py [new file with mode: 0644]
src/partners/models.py

index 7e543df..c9c3edc 100644 (file)
@@ -21,6 +21,7 @@ class BookImportForm(forms.Form):
     logo_mono = forms.CharField(required=False)
     logo_alt = forms.CharField(required=False)
     can_sell = forms.BooleanField(required=False)
     logo_mono = forms.CharField(required=False)
     logo_alt = forms.CharField(required=False)
     can_sell = forms.BooleanField(required=False)
+    isbn_mp3 = forms.CharField(required=False)
 
     def clean(self):
         from django.core.files.base import ContentFile
 
     def clean(self):
         from django.core.files.base import ContentFile
@@ -41,8 +42,9 @@ class BookImportForm(forms.Form):
                                   logo=self.cleaned_data['logo'],
                                   logo_mono=self.cleaned_data['logo_mono'],
                                   logo_alt=self.cleaned_data['logo_alt'],
                                   logo=self.cleaned_data['logo'],
                                   logo_mono=self.cleaned_data['logo_mono'],
                                   logo_alt=self.cleaned_data['logo_alt'],
-                                  can_sell=self.cleaned_data['can_sell'],
-                                  **kwargs)
+                                  can_sell=self.cleaned_data['can_sell'], 
+                                  isbn_mp3=self.cleaned_data['isbn_mp3'],
+                                 **kwargs)
 
 
 FORMATS = [(f, f.upper()) for f in Book.ebook_formats]
 
 
 FORMATS = [(f, f.upper()) for f in Book.ebook_formats]
diff --git a/src/catalogue/migrations/0056_book_isbn_mp3.py b/src/catalogue/migrations/0056_book_isbn_mp3.py
new file mode 100644 (file)
index 0000000..bb24a7e
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 4.0.8 on 2026-04-24 10:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0055_book_can_sell'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='book',
+            name='isbn_mp3',
+            field=models.CharField(blank=True, max_length=32, verbose_name='ISBN audiobooka'),
+        ),
+    ]
index 8597eac..fddce42 100644 (file)
@@ -62,6 +62,7 @@ class Book(models.Model):
     preview_key = models.CharField(max_length=32, blank=True, null=True)
     findable = models.BooleanField('wyszukiwalna', default=True, db_index=True)
     can_sell = models.BooleanField('do sprzedaży', default=True)
     preview_key = models.CharField(max_length=32, blank=True, null=True)
     findable = models.BooleanField('wyszukiwalna', default=True, db_index=True)
     can_sell = models.BooleanField('do sprzedaży', default=True)
+    isbn_mp3 = models.CharField('ISBN audiobooka', max_length=32, blank=True)
 
     # files generated during publication
     xml_file = fields.XmlField(storage=bofh_storage, with_etag=False)
 
     # files generated during publication
     xml_file = fields.XmlField(storage=bofh_storage, with_etag=False)
@@ -684,7 +685,7 @@ class Book(models.Model):
 
     @classmethod
     def from_text_and_meta(cls, raw_file, book_info, overwrite=False, dont_build=None, search_index=True,
 
     @classmethod
     def from_text_and_meta(cls, raw_file, book_info, overwrite=False, dont_build=None, search_index=True,
-                           remote_gallery_url=None, days=0, findable=True, logo=None, logo_mono=None, logo_alt=None, can_sell=None):
+                           remote_gallery_url=None, days=0, findable=True, logo=None, logo_mono=None, logo_alt=None, can_sell=None, isbn_mp3=None):
         from catalogue import tasks
 
         if dont_build is None:
         from catalogue import tasks
 
         if dont_build is None:
@@ -741,6 +742,8 @@ class Book(models.Model):
             extra['logo_alt'] = logo_alt
         if can_sell is not None:
             book.can_sell = can_sell
             extra['logo_alt'] = logo_alt
         if can_sell is not None:
             book.can_sell = can_sell
+        if isbn_mp3 is not None:
+            book.isbn_mp3 = isbn_mp3
         book.extra_info = json.dumps(extra)
         book.load_abstract()
         book.load_toc()
         book.extra_info = json.dumps(extra)
         book.load_abstract()
         book.load_toc()
index 3ad6775..5083f60 100644 (file)
@@ -6,10 +6,14 @@ class PriceLevelInline(admin.TabularInline):
     model = models.PriceLevel
     extra = 0
 
     model = models.PriceLevel
     extra = 0
 
+class AudioPriceLevelInline(admin.TabularInline):
+    model = models.AudioPriceLevel
+    extra = 0
 
 @admin.register(models.Partner)
 class PartnerAdmin(admin.ModelAdmin):
     inlines = [
         PriceLevelInline,
 
 @admin.register(models.Partner)
 class PartnerAdmin(admin.ModelAdmin):
     inlines = [
         PriceLevelInline,
+        AudioPriceLevelInline,
     ]
 
     ]
 
index df250fd..f88eedf 100644 (file)
@@ -8,4 +8,6 @@ from . import views
 urlpatterns = [
     path('<slug:key>/books/',
         views.PartnerBooksView.as_view()),
 urlpatterns = [
     path('<slug:key>/books/',
         views.PartnerBooksView.as_view()),
+    path('<slug:key>/audiobooks/',
+        views.PartnerAudiobooksView.as_view()),
 ]
 ]
index ccae25e..a2e1de5 100644 (file)
@@ -39,6 +39,52 @@ class PartnerBookSerializer(BookSerializer2):
         return self.context['partner'].get_price(obj.pages)
 
 
         return self.context['partner'].get_price(obj.pages)
 
 
+class PartnerAudiobookSerializer(BookSerializer2):
+    price = serializers.SerializerMethodField()
+    duration = serializers.SerializerMethodField()
+    files = serializers.SerializerMethodField()
+
+    class Meta:
+        model = Book
+        fields = [
+            'slug', 'title',
+            'href', 'url', 'language',
+            'authors', 'translators',
+            'epochs', 'genres', 'kinds',
+            'files',
+            'cover',
+            'isbn_mp3',
+            'abstract',
+            'content_warnings', 'audiences',
+            'changed_at', 'duration',
+            'price',
+        ]
+
+    def get_duration(self, obj):
+        return obj.get_audiobooks(True)[2]
+
+    def get_files(self, obj):
+        def get_for_single(b):
+            fs = []
+            for m in b.media.filter(type='mp3'):
+                fs.append({
+                    "name": m.name,
+                    "part_name": m.part_name,
+                    "url": 'https://wolnelektury.pl' + m.file.url,
+                })
+            for c in b.get_children():
+                fs.extend(get_for_single(c))
+            return fs
+        return get_for_single(b)
+
+    def get_price(self, obj):
+        duration = obj.get_audiobooks(True)[2]
+        if not duration:
+            return None
+        duration /= 60
+        return self.context['partner'].get_audio_price(obj.pages)
+
+
 @method_decorator(never_cache, name='dispatch')
 class PartnerBooksView(ListAPIView):
     serializer_class = PartnerBookSerializer
 @method_decorator(never_cache, name='dispatch')
 class PartnerBooksView(ListAPIView):
     serializer_class = PartnerBookSerializer
@@ -50,3 +96,16 @@ class PartnerBooksView(ListAPIView):
 
     def get_queryset(self):
         return Book.objects.filter(parent=None).filter(can_sell=True).exclude(pages=None)
 
     def get_queryset(self):
         return Book.objects.filter(parent=None).filter(can_sell=True).exclude(pages=None)
+
+
+@method_decorator(never_cache, name='dispatch')
+class PartnerAudiobooksView(ListAPIView):
+    serializer_class = PartnerAudiobookSerializer
+
+    def get_serializer_context(self):
+        ctx = super().get_serializer_context()
+        ctx['partner'] = get_object_or_404(models.Partner, key=self.kwargs['key'])
+        return ctx
+
+    def get_queryset(self):
+        return Book.objects.exclude(isbn_mp3='')
diff --git a/src/partners/migrations/0003_audiopricelevel.py b/src/partners/migrations/0003_audiopricelevel.py
new file mode 100644 (file)
index 0000000..9bd1f21
--- /dev/null
@@ -0,0 +1,26 @@
+# Generated by Django 4.0.8 on 2026-04-24 10:48
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('partners', '0002_alter_pricelevel_price'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='AudioPriceLevel',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('min_minutes', models.IntegerField(blank=True, null=True)),
+                ('price', models.DecimalField(decimal_places=2, max_digits=10)),
+                ('partner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='partners.partner')),
+            ],
+            options={
+                'ordering': ('price',),
+            },
+        ),
+    ]
index 32e8a1b..583c34c 100644 (file)
@@ -16,6 +16,14 @@ class Partner(models.Model):
             return None
         return price_obj.price
 
             return None
         return price_obj.price
 
+    def get_audio_price(self, minutes):
+        price_obj = self.audiopricelevel_set.exclude(
+            min_minutes__gt=minutes
+        ).order_by('-price').first()
+        if price_obj is None:
+            return None
+        return price_obj.price
+        
 
 class PriceLevel(models.Model):
     partner = models.ForeignKey(Partner, models.CASCADE)
 
 class PriceLevel(models.Model):
     partner = models.ForeignKey(Partner, models.CASCADE)
@@ -24,3 +32,12 @@ class PriceLevel(models.Model):
 
     class Meta:
         ordering = ('price',)
 
     class Meta:
         ordering = ('price',)
+
+
+class AudioPriceLevel(models.Model):
+    partner = models.ForeignKey(Partner, models.CASCADE)
+    min_minutes = models.IntegerField(null=True, blank=True)
+    price = models.DecimalField(max_digits=10, decimal_places=2)
+
+    class Meta:
+        ordering = ('price',)