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%

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 1 mo ago (Aug 20, 2026) parent a4b7446

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