models.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import logging
  2. from typing import Dict, Optional
  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 imagekit.models import ImageSpecField
  12. from imagekit.processors import ResizeToFit
  13. from music.constants import JELLYFIN_POST_KEYS
  14. from scrobbles.mixins import ObjectWithGenres, ScrobblableMixin
  15. from taggit.managers import TaggableManager
  16. from videos.imdb import lookup_video_from_imdb
  17. logger = logging.getLogger(__name__)
  18. BNULL = {"blank": True, "null": True}
  19. class Series(TimeStampedModel):
  20. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  21. name = models.CharField(max_length=255)
  22. plot = models.TextField(**BNULL)
  23. imdb_id = models.CharField(max_length=20, **BNULL)
  24. imdb_rating = models.FloatField(**BNULL)
  25. cover_image = models.ImageField(upload_to="videos/series/", **BNULL)
  26. cover_small = ImageSpecField(
  27. source="cover_image",
  28. processors=[ResizeToFit(100, 100)],
  29. format="JPEG",
  30. options={"quality": 60},
  31. )
  32. cover_medium = ImageSpecField(
  33. source="cover_image",
  34. processors=[ResizeToFit(300, 300)],
  35. format="JPEG",
  36. options={"quality": 75},
  37. )
  38. preferred_source = models.CharField(max_length=100, **BNULL)
  39. genre = TaggableManager(through=ObjectWithGenres)
  40. class Meta:
  41. verbose_name_plural = "series"
  42. def __str__(self):
  43. return self.name
  44. def get_absolute_url(self):
  45. return reverse("videos:series_detail", kwargs={"slug": self.uuid})
  46. def imdb_link(self):
  47. return f"https://www.imdb.com/title/tt{self.imdb_id}"
  48. @property
  49. def primary_image_url(self) -> str:
  50. url = ""
  51. if self.cover_image:
  52. url = self.cover_image_medium.url
  53. return url
  54. def scrobbles_for_user(self, user_id: int, include_playing=False):
  55. from scrobbles.models import Scrobble
  56. played_query = models.Q(played_to_completion=True)
  57. if include_playing:
  58. played_query = models.Q()
  59. return Scrobble.objects.filter(
  60. played_query,
  61. video__tv_series=self,
  62. user=user_id,
  63. ).order_by("-timestamp")
  64. def last_scrobbled_episode(self, user_id: int) -> Optional["Video"]:
  65. episode = None
  66. last_scrobble = self.scrobbles_for_user(
  67. user_id, include_playing=True
  68. ).first()
  69. if last_scrobble:
  70. episode = last_scrobble.media_obj
  71. return episode
  72. def is_episode_playing(self, user_id: int) -> bool:
  73. last_scrobble = self.scrobbles_for_user(
  74. user_id, include_playing=True
  75. ).first()
  76. return not last_scrobble.played_to_completion
  77. def fix_metadata(self, force_update=False):
  78. name_or_id = self.name
  79. if self.imdb_id:
  80. name_or_id = self.imdb_id
  81. imdb_dict = lookup_video_from_imdb(name_or_id)
  82. if not imdb_dict:
  83. logger.warn(f"No imdb data for {self}")
  84. return
  85. self.imdb_id = imdb_dict.data.get("imdbID")
  86. self.imdb_rating = imdb_dict.data.get("arithmetic mean")
  87. self.plot = imdb_dict.data.get("plot outline")
  88. self.save(update_fields=["imdb_id", "imdb_rating", "plot"])
  89. cover_url = imdb_dict.get("cover url")
  90. if (not self.cover_image or force_update) and cover_url:
  91. r = requests.get(cover_url)
  92. if r.status_code == 200:
  93. fname = f"{self.name}_{self.uuid}.jpg"
  94. self.cover_image.save(fname, ContentFile(r.content), save=True)
  95. if genres := imdb_dict.data.get("genres"):
  96. self.genre.add(*genres)
  97. class Video(ScrobblableMixin):
  98. COMPLETION_PERCENT = getattr(settings, "VIDEO_COMPLETION_PERCENT", 90)
  99. SECONDS_TO_STALE = getattr(settings, "VIDEO_SECONDS_TO_STALE", 14400)
  100. class VideoType(models.TextChoices):
  101. UNKNOWN = "U", _("Unknown")
  102. TV_EPISODE = "E", _("TV Episode")
  103. MOVIE = "M", _("Movie")
  104. SKATE_VIDEO = "S", _("Skate Video")
  105. video_type = models.CharField(
  106. max_length=1,
  107. choices=VideoType.choices,
  108. default=VideoType.UNKNOWN,
  109. )
  110. overview = models.TextField(**BNULL)
  111. tagline = models.TextField(**BNULL)
  112. year = models.IntegerField(**BNULL)
  113. # TV show specific fields
  114. tv_series = models.ForeignKey(Series, on_delete=models.DO_NOTHING, **BNULL)
  115. season_number = models.IntegerField(**BNULL)
  116. episode_number = models.IntegerField(**BNULL)
  117. next_imdb_id = models.CharField(max_length=20, **BNULL)
  118. imdb_id = models.CharField(max_length=20, **BNULL)
  119. imdb_rating = models.FloatField(**BNULL)
  120. cover_image = models.ImageField(upload_to="videos/video/", **BNULL)
  121. cover_image_small = ImageSpecField(
  122. source="cover_image",
  123. processors=[ResizeToFit(100, 100)],
  124. format="JPEG",
  125. options={"quality": 60},
  126. )
  127. cover_image_medium = ImageSpecField(
  128. source="cover_image",
  129. processors=[ResizeToFit(300, 300)],
  130. format="JPEG",
  131. options={"quality": 75},
  132. )
  133. tvrage_id = models.CharField(max_length=20, **BNULL)
  134. tvdb_id = models.CharField(max_length=20, **BNULL)
  135. tmdb_id = models.CharField(max_length=20, **BNULL)
  136. plot = models.TextField(**BNULL)
  137. year = models.IntegerField(**BNULL)
  138. class Meta:
  139. unique_together = [["title", "imdb_id"]]
  140. def __str__(self):
  141. if self.video_type == self.VideoType.TV_EPISODE:
  142. return f"{self.tv_series} - Season {self.season_number}, Episode {self.episode_number}"
  143. return self.title
  144. def get_absolute_url(self):
  145. return reverse("videos:video_detail", kwargs={"slug": self.uuid})
  146. @property
  147. def subtitle(self):
  148. if self.tv_series:
  149. return self.tv_series
  150. return ""
  151. @property
  152. def imdb_link(self):
  153. return f"https://www.imdb.com/title/tt{self.imdb_id}"
  154. @property
  155. def info_link(self):
  156. return self.imdb_link
  157. @property
  158. def link(self):
  159. return self.imdb_link
  160. @property
  161. def primary_image_url(self) -> str:
  162. url = ""
  163. if self.cover_image:
  164. url = self.cover_image_medium.url
  165. return url
  166. def fix_metadata(self, force_update=False):
  167. imdb_dict = lookup_video_from_imdb(self.imdb_id)
  168. if not imdb_dict:
  169. logger.warn(f"No imdb data for {self}")
  170. return
  171. if imdb_dict.get("runtimes") and len(imdb_dict.get("runtimes")) > 0:
  172. self.run_time_seconds = int(imdb_dict.get("runtimes")[0]) * 60
  173. if (
  174. imdb_dict.get("run_time_seconds")
  175. and imdb_dict.get("run_time_seconds") > 0
  176. ):
  177. self.run_time_seconds = int(imdb_dict.get("run_time_seconds"))
  178. self.imdb_rating = imdb_dict.get("imdb_rating")
  179. self.plot = imdb_dict.get("plot")
  180. self.year = imdb_dict.get("year")
  181. self.save(
  182. update_fields=["imdb_rating", "plot", "year", "run_time_seconds"]
  183. )
  184. cover_url = imdb_dict.get("cover_url")
  185. if (not self.cover_image or force_update) and cover_url:
  186. r = requests.get(cover_url)
  187. if r.status_code == 200:
  188. fname = f"{self.title}_{self.uuid}.jpg"
  189. self.cover_image.save(fname, ContentFile(r.content), save=True)
  190. if genres := imdb_dict.get("genres"):
  191. self.genre.add(*genres)
  192. def scrape_cover_from_url(
  193. self, cover_url: str, force_update: bool = False
  194. ):
  195. if not self.cover_image or force_update:
  196. r = requests.get(cover_url)
  197. if r.status_code == 200:
  198. fname = f"{self.title}_{self.uuid}.jpg"
  199. self.cover_image.save(fname, ContentFile(r.content), save=True)
  200. @classmethod
  201. def find_or_create(
  202. cls, data_dict: dict, post_keys: dict = JELLYFIN_POST_KEYS
  203. ) -> Optional["Video"]:
  204. """Thes smallest of wrappers around our actual get or create utility."""
  205. from videos.utils import get_or_create_video
  206. return get_or_create_video(data_dict, post_keys)