SPB Git forge

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

52commits 1branches 0releases
11.3 MBsize
maindefault branch
19 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%
5.2 KB · 119 lines python
Raw Blame History
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