diff --git a/vrobbler/apps/books/koreader.py b/vrobbler/apps/books/koreader.py index aa36863..8b6cca0 100644 --- a/vrobbler/apps/books/koreader.py +++ b/vrobbler/apps/books/koreader.py @@ -91,14 +91,14 @@ def get_book_map_from_sqlite(rows: Iterable) -> dict: if author_str == "N/A": continue - author, created = Author.objects.get_or_create( - name=author_str - ) - if created: - author.openlibrary_id = get_author_openlibrary_id( - author_str + author = Author.objects.filter(name=author_str).first() + if not author: + author = Author.objects.create( + name=author_str, + openlibrary_id=get_author_openlibrary_id( + author_str + ), ) - author.save(update_fields=["openlibrary_id"]) author.fix_metadata() logger.debug(f"Created author {author}") book.authors.add(author) diff --git a/vrobbler/apps/books/models.py b/vrobbler/apps/books/models.py index fa3f9cc..aed4554 100644 --- a/vrobbler/apps/books/models.py +++ b/vrobbler/apps/books/models.py @@ -13,8 +13,13 @@ from django.core.files.base import ContentFile from django.db import models from django.urls import reverse from django_extensions.db.models import TimeStampedModel -from scrobbles.mixins import LongPlayScrobblableMixin, ScrobblableMixin +from scrobbles.mixins import ( + LongPlayScrobblableMixin, + ObjectWithGenres, + ScrobblableMixin, +) from scrobbles.utils import get_scrobbles_for_media +from taggit.managers import TaggableManager logger = logging.getLogger(__name__) User = get_user_model() @@ -76,6 +81,8 @@ class Book(LongPlayScrobblableMixin): openlibrary_id = models.CharField(max_length=255, **BNULL) cover = models.ImageField(upload_to="books/covers/", **BNULL) + genre = TaggableManager(through=ObjectWithGenres) + def __str__(self): return f"{self.title} by {self.author}" @@ -135,10 +142,15 @@ class Book(LongPlayScrobblableMixin): # Pop this, so we can look it up later cover_url = data.pop("cover_url", "") + subject_key_list = data.pop("subject_key_list", "") + # Fun trick for updating all fields at once Book.objects.filter(pk=self.id).update(**data) self.refresh_from_db() + if subject_key_list: + self.genre.add(*subject_key_list) + if cover_url: r = requests.get(cover_url) if r.status_code == 200: diff --git a/vrobbler/apps/books/openlibrary.py b/vrobbler/apps/books/openlibrary.py index 371278e..e204c18 100644 --- a/vrobbler/apps/books/openlibrary.py +++ b/vrobbler/apps/books/openlibrary.py @@ -129,4 +129,5 @@ def lookup_book_from_openlibrary( "pages": top.get("number_of_pages_median", None), "cover_url": COVER_URL.format(id=ol_id), "ol_author_id": ol_author_id, + "subject_key_list": top.get("subject_key", []), } diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index 2c79f25..52d13cf 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -11,6 +11,7 @@ from django.contrib.auth import get_user_model from django.db import models from django.urls import reverse from django.utils import timezone +from django.utils.functional import cached_property from django_extensions.db.models import TimeStampedModel from music.lastfm import LastFM from music.models import Artist, Track @@ -727,7 +728,7 @@ class Scrobble(TimeStampedModel): scrobble_data.pop("timestamp", None) or timezone.now() ) - scrobble_data.pop("timestamp") + scrobble_data.pop("timestamp", None) update_fields = [] for key, value in scrobble_data.items(): setattr(self, key, value) @@ -796,3 +797,96 @@ class Scrobble(TimeStampedModel): f"{self.id} - {self.playback_position_seconds} - {self.source}" ) self.save(update_fields=["playback_position_seconds"]) + + +class ScrobbledPage(TimeStampedModel): + scrobble = models.ForeignKey(Scrobble, on_delete=models.DO_NOTHING) + number = models.IntegerField() + start_time = models.DateTimeField(**BNULL) + end_time = models.DateTimeField(**BNULL) + duration_seconds = models.IntegerField(**BNULL) + notes = models.CharField(max_length=255, **BNULL) + + def __str__(self): + return f"Page {self.number} of {self.book.pages} in {self.book.title}" + + def save(self, *args, **kwargs): + if not self.end_time and self.duration_seconds: + self._set_end_time() + + return super(ScrobbledPage, self).save(*args, **kwargs) + + @cached_property + def book(self): + return self.scrobble.book + + @property + def next(self): + user_pages_qs = self.book.scrobbledpage_set.filter( + user=self.scrobble.user + ) + page = user_pages_qs.filter(number=self.number + 1).first() + if not page: + page = ( + user_pages_qs.filter(created__gt=self.created) + .order_by("created") + .first() + ) + return page + + @property + def previous(self): + user_pages_qs = self.book.scrobbledpage_set.filter( + user=self.scrobble.user + ) + page = user_pages_qs.filter(number=self.number - 1).first() + if not page: + page = ( + user_pages_qs.filter(created__lt=self.created) + .order_by("-created") + .first() + ) + return page + + @property + def seconds_to_next_page(self) -> int: + seconds = 999999 # Effectively infnity time as we have no next + if not self.end_time: + self._set_end_time() + if self.next: + seconds = (self.next.start_time - self.end_time).seconds + return seconds + + @property + def is_scrobblable(self) -> bool: + """A page defines the start of a scrobble if the seconds to next page + are greater than an hour, or 3600 seconds, and it's not a single page, + so the next seconds to next_page is less than an hour as well. + + As a special case, the first recorded page is a scrobble, so we establish + when the book was started. + + """ + is_scrobblable = False + over_an_hour_since_last_page = False + if not self.previous: + is_scrobblable = True + + if self.previous: + over_an_hour_since_last_page = ( + self.previous.seconds_to_next_page >= 3600 + ) + blip = self.seconds_to_next_page >= 3600 + + if over_an_hour_since_last_page and not blip: + is_scrobblable = True + return is_scrobblable + + def _set_end_time(self) -> None: + if self.end_time: + return + + self.end_time = self.start_time + datetime.timedelta( + seconds=self.duration_seconds + ) + self.save(update_fields=["end_time"])