# ============================================================================== # Author: Simon-Pierre Boucher # File: creaka/connectors/podcastindex.py # Desc: Connecteur ENRICHISSEMENT « podcastindex » — API Podcast Index # (OFFICIELLE, gratuite ; clé + secret requis, inscription gratuite : # https://api.podcastindex.org). Palier 2 (§9, §10). Sans clés : # passage sauté PROPREMENT (SkipSource, comme twitch). # ============================================================================== """Enrichissement des balados via l'API Podcast Index. ``/podcasts/byfeedurl`` retourne pour un flux : episodeCount, lastUpdateTime, itunesId, image/artwork, catégories — reach/fraîcheur/avatar des balados. Authentification officielle : en-têtes X-Auth-Key + X-Auth-Date + Authorization = sha1(key + secret + date). Complément (avec clés) du connecteur sans clé `balados-rss` : mêmes champs, mais en UNE requête légère par balado au lieu de télécharger le flux complet. """ from __future__ import annotations import hashlib import os import time from datetime import datetime, timedelta, timezone from ..normalize import map_niche from ..schema import Creator, now_iso from .base import BaseConnector, SkipSource from .balados_itunes import DORMANT_AFTER_DAYS, _GENRE_NICHE API_URL = "https://api.podcastindex.org/api/1.0/podcasts/byfeedurl" class PodcastIndexConnector(BaseConnector): source_id = "podcastindex" kind = "enrichment" request_delay = 0.4 max_feeds = 200 # cap par passage revisit_days = 7 # re-visite hebdomadaire par balado def __init__(self) -> None: super().__init__() self.errors = 0 def _auth_headers(self, key: str, secret: str) -> dict: ts = str(int(time.time())) digest = hashlib.sha1((key + secret + ts).encode("utf-8")).hexdigest() return {"X-Auth-Key": key, "X-Auth-Date": ts, "Authorization": digest} def _needs_visit(self, metrics: dict) -> bool: raw = metrics.get("pi_checked") if not raw: return True try: checked = datetime.fromisoformat(raw.replace("Z", "+00:00")) except ValueError: return True return (datetime.now(timezone.utc) - checked > timedelta(days=self.revisit_days)) def enrich(self, creators: list[Creator]) -> list[Creator]: key = os.environ.get("PODCASTINDEX_API_KEY", "") secret = os.environ.get("PODCASTINDEX_API_SECRET", "") if not key or not secret: # pas de clés → passage sauté PROPREMENT (§15) ; le connecteur # sans clé balados-rss couvre les mêmes champs en attendant raise SkipSource("clés PODCASTINDEX_API_KEY/PODCASTINDEX_API_SECRET " "manquantes (.env — inscription gratuite : " "https://api.podcastindex.org)") enriched: list[Creator] = [] fetched = 0 now = datetime.now(timezone.utc) for cr in creators: if fetched >= self.max_feeds: break acc = next((a for a in cr.platforms if a.platform == "podcast" and a.metrics.get("feed_url")), None) if acc is None or not self._needs_visit(acc.metrics): continue try: resp = self.get(API_URL, params={"url": acc.metrics["feed_url"]}, headers=self._auth_headers(key, secret)) feed = resp.json().get("feed") or {} except Exception: self.errors += 1 continue fetched += 1 if not feed or not isinstance(feed, dict): continue acc.metrics["pi_checked"] = now_iso() acc.last_checked = now_iso() if feed.get("episodeCount"): acc.metrics["episodes"] = feed["episodeCount"] if feed.get("itunesId"): acc.metrics["itunes_id"] = feed["itunesId"] last_ts = feed.get("lastUpdateTime") or feed.get("newestItemPubdate") if last_ts: last = datetime.fromtimestamp(int(last_ts), tz=timezone.utc) acc.metrics["last_episode"] = last.strftime("%Y-%m-%dT%H:%M:%SZ") if now - last > timedelta(days=DORMANT_AFTER_DAYS): acc.metrics["dormant"] = True if all(a.platform == "podcast" for a in cr.platforms): cr.status = "inactive" # §13, réversible au retour else: acc.metrics.pop("dormant", None) image = (feed.get("image") or feed.get("artwork") or "").strip() if image.startswith("http") and not cr.avatar_url: cr.avatar_url = image cats = feed.get("categories") or {} niches = {map_niche(_GENRE_NICHE.get(c, c)) for c in cats.values() if isinstance(c, str)} niches.discard("autre") if niches: cr.niches = sorted(set(cr.niches or []) | niches) enriched.append(cr) return enriched