Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)
Python 73.6%
HTML 13.2%
TypeScript 6%
JavaScript 4.5%
CSS 1.7%
Dockerfile 0.6%
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File: creaka/connectors/podcastindex.py4# Desc: Connecteur ENRICHISSEMENT « podcastindex » — API Podcast Index5# (OFFICIELLE, gratuite ; clé + secret requis, inscription gratuite :6# https://api.podcastindex.org). Palier 2 (§9, §10). Sans clés :7# passage sauté PROPREMENT (SkipSource, comme twitch).8# ==============================================================================9"""Enrichissement des balados via l'API Podcast Index.1011``/podcasts/byfeedurl`` retourne pour un flux : episodeCount,12lastUpdateTime, itunesId, image/artwork, catégories — reach/fraîcheur/avatar13des balados. Authentification officielle : en-têtes X-Auth-Key + X-Auth-Date14+ Authorization = sha1(key + secret + date).1516Complément (avec clés) du connecteur sans clé `balados-rss` : mêmes champs,17mais en UNE requête légère par balado au lieu de télécharger le flux complet.18"""19from __future__ import annotations2021import hashlib22import os23import time24from datetime import datetime, timedelta, timezone2526from ..normalize import map_niche27from ..schema import Creator, now_iso28from .base import BaseConnector, SkipSource29from .balados_itunes import DORMANT_AFTER_DAYS, _GENRE_NICHE3031API_URL = "https://api.podcastindex.org/api/1.0/podcasts/byfeedurl"323334class PodcastIndexConnector(BaseConnector):35 source_id = "podcastindex"36 kind = "enrichment"37 request_delay = 0.438 max_feeds = 200 # cap par passage39 revisit_days = 7 # re-visite hebdomadaire par balado4041 def __init__(self) -> None:42 super().__init__()43 self.errors = 04445 def _auth_headers(self, key: str, secret: str) -> dict:46 ts = str(int(time.time()))47 digest = hashlib.sha1((key + secret + ts).encode("utf-8")).hexdigest()48 return {"X-Auth-Key": key, "X-Auth-Date": ts, "Authorization": digest}4950 def _needs_visit(self, metrics: dict) -> bool:51 raw = metrics.get("pi_checked")52 if not raw:53 return True54 try:55 checked = datetime.fromisoformat(raw.replace("Z", "+00:00"))56 except ValueError:57 return True58 return (datetime.now(timezone.utc) - checked59 > timedelta(days=self.revisit_days))6061 def enrich(self, creators: list[Creator]) -> list[Creator]:62 key = os.environ.get("PODCASTINDEX_API_KEY", "")63 secret = os.environ.get("PODCASTINDEX_API_SECRET", "")64 if not key or not secret:65 # pas de clés → passage sauté PROPREMENT (§15) ; le connecteur66 # sans clé balados-rss couvre les mêmes champs en attendant67 raise SkipSource("clés PODCASTINDEX_API_KEY/PODCASTINDEX_API_SECRET "68 "manquantes (.env — inscription gratuite : "69 "https://api.podcastindex.org)")70 enriched: list[Creator] = []71 fetched = 072 now = datetime.now(timezone.utc)73 for cr in creators:74 if fetched >= self.max_feeds:75 break76 acc = next((a for a in cr.platforms77 if a.platform == "podcast" and a.metrics.get("feed_url")),78 None)79 if acc is None or not self._needs_visit(acc.metrics):80 continue81 try:82 resp = self.get(API_URL,83 params={"url": acc.metrics["feed_url"]},84 headers=self._auth_headers(key, secret))85 feed = resp.json().get("feed") or {}86 except Exception:87 self.errors += 188 continue89 fetched += 190 if not feed or not isinstance(feed, dict):91 continue92 acc.metrics["pi_checked"] = now_iso()93 acc.last_checked = now_iso()94 if feed.get("episodeCount"):95 acc.metrics["episodes"] = feed["episodeCount"]96 if feed.get("itunesId"):97 acc.metrics["itunes_id"] = feed["itunesId"]98 last_ts = feed.get("lastUpdateTime") or feed.get("newestItemPubdate")99 if last_ts:100 last = datetime.fromtimestamp(int(last_ts), tz=timezone.utc)101 acc.metrics["last_episode"] = last.strftime("%Y-%m-%dT%H:%M:%SZ")102 if now - last > timedelta(days=DORMANT_AFTER_DAYS):103 acc.metrics["dormant"] = True104 if all(a.platform == "podcast" for a in cr.platforms):105 cr.status = "inactive" # §13, réversible au retour106 else:107 acc.metrics.pop("dormant", None)108 image = (feed.get("image") or feed.get("artwork") or "").strip()109 if image.startswith("http") and not cr.avatar_url:110 cr.avatar_url = image111 cats = feed.get("categories") or {}112 niches = {map_niche(_GENRE_NICHE.get(c, c))113 for c in cats.values() if isinstance(c, str)}114 niches.discard("autre")115 if niches:116 cr.niches = sorted(set(cr.niches or []) | niches)117 enriched.append(cr)118 return enriched119