瀏覽代碼

[bricksets] Add brick set scrobble type

Colin Powell 8 月之前
父節點
當前提交
7e961076b4

+ 0 - 0
vrobbler/apps/bricksets/__init__.py


+ 23 - 0
vrobbler/apps/bricksets/admin.py

@@ -0,0 +1,23 @@
+from django.contrib import admin
+
+from bricksets.models import BrickSet
+from scrobbles.admin import ScrobbleInline
+
+
+class BrickSetInline(admin.TabularInline):
+    model = BrickSet
+    extra = 0
+
+
+@admin.register(BrickSet)
+class BrickSetAdmin(admin.ModelAdmin):
+    date_hierarchy = "created"
+    list_display = (
+        "uuid",
+        "title",
+    )
+    ordering = ("-created",)
+    search_fields = ("title",)
+    inlines = [
+        ScrobbleInline,
+    ]

+ 5 - 0
vrobbler/apps/bricksets/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class BrickSetsConfig(AppConfig):
+    name = "bricksets"

+ 106 - 0
vrobbler/apps/bricksets/migrations/0001_initial.py

@@ -0,0 +1,106 @@
+# Generated by Django 4.2.15 on 2024-09-07 05:38
+
+from django.db import migrations, models
+import django_extensions.db.fields
+import taggit.managers
+import uuid
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ("scrobbles", "0059_remove_scrobble_book_koreader_hash_and_more"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="BrickSet",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "created",
+                    django_extensions.db.fields.CreationDateTimeField(
+                        auto_now_add=True, verbose_name="created"
+                    ),
+                ),
+                (
+                    "modified",
+                    django_extensions.db.fields.ModificationDateTimeField(
+                        auto_now=True, verbose_name="modified"
+                    ),
+                ),
+                (
+                    "uuid",
+                    models.UUIDField(
+                        blank=True,
+                        default=uuid.uuid4,
+                        editable=False,
+                        null=True,
+                    ),
+                ),
+                (
+                    "title",
+                    models.CharField(blank=True, max_length=255, null=True),
+                ),
+                (
+                    "run_time_seconds",
+                    models.IntegerField(blank=True, null=True),
+                ),
+                (
+                    "run_time_ticks",
+                    models.PositiveBigIntegerField(blank=True, null=True),
+                ),
+                (
+                    "number",
+                    models.CharField(blank=True, max_length=10, null=True),
+                ),
+                ("release_year", models.IntegerField(blank=True, null=True)),
+                ("piece_count", models.IntegerField(blank=True, null=True)),
+                (
+                    "brickset_rating",
+                    models.DecimalField(
+                        blank=True, decimal_places=1, max_digits=3, null=True
+                    ),
+                ),
+                (
+                    "lego_item_number",
+                    models.CharField(blank=True, max_length=10, null=True),
+                ),
+                (
+                    "box_image",
+                    models.ImageField(
+                        blank=True, null=True, upload_to="brickset/boxes/"
+                    ),
+                ),
+                (
+                    "set_image",
+                    models.ImageField(
+                        blank=True, null=True, upload_to="brickset/sets/"
+                    ),
+                ),
+                (
+                    "genre",
+                    taggit.managers.TaggableManager(
+                        blank=True,
+                        help_text="A comma-separated list of tags.",
+                        through="scrobbles.ObjectWithGenres",
+                        to="scrobbles.Genre",
+                        verbose_name="Tags",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+        ),
+    ]

+ 0 - 0
vrobbler/apps/bricksets/migrations/__init__.py


+ 63 - 0
vrobbler/apps/bricksets/models.py

@@ -0,0 +1,63 @@
+from django.apps import apps
+from django.db import models
+from django.urls import reverse
+from django_extensions.db.models import TimeStampedModel
+from imagekit.models import ImageSpecField
+from imagekit.processors import ResizeToFit
+from scrobbles.dataclasses import BrickSetLogData
+from scrobbles.mixins import LongPlayScrobblableMixin
+
+BNULL = {"blank": True, "null": True}
+
+
+class BrickSet(LongPlayScrobblableMixin):
+    """"""
+
+    number = models.CharField(max_length=10, **BNULL)
+    release_year = models.IntegerField(**BNULL)
+    piece_count = models.IntegerField(**BNULL)
+    brickset_rating = models.DecimalField(max_digits=3, decimal_places=1, **BNULL)
+    lego_item_number = models.CharField(max_length=10, **BNULL)
+    box_image = models.ImageField(upload_to="brickset/boxes/", **BNULL)
+    box_image_small = ImageSpecField(
+        source="box_image",
+        processors=[ResizeToFit(100, 100)],
+        format="JPEG",
+        options={"quality": 60},
+    )
+    box_image_medium = ImageSpecField(
+        source="box_image",
+        processors=[ResizeToFit(300, 300)],
+        format="JPEG",
+        options={"quality": 75},
+    )
+    set_image = models.ImageField(upload_to="brickset/sets/", **BNULL)
+    set_image_small = ImageSpecField(
+        source="set_image",
+        processors=[ResizeToFit(100, 100)],
+        format="JPEG",
+        options={"quality": 60},
+    )
+    set_image_medium = ImageSpecField(
+        source="set_image",
+        processors=[ResizeToFit(300, 300)],
+        format="JPEG",
+        options={"quality": 75},
+    )
+
+    def get_absolute_url(self):
+        return reverse("bricksets:brickset_detail", kwargs={"slug": self.uuid})
+
+    @property
+    def logdata_cls(self):
+        return BrickSetLogData
+
+    @classmethod
+    def find_or_create(cls, title: str) -> "BrickSet":
+        return cls.objects.filter(title=title).first()
+
+    @property
+    def primary_image_url(self) -> str:
+        if self.box_image:
+            return self.box_image.url
+        return ""

+ 14 - 0
vrobbler/apps/bricksets/urls.py

@@ -0,0 +1,14 @@
+from django.urls import path
+from bricksets import views
+
+app_name = "bricksets"
+
+
+urlpatterns = [
+    path("bricksets/", views.BrickSetListView.as_view(), name="brickset_list"),
+    path(
+        "bricksets/<slug:slug>/",
+        views.BrickSetDetailView.as_view(),
+        name="brickset_detail",
+    ),
+]

+ 10 - 0
vrobbler/apps/bricksets/views.py

@@ -0,0 +1,10 @@
+from bricksets.models import BrickSet
+from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView
+
+
+class BrickSetListView(ScrobbleableListView):
+    model = BrickSet
+
+
+class BrickSetDetailView(ScrobbleableDetailView):
+    model = BrickSet

+ 2 - 0
vrobbler/apps/scrobbles/constants.py

@@ -6,6 +6,7 @@ JELLYFIN_AUDIO_ITEM_TYPES = ["Audio"]
 LONG_PLAY_MEDIA = {
     "videogames": "VideoGame",
     "books": "Book",
+    "bricksets": "BrickSet",
 }
 
 PLAY_AGAIN_MEDIA = {
@@ -13,6 +14,7 @@ PLAY_AGAIN_MEDIA = {
     "books": "Book",
     "boardgames": "BoardGame",
     "moods": "Mood",
+    "bricksets": "BrickSet",
 }
 
 MEDIA_END_PADDING_SECONDS = {

+ 15 - 3
vrobbler/apps/scrobbles/dataclasses.py

@@ -30,6 +30,9 @@ class JSONDataclass(JSONWizard):
     def json(self):
         return json.dumps(self.asdict)
 
+class LongPlayLogData(JSONDataclass):
+    serial_scrobble_id: Optional[int]
+    long_play_complete: Optional[bool]
 
 @dataclass
 class BoardGameScoreLogData(JSONDataclass):
@@ -69,7 +72,7 @@ class BoardGameScoreLogData(JSONDataclass):
 
 
 @dataclass
-class BoardGameLogData(JSONDataclass):
+class BoardGameLogData(LongPlayLogData):
     players: Optional[list[BoardGameScoreLogData]] = None
     location: Optional[str] = None
     geo_location_id: Optional[int] = None
@@ -92,10 +95,13 @@ class BookPageLogData(JSONDataclass):
 
 
 @dataclass
-class BookLogData(JSONDataclass):
+class BookLogData(LongPlayLogData):
     koreader_hash: Optional[str]
     pages_read: Optional[int]
     page_data: Optional[list[BookPageLogData]]
+    page_end: Optional[int]
+    page_start: Optional[int]
+    serial_scrobble_id: Optional[int]
 
 
 @dataclass
@@ -148,7 +154,13 @@ class VideoLogData(JSONDataclass):
 
 
 @dataclass
-class VideoGameLogData(JSONDataclass):
+class VideoGameLogData(LongPlayLogData):
     emulated: bool = False
     console: Optional[str] = None
     emulator: Optional[str] = None
+
+
+@dataclass
+class BrickSetLogData(LongPlayLogData):
+    built_with_names: Optional[list[str]]
+    built_with_user_ids: Optional[list[str]]

+ 25 - 0
vrobbler/apps/scrobbles/migrations/0060_scrobble_brickset.py

@@ -0,0 +1,25 @@
+# Generated by Django 4.2.15 on 2024-09-07 05:38
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("bricksets", "0001_initial"),
+        ("scrobbles", "0059_remove_scrobble_book_koreader_hash_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="scrobble",
+            name="brickset",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.DO_NOTHING,
+                to="bricksets.brickset",
+            ),
+        ),
+    ]

+ 35 - 0
vrobbler/apps/scrobbles/migrations/0061_alter_scrobble_media_type.py

@@ -0,0 +1,35 @@
+# Generated by Django 4.2.15 on 2024-09-07 05:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("scrobbles", "0060_scrobble_brickset"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="scrobble",
+            name="media_type",
+            field=models.CharField(
+                choices=[
+                    ("Video", "Video"),
+                    ("Track", "Track"),
+                    ("PodcastEpisode", "Podcast episode"),
+                    ("SportEvent", "Sport event"),
+                    ("Book", "Book"),
+                    ("VideoGame", "Video game"),
+                    ("BoardGame", "Board game"),
+                    ("GeoLocation", "GeoLocation"),
+                    ("WebPage", "Web Page"),
+                    ("LifeEvent", "Life event"),
+                    ("Mood", "Mood"),
+                    ("BrickSet", "Brick set"),
+                ],
+                default="Video",
+                max_length=14,
+            ),
+        ),
+    ]

+ 5 - 0
vrobbler/apps/scrobbles/mixins.py

@@ -96,6 +96,11 @@ class ScrobblableMixin(TimeStampedModel):
     def find_or_create(cls) -> None:
         logger.warning("find_or_create() not implemented yet")
 
+    def __str__(self):
+        if self.title:
+            return self.title
+        return str(self.uuid)
+
 
 class LongPlayScrobblableMixin(ScrobblableMixin):
     class Meta:

+ 5 - 0
vrobbler/apps/scrobbles/models.py

@@ -10,6 +10,7 @@ import pytz
 from boardgames.models import BoardGame
 from books.koreader import process_koreader_sqlite_file
 from books.models import Book
+from bricksets.models import BrickSet
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.db import models
@@ -485,6 +486,7 @@ class Scrobble(TimeStampedModel):
         WEBPAGE = "WebPage", "Web Page"
         LIFE_EVENT = "LifeEvent", "Life event"
         MOOD = "Mood", "Mood"
+        BRICKSET = "BrickSet", "Brick set"
 
     uuid = models.UUIDField(editable=False, **BNULL)
     video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
@@ -510,6 +512,7 @@ class Scrobble(TimeStampedModel):
         LifeEvent, on_delete=models.DO_NOTHING, **BNULL
     )
     mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL)
+    brickset = models.ForeignKey(BrickSet, on_delete=models.DO_NOTHING, **BNULL)
     media_type = models.CharField(
         max_length=14, choices=MediaType.choices, default=MediaType.VIDEO
     )
@@ -881,6 +884,8 @@ class Scrobble(TimeStampedModel):
             media_obj = self.life_event
         if self.mood:
             media_obj = self.mood
+        if self.brickset:
+            media_obj = self.brickset
         return media_obj
 
     def __str__(self):

+ 1 - 0
vrobbler/settings-testing.py

@@ -107,6 +107,7 @@ INSTALLED_APPS = [
     "sports",
     "books",
     "boardgames",
+    "bricksets",
     "videogames",
     "locations",
     "webpages",

+ 1 - 0
vrobbler/settings.py

@@ -121,6 +121,7 @@ INSTALLED_APPS = [
     "sports",
     "books",
     "boardgames",
+    "bricksets",
     "videogames",
     "locations",
     "webpages",

+ 2 - 0
vrobbler/urls.py

@@ -6,6 +6,7 @@ import vrobbler.apps.scrobbles.views as scrobbles_views
 from vrobbler.apps.books.api.views import AuthorViewSet, BookViewSet
 from vrobbler.apps.music import urls as music_urls
 from vrobbler.apps.books import urls as book_urls
+from vrobbler.apps.bricksets import urls as bricksets_urls
 from vrobbler.apps.sports import urls as sports_urls
 from vrobbler.apps.podcasts import urls as podcast_urls
 from vrobbler.apps.videogames import urls as videogame_urls
@@ -69,6 +70,7 @@ urlpatterns = [
     path("", include(video_urls, namespace="videos")),
     path("", include(videogame_urls, namespace="videogames")),
     path("", include(boardgame_urls, namespace="boardgames")),
+    path("", include(bricksets_urls, namespace="bricksets")),
     path("", include(sports_urls, namespace="sports")),
     path("", include(locations_urls, namespace="locations")),
     path("", include(webpages_urls, namespace="webpages")),