From d04db0ecb50d7ee2be9beffe0906c1c697423f6e Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Sun, 3 Aug 2025 01:59:56 -0400 Subject: [PATCH] [scrobbles] Fix dataclass parsing and add puzzles to urls --- vrobbler/apps/beers/models.py | 11 +- vrobbler/apps/boardgames/models.py | 79 ++++++++- vrobbler/apps/books/models.py | 38 ++++- vrobbler/apps/bricksets/models.py | 14 +- vrobbler/apps/foods/models.py | 10 +- vrobbler/apps/lifeevents/models.py | 9 +- vrobbler/apps/moods/models.py | 10 +- vrobbler/apps/puzzles/models.py | 15 +- vrobbler/apps/scrobbles/dataclasses.py | 217 ++----------------------- vrobbler/apps/scrobbles/models.py | 14 +- vrobbler/apps/tasks/models.py | 2 +- vrobbler/apps/trails/models.py | 15 +- vrobbler/apps/videogames/models.py | 20 ++- vrobbler/apps/videos/models.py | 11 ++ vrobbler/urls.py | 7 +- 15 files changed, 240 insertions(+), 232 deletions(-) diff --git a/vrobbler/apps/beers/models.py b/vrobbler/apps/beers/models.py index 8a4cf94..ac9a3d3 100644 --- a/vrobbler/apps/beers/models.py +++ b/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) diff --git a/vrobbler/apps/boardgames/models.py b/vrobbler/apps/boardgames/models.py index d16c562..ffb071d 100644 --- a/vrobbler/apps/boardgames/models.py +++ b/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) diff --git a/vrobbler/apps/books/models.py b/vrobbler/apps/books/models.py index 7de5073..7202ed7 100644 --- a/vrobbler/apps/books/models.py +++ b/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) diff --git a/vrobbler/apps/bricksets/models.py b/vrobbler/apps/bricksets/models.py index d1ded0b..91d0eb3 100644 --- a/vrobbler/apps/bricksets/models.py +++ b/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): """""" diff --git a/vrobbler/apps/foods/models.py b/vrobbler/apps/foods/models.py index 926fc4a..54a5e24 100644 --- a/vrobbler/apps/foods/models.py +++ b/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) diff --git a/vrobbler/apps/lifeevents/models.py b/vrobbler/apps/lifeevents/models.py index 03e6613..aa4ec2a 100644 --- a/vrobbler/apps/lifeevents/models.py +++ b/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) diff --git a/vrobbler/apps/moods/models.py b/vrobbler/apps/moods/models.py index 9489200..70b0a2a 100644 --- a/vrobbler/apps/moods/models.py +++ b/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) diff --git a/vrobbler/apps/puzzles/models.py b/vrobbler/apps/puzzles/models.py index 5acc884..b10fcff 100644 --- a/vrobbler/apps/puzzles/models.py +++ b/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 diff --git a/vrobbler/apps/scrobbles/dataclasses.py b/vrobbler/apps/scrobbles/dataclasses.py index ce561f3..d77a35c 100644 --- a/vrobbler/apps/scrobbles/dataclasses.py +++ b/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 BaseLogData(JSONDataclass): + details: Optional[str] = None + notes: Optional[str] = None +@dataclass 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): + complete: Optional[bool] = None 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 +class WithPeopleLogData(JSONDataclass): + with_people_ids: Optional[list[int]] = None + @property + def with_people(self) -> list["Person"]: + from people.models import Person -@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 - 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 - - -@dataclass -class VideoGameLogData(LongPlayLogData): - 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 - - -@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] diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index cd2bd9d..27169b6 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/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() diff --git a/vrobbler/apps/tasks/models.py b/vrobbler/apps/tasks/models.py index 33c02a8..1cfabc3 100644 --- a/vrobbler/apps/tasks/models.py +++ b/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""" diff --git a/vrobbler/apps/trails/models.py b/vrobbler/apps/trails/models.py index 2f56cda..b771b1a 100644 --- a/vrobbler/apps/trails/models.py +++ b/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" diff --git a/vrobbler/apps/videogames/models.py b/vrobbler/apps/videogames/models.py index a1e8955..8ad23fa 100644 --- a/vrobbler/apps/videogames/models.py +++ b/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) diff --git a/vrobbler/apps/videos/models.py b/vrobbler/apps/videos/models.py index bd03697..69d1156 100644 --- a/vrobbler/apps/videos/models.py +++ b/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: diff --git a/vrobbler/urls.py b/vrobbler/urls.py index f8134d8..d0c8468 100644 --- a/vrobbler/urls.py +++ b/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")),