From f5743451874346b0dcca3fc18f767ef1d153e84d Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 22 Aug 2025 17:05:25 +0200 Subject: [PATCH] Audio bookmarks --- src/bookmarks/api/views.py | 4 +- ..._audio_timestamp_bookmark_mode_and_more.py | 29 +++++++++ src/bookmarks/models.py | 64 ++++++++++++++++--- src/social/api/views.py | 2 + 4 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 src/bookmarks/migrations/0004_bookmark_audio_timestamp_bookmark_mode_and_more.py diff --git a/src/bookmarks/api/views.py b/src/bookmarks/api/views.py index 38408c1da..32449da8c 100644 --- a/src/bookmarks/api/views.py +++ b/src/bookmarks/api/views.py @@ -25,8 +25,8 @@ class BookmarkSerializer(serializers.ModelSerializer): class Meta: model = models.Bookmark - fields = ['book', 'anchor', 'note', 'href', 'uuid', 'location', 'timestamp', 'deleted'] - read_only_fields = ['uuid'] + fields = ['book', 'anchor', 'audio_timestamp', 'mode', 'note', 'href', 'uuid', 'location', 'timestamp', 'deleted'] + read_only_fields = ['uuid', 'mode'] diff --git a/src/bookmarks/migrations/0004_bookmark_audio_timestamp_bookmark_mode_and_more.py b/src/bookmarks/migrations/0004_bookmark_audio_timestamp_bookmark_mode_and_more.py new file mode 100644 index 000000000..87dc168b7 --- /dev/null +++ b/src/bookmarks/migrations/0004_bookmark_audio_timestamp_bookmark_mode_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.8 on 2025-08-22 14:52 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookmarks', '0003_bookmark_deleted_bookmark_reported_timestamp_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='bookmark', + name='audio_timestamp', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='bookmark', + name='mode', + field=models.CharField(choices=[('text', 'text'), ('audio', 'audio')], default='text', max_length=64), + ), + migrations.AlterField( + model_name='bookmark', + name='reported_timestamp', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/src/bookmarks/models.py b/src/bookmarks/models.py index 583cd55c7..8747ffbd3 100644 --- a/src/bookmarks/models.py +++ b/src/bookmarks/models.py @@ -10,6 +10,11 @@ class Bookmark(Syncable, models.Model): user = models.ForeignKey('auth.User', models.CASCADE) book = models.ForeignKey('catalogue.Book', models.CASCADE) anchor = models.CharField(max_length=100, blank=True) + audio_timestamp = models.IntegerField(null=True, blank=True) + mode = models.CharField(max_length=64, choices=[ + ('text', 'text'), + ('audio', 'audio'), + ], default='text') created_at = models.DateTimeField(auto_now_add=True) note = models.TextField(blank=True) updated_at = models.DateTimeField(auto_now=True) @@ -23,32 +28,73 @@ class Bookmark(Syncable, models.Model): def __str__(self): return str(self.uuid) + def save(self, *args, **kwargs): + # TODO: placeholder. + try: + audio_l = self.book.get_audio_length() + except: + audio_l = 60 + if self.anchor: + self.mode = 'text' + if audio_l: + self.audio_timestamp = audio_l * .4 + if self.audio_timestamp: + self.mode = 'audio' + if self.audio_timestamp > audio_l: + self.audio_timestamp = audio_l + if audio_l: + self.anchor = 'f20' + return super().save(*args, **kwargs) + @classmethod def create_from_data(cls, user, data): if data.get('location'): return cls.get_by_location(user, data['location'], create=True) elif data.get('book') and data.get('anchor'): return cls.objects.create(user=user, book=data['book'], anchor=data['anchor']) + elif data.get('book') and data.get('audio_timestamp'): + return cls.objects.create(user=user, book=data['book'], audio_timestamp=data['audio_timestamp']) @property def timestamp(self): return self.updated_at.timestamp() def location(self): - return f'{self.book.slug}/{self.anchor}' + if self.mode == 'text': + return f'{self.book.slug}/{self.anchor}' + else: + return f'{self.book.slug}/audio/{self.audio_timestamp}' @classmethod def get_by_location(cls, user, location, create=False): Book = apps.get_model('catalogue', 'Book') try: - slug, anchor = location.split('/') + slug, anchor = location.split('/', 1) except: return None - instance = cls.objects.filter( - user=user, - book__slug=slug, - anchor=anchor - ).first() + if '/' in anchor: + try: + mode, audio_timestamp = anchor.split('/', 1) + assert mode == 'audio' + audio_timestamp = int(audio_timestamp) + except: + return None + anchor = '' + instance = cls.objects.filter( + user=user, + book__slug=slug, + mode=mode, + audio_timestamp=audio_timestamp, + ).first() + else: + mode = 'text' + audio_timestamp = None + instance = cls.objects.filter( + user=user, + book__slug=slug, + mode='text', + anchor=anchor, + ).first() if instance is None and create: try: book = Book.objects.get(slug=slug) @@ -57,7 +103,9 @@ class Bookmark(Syncable, models.Model): instance = cls.objects.create( user=user, book=book, - anchor=anchor + mode=mode, + anchor=anchor, + audio_timestamp=audio_timestamp, ) return instance diff --git a/src/social/api/views.py b/src/social/api/views.py index 91d882da1..4661df0c9 100644 --- a/src/social/api/views.py +++ b/src/social/api/views.py @@ -418,6 +418,8 @@ class SyncView(ListAPIView): def post(self, request): new_ids = [] data = request.data + if not isinstance(data, list): + raise serializers.ValidationError('Payload should be a list') for item in data: instance = self.get_instance(request.user, item) ser = self.get_serializer( -- 2.20.1