SPB Git

spb/crea-ka

Public

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

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

Simon-Pierre Boucher committed (Aug 20, 2026) parent a4b7446 Browse files

1 changed file +79 −11

Unified Split
modified creaka/connectors/palmares_apple.py +79 −11
@@ -1,10 +1,11 @@
1 # ============================================================================== 1 # ==============================================================================
2 # Author: Simon-Pierre Boucher <contact@spboucher.ai> 2 # Author: Simon-Pierre Boucher <contact@spboucher.ai>
3 # File: creaka/connectors/palmares_apple.py 3 # File: creaka/connectors/palmares_apple.py
4−# Desc: Connecteur DÉCOUVERTE — balados québécois dans le TOP 100 canadien 4+# Desc: Connecteur DÉCOUVERTE — balados québécois dans les palmarès canadiens
5−# d'Apple Podcasts. Mode d'accès : API marketing officielle d'Apple 5+# d'Apple Podcasts : top 100 général (API marketing officielle,
6−# (rss.marketingtools.apple.com, gratuite, sans clé) + lookup iTunes 6+# rss.marketingtools.apple.com) + tops 100 PAR GENRE (API RSS iTunes
7−# groupé + tête de flux RSS pour l'attribution QC. Palier 2 (§9). V3. 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 """Palmarès Apple Podcasts Canada → balados québécois qui percent. 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 un balado QC du top 100 canadien est un créateur qui compte. Complémentaire 28 un balado QC du top 100 canadien est un créateur qui compte. Complémentaire
28 de balados-itunes (recherche thématique) — même clé de compte 29 de balados-itunes (recherche thématique) — même clé de compte
29 (podcast:collectionId) → fusion naturelle des fiches. 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 from __future__ import annotations 39 from __future__ import annotations
32 40
@@ -42,8 +50,20 @@ from .youtube_recherche import is_quebec
42 50
43 CHARTS_URL = ("https://rss.marketingtools.apple.com/api/v2/ca/podcasts/" 51 CHARTS_URL = ("https://rss.marketingtools.apple.com/api/v2/ca/podcasts/"
44 "top/100/podcasts.json") 52 "top/100/podcasts.json")
53+GENRE_CHART_URL = ("https://itunes.apple.com/ca/rss/toppodcasts/"
54+ "limit=100/genre={gid}/json")
45 LOOKUP_URL = "https://itunes.apple.com/lookup" 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 _HEAD_BYTES = 40_000 # tête du flux RSS lue pour l'attribution QC 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 data = self.get(CHARTS_URL).json() 101 data = self.get(CHARTS_URL).json()
82 return (data.get("feed") or {}).get("results") or [] 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 def _lookup(self, ids: list[str]) -> dict[str, dict]: 123 def _lookup(self, ids: list[str]) -> dict[str, dict]:
85 """Lookup iTunes GROUPÉ (officiel) : une seule requête pour le top.""" 124 """Lookup iTunes GROUPÉ (officiel) : une seule requête pour le top."""
86 resp = self.get(LOOKUP_URL, params={"id": ",".join(ids), 125 resp = self.get(LOOKUP_URL, params={"id": ",".join(ids),
@@ -108,18 +147,42 @@ class PalmaresAppleConnector(BaseConnector):
108 print(f"[crea-ka] ⚠ palmares-apple : palmarès indisponible : {exc}", 147 print(f"[crea-ka] ⚠ palmares-apple : palmarès indisponible : {exc}",
109 file=sys.stderr) 148 file=sys.stderr)
110 self.errors += 1 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 return [] 174 return []
112 details: dict[str, dict] = {} 175 details: dict[str, dict] = {}
113 try: 176 try:
114− details = self._lookup([e["id"] for e in chart if e.get("id")]) 177+ details = self._lookup(list(candidates))
115 except Exception: 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 creators: list[Creator] = [] 180 creators: list[Creator] = []
118− for rank, entry in enumerate(chart, start=1): 181+ for cid, entry in candidates.items():
119− cid = str(entry.get("id") or "") 182+ rank = entry["rank"]
120 name = (entry.get("name") or "").strip() 183 name = (entry.get("name") or "").strip()
121 artist = (entry.get("artistName") or "").strip() 184 artist = (entry.get("artistName") or "").strip()
122− if not cid or not name: 185+ if not name:
123 continue 186 continue
124 info = details.get(cid) or {} 187 info = details.get(cid) or {}
125 feed = info.get("feedUrl") or "" 188 feed = info.get("feedUrl") or ""
@@ -137,8 +200,11 @@ class PalmaresAppleConnector(BaseConnector):
137 continue 200 continue
138 acc = account("podcast", cid, "profil_source", url=url) 201 acc = account("podcast", cid, "profil_source", url=url)
139 dormant = _dormant(info.get("releaseDate")) 202 dormant = _dormant(info.get("releaseDate"))
203+ chart = entry.get("chart") # None = top général, sinon genre
140 acc.metrics = {k: v for k, v in { 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 "episodes": info.get("trackCount"), 208 "episodes": info.get("trackCount"),
143 "genre": info.get("primaryGenreName"), 209 "genre": info.get("primaryGenreName"),
144 "feed_url": feed, 210 "feed_url": feed,
@@ -162,7 +228,9 @@ class PalmaresAppleConnector(BaseConnector):
162 or entry.get("artworkUrl100"), 228 or entry.get("artworkUrl100"),
163 status="inactive" if dormant else "active", 229 status="inactive" if dormant else "active",
164 platforms=[acc], 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 source_ids=[f"{self.source_id}:{cid}"], 234 source_ids=[f"{self.source_id}:{cid}"],
167 )) 235 ))
168 return creators 236 return creators
169 237
Menu

Theme