瀏覽代碼

[scrobbles] Fix dataclass parsing and add puzzles to urls

Colin Powell 3 周之前
父節點
當前提交
d04db0ecb5

+ 9 - 2
vrobbler/apps/beers/models.py

@@ -1,18 +1,25 @@
+from dataclasses import dataclass
+from typing import Optional
 from uuid import uuid4
 
-from beers.untappd import get_beer_from_untappd_id, get_rating_from_soup
+from beers.untappd import get_beer_from_untappd_id
 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 BeerLogData
+from scrobbles.dataclasses import BaseLogData
 from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
 
 BNULL = {"blank": True, "null": True}
 
 
+@dataclass
+class BeerLogData(BaseLogData):
+    rating: Optional[str] = None
+
+
 class BeerStyle(TimeStampedModel):
     uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
     name = models.CharField(max_length=255)

+ 77 - 2
vrobbler/apps/boardgames/models.py

@@ -1,4 +1,6 @@
+from functools import cached_property
 import logging
+from dataclasses import dataclass
 from datetime import datetime
 from typing import Optional
 from uuid import uuid4
@@ -12,14 +14,87 @@ 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 BoardGameLogData
-from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
 from locations.models import GeoLocation
+from people.models import Person
+from scrobbles.dataclasses import BaseLogData, LongPlayLogData
+from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
 
 logger = logging.getLogger(__name__)
 BNULL = {"blank": True, "null": True}
 
 
+@dataclass
+class BoardGameScoreLogData(BaseLogData):
+    person_id: Optional[int] = None
+    bgg_username: Optional[str] = None
+    color: Optional[str] = None
+    character: Optional[str] = None
+    team: Optional[str] = None
+    score: Optional[int] = None
+    win: Optional[bool] = None
+    new: Optional[bool] = None
+    rank: Optional[int] = None
+    seat_order: Optional[int] = None
+    role: Optional[str] = None
+    rank: Optional[int] = None
+    seat_order: Optional[int] = None
+    role: Optional[str] = None
+    lichess_username: Optional[str] = None
+
+    @property
+    def person(self) -> Optional[Person]:
+        return Person.objects.filter(id=self.person_id).first()
+
+    @property
+    def name(self) -> str:
+        name = ""
+        if self.person:
+            name = self.person.name
+        return name
+
+    def __str__(self) -> str:
+        out = self.name
+        if self.score:
+            out += f" {self.score}"
+        if self.color:
+            out += f" ({self.color})"
+        if self.win:
+            out += f" [W]"
+        return out
+
+
+@dataclass
+class BoardGameLogData(BaseLogData, LongPlayLogData):
+    players: Optional[list[BoardGameScoreLogData]] = None
+    location_id: Optional[int] = None
+    difficulty: Optional[int] = None
+    solo: Optional[bool] = None
+    two_handed: Optional[bool] = None
+    expansion_ids: Optional[int] = None
+    moves: Optional[list] = None
+    rated: Optional[str] = None
+    speed: Optional[str] = None
+    variant: Optional[str] = None
+    lichess_id: Optional[int] = None
+    board: Optional[str] = None
+    rounds: Optional[int] = None
+    details: Optional[str] = None
+    # Legacy
+    learning: Optional[bool] = None
+    scenario: Optional[str] = None
+
+    @cached_property
+    def player_log(self) -> str:
+        if self.players:
+            return ", ".join(
+                [
+                    BoardGameScoreLogData(**player).__str__()
+                    for player in self.players
+                ]
+            )
+        return ""
+
+
 class BoardGamePublisher(TimeStampedModel):
     name = models.CharField(max_length=255)
     uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)

+ 32 - 6
vrobbler/apps/books/models.py

@@ -1,6 +1,8 @@
 from collections import OrderedDict
+from dataclasses import dataclass
 import logging
 from datetime import timedelta, datetime
+from typing import Optional
 from uuid import uuid4
 
 import requests
@@ -35,9 +37,9 @@ from vrobbler.apps.books.locg import (
     lookup_comic_from_locg,
     lookup_comic_writer_by_locg_slug,
 )
-from vrobbler.apps.books.sources.google import lookup_book_from_google
-from vrobbler.apps.books.sources.semantic import lookup_paper_from_semantic
-from vrobbler.apps.scrobbles.dataclasses import BookLogData
+from books.sources.google import lookup_book_from_google
+from books.sources.semantic import lookup_paper_from_semantic
+from scrobbles.dataclasses import BaseLogData, LongPlayLogData
 
 COMICVINE_API_KEY = getattr(settings, "COMICVINE_API_KEY", "")
 
@@ -46,6 +48,23 @@ User = get_user_model()
 BNULL = {"blank": True, "null": True}
 
 
+@dataclass
+class BookPageLogData(BaseLogData):
+    page_number: Optional[int] = None
+    end_ts: Optional[int] = None
+    start_ts: Optional[int] = None
+    duration: Optional[int] = None
+
+
+@dataclass
+class BookLogData(BaseLogData, LongPlayLogData):
+    koreader_hash: Optional[str] = None
+    page_data: Optional[dict[int, BookPageLogData]] = None
+    pages_read: Optional[int] = None
+    page_start: Optional[int] = None
+    page_end: Optional[int] = None
+
+
 class Author(TimeStampedModel):
     name = models.CharField(max_length=255)
     uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
@@ -161,7 +180,9 @@ class Book(LongPlayScrobblableMixin):
         return reverse("books:book_detail", kwargs={"slug": self.uuid})
 
     @classmethod
-    def find_or_create(cls, title: str, enrich: bool = False, commit: bool = True):
+    def find_or_create(
+        cls, title: str, enrich: bool = False, commit: bool = True
+    ):
         """Given a title, get a Book instance.
 
         If the book is not already in our database, or overwrite is True,
@@ -173,10 +194,15 @@ class Book(LongPlayScrobblableMixin):
         # TODO use either a Google Books id identifier or author name like for tracks
         book, created = cls.objects.get_or_create(title=title)
         if not created:
-            logger.info("Found exact match for book by title", extra={"title": title})
+            logger.info(
+                "Found exact match for book by title", extra={"title": title}
+            )
 
         if not enrich:
-            logger.info("Found book by title, but not enriching", extra={"title": title})
+            logger.info(
+                "Found book by title, but not enriching",
+                extra={"title": title},
+            )
             return book
 
         book_dict = lookup_book_from_google(title)

+ 11 - 3
vrobbler/apps/bricksets/models.py

@@ -1,15 +1,23 @@
-from django.apps import apps
+from dataclasses import dataclass
 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
+from vrobbler.apps.scrobbles.dataclasses import (
+    BaseLogData,
+    LongPlayLogData,
+    WithPeopleLogData,
+)
 
 BNULL = {"blank": True, "null": True}
 
 
+@dataclass
+class BrickSetLogData(BaseLogData, WithPeopleLogData, LongPlayLogData):
+    pass
+
+
 class BrickSet(LongPlayScrobblableMixin):
     """"""
 

+ 9 - 1
vrobbler/apps/foods/models.py

@@ -1,3 +1,5 @@
+from dataclasses import dataclass
+from typing import Optional
 from uuid import uuid4
 
 from django.apps import apps
@@ -6,12 +8,18 @@ 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 FoodLogData
+from scrobbles.dataclasses import BaseLogData
 from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
 
 BNULL = {"blank": True, "null": True}
 
 
+@dataclass
+class FoodLogData(BaseLogData):
+    meal: Optional[str] = None
+    rating: Optional[str] = None
+
+
 class FoodCategory(TimeStampedModel):
     uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
     name = models.CharField(max_length=255)

+ 8 - 1
vrobbler/apps/lifeevents/models.py

@@ -1,12 +1,19 @@
+from dataclasses import dataclass
+from typing import Optional
 from django.apps import apps
 from django.db import models
 from django.urls import reverse
-from scrobbles.dataclasses import LifeEventLogData
+from scrobbles.dataclasses import BaseLogData, WithPeopleLogData
 from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
 
 BNULL = {"blank": True, "null": True}
 
 
+@dataclass
+class LifeEventLogData(BaseLogData, WithPeopleLogData):
+    pass
+
+
 class LifeEvent(ScrobblableMixin):
     description = models.TextField(**BNULL)
 

+ 8 - 2
vrobbler/apps/moods/models.py

@@ -1,19 +1,25 @@
 import logging
+from dataclasses import dataclass
+from typing import Optional
 
 from django.contrib.auth import get_user_model
 from django.db import models
 from django.urls import reverse
 from imagekit.models import ImageSpecField
 from imagekit.processors import ResizeToFit
+from scrobbles.dataclasses import BaseLogData
 from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
 
-from vrobbler.apps.scrobbles.dataclasses import MoodLogData
-
 logger = logging.getLogger(__name__)
 BNULL = {"blank": True, "null": True}
 User = get_user_model()
 
 
+@dataclass
+class MoodLogData(BaseLogData):
+    reasons: Optional[str] = None
+
+
 class Mood(ScrobblableMixin):
     description = models.TextField(**BNULL)
     image = models.ImageField(upload_to="moods/", **BNULL)

+ 14 - 1
vrobbler/apps/puzzles/models.py

@@ -1,3 +1,5 @@
+from dataclasses import dataclass
+from typing import Optional
 from uuid import uuid4
 
 import requests
@@ -9,12 +11,19 @@ from django_extensions.db.models import TimeStampedModel
 from imagekit.models import ImageSpecField
 from imagekit.processors import ResizeToFit
 from puzzles.sources import ipdb
-from scrobbles.dataclasses import PuzzleLogData
+from scrobbles.dataclasses import JSONDataclass
 from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
 
 BNULL = {"blank": True, "null": True}
 
 
+@dataclass
+class PuzzleLogData(JSONDataclass):
+    with_people: Optional[int] = None
+    rating: Optional[str] = None
+    notes: Optional[str] = None
+
+
 class PuzzleManufacturer(TimeStampedModel):
     uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
     name = models.CharField(max_length=255)
@@ -58,6 +67,10 @@ class Puzzle(ScrobblableMixin):
     def __str__(self):
         return f"{self.title} ({self.pieces_count}) by {self.manufacturer}"
 
+    @property
+    def logdata_cls(self):
+        return PuzzleLogData
+
     @property
     def subtitle(self):
         return self.manufacturer.name

+ 12 - 203
vrobbler/apps/scrobbles/dataclasses.py

@@ -1,5 +1,3 @@
-from functools import cached_property
-import inspect
 import json
 from dataclasses import asdict, dataclass
 from typing import Optional
@@ -33,214 +31,25 @@ class JSONDataclass(JSONWizard):
 
 
 @dataclass
-class ScrobbleLogData(JSONDataclass):
-    description: Optional[str] = None
-
-
-class LongPlayLogData(JSONDataclass):
-    serial_scrobble_id: Optional[int]
-    long_play_complete: bool = False
-
-
-class WithOthersLogData(JSONDataclass):
-    with_user_ids: Optional[list[int]] = None
-    with_names_str: Optional[list[str]] = None
-
-    @property
-    def with_names(self) -> list[str]:
-        with_names = []
-        if self.with_user_ids:
-            with_names += [u.full_name for u in self.with_users if u]
-        if self.with_names_str:
-            with_names += [u for u in self.with_names_str]
-        return with_names
-
-    @property
-    def with_users(self) -> list[User]:
-        with_users = []
-        if self.with_user_ids:
-            with_users = [
-                User.objects.filter(id=i).first() for i in self.with_user_ids
-            ]
-        return with_users
-
-
-@dataclass
-class BoardGameScoreLogData(JSONDataclass):
-    person_id: Optional[int] = None
-    bgg_username: str = ""
-    color: Optional[str] = None
-    character: Optional[str] = None
-    team: Optional[str] = None
-    score: Optional[int] = None
-    win: Optional[bool] = None
-    new: Optional[bool] = None
-    rank: Optional[int] = None
-    seat_order: Optional[int] = None
-    role: Optional[str] = None
-    rank: Optional[int] = None
-    seat_order: Optional[int] = None
-    role: Optional[str] = None
-    lichess_username: Optional[str] = None
-    # Legacy
-    user_id: Optional[int] = None
-    name_str: Optional[str] = None
-
-
-    @property
-    def person(self) -> Optional[Person]:
-        return Person.objects.filter(id=self.person_id).first()
-
-    @property
-    def name(self) -> str:
-        name = ""
-        if self.person:
-            name = self.person.name
-        return name
-
-    def __str__(self) -> str:
-        out = self.name
-        if self.score:
-            out += f" {self.score}"
-        if self.color:
-            out += f" ({self.color})"
-        if self.win:
-            out += f" [W]"
-        return out
-
-
-@dataclass
-class BoardGameLogData(LongPlayLogData):
-    serial_scrobble_id: Optional[int] = None
-    long_play_complete: Optional[bool] = None
-    players: Optional[list[BoardGameScoreLogData]] = None
-    location_id: Optional[int] = None
-    difficulty: Optional[int] = None
-    solo: Optional[bool] = None
-    two_handed: Optional[bool] = None
-    expansion_ids: Optional[int] = None
-    moves: Optional[list] = None
-    rated: Optional[str] = None
-    speed: Optional[str] = None
-    variant: Optional[str] = None
-    lichess_id: Optional[int] = None
-    board: Optional[str] = None
-    rounds: Optional[int] = None
-    details: Optional[str] = None
-    # Legacy
-    learning: Optional[bool] = None
-    location: Optional[str] = None
-    geo_location_id: Optional[int] = None
-    scenario: Optional[str] = None
-    @cached_property
-    def geo_location(self) -> Optional[GeoLocation]:
-        if self.geo_location_id:
-            return GeoLocation.objects.filter(id=self.geo_location_id).first()
-
-    @cached_property
-    def player_log(self) -> str:
-        if self.players:
-            return ", ".join([BoardGameScoreLogData(**player).__str__() for player in self.players])
-        return ""
-
-@dataclass
-class BookPageLogData(JSONDataclass):
-    page_number: Optional[int] = None
-    end_ts: Optional[int] = None
-    start_ts: Optional[int] = None
-    duration: Optional[int] = None
-
-
-@dataclass
-class BookLogData(LongPlayLogData):
-    long_play_complete: Optional[bool] = None
-    koreader_hash: Optional[str] = None
-    page_data: Optional[dict[int, BookPageLogData]] = None
-    pages_read: Optional[int] = None
-    page_start: Optional[int] = None
-    page_end: Optional[int] = None
-    serial_scrobble_id: Optional[int] = None
-    details: Optional[str] = None
-
-
-@dataclass
-class LifeEventLogData(WithOthersLogData):
-    with_user_ids: Optional[list[int]] = None
-    with_names_str: Optional[list[str]] = None
-    location: Optional[str] = None
-    geo_location_id: Optional[int] = None
+class BaseLogData(JSONDataclass):
     details: Optional[str] = None
-
-    def geo_location(self):
-        return GeoLocation.objects.filter(id=self.geo_location_id).first()
-
-
-@dataclass
-class MoodLogData(JSONDataclass):
-    reasons: Optional[str] = None
-
-
-@dataclass
-class VideoLogData(JSONDataclass):
-    title: str
-    video_type: str
-    run_time_seconds: int
-    kind: str
-    year: Optional[int]
-    episode_number: Optional[int] = None
-    source_url: Optional[str] = None
-    imdbID: Optional[str] = None
-    season_number: Optional[int] = None
-    cover_url: Optional[str] = None
-    next_imdb_id: Optional[int] = None
-    tv_series_id: Optional[str] = None
+    notes: Optional[str] = None
 
 
 @dataclass
-class VideoGameLogData(LongPlayLogData):
+class LongPlayLogData(JSONDataclass):
+    complete: Optional[bool] = None
     serial_scrobble_id: Optional[int] = None
-    long_play_complete: Optional[bool] = False
-    console: Optional[str] = None
-    emulated: Optional[bool] = False
-    emulator: Optional[str] = None
 
 
 @dataclass
-class BrickSetLogData(LongPlayLogData, WithOthersLogData):
-    serial_scrobble_id: Optional[int]
-    long_play_complete: bool = False
-    with_user_ids: Optional[list[int]] = None
-    with_names_str: Optional[list[str]] = None
-
-
-@dataclass
-class TrailLogData(WithOthersLogData):
-    with_user_ids: Optional[list[int]] = None
-    with_names_str: Optional[list[str]] = None
-    details: Optional[str] = None
-    effort: Optional[str] = None
-    difficulty: Optional[str] = None
-
-
-@dataclass
-class BeerLogData(WithOthersLogData):
-    with_user_ids: Optional[list[int]] = None
-    with_names_str: Optional[list[str]] = None
-    details: Optional[str] = None
-    rating: Optional[str] = None
-    notes: Optional[str] = None
-
-
-@dataclass
-class FoodLogData(JSONDataclass):
-    meal: Optional[str] = None
-    details: Optional[str] = None
-    rating: Optional[str] = None
-    notes: Optional[str] = None
+class WithPeopleLogData(JSONDataclass):
+    with_people_ids: Optional[list[int]] = None
 
+    @property
+    def with_people(self) -> list["Person"]:
+        from people.models import Person
 
-@dataclass
-class PuzzleLogData(JSONDataclass):
-    with_others: Optional[str] = None
-    rating: Optional[str] = None
-    notes: Optional[str] = None
+        if not self.with_people_ids:
+            return []
+        return [Person.objects.filter(id=pid) for pid in self.with_people_ids]

+ 11 - 3
vrobbler/apps/scrobbles/models.py

@@ -14,6 +14,7 @@ from boardgames.models import BoardGame
 from books.koreader import process_koreader_sqlite_file
 from books.models import Book, Paper
 from bricksets.models import BrickSet
+from dataclass_wizard.errors import ParseError
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.core.files import File
@@ -718,11 +719,11 @@ class Scrobble(TimeStampedModel):
                 )
 
     @property
-    def logdata(self) -> Optional[logdata.JSONDataclass]:
+    def logdata(self) -> Optional[logdata.BaseLogData]:
         if self.media_obj:
             logdata_cls = self.media_obj.logdata_cls
         else:
-            logdata_cls = logdata.ScrobbleLogData
+            logdata_cls = logdata.BaseLogData
 
         log_dict = self.log
         if isinstance(self.log, str):
@@ -736,7 +737,14 @@ class Scrobble(TimeStampedModel):
         if not log_dict:
             log_dict = {}
 
-        return logdata_cls(**log_dict)
+        try:
+            return logdata_cls.from_dict(log_dict)
+        except ParseError:
+            logger.warning(
+                "Could not parse log data",
+                extra={"log_dict": log_dict, "scrobble_id": self.id},
+            )
+            return logdata_cls()
 
     def redirect_url(self, user_id) -> str:
         user = User.objects.filter(id=user_id).first()

+ 1 - 1
vrobbler/apps/tasks/models.py

@@ -34,7 +34,7 @@ class TaskLogData(JSONDataclass):
     source: Optional[str] = None
     source_id: Optional[str] = None
     timestamps: Optional[list] = None
-
+    details: Optional[str] = None
 
     def notes_as_str(self) -> str:
         """Return formatted notes with line breaks and no keys"""

+ 12 - 3
vrobbler/apps/trails/models.py

@@ -1,14 +1,23 @@
-from django.utils.translation import gettext_lazy as _
+from dataclasses import dataclass
+from typing import Optional
+
 from django.apps import apps
 from django.db import models
 from django.urls import reverse
-from scrobbles.dataclasses import TrailLogData
-from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
+from django.utils.translation import gettext_lazy as _
 from locations.models import GeoLocation
+from scrobbles.dataclasses import BaseLogData, WithPeopleLogData
+from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
 
 BNULL = {"blank": True, "null": True}
 
 
+@dataclass
+class TrailLogData(BaseLogData, WithPeopleLogData):
+    effort: Optional[str] = None
+    difficulty: Optional[str] = None
+
+
 class Trail(ScrobblableMixin):
     class PrincipalType(models.TextChoices):
         WOODS = "WOODS"

+ 19 - 1
vrobbler/apps/videogames/models.py

@@ -1,4 +1,6 @@
+from dataclasses import dataclass
 import logging
+from typing import Optional
 from uuid import uuid4
 
 from django.conf import settings
@@ -8,7 +10,11 @@ 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 VideoGameLogData
+from scrobbles.dataclasses import (
+    BaseLogData,
+    LongPlayLogData,
+    WithPeopleLogData,
+)
 from scrobbles.mixins import LongPlayScrobblableMixin, ScrobblableConstants
 from scrobbles.utils import get_scrobbles_for_media
 from videogames.igdb import lookup_game_id_from_gdb
@@ -18,6 +24,18 @@ BNULL = {"blank": True, "null": True}
 User = get_user_model()
 
 
+@dataclass
+class VideoGameLogData(BaseLogData, LongPlayLogData, WithPeopleLogData):
+    platform_id: Optional[int] = None
+    emulated: Optional[bool] = False
+    emulator: Optional[str] = None
+
+    def platform(self):
+        if not self.platform_id:
+            return
+        return VideoGamePlatform.objects.filter(id=self.platform_id).first()
+
+
 class VideoGamePlatform(TimeStampedModel):
     name = models.CharField(max_length=255)
     uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)

+ 11 - 0
vrobbler/apps/videos/models.py

@@ -1,3 +1,4 @@
+from dataclasses import dataclass
 import logging
 from typing import Optional
 from uuid import uuid4
@@ -22,6 +23,7 @@ from videos.metadata import VideoMetadata
 from videos.sources.imdb import lookup_video_from_imdb
 from videos.sources.tmdb import lookup_video_from_tmdb
 from videos.sources.youtube import lookup_video_from_youtube
+from vrobbler.apps.scrobbles.dataclasses import BaseLogData, WithPeopleLogData
 
 YOUTUBE_VIDEO_URL = "https://www.youtube.com/watch?v="
 YOUTUBE_CHANNEL_URL = "https://www.youtube.com/channel/"
@@ -31,6 +33,11 @@ logger = logging.getLogger(__name__)
 BNULL = {"blank": True, "null": True}
 
 
+@dataclass
+class VideoLogData(BaseLogData, WithPeopleLogData):
+    rating: Optional[int] = None
+
+
 class Channel(TimeStampedModel):
     uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
     name = models.CharField(max_length=255)
@@ -243,6 +250,10 @@ class Video(ScrobblableMixin):
     def get_absolute_url(self):
         return reverse("videos:video_detail", kwargs={"slug": self.uuid})
 
+    @property
+    def logdata_cls(self):
+        return VideoLogData
+
     @property
     def subtitle(self):
         if self.tv_series:

+ 5 - 2
vrobbler/urls.py

@@ -40,11 +40,13 @@ from vrobbler.apps.profiles import urls as profiles_urls
 from vrobbler.apps.trails import urls as trails_urls
 from vrobbler.apps.beers import urls as beers_urls
 from vrobbler.apps.foods import urls as foods_urls
+from vrobbler.apps.puzzles import urls as puzzles_urls
 from vrobbler.apps.videogames import urls as videogame_urls
 from vrobbler.apps.videos import urls as video_urls
 from vrobbler.apps.videos.api.views import SeriesViewSet, VideoViewSet
 from vrobbler.apps.webpages import urls as webpages_urls
-#from vrobbler.apps.modern_ui import urls as modern_ui_urls
+
+# from vrobbler.apps.modern_ui import urls as modern_ui_urls
 
 router = routers.DefaultRouter()
 router.register(r"scrobbles", ScrobbleViewSet)
@@ -73,7 +75,7 @@ urlpatterns = [
     path("admin/", admin.site.urls),
     path("accounts/", include("allauth.urls")),
     path("o/", include(oauth2_urls)),
-    #path("modern_ui/", include(modern_ui_urls, namespace="modern_ui")),
+    # path("modern_ui/", include(modern_ui_urls, namespace="modern_ui")),
     path("", include(music_urls, namespace="music")),
     path("", include(book_urls, namespace="books")),
     path("", include(video_urls, namespace="videos")),
@@ -85,6 +87,7 @@ urlpatterns = [
     path("", include(trails_urls, namespace="trails")),
     path("", include(beers_urls, namespace="beers")),
     path("", include(foods_urls, namespace="foods")),
+    path("", include(puzzles_urls, namespace="puzzles")),
     path("", include(tasks_urls, namespace="tasks")),
     path("", include(webpages_urls, namespace="webpages")),
     path("", include(podcast_urls, namespace="podcasts")),