183 lines
4.8 KiB
Python
183 lines
4.8 KiB
Python
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()
|
|
)
|