From 5cb736f7ca55179d06965cc9874d0b059299d933 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 24 Apr 2026 12:50:48 +0200 Subject: [PATCH] Serve partner audiobooks with isbns --- src/catalogue/forms.py | 6 +- .../migrations/0056_book_isbn_mp3.py | 18 ++++++ src/catalogue/models/book.py | 5 +- src/partners/admin.py | 4 ++ src/partners/api/urls.py | 2 + src/partners/api/views.py | 59 +++++++++++++++++++ .../migrations/0003_audiopricelevel.py | 26 ++++++++ src/partners/models.py | 17 ++++++ 8 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 src/catalogue/migrations/0056_book_isbn_mp3.py create mode 100644 src/partners/migrations/0003_audiopricelevel.py diff --git a/src/catalogue/forms.py b/src/catalogue/forms.py index 7e543df29..c9c3edc23 100644 --- a/src/catalogue/forms.py +++ b/src/catalogue/forms.py @@ -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) + isbn_mp3 = forms.CharField(required=False) 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'], - 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] diff --git a/src/catalogue/migrations/0056_book_isbn_mp3.py b/src/catalogue/migrations/0056_book_isbn_mp3.py new file mode 100644 index 000000000..bb24a7e93 --- /dev/null +++ b/src/catalogue/migrations/0056_book_isbn_mp3.py @@ -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'), + ), + ] diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py index 8597eac75..fddce429c 100644 --- a/src/catalogue/models/book.py +++ b/src/catalogue/models/book.py @@ -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) + 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) @@ -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, - 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: @@ -741,6 +742,8 @@ class Book(models.Model): 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() diff --git a/src/partners/admin.py b/src/partners/admin.py index 3ad6775ce..5083f6058 100644 --- a/src/partners/admin.py +++ b/src/partners/admin.py @@ -6,10 +6,14 @@ class PriceLevelInline(admin.TabularInline): model = models.PriceLevel extra = 0 +class AudioPriceLevelInline(admin.TabularInline): + model = models.AudioPriceLevel + extra = 0 @admin.register(models.Partner) class PartnerAdmin(admin.ModelAdmin): inlines = [ PriceLevelInline, + AudioPriceLevelInline, ] diff --git a/src/partners/api/urls.py b/src/partners/api/urls.py index df250fdd6..f88eedff1 100644 --- a/src/partners/api/urls.py +++ b/src/partners/api/urls.py @@ -8,4 +8,6 @@ from . import views urlpatterns = [ path('/books/', views.PartnerBooksView.as_view()), + path('/audiobooks/', + views.PartnerAudiobooksView.as_view()), ] diff --git a/src/partners/api/views.py b/src/partners/api/views.py index ccae25ebd..a2e1de597 100644 --- a/src/partners/api/views.py +++ b/src/partners/api/views.py @@ -39,6 +39,52 @@ class PartnerBookSerializer(BookSerializer2): 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 @@ -50,3 +96,16 @@ class PartnerBooksView(ListAPIView): 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 index 000000000..9bd1f2148 --- /dev/null +++ b/src/partners/migrations/0003_audiopricelevel.py @@ -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',), + }, + ), + ] diff --git a/src/partners/models.py b/src/partners/models.py index 32e8a1bb2..583c34c79 100644 --- a/src/partners/models.py +++ b/src/partners/models.py @@ -16,6 +16,14 @@ class Partner(models.Model): 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) @@ -24,3 +32,12 @@ class PriceLevel(models.Model): 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',) -- 2.20.1