models.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import logging
  2. from typing import Dict
  3. from uuid import uuid4
  4. import requests
  5. from django.conf import settings
  6. from django.core.files.base import ContentFile
  7. from django.db import models
  8. from django.urls import reverse
  9. from django.utils.translation import gettext_lazy as _
  10. from django_extensions.db.models import TimeStampedModel
  11. from scrobbles.mixins import ScrobblableMixin
  12. from scrobbles.utils import convert_to_seconds
  13. from taggit.managers import TaggableManager
  14. from videos.imdb import lookup_video_from_imdb
  15. logger = logging.getLogger(__name__)
  16. BNULL = {"blank": True, "null": True}
  17. class Series(TimeStampedModel):
  18. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  19. name = models.CharField(max_length=255)
  20. plot = models.TextField(**BNULL)
  21. imdb_id = models.CharField(max_length=20, **BNULL)
  22. imdb_rating = models.FloatField(**BNULL)
  23. cover_image = models.ImageField(upload_to="videos/series/", **BNULL)
  24. genres = TaggableManager()
  25. class Meta:
  26. verbose_name_plural = "series"
  27. def __str__(self):
  28. return self.name
  29. def get_absolute_url(self):
  30. return reverse("videos:series_detail", kwargs={"slug": self.uuid})
  31. def imdb_link(self):
  32. return f"https://www.imdb.com/title/tt{self.imdb_id}"
  33. def scrobbles_for_user(self, user_id: int):
  34. from scrobbles.models import Scrobble
  35. return Scrobble.objects.filter(
  36. video__tv_series=self, user=user_id, played_to_completion=True
  37. ).order_by("-timestamp")
  38. def fix_metadata(self, force_update=False):
  39. imdb_dict = lookup_video_from_imdb(self.name, kind="tv series")
  40. if not imdb_dict:
  41. logger.warn(f"No imdb data for {self}")
  42. return
  43. self.imdb_id = imdb_dict.data.get("imdbID")
  44. self.imdb_rating = imdb_dict.data.get("arithmetic mean")
  45. self.plot = imdb_dict.data.get("plot outline")
  46. self.save(update_fields=["imdb_id", "imdb_rating", "plot"])
  47. cover_url = imdb_dict.get("cover url")
  48. if (not self.cover_image or force_update) and cover_url:
  49. r = requests.get(cover_url)
  50. if r.status_code == 200:
  51. fname = f"{self.name}_{self.uuid}.jpg"
  52. self.cover_image.save(fname, ContentFile(r.content), save=True)
  53. class Video(ScrobblableMixin):
  54. COMPLETION_PERCENT = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
  55. SECONDS_TO_STALE = getattr(settings, "VIDEO_SECONDS_TO_STALE", 14400)
  56. class VideoType(models.TextChoices):
  57. UNKNOWN = "U", _("Unknown")
  58. TV_EPISODE = "E", _("TV Episode")
  59. MOVIE = "M", _("Movie")
  60. video_type = models.CharField(
  61. max_length=1,
  62. choices=VideoType.choices,
  63. default=VideoType.UNKNOWN,
  64. )
  65. overview = models.TextField(**BNULL)
  66. tagline = models.TextField(**BNULL)
  67. year = models.IntegerField(**BNULL)
  68. # TV show specific fields
  69. tv_series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
  70. season_number = models.IntegerField(**BNULL)
  71. episode_number = models.IntegerField(**BNULL)
  72. imdb_id = models.CharField(max_length=20, **BNULL)
  73. imdb_rating = models.FloatField(**BNULL)
  74. cover_image = models.ImageField(upload_to="videos/video/", **BNULL)
  75. tvrage_id = models.CharField(max_length=20, **BNULL)
  76. tvdb_id = models.CharField(max_length=20, **BNULL)
  77. plot = models.TextField(**BNULL)
  78. year = models.IntegerField(**BNULL)
  79. class Meta:
  80. unique_together = [["title", "imdb_id"]]
  81. def __str__(self):
  82. if self.video_type == self.VideoType.TV_EPISODE:
  83. return f"{self.tv_series} - Season {self.season_number}, Episode {self.episode_number}"
  84. return self.title
  85. def get_absolute_url(self):
  86. return reverse("videos:video_detail", kwargs={"slug": self.uuid})
  87. @property
  88. def subtitle(self):
  89. if self.tv_series:
  90. return self.tv_series
  91. return ""
  92. @property
  93. def imdb_link(self):
  94. return f"https://www.imdb.com/title/tt{self.imdb_id}"
  95. @property
  96. def info_link(self):
  97. return self.imdb_link
  98. @property
  99. def link(self):
  100. return self.imdb_link
  101. def fix_metadata(self, force_update=False):
  102. imdb_dict = lookup_video_from_imdb(self.imdb_id)
  103. if not imdb_dict:
  104. logger.warn(f"No imdb data for {self}")
  105. return
  106. if imdb_dict.get("runtimes") and len(imdb_dict.get("runtimes")) > 0:
  107. self.run_time_seconds = int(imdb_dict.get("runtimes")[0]) * 60
  108. self.imdb_rating = imdb_dict.data.get("rating")
  109. self.plot = imdb_dict.data.get("plot")
  110. self.year = imdb_dict.data.get("year")
  111. self.save(
  112. update_fields=["imdb_rating", "plot", "year", "run_time_seconds"]
  113. )
  114. cover_url = imdb_dict.get("cover url")
  115. if (not self.cover_image or force_update) and cover_url:
  116. r = requests.get(cover_url)
  117. if r.status_code == 200:
  118. fname = f"{self.title}_{self.uuid}.jpg"
  119. self.cover_image.save(fname, ContentFile(r.content), save=True)
  120. @classmethod
  121. def find_or_create(cls, data_dict: Dict) -> "Video":
  122. """Given a data dict from Jellyfin, does the heavy lifting of looking up
  123. the video and, if need, TV Series, creating both if they don't yet
  124. exist.
  125. """
  126. from videos.utils import (
  127. get_or_create_video,
  128. get_or_create_video_from_jellyfin,
  129. )
  130. if "NotificationType" not in data_dict.keys():
  131. name_or_id = data_dict.get("imdb_id") or data_dict.get("title")
  132. video = get_or_create_video(name_or_id)
  133. return video
  134. if not data_dict.get("Provider_imdb"):
  135. title = data_dict.get("Name", "")
  136. logger.warn(
  137. f"No IMDB ID from Jellyfin, check metadata for {title}"
  138. )
  139. return
  140. return get_or_create_video_from_jellyfin(data_dict)