瀏覽代碼

Add genres to books

Colin Powell 1 年之前
父節點
當前提交
b7e15da87a

+ 7 - 7
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)

+ 13 - 1
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:

+ 1 - 0
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", []),
     }

+ 95 - 1
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"])