models.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import logging
  2. from uuid import uuid4
  3. import requests
  4. from django.apps import apps
  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 podcasts.sources.podcastindex import lookup_podcast_from_podcastindex
  12. from scrobbles.mixins import (
  13. ObjectWithGenres,
  14. ScrobblableConstants,
  15. ScrobblableMixin,
  16. )
  17. from taggit.managers import TaggableManager
  18. logger = logging.getLogger(__name__)
  19. BNULL = {"blank": True, "null": True}
  20. class Producer(TimeStampedModel):
  21. name = models.CharField(max_length=255)
  22. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  23. def __str__(self):
  24. return f"{self.name}"
  25. @classmethod
  26. def find_or_create(cls, name):
  27. producer = cls.objects.filter(name__iexact=name).first()
  28. if not producer:
  29. producer = cls.objects.create(name=name)
  30. return producer
  31. class Podcast(TimeStampedModel):
  32. name = models.CharField(max_length=255)
  33. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  34. producer = models.ForeignKey(
  35. Producer, on_delete=models.DO_NOTHING, **BNULL
  36. )
  37. podcastindex_id = models.CharField(max_length=100, **BNULL)
  38. owner = models.CharField(max_length=150, *BNULL)
  39. description = models.TextField(**BNULL)
  40. active = models.BooleanField(default=True)
  41. feed_url = models.URLField(**BNULL)
  42. site_link = models.URLField(**BNULL)
  43. description = models.TextField(**BNULL)
  44. cover_image = models.ImageField(upload_to="podcasts/covers/", **BNULL)
  45. itunes_id = models.TextField(max_length=15, **BNULL)
  46. dead_date = models.DateField(**BNULL)
  47. genre = TaggableManager(through=ObjectWithGenres)
  48. def __str__(self):
  49. return f"{self.name}"
  50. def get_absolute_url(self):
  51. return reverse("podcasts:podcast_detail", kwargs={"slug": self.uuid})
  52. def scrobbles(self, user):
  53. Scrobble = apps.get_model("scrobbles", "Scrobble")
  54. return Scrobble.objects.filter(
  55. user=user, podcast_episode__podcast=self
  56. ).order_by("-timestamp")
  57. @property
  58. def itunes_link(self) -> str:
  59. if not self.itunes_id:
  60. return ""
  61. return f"https://podcasts.apple.com/us/podcast/id{self.itunes_id}"
  62. def fix_metadata(self, force=False):
  63. if self.podcastindex_id and not force:
  64. logger.warning(
  65. "Podcast already has PodcastIndex ID, use force=True to overwrite"
  66. )
  67. return
  68. podcast_dict = lookup_podcast_from_podcastindex(self.name)
  69. if not podcast_dict:
  70. logger.info(
  71. "No podcast data found from PodcastIndex. Are credentials setup?"
  72. )
  73. return
  74. genres = podcast_dict.pop("genres")
  75. if genres:
  76. self.genre.add(*genres)
  77. cover_url = podcast_dict.pop("image_url")
  78. if (not self.cover_image or force) and cover_url:
  79. r = requests.get(cover_url)
  80. if r.status_code == 200:
  81. fname = f"{self.name}_{self.uuid}.jpg"
  82. self.cover_image.save(fname, ContentFile(r.content), save=True)
  83. for attr, value in podcast_dict.items():
  84. setattr(self, attr, value)
  85. self.save()
  86. class PodcastEpisode(ScrobblableMixin):
  87. COMPLETION_PERCENT = getattr(settings, "PODCAST_COMPLETION_PERCENT", 90)
  88. podcast = models.ForeignKey(Podcast, on_delete=models.DO_NOTHING)
  89. number = models.IntegerField(**BNULL)
  90. pub_date = models.DateField(**BNULL)
  91. mopidy_uri = models.CharField(max_length=255, **BNULL)
  92. def get_absolute_url(self):
  93. return reverse(
  94. "podcasts:podcast_detail", kwargs={"slug": self.podcast.uuid}
  95. )
  96. def __str__(self):
  97. return f"{self.title}"
  98. @property
  99. def subtitle(self):
  100. return self.podcast
  101. @property
  102. def strings(self) -> ScrobblableConstants:
  103. return ScrobblableConstants(verb="Listening", tags="microphone")
  104. @property
  105. def info_link(self):
  106. return ""
  107. @property
  108. def primary_image_url(self) -> str:
  109. url = ""
  110. if self.podcast.cover_image:
  111. url = self.podcast.cover_image.url
  112. return url
  113. @classmethod
  114. def find_or_create(
  115. cls,
  116. title: str,
  117. pub_date: str,
  118. episode_num: int = 0,
  119. base_run_time_seconds: int = 2400,
  120. mopidy_uri: str = "",
  121. podcast_name: str = "",
  122. podcast_producer: str = "",
  123. podcast_description: str = "",
  124. enrich: bool = True,
  125. ) -> "PodcastEpisode":
  126. """Given a data dict from Mopidy, finds or creates a podcast and
  127. producer before saving the epsiode so it can be scrobbled.
  128. """
  129. log_context={"mopidy_uri": mopidy_uri, "media_type": "Podcast"}
  130. producer = None
  131. if podcast_producer:
  132. producer = Producer.find_or_create(podcast_producer)
  133. podcast, created = Podcast.objects.get_or_create(name=podcast_name, defaults={"description": podcast_description})
  134. log_context["podcast_id"] = podcast.id
  135. log_context["podcast_name"] = podcast.name
  136. if created:
  137. logger.info("Created new podcast", extra=log_context)
  138. if enrich and created:
  139. logger.info("Enriching new podcast", extra=log_context)
  140. podcast.fix_metadata()
  141. episode, created = cls.objects.get_or_create(
  142. title=title,
  143. podcast=podcast,
  144. defaults={
  145. "base_run_time_seconds": base_run_time_seconds,
  146. "number": episode_num,
  147. "pub_date": pub_date,
  148. "mopidy_uri": mopidy_uri,
  149. }
  150. )
  151. if created:
  152. log_context["episode_id"] = episode.id
  153. log_context["episode_title"] = episode.title
  154. logger.info("Created new podcast episode", extra=log_context)
  155. return episode