Compare commits

..

6 Commits
27 ... 29

5 changed files with 59 additions and 56 deletions

View File

@ -479,6 +479,10 @@ https://life.lab.unbl.ink/scrobble/e39779c8-62a5-46a6-bdef-fb7662810dc6/start/
- Note taken on [2025-09-30 Tue 09:33] - Note taken on [2025-09-30 Tue 09:33]
This may have already been resolved ... need to just confirm it. This may have already been resolved ... need to just confirm it.
* Version 29.0 [1/1]
** DONE HOTFIX podcast lookups, final
* Version 28.0 [1/1]
** DONE HOTFIX podcast lookups
* Version 27.0 [3/3] * Version 27.0 [3/3]
** DONE [#A] Fix bug where podcast scrobbling creates duplicate Podcast :project:vrobbler:scrobbling:podcasts:bug:personal: ** DONE [#A] Fix bug where podcast scrobbling creates duplicate Podcast :project:vrobbler:scrobbling:podcasts:bug:personal:
:PROPERTIES: :PROPERTIES:

View File

@ -115,6 +115,12 @@ def mopidy_podcast_request_data():
mopidy_uri=mopidy_uri, artist="NPR", album="Up First" mopidy_uri=mopidy_uri, artist="NPR", album="Up First"
).request_json ).request_json
@pytest.fixture
def mopidy_podcast_https_request_data():
mopidy_uri = "podcast+https://feeds.npr.org/510318/podcast.xml#85b9c4c4-ae09-43d9-8853-31ccf43f68e6"
return MopidyRequest(
mopidy_uri=mopidy_uri, artist="NPR", album="Up First"
).request_json
class JellyfinTrackRequest: class JellyfinTrackRequest:
name = "Emotion" name = "Emotion"

View File

@ -143,13 +143,13 @@ class PodcastEpisode(ScrobblableMixin):
def find_or_create( def find_or_create(
cls, cls,
title: str, title: str,
podcast_name: str,
podcast_description: str,
pub_date: str, pub_date: str,
number: int = 0, episode_num: int = 0,
mopidy_uri: str = "",
producer_name: str = "",
run_time_seconds: int = 1800, run_time_seconds: int = 1800,
mopidy_uri: str = "",
podcast_name: str = "",
podcast_producer: str = "",
podcast_description: str = "",
enrich: bool = True, enrich: bool = True,
) -> "PodcastEpisode": ) -> "PodcastEpisode":
"""Given a data dict from Mopidy, finds or creates a podcast and """Given a data dict from Mopidy, finds or creates a podcast and
@ -158,8 +158,8 @@ class PodcastEpisode(ScrobblableMixin):
""" """
log_context={"mopidy_uri": mopidy_uri, "media_type": "Podcast"} log_context={"mopidy_uri": mopidy_uri, "media_type": "Podcast"}
producer = None producer = None
if producer_name: if podcast_producer:
producer = Producer.find_or_create(producer_name) producer = Producer.find_or_create(podcast_producer)
podcast, created = Podcast.objects.get_or_create(name=podcast_name, defaults={"description": podcast_description}) podcast, created = Podcast.objects.get_or_create(name=podcast_name, defaults={"description": podcast_description})
log_context["podcast_id"] = podcast.id log_context["podcast_id"] = podcast.id
@ -175,7 +175,7 @@ class PodcastEpisode(ScrobblableMixin):
podcast=podcast, podcast=podcast,
defaults={ defaults={
"run_time_seconds": run_time_seconds, "run_time_seconds": run_time_seconds,
"number": number, "number": episode_num,
"pub_date": pub_date, "pub_date": pub_date,
"mopidy_uri": mopidy_uri, "mopidy_uri": mopidy_uri,
} }

View File

@ -4,6 +4,7 @@ from typing import Any
from urllib.parse import unquote from urllib.parse import unquote
import feedparser import feedparser
import requests
from dateutil.parser import ParserError, parse from dateutil.parser import ParserError, parse
from podcasts.models import PodcastEpisode from podcasts.models import PodcastEpisode
@ -27,33 +28,43 @@ def fetch_metadata_from_rss(uri: str) -> dict[str, Any]:
log_context = {"mopidy_uri": uri, "media_type": "Podcast"} log_context = {"mopidy_uri": uri, "media_type": "Podcast"}
podcast_data: dict[str, Any] = {} podcast_data: dict[str, Any] = {}
rss_url = uri.split("#")[0].split("podcast+")[1]
target_guid = uri.split("#")[1]
log_context["rss_url"] = rss_url
log_context["target_guid"] = target_guid
try: try:
feed = feedparser.parse(uri.split("#")[0]) resp = requests.get(rss_url, timeout=10)
target_guid = uri.split("#")[1] feed = feedparser.parse(resp.text)
except IndexError: except IndexError:
logger.warning("Tried to parse uri as RSS feed, but no target found", extra=log_context) logger.warning("Tried to parse uri as RSS feed, but no target found", extra=log_context)
return podcast_data return podcast_data
podcast_publisher = feed.feed.get("itunes_publisher") podcast_publisher = getattr(feed.feed, "itunes_publisher", "")
podcast_owner = feed.feed.itunes_owner.get("name") if isinstance(feed.feed.itunes_owner, dict) else feed.feed.itunes_owner try:
podcast_other = feed.feed.get("managingeditor") or feed.feed.get("copyright") podcast_owner = feed.feed.itunes_owner.get("name") if isinstance(feed.feed.itunes_owner, dict) else feed.feed.itunes_owner
podcast_other = feed.feed.get("managingeditor") or feed.feed.get("copyright")
except AttributeError:
podcast_owner = None
podcast_other = None
podcast_data = { podcast_data = {
"podcast_name": feed.feed.get("title", "Unknown Podcast"), "podcast_name": getattr(feed.feed, "title", ""),
"podcast_description": feed.feed.get("description", ""), # "podcast_description": getattr(feed.feed, "description", ""),
"podcast_link": feed.feed.get("link", ""), # "podcast_link": getattr(feed.feed, "link", ""),
"podcast_producer": podcast_publisher or podcast_owner or podcast_other "podcast_producer": podcast_publisher or podcast_owner or podcast_other
} }
for entry in feed.entries: for entry in feed.entries:
if target_guid in target_guid: if entry.get("guid") == target_guid:
logger.info("🎧 Episode found in RSS feed", extra=log_context) logger.info("🎧 Episode found in RSS feed", extra=log_context)
podcast_data["episode_name"] = entry.title podcast_data["title"] = entry.title
podcast_data["episode_num"] = entry.guid podcast_data["episode_num"] = int(entry.get("itunes_episode", 0))
podcast_data["episode_pub_date"] = entry.get("published", None) podcast_data["pub_date"] = parse(entry.get("published", None))
podcast_data["episode_description"] = entry.get("description", None) podcast_data["run_time_seconds"] = parse_duration(entry.get("itunes_duration", None))
podcast_data["episode_url"] = entry.enclosures[0].href if entry.get("enclosures") else None # podcast_data["description"] = entry.get("description", None)
podcast_data["episode_runtime_seconds"] = parse_duration(entry.get("itunes_duration", None)) # podcast_data["episode_url"] = entry.enclosures[0].href if entry.get("enclosures") else None
return podcast_data return podcast_data
else: else:
logger.info("Episode not found in RSS feed.") logger.info("Episode not found in RSS feed.")
@ -63,14 +74,14 @@ def parse_mopidy_uri(uri: str) -> dict[str, Any]:
podcast_data: dict[str, Any] = {} podcast_data: dict[str, Any] = {}
logger.debug(f"Parsing URI: {uri}") logger.debug(f"Parsing URI: {uri}")
if "https://" in uri: if "podcast+https" in uri:
return fetch_metadata_from_rss(uri) return fetch_metadata_from_rss(uri)
parsed_uri = os.path.splitext(unquote(uri))[0].split("/") parsed_uri = os.path.splitext(unquote(uri))[0].split("/")
podcast_data = { podcast_data = {
"episode_filename": parsed_uri[-1], "title": parsed_uri[-1],
"episode_num": None, "episode_num": None,
"podcast_name": parsed_uri[-2].strip(), "podcast_name": parsed_uri[-2].strip(),
"pub_date": None, "pub_date": None,
@ -105,30 +116,14 @@ def parse_mopidy_uri(uri: str) -> dict[str, Any]:
if podcast_data["episode_num"]: if podcast_data["episode_num"]:
gap_to_strip += episode_num_pad gap_to_strip += episode_num_pad
podcast_data["episode_name"] = episode_str[gap_to_strip:].replace("-", " ").strip() podcast_data["title"] = episode_str[gap_to_strip:].replace("-", " ").strip()
return podcast_data return podcast_data
def get_or_create_podcast(post_data: dict) -> PodcastEpisode: def get_or_create_podcast(post_data: dict) -> PodcastEpisode:
logger.info("Looking up podcast", extra={"post_data": post_data, "media_type": "Podcast"}) logger.info("Looking up podcast", extra={"post_data": post_data, "media_type": "Podcast"})
mopidy_uri = post_data.get("mopidy_uri", "") mopidy_uri = post_data.get("mopidy_uri", "")
parsed_data = parse_mopidy_uri(mopidy_uri) parsed_data = parse_mopidy_uri(mopidy_uri)
producer_name = parsed_data.get("podcast_producer", post_data.get("artist", ""))
podcast_name = parsed_data.get("podcast_name", post_data.get("album", ""))
episode_name = parsed_data.get("episode_title", parsed_data.get("episode_filename", ""))
run_time_seconds = parsed_data.get("episode_runtime_seconds", post_data.get("run_time", 2700))
episode_dict = { return PodcastEpisode.find_or_create(**parsed_data)
"title": episode_name,
"podcast_name": podcast_name,
"podcast_description": parsed_data.get("podcast_description"),
"pub_date": parsed_data.get("pub_date"),
"number": parsed_data.get("episode_num"),
"mopidy_uri": mopidy_uri,
"producer_name": producer_name,
"run_time_seconds": run_time_seconds,
}
return PodcastEpisode.find_or_create(**episode_dict)

View File

@ -56,18 +56,11 @@ def mopidy_scrobble_media(post_data: dict, user_id: int) -> Scrobble:
if media_type == Scrobble.MediaType.PODCAST_EPISODE: if media_type == Scrobble.MediaType.PODCAST_EPISODE:
parsed_data = parse_mopidy_uri(post_data.get("mopidy_uri", "")) parsed_data = parse_mopidy_uri(post_data.get("mopidy_uri", ""))
podcast_name = post_data.get( if not parsed_data:
"album", parsed_data.get("podcast_name", "") logger.warning("Tried to scrobble podcast but no uri found", extra={"post_data": post_data})
) return Scrobble()
media_obj = PodcastEpisode.find_or_create( media_obj = PodcastEpisode.find_or_create(**parsed_data)
title=parsed_data.get("episode_filename", ""),
podcast_name=podcast_name,
producer_name=post_data.get("artist", ""),
number=parsed_data.get("episode_num", ""),
pub_date=parsed_data.get("pub_date", ""),
mopidy_uri=post_data.get("mopidy_uri", ""),
)
else: else:
media_obj = Track.find_or_create( media_obj = Track.find_or_create(
title=post_data.get("name", ""), title=post_data.get("name", ""),
@ -875,8 +868,13 @@ def manual_scrobble_webpage(
) )
scrobble = Scrobble.create_or_update(webpage, user_id, scrobble_dict) scrobble = Scrobble.create_or_update(webpage, user_id, scrobble_dict)
# possibly async this?
scrobble.push_to_archivebox() if action == "stop":
scrobble.stop(force_finish=True)
else:
# possibly async this?
scrobble.push_to_archivebox()
return scrobble return scrobble