Files
vrobbler/vrobbler/apps/podcasts/models.py

186 lines
5.5 KiB
Python

import logging
from uuid import uuid4
import requests
from django.apps import apps
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_extensions.db.models import TimeStampedModel
from podcasts.sources.podcastindex import lookup_podcast_from_podcastindex
from scrobbles.mixins import (
ObjectWithGenres,
ScrobblableConstants,
ScrobblableMixin,
)
from taggit.managers import TaggableManager
logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
class Producer(TimeStampedModel):
name = models.CharField(max_length=255)
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
def __str__(self):
return f"{self.name}"
@classmethod
def find_or_create(cls, name):
producer = cls.objects.filter(name__iexact=name).first()
if not producer:
producer = cls.objects.create(name=name)
return producer
class Podcast(TimeStampedModel):
name = models.CharField(max_length=255)
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
producer = models.ForeignKey(
Producer, on_delete=models.DO_NOTHING, **BNULL
)
podcastindex_id = models.CharField(max_length=100, **BNULL)
owner = models.CharField(max_length=150, *BNULL)
description = models.TextField(**BNULL)
active = models.BooleanField(default=True)
feed_url = models.URLField(**BNULL)
site_link = models.URLField(**BNULL)
description = models.TextField(**BNULL)
cover_image = models.ImageField(upload_to="podcasts/covers/", **BNULL)
itunes_id = models.TextField(max_length=15, **BNULL)
dead_date = models.DateField(**BNULL)
genre = TaggableManager(through=ObjectWithGenres)
def __str__(self):
return f"{self.name}"
def get_absolute_url(self):
return reverse("podcasts:podcast_detail", kwargs={"slug": self.uuid})
def scrobbles(self, user):
Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter(
user=user, podcast_episode__podcast=self
).order_by("-timestamp")
@property
def itunes_link(self) -> str:
if not self.itunes_id:
return ""
return f"https://podcasts.apple.com/us/podcast/id{self.itunes_id}"
def fix_metadata(self, force=False):
if self.podcastindex_id and not force:
logger.warning(
"Podcast already has PodcastIndex ID, use force=True to overwrite"
)
return
podcast_dict = lookup_podcast_from_podcastindex(self.name)
if not podcast_dict:
logger.info(
"No podcast data found from PodcastIndex. Are credentials setup?"
)
return
genres = podcast_dict.pop("genres")
if genres:
self.genre.add(*genres)
cover_url = podcast_dict.pop("image_url")
if (not self.cover_image or force) and cover_url:
r = requests.get(cover_url)
if r.status_code == 200:
fname = f"{self.name}_{self.uuid}.jpg"
self.cover_image.save(fname, ContentFile(r.content), save=True)
for attr, value in podcast_dict.items():
setattr(self, attr, value)
self.save()
class PodcastEpisode(ScrobblableMixin):
COMPLETION_PERCENT = getattr(settings, "PODCAST_COMPLETION_PERCENT", 90)
podcast = models.ForeignKey(Podcast, on_delete=models.DO_NOTHING)
number = models.IntegerField(**BNULL)
pub_date = models.DateField(**BNULL)
mopidy_uri = models.CharField(max_length=255, **BNULL)
def get_absolute_url(self):
return reverse(
"podcasts:podcast_detail", kwargs={"slug": self.podcast.uuid}
)
def __str__(self):
return f"{self.title}"
@property
def subtitle(self):
return self.podcast
@property
def strings(self) -> ScrobblableConstants:
return ScrobblableConstants(verb="Listening", tags="microphone")
@property
def info_link(self):
return ""
@property
def primary_image_url(self) -> str:
url = ""
if self.podcast.cover_image:
url = self.podcast.cover_image.url
return url
@classmethod
def find_or_create(
cls,
title: str,
podcast_name: str,
pub_date: str,
number: int = 0,
mopidy_uri: str = "",
producer_name: str = "",
run_time_seconds: int = 1800,
enrich: bool = True,
) -> "PodcastEpisode":
"""Given a data dict from Mopidy, finds or creates a podcast and
producer before saving the epsiode so it can be scrobbled.
"""
producer = None
if producer_name:
producer = Producer.find_or_create(producer_name)
podcast = Podcast.objects.filter(
name__iexact=podcast_name,
).first()
if not podcast:
podcast = Podcast.objects.create(
name=podcast_name, producer=producer
)
if enrich:
podcast.fix_metadata()
episode = cls.objects.filter(
title__iexact=title, podcast=podcast
).first()
if not episode:
episode = cls.objects.create(
title=title,
podcast=podcast,
run_time_seconds=run_time_seconds,
number=number,
pub_date=pub_date,
mopidy_uri=mopidy_uri,
)
return episode