palmares-apple V4 : balayage des tops 100 par genre (le top général CA est devenu 100 % anglophone → rendement 0 et alertes) ; préfiltre nom/artiste/résumé, attribution QC inchangée, métriques chart_genre/chart_rank_genre
1 changed file +79 −11
modified
creaka/connectors/palmares_apple.py
+79 −11
@@ -1,10 +1,11 @@ | ||
| 1 | 1 | # ============================================================================== |
| 2 | 2 | # Author: Simon-Pierre Boucher <contact@spboucher.ai> |
| 3 | 3 | # File: creaka/connectors/palmares_apple.py |
| 4 | −# Desc: Connecteur DÉCOUVERTE — balados québécois dans le TOP 100 canadien | |
| 5 | −# d'Apple Podcasts. Mode d'accès : API marketing officielle d'Apple | |
| 6 | −# (rss.marketingtools.apple.com, gratuite, sans clé) + lookup iTunes | |
| 7 | −# groupé + tête de flux RSS pour l'attribution QC. Palier 2 (§9). V3. | |
| 4 | +# Desc: Connecteur DÉCOUVERTE — balados québécois dans les palmarès canadiens | |
| 5 | +# d'Apple Podcasts : top 100 général (API marketing officielle, | |
| 6 | +# rss.marketingtools.apple.com) + tops 100 PAR GENRE (API RSS iTunes | |
| 7 | +# officielle, itunes.apple.com/ca/rss) + lookup iTunes groupé + tête de | |
| 8 | +# flux RSS pour l'attribution QC. Palier 2 (§9). V4. | |
| 8 | 9 | # ============================================================================== |
| 9 | 10 | """Palmarès Apple Podcasts Canada → balados québécois qui percent. |
| 10 | 11 | |
@@ -27,6 +28,13 @@ surtout anglo-canadien, le rendement est faible mais de très haute valeur : | ||
| 27 | 28 | un balado QC du top 100 canadien est un créateur qui compte. Complémentaire |
| 28 | 29 | de balados-itunes (recherche thématique) — même clé de compte |
| 29 | 30 | (podcast:collectionId) → fusion naturelle des fiches. |
| 31 | + | |
| 32 | +V4 (2026-08-20) : le top 100 général CA est devenu 100 % anglophone (0 balado | |
| 33 | +QC pendant plusieurs passages → alertes). On balaie EN PLUS les tops 100 par | |
| 34 | +genre de l'API RSS iTunes (officielle, sans clé) : le préfiltre nom/artiste/ | |
| 35 | +résumé limite les lectures RSS, et l'attribution finale reste identique | |
| 36 | +(nom/artiste QC, sinon tête de flux fr + marqueur QC — jamais le résumé seul, | |
| 37 | +qui sert uniquement de préfiltre). | |
| 30 | 38 | """ |
| 31 | 39 | from __future__ import annotations |
| 32 | 40 | |
@@ -42,8 +50,20 @@ from .youtube_recherche import is_quebec | ||
| 42 | 50 | |
| 43 | 51 | CHARTS_URL = ("https://rss.marketingtools.apple.com/api/v2/ca/podcasts/" |
| 44 | 52 | "top/100/podcasts.json") |
| 53 | +GENRE_CHART_URL = ("https://itunes.apple.com/ca/rss/toppodcasts/" | |
| 54 | + "limit=100/genre={gid}/json") | |
| 45 | 55 | LOOKUP_URL = "https://itunes.apple.com/lookup" |
| 46 | 56 | |
| 57 | +# genres Apple Podcasts balayés en plus du top général (id officiel → libellé) | |
| 58 | +_GENRE_CHARTS = { | |
| 59 | + 1301: "Arts", 1321: "Affaires", 1303: "Humour", 1304: "Éducation", | |
| 60 | + 1483: "Fiction", 1511: "Gouvernement", 1512: "Santé et mieux-être", | |
| 61 | + 1487: "Histoire", 1305: "Enfants et famille", 1502: "Loisirs", | |
| 62 | + 1310: "Musique", 1489: "Nouvelles", 1314: "Religion et spiritualité", | |
| 63 | + 1533: "Sciences", 1324: "Société et culture", 1545: "Sports", | |
| 64 | + 1318: "Technologies", 1488: "Crimes véritables", 1309: "Télé et cinéma", | |
| 65 | +} | |
| 66 | + | |
| 47 | 67 | _HEAD_BYTES = 40_000 # tête du flux RSS lue pour l'attribution QC |
| 48 | 68 | |
| 49 | 69 | |
@@ -81,6 +101,25 @@ class PalmaresAppleConnector(BaseConnector): | ||
| 81 | 101 | data = self.get(CHARTS_URL).json() |
| 82 | 102 | return (data.get("feed") or {}).get("results") or [] |
| 83 | 103 | |
| 104 | + def _genre_chart(self, gid: int) -> list[dict]: | |
| 105 | + """Top 100 CA d'un genre (API RSS iTunes) → entrées normalisées.""" | |
| 106 | + data = self.get(GENRE_CHART_URL.format(gid=gid)).json() | |
| 107 | + out: list[dict] = [] | |
| 108 | + for rank, e in enumerate((data.get("feed") or {}).get("entry") or [], | |
| 109 | + start=1): | |
| 110 | + cid = (((e.get("id") or {}).get("attributes") or {}) | |
| 111 | + .get("im:id") or "") | |
| 112 | + out.append({ | |
| 113 | + "id": cid, | |
| 114 | + "name": ((e.get("im:name") or {}).get("label") or "").strip(), | |
| 115 | + "artistName": ((e.get("im:artist") or {}).get("label") | |
| 116 | + or "").strip(), | |
| 117 | + "summary": ((e.get("summary") or {}).get("label") or ""), | |
| 118 | + "url": ((e.get("id") or {}).get("label") or ""), | |
| 119 | + "rank": rank, | |
| 120 | + }) | |
| 121 | + return out | |
| 122 | + | |
| 84 | 123 | def _lookup(self, ids: list[str]) -> dict[str, dict]: |
| 85 | 124 | """Lookup iTunes GROUPÉ (officiel) : une seule requête pour le top.""" |
| 86 | 125 | resp = self.get(LOOKUP_URL, params={"id": ",".join(ids), |
@@ -108,18 +147,42 @@ class PalmaresAppleConnector(BaseConnector): | ||
| 108 | 147 | print(f"[crea-ka] ⚠ palmares-apple : palmarès indisponible : {exc}", |
| 109 | 148 | file=sys.stderr) |
| 110 | 149 | self.errors += 1 |
| 150 | + chart = [] | |
| 151 | + candidates: dict[str, dict] = {} | |
| 152 | + for rank, entry in enumerate(chart, start=1): | |
| 153 | + cid = str(entry.get("id") or "") | |
| 154 | + if cid: | |
| 155 | + candidates[cid] = {**entry, "rank": rank, "chart": None} | |
| 156 | + # tops par genre : préfiltre nom/artiste/résumé (limite les lectures | |
| 157 | + # RSS) — l'attribution finale reste nom/artiste ou tête de flux | |
| 158 | + for gid, label in _GENRE_CHARTS.items(): | |
| 159 | + try: | |
| 160 | + genre_entries = self._genre_chart(gid) | |
| 161 | + except Exception: | |
| 162 | + self.errors += 1 | |
| 163 | + continue | |
| 164 | + for entry in genre_entries: | |
| 165 | + cid = str(entry.get("id") or "") | |
| 166 | + if not cid or cid in candidates: | |
| 167 | + continue | |
| 168 | + if not is_quebec(f"{entry.get('name', '')} " | |
| 169 | + f"{entry.get('artistName', '')} " | |
| 170 | + f"{entry.get('summary', '')}"): | |
| 171 | + continue | |
| 172 | + candidates[cid] = {**entry, "chart": label} | |
| 173 | + if not candidates: | |
| 111 | 174 | return [] |
| 112 | 175 | details: dict[str, dict] = {} |
| 113 | 176 | try: |
| 114 | − details = self._lookup([e["id"] for e in chart if e.get("id")]) | |
| 177 | + details = self._lookup(list(candidates)) | |
| 115 | 178 | except Exception: |
| 116 | − self.errors += 1 # dégradé : on continue avec le palmarès seul | |
| 179 | + self.errors += 1 # dégradé : on continue avec les palmarès seuls | |
| 117 | 180 | creators: list[Creator] = [] |
| 118 | − for rank, entry in enumerate(chart, start=1): | |
| 119 | − cid = str(entry.get("id") or "") | |
| 181 | + for cid, entry in candidates.items(): | |
| 182 | + rank = entry["rank"] | |
| 120 | 183 | name = (entry.get("name") or "").strip() |
| 121 | 184 | artist = (entry.get("artistName") or "").strip() |
| 122 | − if not cid or not name: | |
| 185 | + if not name: | |
| 123 | 186 | continue |
| 124 | 187 | info = details.get(cid) or {} |
| 125 | 188 | feed = info.get("feedUrl") or "" |
@@ -137,8 +200,11 @@ class PalmaresAppleConnector(BaseConnector): | ||
| 137 | 200 | continue |
| 138 | 201 | acc = account("podcast", cid, "profil_source", url=url) |
| 139 | 202 | dormant = _dormant(info.get("releaseDate")) |
| 203 | + chart = entry.get("chart") # None = top général, sinon genre | |
| 140 | 204 | acc.metrics = {k: v for k, v in { |
| 141 | − "chart_rank_ca": rank, # rang du top 100 CA | |
| 205 | + "chart_rank_ca": None if chart else rank, | |
| 206 | + "chart_genre": chart, | |
| 207 | + "chart_rank_genre": rank if chart else None, | |
| 142 | 208 | "episodes": info.get("trackCount"), |
| 143 | 209 | "genre": info.get("primaryGenreName"), |
| 144 | 210 | "feed_url": feed, |
@@ -162,7 +228,9 @@ class PalmaresAppleConnector(BaseConnector): | ||
| 162 | 228 | or entry.get("artworkUrl100"), |
| 163 | 229 | status="inactive" if dormant else "active", |
| 164 | 230 | platforms=[acc], |
| 165 | − notes=f"Top 100 balados Apple Podcasts Canada (rang {rank}).", | |
| 231 | + notes=(f"Top 100 balados Apple Podcasts Canada — " | |
| 232 | + f"{chart} (rang {rank})." if chart else | |
| 233 | + f"Top 100 balados Apple Podcasts Canada (rang {rank})."), | |
| 166 | 234 | source_ids=[f"{self.source_id}:{cid}"], |
| 167 | 235 | )) |
| 168 | 236 | return creators |
| 169 | 237 | |