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%
8.7 KB · 164 lines python
Raw Blame History
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File:   creaka/connectors/balados_itunes.py4# Desc:   Connecteur DÉCOUVERTE — balados québécois via l'API de recherche5#         iTunes/Apple Podcasts (OFFICIELLE, gratuite, sans clé). Palier 2-36#         (§9). Filtre conservateur : marqueur québécois requis dans le nom du7#         balado ou de l'artiste.8# ==============================================================================9"""Découverte des podcasteurs québécois via l'API iTunes Search.1011`itunes.apple.com/search?media=podcast` retourne pour chaque balado : nom,12artiste, pochette (artworkUrl600), genres, nb d'épisodes, flux RSS et page13Apple Podcasts. Le balado est le compte SOURCE de la fiche → profil_source.14"""15from __future__ import annotations1617from datetime import datetime, timedelta, timezone1819from ..identity import account20from ..normalize import slugify21from ..schema import Creator22from .base import BaseConnector23from .youtube_recherche import is_quebec2425SEARCH_URL = "https://itunes.apple.com/search"2627# au-delà de 12 mois sans épisode : balado dormant (flag public dans le doc ;28# la fiche reste dans l'annuaire — §13, on ne supprime jamais brutalement)29DORMANT_AFTER_DAYS = 365303132def _dormant(release_date: str | None) -> bool:33    """Vrai si le dernier épisode (releaseDate iTunes) date de > 12 mois."""34    if not release_date:35        return False36    try:37        last = datetime.fromisoformat(release_date.replace("Z", "+00:00"))38    except ValueError:39        return False40    return datetime.now(timezone.utc) - last > timedelta(days=DORMANT_AFTER_DAYS)4142QUERIES = [43    "québécois", "québécoise", "balado québec", "podcast québec", "montréal",44    "humour québécois", "balado montréal", "saguenay", "gatineau", "sherbrooke",45    "trois-rivières", "ville de québec balado", "balado gaspésie", "mauricie",46    "balado estrie", "rimouski", "abitibi", "lévis balado", "laval balado",47    "balado", "podcast montréal", "balado hockey", "canadiens de montréal",48    "histoire du québec", "crime québec", "balado finance québec",49    "balado maman québec", "balado couple québec", "balado santé québec",50    "balado sport québec", "balado politique québec", "balado culture québec",51    "balado musique québec", "balado cinéma québec", "balado jeux vidéo québec",52    "balado plein air québec", "balado immobilier québec", "balado entrepreneur québec",53    "outaouais", "lanaudière", "laurentides balado", "montérégie",54    "côte-nord balado", "charlevoix balado", "beauce balado",55    "drummondville", "victoriaville balado", "granby balado", "chicoutimi",56    # longue traîne : villes + créneaux (nano/micro podcasteurs locaux)57    "longueuil", "terrebonne balado", "repentigny balado", "saint-jérôme",58    "saint-hyacinthe", "shawinigan", "rouyn-noranda", "val-d'or",59    "sept-îles", "baie-comeau", "alma balado", "matane", "rivière-du-loup",60    "sorel-tracy", "magog balado", "joliette balado", "châteauguay",61    "balado chasse québec", "balado pêche québec", "balado agriculture québec",62    "balado construction québec", "balado véhicule québec", "balado techno québec",63    "balado ados québec", "balado église québec", "balado histoire vraie québec",64    "balado humour absurde québec", "balado société québec", "balado femmes québec",65    "balado papa québec", "balado éducation québec", "balado science québec",66    "balado littérature québec", "balado poésie québec", "balado bd québec",67    "balado jeux de société québec", "balado vin québec", "balado bière québec",68    # vague 4 : villes restantes + créneaux supplémentaires69    "mascouche", "blainville balado", "brossard", "mirabel", "boucherville",70    "chambly", "varennes balado", "sainte-julie", "beloeil", "mont-tremblant",71    "sainte-adèle", "thetford mines", "la tuque", "gaspé", "roberval",72    "dolbeau", "amqui", "montmagny", "saint-georges beauce", "cowansville",73    "lachute", "salaberry-de-valleyfield", "sainte-thérèse", "l'assomption",74    "rawdon", "baie-saint-paul", "la malbaie", "chibougamau",75    "balado ia québec", "balado crypto québec", "balado marketing québec",76    "balado droit québec", "balado médecine québec", "balado psycho québec",77    "balado philosophie québec", "balado environnement québec",78    "balado autochtone", "balado voyage québec", "balado spiritualité québec",79    "balado true crime québec", "balado paranormal québec",80    "balado nostalgie québec", "balado télé québec", "balado wrestling québec",81    "balado lutte québec", "balado course québec", "balado vélo québec",82    "balado golf québec", "balado pêche à la mouche québec",83    "balado gestion québec", "balado rh québec", "balado photo québec",84    "balado musique émergente québec", "balado métal québec",85    "balado country québec", "balado horreur québec", "balado cinéma d'horreur",86    "balado animation québec", "balado improvisation québec",87]8889_GENRE_NICHE = {90    "Comedy": "humour", "Food": "bouffe-resto", "Sports": "sport-fitness",91    "Music": "musique", "Business": "finance-affaires", "Investing": "finance-affaires",92    "Health & Fitness": "sante-mieux-etre", "Education": "education",93    "True Crime": "actualite-opinion", "News": "actualite-opinion",94    "Politics": "actualite-opinion", "Society & Culture": "actualite-opinion",95    "TV & Film": "actualite-opinion", "Leisure": "gaming",96    "Video Games": "gaming", "Technology": "tech", "Arts": "arts",97    "Parenting": "famille-parentalite", "Kids & Family": "famille-parentalite",98    "Fiction": "arts", "Religion & Spirituality": "sante-mieux-etre",99    "Science": "education", "History": "education", "Wilderness": "plein-air",100}101102103class BaladosItunesConnector(BaseConnector):104    source_id = "balados-itunes"105    kind = "discovery"106    request_delay = 1.5107108    def fetch(self) -> list[Creator]:109        creators: list[Creator] = []110        seen: set[int] = set()111        for query in QUERIES:112            try:113                resp = self.get(SEARCH_URL, params={114                    "media": "podcast", "term": query, "country": "CA",115                    "limit": 200})116                results = resp.json().get("results") or []117            except Exception:118                continue119            for r in results:120                cid = r.get("collectionId")121                name = (r.get("collectionName") or "").strip()122                artist = (r.get("artistName") or "").strip()123                if not cid or cid in seen or not name:124                    continue125                # filtre conservateur : marqueur QC dans le nom du balado/artiste126                if not is_quebec(f"{name} {artist}"):127                    continue128                seen.add(cid)129                feed = r.get("feedUrl") or ""130                url = r.get("collectionViewUrl") or feed131                if not url:132                    continue133                acc = account("podcast", str(cid), "profil_source", url=url)134                dormant = _dormant(r.get("releaseDate"))135                acc.metrics = {k: v for k, v in {136                    "episodes": r.get("trackCount"),137                    "genre": r.get("primaryGenreName"),138                    "feed_url": feed,                     # site du balado (RSS)139                    "artist_id": r.get("artistId"),140                    "last_episode": r.get("releaseDate"),141                    "dormant": dormant or None,           # >12 mois sans épisode142                }.items() if v}143                # genres[] complets → niches §6.1 (le genre principal inclus)144                genres = [g for g in (r.get("genres") or []) if isinstance(g, str)]145                niches = sorted({_GENRE_NICHE[g] for g in146                                 [r.get("primaryGenreName"), *genres]147                                 if g in _GENRE_NICHE}) or ["actualite-opinion"]148                creators.append(Creator(149                    source=self.source_id,150                    external_id=str(cid),151                    display_name=name,152                    bio=f"Balado de {artist}." if artist and artist != name else "",153                    niches=niches,154                    creator_type="podcasteur",155                    avatar_url=r.get("artworkUrl600") or r.get("artworkUrl100"),156                    # §13 : plus de publication depuis 12 mois → inactive157                    # (réversible : redevient actif dès un nouvel épisode)158                    status="inactive" if dormant else "active",159                    platforms=[acc],160                    notes="Référencé sur Apple Podcasts (API de recherche officielle).",161                    source_ids=[f"{self.source_id}:{cid}"],162                ))163        return creators164