[podcasts] Fixes enrichment of podcasts with podcastindex

This commit is contained in:
2025-04-07 13:30:29 -04:00
parent bcc3f46806
commit 4767cc7e52
7 changed files with 265 additions and 55 deletions

View File

@ -1,5 +1,4 @@
import logging
from typing import Dict, Optional
from uuid import uuid4
import requests
@ -10,8 +9,16 @@ 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.scrapers import scrape_data_from_google_podcasts
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
from scrobbles.mixins import (
ObjectWithGenres,
ScrobblableConstants,
ScrobblableMixin,
)
from taggit.managers import TaggableManager
from podcasts.sources.podcastindex import (
lookup_podcast_from_podcastindex,
)
logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True}
@ -24,6 +31,13 @@ class Producer(TimeStampedModel):
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)
@ -31,11 +45,17 @@ class Podcast(TimeStampedModel):
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)
google_podcasts_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}"
@ -49,32 +69,43 @@ class Podcast(TimeStampedModel):
user=user, podcast_episode__podcast=self
).order_by("-timestamp")
def scrape_google_podcasts(self, force=False):
podcast_dict = {}
if not self.cover_image or force:
podcast_dict = scrape_data_from_google_podcasts(self.name)
if podcast_dict:
if not self.producer:
self.producer, created = Producer.objects.get_or_create(
name=podcast_dict["producer"]
)
self.description = podcast_dict.get("description")
self.google_podcasts_url = podcast_dict.get("google_url")
self.save(
update_fields=[
"description",
"producer",
"google_podcasts_url",
]
)
@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")
cover_url = podcast_dict.get("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)
@ -108,42 +139,45 @@ class PodcastEpisode(ScrobblableMixin):
@classmethod
def find_or_create(
cls, podcast_dict: Dict, producer_dict: Dict, episode_dict: Dict
) -> Optional["Episode"]:
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.
"""
if not podcast_dict.get("name"):
logger.warning(f"No name from source for podcast, not scrobbling")
return
producer = None
if producer_dict.get("name"):
producer, producer_created = Producer.objects.get_or_create(
**producer_dict
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 producer_created:
logger.debug(f"Created new producer {producer}")
else:
logger.debug(f"Found producer {producer}")
if enrich:
podcast.fix_metadata()
if producer:
podcast_dict["producer_id"] = producer.id
podcast, podcast_created = Podcast.objects.get_or_create(
**podcast_dict
)
if podcast_created:
logger.debug(f"Created new podcast {podcast}")
else:
logger.debug(f"Found podcast {podcast}")
episode_dict["podcast_id"] = podcast.id
episode, created = cls.objects.get_or_create(**episode_dict)
if created:
logger.debug(f"Created new episode: {episode}")
else:
logger.debug(f"Found episode {episode}")
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