models.py 8.3 KB

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