from dataclasses import dataclass import logging from typing import Optional from uuid import uuid4 from django.apps import apps from django.db import models from django.urls import reverse from django.utils import timezone from django_extensions.db.models import TimeStampedModel from scrobbles.utils import get_scrobbles_for_media from taggit.managers import TaggableManager from taggit.models import GenericTaggedItemBase, TagBase BNULL = {"blank": True, "null": True} logger = logging.getLogger(__name__) @dataclass class ScrobblableConstants: verb: str tags: str priority: str def __init__( self, verb: str = "Scrobbling", tags: str = "green_square", priority: str = "default", ): self.verb = verb self.tags = tags self.priority = priority class Genre(TagBase): source = models.CharField(max_length=255, **BNULL) class Meta: verbose_name = "Genre" verbose_name_plural = "Genres" class ObjectWithGenres(GenericTaggedItemBase): tag = models.ForeignKey( Genre, on_delete=models.CASCADE, related_name="%(app_label)s_%(class)s_items", **BNULL, ) class ScrobblableMixin(TimeStampedModel): SECONDS_TO_STALE = 1600 COMPLETION_PERCENT = 100 uuid = models.UUIDField(default=uuid4, editable=False, **BNULL) title = models.CharField(max_length=255, **BNULL) base_run_time_seconds = models.IntegerField(**BNULL) genre = TaggableManager(through=ObjectWithGenres, blank=True, verbose_name="Genre") tags = TaggableManager(blank=True, verbose_name="Tags") class Meta: abstract = True def __str__(self) -> str: return f"{self.title} - {self.subtitle}" @property def run_time_seconds(self) -> int: run_time = 900 if self.base_run_time_seconds: run_time = self.base_run_time_seconds return run_time @classmethod def is_long_play_media(cls) -> bool: return False def scrobble_for_user( self, user_id, source: str = "Vrobbler", source_id: Optional[str] = None, playback_position_seconds: int = 0, status: str = "started", log: Optional[dict] = None, ): Scrobble = apps.get_model("scrobbles", "Scrobble") scrobble_data = { "user_id": user_id, "timestamp": timezone.now(), "source": source, "source_id": source_id, "status": status, "playback_position_seconds": playback_position_seconds, } if log: scrobble_data["log"] = log logger.info( "[scrobble_for_user] called", extra={ "id": self.id, "media_type": self.__class__.__name__, "user_id": user_id, "scrobble_data": scrobble_data, }, ) return Scrobble.create_or_update(self, user_id, scrobble_data) @property def start_url(self): return reverse("scrobbles:start", kwargs={"media_uuid": self.uuid}) @property def strings(self) -> ScrobblableConstants: return ScrobblableConstants() @property def primary_image_url(self) -> str: logger.warning(f"Not implemented yet") return "" @property def logdata_cls(self) -> None: from scrobbles.dataclasses import BaseLogData return BaseLogData @property def subtitle(self) -> str: return "" def fix_metadata(self) -> None: logger.warning("fix_metadata() not implemented yet") @classmethod def find_or_create(cls): logger.warning("find_or_create() not implemented yet") def __str__(self) -> str: if self.title: return str(self.title) return str(self.uuid) class LongPlayScrobblableMixin(ScrobblableMixin): class Meta: abstract = True @classmethod def is_long_play_media(cls) -> bool: return True def is_complete(self) -> bool: if self.log: return bool(self.log.get("long_play_complete", None)) return False def get_longplay_finish_url(self): return reverse("scrobbles:longplay-finish", kwargs={"media_uuid": self.uuid}) def first_long_play_scrobble_for_user(self, user) -> Optional["Scrobble"]: last = self.last_long_play_scrobble_for_user(user) if not last: return None current = last while current.long_play_last_scrobble and not current.long_play_last_scrobble.long_play_complete: current = current.long_play_last_scrobble return current def last_long_play_scrobble_for_user(self, user) -> Optional["Scrobble"]: return ( get_scrobbles_for_media(self, user) .filter(long_play_complete=False) .order_by("-timestamp") .first() )