SPB Git

spb/crea-ka

Public

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

Fiches légendaires : upgrade majeur des 13 acteurs Apify (bannières, covers TikTok, miniatures YouTube/Twitch, media X, previews Snapchat, /about YouTube, socialMedias Twitch GQL, liens sociaux Patreon) + pipeline étendu (banner_url, metric_maps, cross-links), snapshots quotidiens d'audience, insights/Ka Score /100 par créateur (croissance 7j/30j, engagement pondéré, rythme), fiche frontend enrichie (bannière héro, Ka Score, sparkline, bloc live) — acteurs versionnés dans actors/, 8 poussés et validés par runs de test

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 21, 2026) parent e7c36ce Browse files

98 changed files +4,340 −13

98 changed files
added actors/ka-discord/.actor/Dockerfile +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: Dockerfile (ka-discord)
4+# Desc: Acteur Apify KA — Dockerfile
5+# ==============================================================================
6+FROM apify/actor-python:3.12
7+COPY requirements.txt ./
8+RUN pip install --no-cache-dir -r requirements.txt
9+COPY . ./
10+CMD ["python3", "-m", "src"]
added actors/ka-discord/.actor/actor.json +10 −0
@@ -0,0 +1,10 @@
1+{
2+ "actorSpecification": 1,
3+ "name": "ka-discord",
4+ "title": "KA Discord — serveurs via invitation",
5+ "description": "Serveurs Discord via l'API publique d'invitation : membres approximatifs, en ligne, boosts, nom/description. Code d'invit sensible à la casse.",
6+ "version": "0.1",
7+ "buildTag": "latest",
8+ "input": "./input_schema.json",
9+ "dockerfile": "./Dockerfile"
10+}
\ No newline at end of file
added actors/ka-discord/.actor/input_schema.json +38 −0
@@ -0,0 +1,38 @@
1+{
2+ "title": "ka-discord input",
3+ "type": "object",
4+ "schemaVersion": 1,
5+ "properties": {
6+ "usernames": {
7+ "title": "Handles / codes",
8+ "type": "array",
9+ "description": "Handles (patreon) ou codes d'invitation (discord).",
10+ "editor": "stringList",
11+ "prefill": []
12+ },
13+ "requestDelay": {
14+ "title": "Délai (s)",
15+ "type": "integer",
16+ "default": 1,
17+ "minimum": 0,
18+ "description": "Pause entre requêtes."
19+ },
20+ "concurrency": {
21+ "title": "Concurrence",
22+ "type": "integer",
23+ "default": 5,
24+ "minimum": 1,
25+ "maximum": 10,
26+ "description": "Requêtes simultanées."
27+ },
28+ "proxyConfiguration": {
29+ "title": "Proxy",
30+ "type": "object",
31+ "editor": "proxy",
32+ "description": "Proxy Apify.",
33+ "prefill": {
34+ "useApifyProxy": true
35+ }
36+ }
37+ }
38+}
\ No newline at end of file
added actors/ka-discord/requirements.txt +7 −0
@@ -0,0 +1,7 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: requirements.txt (ka-discord)
4+# Desc: Acteur Apify KA — requirements.txt
5+# ==============================================================================
6+apify
7+curl_cffi>=0.9
added actors/ka-discord/src/__main__.py +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/__main__.py (ka-discord)
4+# Desc: Point d'entrée de l'acteur (asyncio.run).
5+# ==============================================================================
6+import asyncio
7+
8+from .main import main
9+
10+asyncio.run(main())
added actors/ka-discord/src/main.py +87 −0
@@ -0,0 +1,87 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/main.py (ka-discord)
4+# Desc: Serveurs Discord via l'API PUBLIQUE d'invitation
5+# /api/v9/invites/{code}?with_counts=true : membres approximatifs,
6+# présents en ligne, nom/description/boosts du serveur. Le CODE d'invit
7+# est SENSIBLE À LA CASSE → il vient de l'URL, pas du handle minusculé.
8+# ==============================================================================
9+from __future__ import annotations
10+
11+import asyncio
12+import json
13+
14+from apify import Actor
15+
16+from .net import Fetcher
17+
18+INVITE_API = ("https://discord.com/api/v9/invites/{code}"
19+ "?with_counts=true&with_expiration=true")
20+
21+
22+async def main() -> None:
23+ async with Actor:
24+ inp = await Actor.get_input() or {}
25+ # ici « usernames » = codes d'invitation (casse préservée par l'appelant)
26+ codes = [c.strip() for c in (inp.get("usernames") or []) if c.strip()]
27+ proxy = await Actor.create_proxy_configuration(
28+ actor_proxy_input=inp.get("proxyConfiguration"))
29+ fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1))
30+ sem = asyncio.Semaphore(int(inp.get("concurrency") or 6))
31+
32+ async def one(code: str) -> None:
33+ async with sem:
34+ miss = {"kind": "profile", "platform": "discord",
35+ "username": code, "found": False}
36+ try:
37+ resp = await fetcher.get(
38+ INVITE_API.format(code=code), session_id=code[:12],
39+ headers={"Accept": "application/json"})
40+ except Exception as exc:
41+ await Actor.push_data({**miss, "error": str(exc)[:200]})
42+ return
43+ if resp.status_code == 404:
44+ await Actor.push_data({**miss, "error": "invite_invalide"})
45+ return
46+ if resp.status_code != 200:
47+ await Actor.push_data(
48+ {**miss, "error": f"http_{resp.status_code}"})
49+ return
50+ try:
51+ d = json.loads(resp.text)
52+ except Exception:
53+ await Actor.push_data({**miss, "error": "bad_json"})
54+ return
55+ guild = d.get("guild") or {}
56+ icon = guild.get("icon")
57+ gid = guild.get("id")
58+ banner_hash = guild.get("banner")
59+ splash_hash = guild.get("splash")
60+ await Actor.push_data({
61+ "kind": "profile",
62+ "platform": "discord",
63+ "found": True,
64+ "username": code,
65+ "guild_id": gid,
66+ "full_name": guild.get("name"),
67+ "biography": guild.get("description"),
68+ "followers": d.get("approximate_member_count"), # membres
69+ "members": d.get("approximate_member_count"),
70+ "online": d.get("approximate_presence_count"),
71+ "boosts": guild.get("premium_subscription_count"),
72+ "verified": ("VERIFIED" in (guild.get("features") or [])),
73+ "partnered": ("PARTNERED" in (guild.get("features") or [])),
74+ "channel": (d.get("channel") or {}).get("name"),
75+ "avatar": (f"https://cdn.discordapp.com/icons/{gid}/{icon}.png"
76+ if gid and icon else None),
77+ "banner": (f"https://cdn.discordapp.com/banners/{gid}/"
78+ f"{banner_hash}.png?size=1024"
79+ if gid and banner_hash else None),
80+ "splash": (f"https://cdn.discordapp.com/splashes/{gid}/"
81+ f"{splash_hash}.jpg?size=1024"
82+ if gid and splash_hash else None),
83+ "vanity_url_code": guild.get("vanity_url_code"),
84+ })
85+
86+ await asyncio.gather(*[one(c) for c in codes])
87+ Actor.log.info(f"terminé : {len(codes)} invitations")
added actors/ka-discord/src/net.py +69 −0
@@ -0,0 +1,69 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/net.py
4+# Desc: Fetch commun des acteurs KA — curl_cffi (empreinte TLS Chrome) +
5+# proxy Apify (résidentiel par défaut), throttling poli + retries
6+# ==============================================================================
7+from __future__ import annotations
8+
9+import asyncio
10+import random
11+import re
12+
13+from apify import Actor
14+from curl_cffi import requests as cffi
15+
16+UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
17+ "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
18+
19+
20+class Fetcher:
21+ """GET poli avec proxy Apify + impersonation Chrome + retries 403/429."""
22+
23+ def __init__(self, proxy_configuration, delay: float = 1.0) -> None:
24+ self.proxy_configuration = proxy_configuration
25+ self.delay = delay
26+ self._lock = asyncio.Lock()
27+ self._last = 0.0
28+
29+ async def _throttle(self) -> None:
30+ async with self._lock:
31+ now = asyncio.get_event_loop().time()
32+ wait = self.delay - (now - self._last)
33+ if wait > 0:
34+ await asyncio.sleep(wait)
35+ self._last = asyncio.get_event_loop().time()
36+
37+ async def get(self, url: str, headers: dict | None = None,
38+ retries: int = 3, session_id: str | None = None,
39+ impersonate: str | None = "chrome"):
40+ hdrs = {"User-Agent": UA,
41+ "Accept-Language": "fr-CA,fr;q=0.9,en;q=0.8"}
42+ if headers:
43+ hdrs.update(headers)
44+ last_exc: Exception | None = None
45+ for attempt in range(retries):
46+ await self._throttle()
47+ proxy = None
48+ if self.proxy_configuration:
49+ sid = session_id or f"s{random.randint(1, 999_999)}"
50+ sid = re.sub(r"[^\w._~]", "", sid)[:40] or "s"
51+ proxy = await self.proxy_configuration.new_url(
52+ session_id=f"{sid}r{attempt}")
53+ try:
54+ resp = await asyncio.to_thread(
55+ cffi.get, url, headers=hdrs, impersonate=impersonate,
56+ timeout=60, allow_redirects=True,
57+ proxies={"http": proxy, "https": proxy} if proxy else None)
58+ if resp.status_code in (403, 407, 429, 502) and attempt < retries - 1:
59+ Actor.log.warning(
60+ f"HTTP {resp.status_code} {url} — retry {attempt + 1}")
61+ await asyncio.sleep(1.5 * (attempt + 1))
62+ continue
63+ return resp
64+ except Exception as exc: # réseau/proxy : on retente
65+ last_exc = exc
66+ await asyncio.sleep(1.5 * (attempt + 1))
67+ if last_exc:
68+ raise last_exc
69+ raise RuntimeError(f"échec après {retries} tentatives : {url}")
added actors/ka-facebook/.actor/Dockerfile +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: Dockerfile (ka-facebook)
4+# Desc: Acteur Apify KA — Dockerfile
5+# ==============================================================================
6+FROM apify/actor-python:3.12
7+COPY requirements.txt ./
8+RUN pip install --no-cache-dir -r requirements.txt
9+COPY . ./
10+CMD ["python3", "-m", "src"]
added actors/ka-facebook/.actor/actor.json +10 −0
@@ -0,0 +1,10 @@
1+{
2+ "actorSpecification": 1,
3+ "name": "ka-facebook",
4+ "title": "KA Facebook — pages publiques",
5+ "description": "Métadonnées de pages Facebook publiques (nom, abonnés/mentions J'aime, catégorie, avatar) extraites du HTML public via proxy résidentiel + empreinte Chrome. Expérimental : Facebook mure agressivement.",
6+ "version": "0.1",
7+ "buildTag": "latest",
8+ "input": "./input_schema.json",
9+ "dockerfile": "./Dockerfile"
10+}
added actors/ka-facebook/.actor/input_schema.json +43 −0
@@ -0,0 +1,43 @@
1+{
2+ "title": "KA Facebook input",
3+ "type": "object",
4+ "schemaVersion": 1,
5+ "properties": {
6+ "usernames": {
7+ "title": "Pages à enrichir",
8+ "type": "array",
9+ "description": "Slugs de pages Facebook (facebook.com/<slug>) ou URLs complètes.",
10+ "editor": "stringList",
11+ "prefill": [
12+ "facebook"
13+ ]
14+ },
15+ "requestDelay": {
16+ "title": "Délai entre requêtes (s)",
17+ "type": "integer",
18+ "default": 2,
19+ "minimum": 0,
20+ "description": "Pause minimale entre deux requêtes sortantes, en secondes."
21+ },
22+ "concurrency": {
23+ "title": "Requêtes simultanées",
24+ "type": "integer",
25+ "default": 2,
26+ "minimum": 1,
27+ "maximum": 6,
28+ "description": "Nombre de requêtes menées en parallèle."
29+ },
30+ "proxyConfiguration": {
31+ "title": "Proxy",
32+ "type": "object",
33+ "editor": "proxy",
34+ "description": "Résidentiel obligatoire (Facebook rejette les IP datacenter).",
35+ "prefill": {
36+ "useApifyProxy": true,
37+ "apifyProxyGroups": [
38+ "RESIDENTIAL"
39+ ]
40+ }
41+ }
42+ }
43+}
\ No newline at end of file
added actors/ka-facebook/requirements.txt +7 −0
@@ -0,0 +1,7 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: requirements.txt (ka-facebook)
4+# Desc: Acteur Apify KA — requirements.txt
5+# ==============================================================================
6+apify
7+curl_cffi>=0.9
added actors/ka-facebook/src/__main__.py +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/__main__.py (ka-facebook)
4+# Desc: Point d'entrée de l'acteur (asyncio.run).
5+# ==============================================================================
6+import asyncio
7+
8+from .main import main
9+
10+asyncio.run(main())
added actors/ka-facebook/src/main.py +121 −0
@@ -0,0 +1,121 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/main.py (ka-facebook)
4+# Desc: Pages Facebook publiques — extraction best-effort du HTML servi aux
5+# navigateurs non connectés : compteur d'abonnés (JSON embarqué
6+# follower_count / texte « N followers/abonnés »), nom (og:title),
7+# avatar (og:image), catégorie. EXPÉRIMENTAL : FB mure agressivement ;
8+# un résultat found=False n'est pas une erreur du pipeline.
9+# ==============================================================================
10+from __future__ import annotations
11+
12+import asyncio
13+import html as htmllib
14+import re
15+
16+from apify import Actor
17+
18+from .net import Fetcher
19+
20+PAGE_URL = "https://www.facebook.com/{u}"
21+
22+_OG_RE = {
23+ "title": re.compile(r'<meta property="og:title" content="([^"]*)"'),
24+ "image": re.compile(r'<meta property="og:image" content="([^"]*)"'),
25+ "description": re.compile(
26+ r'<meta (?:property="og:description"|name="description") '
27+ r'content="([^"]*)"'),
28+}
29+_FOLLOWER_RES = (
30+ re.compile(r'"follower_count"\s*:\s*(\d+)'),
31+ re.compile(r'"global_likers_count"\s*:\s*(\d+)'),
32+)
33+# texte « 1,2 M followers » / « 12 k abonnés » / « 4 016 971 mentions J’aime »
34+_TEXT_COUNT_RE = re.compile(
35+ r'([\d][\d\s  .,]*)\s*([KkMm]?)\s*'
36+ r'(?:followers|abonnés|mentions\s+J’aime|J’aime|likes)',
37+ re.I)
38+_CATEGORY_RE = re.compile(r'"category_name"\s*:\s*"([^"]+)"')
39+_VERIFIED_RE = re.compile(r'"is_verified"\s*:\s*(true|false)')
40+
41+
42+def parse_text_count(text: str) -> int | None:
43+ m = _TEXT_COUNT_RE.search(text or "")
44+ if not m:
45+ return None
46+ num = m.group(1).replace(" ", "").replace(" ", "").replace(",", ".")
47+ try:
48+ val = float(num)
49+ except ValueError:
50+ return None
51+ unit = (m.group(2) or "").lower()
52+ if unit == "k":
53+ val *= 1_000
54+ elif unit == "m":
55+ val *= 1_000_000
56+ return int(val)
57+
58+
59+async def main() -> None:
60+ async with Actor:
61+ inp = await Actor.get_input() or {}
62+ usernames = []
63+ for raw in (inp.get("usernames") or []):
64+ raw = (raw or "").strip()
65+ if not raw:
66+ continue
67+ raw = re.sub(r"^https?://(www\.|m\.)?facebook\.com/", "", raw)
68+ usernames.append(raw.strip("/").split("?")[0])
69+ proxy = await Actor.create_proxy_configuration(
70+ actor_proxy_input=inp.get("proxyConfiguration"))
71+ fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 2))
72+ sem = asyncio.Semaphore(int(inp.get("concurrency") or 2))
73+
74+ async def one(u: str) -> None:
75+ async with sem:
76+ miss = {"kind": "profile", "platform": "facebook",
77+ "username": u, "found": False}
78+ try:
79+ resp = await fetcher.get(PAGE_URL.format(u=u),
80+ session_id=u)
81+ except Exception as exc:
82+ await Actor.push_data({**miss, "error": str(exc)[:200]})
83+ return
84+ text = resp.text or ""
85+ if resp.status_code != 200 or len(text) < 5000:
86+ await Actor.push_data(
87+ {**miss, "error": f"http_{resp.status_code}"})
88+ return
89+ followers = None
90+ for rx in _FOLLOWER_RES:
91+ m = rx.search(text)
92+ if m:
93+ followers = int(m.group(1))
94+ break
95+ og = {k: htmllib.unescape(m.group(1)) if (m := rx.search(text))
96+ else None for k, rx in _OG_RE.items()}
97+ if followers is None:
98+ followers = parse_text_count(og.get("description") or "")
99+ if followers is None: # texte « N followers/abonnés » du corps
100+ followers = parse_text_count(text)
101+ if followers is None and not og.get("title"):
102+ await Actor.push_data({**miss, "error": "login_wall"})
103+ return
104+ cat = _CATEGORY_RE.search(text)
105+ ver = _VERIFIED_RE.search(text)
106+ await Actor.push_data({
107+ "kind": "profile",
108+ "platform": "facebook",
109+ "found": True,
110+ "username": u,
111+ "full_name": og.get("title"),
112+ "biography": og.get("description"),
113+ "followers": followers,
114+ "category": cat.group(1) if cat else None,
115+ "is_verified": (ver.group(1) == "true") if ver else None,
116+ "avatar": og.get("image"),
117+ "og_description": og.get("description"),
118+ })
119+
120+ await asyncio.gather(*[one(u) for u in usernames])
121+ Actor.log.info(f"terminé : {len(usernames)} pages")
added actors/ka-facebook/src/net.py +69 −0
@@ -0,0 +1,69 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/net.py
4+# Desc: Fetch commun des acteurs KA — curl_cffi (empreinte TLS Chrome) +
5+# proxy Apify (résidentiel par défaut), throttling poli + retries
6+# ==============================================================================
7+from __future__ import annotations
8+
9+import asyncio
10+import random
11+import re
12+
13+from apify import Actor
14+from curl_cffi import requests as cffi
15+
16+UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
17+ "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
18+
19+
20+class Fetcher:
21+ """GET poli avec proxy Apify + impersonation Chrome + retries 403/429."""
22+
23+ def __init__(self, proxy_configuration, delay: float = 1.0) -> None:
24+ self.proxy_configuration = proxy_configuration
25+ self.delay = delay
26+ self._lock = asyncio.Lock()
27+ self._last = 0.0
28+
29+ async def _throttle(self) -> None:
30+ async with self._lock:
31+ now = asyncio.get_event_loop().time()
32+ wait = self.delay - (now - self._last)
33+ if wait > 0:
34+ await asyncio.sleep(wait)
35+ self._last = asyncio.get_event_loop().time()
36+
37+ async def get(self, url: str, headers: dict | None = None,
38+ retries: int = 3, session_id: str | None = None,
39+ impersonate: str | None = "chrome"):
40+ hdrs = {"User-Agent": UA,
41+ "Accept-Language": "fr-CA,fr;q=0.9,en;q=0.8"}
42+ if headers:
43+ hdrs.update(headers)
44+ last_exc: Exception | None = None
45+ for attempt in range(retries):
46+ await self._throttle()
47+ proxy = None
48+ if self.proxy_configuration:
49+ sid = session_id or f"s{random.randint(1, 999_999)}"
50+ sid = re.sub(r"[^\w._~]", "", sid)[:40] or "s"
51+ proxy = await self.proxy_configuration.new_url(
52+ session_id=f"{sid}r{attempt}")
53+ try:
54+ resp = await asyncio.to_thread(
55+ cffi.get, url, headers=hdrs, impersonate=impersonate,
56+ timeout=60, allow_redirects=True,
57+ proxies={"http": proxy, "https": proxy} if proxy else None)
58+ if resp.status_code in (403, 407, 429, 502) and attempt < retries - 1:
59+ Actor.log.warning(
60+ f"HTTP {resp.status_code} {url} — retry {attempt + 1}")
61+ await asyncio.sleep(1.5 * (attempt + 1))
62+ continue
63+ return resp
64+ except Exception as exc: # réseau/proxy : on retente
65+ last_exc = exc
66+ await asyncio.sleep(1.5 * (attempt + 1))
67+ if last_exc:
68+ raise last_exc
69+ raise RuntimeError(f"échec après {retries} tentatives : {url}")
added actors/ka-fansly/.actor/Dockerfile +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: Dockerfile (ka-fansly)
4+# Desc: Acteur Apify KA — Dockerfile
5+# ==============================================================================
6+FROM apify/actor-python:3.12
7+COPY requirements.txt ./
8+RUN pip install --no-cache-dir -r requirements.txt
9+COPY . ./
10+CMD ["python3", "-m", "src"]
added actors/ka-fansly/.actor/actor.json +10 −0
@@ -0,0 +1,10 @@
1+{
2+ "actorSpecification": 1,
3+ "name": "ka-fansly",
4+ "title": "KA Fansly — profils",
5+ "description": "Profils Fansly via l'API publique (abonnés, bio, badge, compteurs de médias, avatar) — lots de 20 handles.",
6+ "version": "0.1",
7+ "buildTag": "latest",
8+ "input": "./input_schema.json",
9+ "dockerfile": "./Dockerfile"
10+}
\ No newline at end of file
added actors/ka-fansly/.actor/input_schema.json +38 −0
@@ -0,0 +1,38 @@
1+{
2+ "title": "ka-fansly input",
3+ "type": "object",
4+ "schemaVersion": 1,
5+ "properties": {
6+ "usernames": {
7+ "title": "Handles à enrichir",
8+ "type": "array",
9+ "description": "Handles/slugs à traiter (sans @).",
10+ "editor": "stringList",
11+ "prefill": []
12+ },
13+ "requestDelay": {
14+ "title": "Délai entre requêtes (s)",
15+ "type": "integer",
16+ "default": 1,
17+ "minimum": 0,
18+ "description": "Pause minimale entre deux requêtes sortantes, en secondes."
19+ },
20+ "concurrency": {
21+ "title": "Requêtes simultanées",
22+ "type": "integer",
23+ "default": 3,
24+ "minimum": 1,
25+ "maximum": 10,
26+ "description": "Nombre de requêtes menées en parallèle."
27+ },
28+ "proxyConfiguration": {
29+ "title": "Proxy",
30+ "type": "object",
31+ "editor": "proxy",
32+ "description": "Configuration du proxy Apify.",
33+ "prefill": {
34+ "useApifyProxy": true
35+ }
36+ }
37+ }
38+}
\ No newline at end of file
added actors/ka-fansly/requirements.txt +7 −0
@@ -0,0 +1,7 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: requirements.txt (ka-fansly)
4+# Desc: Acteur Apify KA — requirements.txt
5+# ==============================================================================
6+apify
7+curl_cffi>=0.9
added actors/ka-fansly/src/__main__.py +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/__main__.py (ka-fansly)
4+# Desc: Point d'entrée de l'acteur (asyncio.run).
5+# ==============================================================================
6+import asyncio
7+
8+from .main import main
9+
10+asyncio.run(main())
added actors/ka-fansly/src/main.py +93 −0
@@ -0,0 +1,93 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/main.py (ka-fansly)
4+# Desc: Profils Fansly via l'API publique apiv3.fansly.com/api/v1/account
5+# (?usernames=) : abonnés (followCount), bio, badge, compteurs de
6+# médias (images/vidéos), avatar/bannière. Lots de 20 handles/requête.
7+# ==============================================================================
8+from __future__ import annotations
9+
10+import asyncio
11+import json
12+
13+from apify import Actor
14+
15+from .net import Fetcher
16+
17+API_URL = ("https://apiv3.fansly.com/api/v1/account"
18+ "?usernames={us}&ngsw-bypass=true")
19+
20+
21+def _loc(media: dict | None) -> str | None:
22+ """Objet média Fansly → URL de la première variante disponible."""
23+ if not isinstance(media, dict):
24+ return None
25+ for variant in ([media] + (media.get("variants") or [])):
26+ for loc in (variant.get("locations") or []):
27+ if loc.get("location"):
28+ return loc["location"]
29+ return None
30+
31+
32+async def main() -> None:
33+ async with Actor:
34+ inp = await Actor.get_input() or {}
35+ usernames = [u.strip().lstrip("@").lower()
36+ for u in (inp.get("usernames") or []) if u and u.strip()]
37+ proxy = await Actor.create_proxy_configuration(
38+ actor_proxy_input=inp.get("proxyConfiguration"))
39+ fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1))
40+
41+ found: set[str] = set()
42+ for i in range(0, len(usernames), 20):
43+ batch = usernames[i:i + 20]
44+ try:
45+ resp = await fetcher.get(API_URL.format(us=",".join(batch)),
46+ session_id=f"b{i}",
47+ headers={"Accept":
48+ "application/json"})
49+ data = (json.loads(resp.text)
50+ if resp.status_code == 200 else {})
51+ except Exception as exc:
52+ for u in batch:
53+ await Actor.push_data(
54+ {"kind": "profile", "platform": "fansly",
55+ "username": u, "found": False,
56+ "error": str(exc)[:200]})
57+ continue
58+ for acc in (data.get("response") or []):
59+ u = str(acc.get("username") or "").lower()
60+ if not u:
61+ continue
62+ found.add(u)
63+ stats = acc.get("timelineStats") or {}
64+ await Actor.push_data({
65+ "kind": "profile",
66+ "platform": "fansly",
67+ "found": True,
68+ "id": acc.get("id"),
69+ "username": u,
70+ "full_name": acc.get("displayName") or u,
71+ "biography": (acc.get("about") or "")[:1000],
72+ "followers": acc.get("followCount"),
73+ "following": acc.get("followingCount"),
74+ "is_verified": bool((acc.get("statusInfo") or {})
75+ .get("verified")
76+ or acc.get("verified")),
77+ "posts_count": acc.get("postCount")
78+ or stats.get("postCount"),
79+ "images_count": stats.get("imageCount"),
80+ "videos_count": stats.get("videoCount"),
81+ "likes": acc.get("accountMediaLikes"),
82+ "avatar": _loc(acc.get("avatar")),
83+ "banner": _loc(acc.get("banner")),
84+ "location": acc.get("location"),
85+ })
86+ for u in batch:
87+ if u not in found:
88+ await Actor.push_data(
89+ {"kind": "profile", "platform": "fansly",
90+ "username": u, "found": False,
91+ "error": f"not_found_{resp.status_code}"})
92+ Actor.log.info(f"terminé : {len(usernames)} profils, "
93+ f"{len(found)} trouvés")
added actors/ka-fansly/src/net.py +69 −0
@@ -0,0 +1,69 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/net.py
4+# Desc: Fetch commun des acteurs KA — curl_cffi (empreinte TLS Chrome) +
5+# proxy Apify (résidentiel par défaut), throttling poli + retries
6+# ==============================================================================
7+from __future__ import annotations
8+
9+import asyncio
10+import random
11+import re
12+
13+from apify import Actor
14+from curl_cffi import requests as cffi
15+
16+UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
17+ "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
18+
19+
20+class Fetcher:
21+ """GET poli avec proxy Apify + impersonation Chrome + retries 403/429."""
22+
23+ def __init__(self, proxy_configuration, delay: float = 1.0) -> None:
24+ self.proxy_configuration = proxy_configuration
25+ self.delay = delay
26+ self._lock = asyncio.Lock()
27+ self._last = 0.0
28+
29+ async def _throttle(self) -> None:
30+ async with self._lock:
31+ now = asyncio.get_event_loop().time()
32+ wait = self.delay - (now - self._last)
33+ if wait > 0:
34+ await asyncio.sleep(wait)
35+ self._last = asyncio.get_event_loop().time()
36+
37+ async def get(self, url: str, headers: dict | None = None,
38+ retries: int = 3, session_id: str | None = None,
39+ impersonate: str | None = "chrome"):
40+ hdrs = {"User-Agent": UA,
41+ "Accept-Language": "fr-CA,fr;q=0.9,en;q=0.8"}
42+ if headers:
43+ hdrs.update(headers)
44+ last_exc: Exception | None = None
45+ for attempt in range(retries):
46+ await self._throttle()
47+ proxy = None
48+ if self.proxy_configuration:
49+ sid = session_id or f"s{random.randint(1, 999_999)}"
50+ sid = re.sub(r"[^\w._~]", "", sid)[:40] or "s"
51+ proxy = await self.proxy_configuration.new_url(
52+ session_id=f"{sid}r{attempt}")
53+ try:
54+ resp = await asyncio.to_thread(
55+ cffi.get, url, headers=hdrs, impersonate=impersonate,
56+ timeout=60, allow_redirects=True,
57+ proxies={"http": proxy, "https": proxy} if proxy else None)
58+ if resp.status_code in (403, 407, 429, 502) and attempt < retries - 1:
59+ Actor.log.warning(
60+ f"HTTP {resp.status_code} {url} — retry {attempt + 1}")
61+ await asyncio.sleep(1.5 * (attempt + 1))
62+ continue
63+ return resp
64+ except Exception as exc: # réseau/proxy : on retente
65+ last_exc = exc
66+ await asyncio.sleep(1.5 * (attempt + 1))
67+ if last_exc:
68+ raise last_exc
69+ raise RuntimeError(f"échec après {retries} tentatives : {url}")
added actors/ka-instagram/.actor/Dockerfile +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: Dockerfile (ka-instagram)
4+# Desc: Acteur Apify KA — Dockerfile
5+# ==============================================================================
6+FROM apify/actor-python:3.12
7+COPY requirements.txt ./
8+RUN pip install --no-cache-dir -r requirements.txt
9+COPY . ./
10+CMD ["python3", "-m", "src"]
added actors/ka-instagram/.actor/actor.json +10 −0
@@ -0,0 +1,10 @@
1+{
2+ "actorSpecification": 1,
3+ "name": "ka-instagram",
4+ "title": "KA Instagram — profils détaillés + recherche",
5+ "description": "Profils Instagram publics ultra détaillés (abonnés, bio, liens, 12 derniers posts avec likes/commentaires/vues, engagement, profils reliés) + recherche d'utilisateurs par mot-clé. Endpoints web publics via proxy résidentiel.",
6+ "version": "0.1",
7+ "buildTag": "latest",
8+ "input": "./input_schema.json",
9+ "dockerfile": "./Dockerfile"
10+}
added actors/ka-instagram/.actor/input_schema.json +49 −0
@@ -0,0 +1,49 @@
1+{
2+ "title": "KA Instagram input",
3+ "type": "object",
4+ "schemaVersion": 1,
5+ "properties": {
6+ "usernames": {
7+ "title": "Handles à enrichir",
8+ "type": "array",
9+ "description": "Handles Instagram (sans @) dont on veut le profil détaillé.",
10+ "editor": "stringList",
11+ "prefill": [
12+ "instagram"
13+ ]
14+ },
15+ "queries": {
16+ "title": "Recherches (découverte)",
17+ "type": "array",
18+ "description": "Mots-clés de recherche d'utilisateurs (topsearch).",
19+ "editor": "stringList"
20+ },
21+ "requestDelay": {
22+ "title": "Délai entre requêtes (s)",
23+ "type": "integer",
24+ "default": 1,
25+ "minimum": 0,
26+ "description": "Pause minimale entre deux requêtes sortantes, en secondes."
27+ },
28+ "concurrency": {
29+ "title": "Requêtes simultanées",
30+ "type": "integer",
31+ "default": 3,
32+ "minimum": 1,
33+ "maximum": 10,
34+ "description": "Nombre de requêtes menées en parallèle."
35+ },
36+ "proxyConfiguration": {
37+ "title": "Proxy",
38+ "type": "object",
39+ "editor": "proxy",
40+ "description": "Résidentiel recommandé (Instagram bloque les IP datacenter).",
41+ "prefill": {
42+ "useApifyProxy": true,
43+ "apifyProxyGroups": [
44+ "RESIDENTIAL"
45+ ]
46+ }
47+ }
48+ }
49+}
\ No newline at end of file
added actors/ka-instagram/requirements.txt +7 −0
@@ -0,0 +1,7 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: requirements.txt (ka-instagram)
4+# Desc: Acteur Apify KA — requirements.txt
5+# ==============================================================================
6+apify
7+curl_cffi>=0.9
added actors/ka-instagram/src/__main__.py +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/__main__.py (ka-instagram)
4+# Desc: Point d'entrée de l'acteur (asyncio.run).
5+# ==============================================================================
6+import asyncio
7+
8+from .main import main
9+
10+asyncio.run(main())
added actors/ka-instagram/src/main.py +243 −0
@@ -0,0 +1,243 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/main.py (ka-instagram)
4+# Desc: Profils Instagram publics ULTRA DÉTAILLÉS via l'endpoint web officiel
5+# web_profile_info (JSON du web public) : identité complète, compteurs,
6+# 12 derniers posts (likes/commentaires/vues/légende/musique/lieu/
7+# co-auteurs/épinglés), agrégats d'engagement et de cadence, profils
8+# reliés — + recherche d'utilisateurs (topsearch) en découverte.
9+# ==============================================================================
10+from __future__ import annotations
11+
12+import asyncio
13+import json
14+import urllib.parse
15+from datetime import datetime, timezone
16+
17+from apify import Actor
18+
19+from .net import Fetcher
20+
21+PROFILE_API = ("https://i.instagram.com/api/v1/users/web_profile_info/"
22+ "?username={u}")
23+SEARCH_API = ("https://www.instagram.com/web/search/topsearch/"
24+ "?context=blended&query={q}&count=30")
25+SEARCH_API_ALT = ("https://i.instagram.com/api/v1/web/search/topsearch/"
26+ "?context=blended&query={q}&count=30")
27+HDRS = {"x-ig-app-id": "936619743392459", "Accept": "application/json"}
28+
29+
30+def _iso(ts) -> str | None:
31+ try:
32+ return datetime.fromtimestamp(int(ts), tz=timezone.utc) \
33+ .strftime("%Y-%m-%dT%H:%M:%SZ")
34+ except (TypeError, ValueError, OSError):
35+ return None
36+
37+
38+def _avg(vals: list) -> int | None:
39+ vals = [v for v in vals if isinstance(v, (int, float))]
40+ return round(sum(vals) / len(vals)) if vals else None
41+
42+
43+def parse_post(node: dict) -> dict:
44+ """Nœud média du fil → post détaillé (métriques, musique, lieu, épinglé)."""
45+ cap = (node.get("edge_media_to_caption") or {}).get("edges") or []
46+ music = node.get("clips_music_attribution_info") or {}
47+ dims = node.get("dimensions") or {}
48+ return {
49+ "id": node.get("id"),
50+ "shortcode": node.get("shortcode"),
51+ "url": f"https://www.instagram.com/p/{node.get('shortcode')}/",
52+ "type": ("reel" if node.get("product_type") == "clips"
53+ else "video" if node.get("is_video") else
54+ "carousel" if node.get("edge_sidecar_to_children")
55+ else "image"),
56+ "caption": ((cap[0].get("node") or {}).get("text", "")
57+ if cap else "")[:500],
58+ "likes": (node.get("edge_liked_by") or {}).get("count"),
59+ "comments": (node.get("edge_media_to_comment") or {}).get("count"),
60+ "video_views": node.get("video_view_count"),
61+ "video_duration_s": node.get("video_duration"),
62+ "timestamp": node.get("taken_at_timestamp"),
63+ "posted_at": _iso(node.get("taken_at_timestamp")),
64+ "display_url": node.get("display_url"),
65+ "thumbnail_url": node.get("thumbnail_src"),
66+ "width": dims.get("width"),
67+ "height": dims.get("height"),
68+ "is_pinned": bool(node.get("pinned_for_users")),
69+ "comments_disabled": node.get("comments_disabled"),
70+ "location": (node.get("location") or {}).get("name"),
71+ "coauthors": [c.get("username")
72+ for c in (node.get("coauthor_producers") or [])
73+ if c.get("username")],
74+ "music": ({"artist": music.get("artist_name"),
75+ "song": music.get("song_name"),
76+ "uses_original_audio": music.get("uses_original_audio")}
77+ if music.get("song_name") or music.get("artist_name")
78+ else None),
79+ "accessibility_caption": (node.get("accessibility_caption")
80+ or "")[:200] or None,
81+ "tagged_users": [((t.get("node") or {}).get("user") or {})
82+ .get("username")
83+ for t in ((node.get("edge_media_to_tagged_user")
84+ or {}).get("edges") or [])][:10],
85+ }
86+
87+
88+def parse_user(user: dict) -> dict:
89+ """Objet user web_profile_info → enregistrement riche standardisé."""
90+ posts = [parse_post((e.get("node") or {}))
91+ for e in ((user.get("edge_owner_to_timeline_media") or {})
92+ .get("edges") or [])]
93+ followers = (user.get("edge_followed_by") or {}).get("count")
94+ avg_likes = _avg([p["likes"] for p in posts])
95+ avg_comments = _avg([p["comments"] for p in posts])
96+ videos = [p for p in posts if p["type"] in ("video", "reel")]
97+ avg_video_views = _avg([p["video_views"] for p in videos])
98+ engagement = None
99+ if followers and avg_likes is not None:
100+ engagement = round(
101+ (avg_likes + (avg_comments or 0)) / followers * 100, 3)
102+ timestamps = sorted(p["timestamp"] for p in posts
103+ if isinstance(p["timestamp"], int))
104+ posts_per_week = None
105+ if len(timestamps) >= 3 and timestamps[-1] > timestamps[0]:
106+ span_weeks = (timestamps[-1] - timestamps[0]) / 604_800
107+ posts_per_week = round((len(timestamps) - 1) / span_weeks, 2)
108+ top_post = max((p for p in posts if isinstance(p["likes"], int)),
109+ key=lambda p: p["likes"], default=None)
110+ related = []
111+ for edge in ((user.get("edge_related_profiles") or {}).get("edges") or []):
112+ n = edge.get("node") or {}
113+ if n.get("username"):
114+ related.append({"username": n["username"],
115+ "full_name": n.get("full_name"),
116+ "is_verified": n.get("is_verified"),
117+ "is_private": n.get("is_private")})
118+ return {
119+ "kind": "profile",
120+ "platform": "instagram",
121+ "found": True,
122+ "id": user.get("id"),
123+ "username": user.get("username"),
124+ "full_name": user.get("full_name"),
125+ "biography": user.get("biography"),
126+ "pronouns": user.get("pronouns") or None,
127+ "external_url": user.get("external_url"),
128+ "bio_links": [b.get("url") for b in (user.get("bio_links") or [])
129+ if b.get("url")],
130+ "followers": followers,
131+ "following": (user.get("edge_follow") or {}).get("count"),
132+ "posts_count": (user.get("edge_owner_to_timeline_media")
133+ or {}).get("count"),
134+ "highlight_reels": user.get("highlight_reel_count"),
135+ "is_verified": user.get("is_verified"),
136+ "is_private": user.get("is_private"),
137+ "is_business": user.get("is_business_account"),
138+ "is_professional": user.get("is_professional_account"),
139+ "category": user.get("category_name"),
140+ "business_category": user.get("business_category_name"),
141+ "business_address": user.get("business_address_json"),
142+ "has_clips": user.get("has_clips"),
143+ "has_channel": user.get("has_channel"),
144+ "has_ar_effects": user.get("has_ar_effects"),
145+ "recently_joined": user.get("is_joined_recently"),
146+ "avatar": user.get("profile_pic_url_hd")
147+ or user.get("profile_pic_url"),
148+ # agrégats calculés sur les 12 derniers posts
149+ "avg_likes": avg_likes,
150+ "avg_comments": avg_comments,
151+ "avg_video_views": avg_video_views,
152+ "engagement_rate_pct": engagement,
153+ "posts_per_week": posts_per_week,
154+ "last_post_at": _iso(timestamps[-1]) if timestamps else None,
155+ "video_share_pct": (round(len(videos) / len(posts) * 100)
156+ if posts else None),
157+ "top_post": ({"url": top_post["url"], "likes": top_post["likes"],
158+ "comments": top_post["comments"],
159+ "type": top_post["type"]} if top_post else None),
160+ "recent_posts": posts,
161+ "related_profiles": related,
162+ }
163+
164+
165+async def main() -> None:
166+ async with Actor:
167+ inp = await Actor.get_input() or {}
168+ usernames = [u.strip().lstrip("@").lower()
169+ for u in (inp.get("usernames") or []) if u and u.strip()]
170+ queries = [q.strip() for q in (inp.get("queries") or [])
171+ if q and q.strip()]
172+ proxy = await Actor.create_proxy_configuration(
173+ actor_proxy_input=inp.get("proxyConfiguration"))
174+ fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1))
175+ sem = asyncio.Semaphore(int(inp.get("concurrency") or 3))
176+
177+ async def one_profile(u: str) -> None:
178+ async with sem:
179+ miss = {"kind": "profile", "platform": "instagram",
180+ "username": u, "found": False}
181+ try:
182+ resp = await fetcher.get(PROFILE_API.format(u=u),
183+ headers=HDRS, session_id=u)
184+ except Exception as exc:
185+ await Actor.push_data({**miss, "error": str(exc)[:200]})
186+ return
187+ if resp.status_code == 404:
188+ await Actor.push_data({**miss, "error": "http_404"})
189+ return
190+ user = None
191+ if resp.status_code == 200:
192+ try:
193+ user = ((json.loads(resp.text).get("data") or {})
194+ .get("user"))
195+ except Exception:
196+ user = None
197+ if not user:
198+ await Actor.push_data(
199+ {**miss, "error": f"http_{resp.status_code}"})
200+ return
201+ await Actor.push_data(parse_user(user))
202+
203+ async def one_search(q: str) -> None:
204+ async with sem:
205+ data = {}
206+ for endpoint in (SEARCH_API, SEARCH_API_ALT):
207+ u2 = endpoint.format(q=urllib.parse.quote(q))
208+ try:
209+ resp = await fetcher.get(u2, headers=HDRS,
210+ session_id=f"srch{q[:4]}")
211+ except Exception as exc:
212+ Actor.log.warning(f"recherche '{q}' : {exc}")
213+ continue
214+ if resp.status_code == 200:
215+ try:
216+ data = json.loads(resp.text)
217+ except Exception:
218+ data = {}
219+ if data.get("users"):
220+ break
221+ if not data.get("users"):
222+ Actor.log.warning(
223+ f"recherche '{q}' : 0 user "
224+ f"(http {resp.status_code}) {resp.text[:200]!r}")
225+ for item in data.get("users") or []:
226+ u = item.get("user") or {}
227+ if not u.get("username") or u.get("is_private"):
228+ continue
229+ await Actor.push_data({
230+ "kind": "search_user",
231+ "platform": "instagram",
232+ "query": q,
233+ "username": u.get("username"),
234+ "full_name": u.get("full_name"),
235+ "is_verified": u.get("is_verified"),
236+ "followers": u.get("follower_count"),
237+ "avatar": u.get("profile_pic_url"),
238+ })
239+
240+ await asyncio.gather(*[one_profile(u) for u in usernames],
241+ *[one_search(q) for q in queries])
242+ Actor.log.info(f"terminé : {len(usernames)} profils, "
243+ f"{len(queries)} recherches")
added actors/ka-instagram/src/net.py +69 −0
@@ -0,0 +1,69 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/net.py
4+# Desc: Fetch commun des acteurs KA — curl_cffi (empreinte TLS Chrome) +
5+# proxy Apify (résidentiel par défaut), throttling poli + retries
6+# ==============================================================================
7+from __future__ import annotations
8+
9+import asyncio
10+import random
11+import re
12+
13+from apify import Actor
14+from curl_cffi import requests as cffi
15+
16+UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
17+ "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
18+
19+
20+class Fetcher:
21+ """GET poli avec proxy Apify + impersonation Chrome + retries 403/429."""
22+
23+ def __init__(self, proxy_configuration, delay: float = 1.0) -> None:
24+ self.proxy_configuration = proxy_configuration
25+ self.delay = delay
26+ self._lock = asyncio.Lock()
27+ self._last = 0.0
28+
29+ async def _throttle(self) -> None:
30+ async with self._lock:
31+ now = asyncio.get_event_loop().time()
32+ wait = self.delay - (now - self._last)
33+ if wait > 0:
34+ await asyncio.sleep(wait)
35+ self._last = asyncio.get_event_loop().time()
36+
37+ async def get(self, url: str, headers: dict | None = None,
38+ retries: int = 3, session_id: str | None = None,
39+ impersonate: str | None = "chrome"):
40+ hdrs = {"User-Agent": UA,
41+ "Accept-Language": "fr-CA,fr;q=0.9,en;q=0.8"}
42+ if headers:
43+ hdrs.update(headers)
44+ last_exc: Exception | None = None
45+ for attempt in range(retries):
46+ await self._throttle()
47+ proxy = None
48+ if self.proxy_configuration:
49+ sid = session_id or f"s{random.randint(1, 999_999)}"
50+ sid = re.sub(r"[^\w._~]", "", sid)[:40] or "s"
51+ proxy = await self.proxy_configuration.new_url(
52+ session_id=f"{sid}r{attempt}")
53+ try:
54+ resp = await asyncio.to_thread(
55+ cffi.get, url, headers=hdrs, impersonate=impersonate,
56+ timeout=60, allow_redirects=True,
57+ proxies={"http": proxy, "https": proxy} if proxy else None)
58+ if resp.status_code in (403, 407, 429, 502) and attempt < retries - 1:
59+ Actor.log.warning(
60+ f"HTTP {resp.status_code} {url} — retry {attempt + 1}")
61+ await asyncio.sleep(1.5 * (attempt + 1))
62+ continue
63+ return resp
64+ except Exception as exc: # réseau/proxy : on retente
65+ last_exc = exc
66+ await asyncio.sleep(1.5 * (attempt + 1))
67+ if last_exc:
68+ raise last_exc
69+ raise RuntimeError(f"échec après {retries} tentatives : {url}")
added actors/ka-kick/.actor/Dockerfile +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: Dockerfile (ka-kick)
4+# Desc: Acteur Apify KA — Dockerfile
5+# ==============================================================================
6+FROM apify/actor-python:3.12
7+COPY requirements.txt ./
8+RUN pip install --no-cache-dir -r requirements.txt
9+COPY . ./
10+CMD ["python3", "-m", "src"]
added actors/ka-kick/.actor/actor.json +10 −0
@@ -0,0 +1,10 @@
1+{
2+ "actorSpecification": 1,
3+ "name": "ka-kick",
4+ "title": "KA Kick — chaînes détaillées",
5+ "description": "Chaînes Kick via l'API v2 publique (abonnés, live, bio + liens sociaux auto-déclarés) — proxy résidentiel pour Cloudflare.",
6+ "version": "0.1",
7+ "buildTag": "latest",
8+ "input": "./input_schema.json",
9+ "dockerfile": "./Dockerfile"
10+}
\ No newline at end of file
added actors/ka-kick/.actor/input_schema.json +43 −0
@@ -0,0 +1,43 @@
1+{
2+ "title": "ka-kick input",
3+ "type": "object",
4+ "schemaVersion": 1,
5+ "properties": {
6+ "usernames": {
7+ "title": "Handles à enrichir",
8+ "type": "array",
9+ "description": "Handles/slugs à traiter (sans @).",
10+ "editor": "stringList",
11+ "prefill": [
12+ "xqc"
13+ ]
14+ },
15+ "requestDelay": {
16+ "title": "Délai entre requêtes (s)",
17+ "type": "integer",
18+ "default": 1,
19+ "minimum": 0,
20+ "description": "Pause minimale entre deux requêtes sortantes, en secondes."
21+ },
22+ "concurrency": {
23+ "title": "Requêtes simultanées",
24+ "type": "integer",
25+ "default": 3,
26+ "minimum": 1,
27+ "maximum": 10,
28+ "description": "Nombre de requêtes menées en parallèle."
29+ },
30+ "proxyConfiguration": {
31+ "title": "Proxy",
32+ "type": "object",
33+ "editor": "proxy",
34+ "description": "Configuration du proxy Apify.",
35+ "prefill": {
36+ "useApifyProxy": true,
37+ "apifyProxyGroups": [
38+ "RESIDENTIAL"
39+ ]
40+ }
41+ }
42+ }
43+}
\ No newline at end of file
added actors/ka-kick/requirements.txt +7 −0
@@ -0,0 +1,7 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: requirements.txt (ka-kick)
4+# Desc: Acteur Apify KA — requirements.txt
5+# ==============================================================================
6+apify
7+curl_cffi>=0.9
added actors/ka-kick/src/__main__.py +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/__main__.py (ka-kick)
4+# Desc: Point d'entrée de l'acteur (asyncio.run).
5+# ==============================================================================
6+import asyncio
7+
8+from .main import main
9+
10+asyncio.run(main())
added actors/ka-kick/src/main.py +103 −0
@@ -0,0 +1,103 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/main.py (ka-kick)
4+# Desc: Chaînes Kick détaillées via l'API publique /api/v2/channels/{slug}
5+# (Cloudflare : proxy résidentiel + empreinte Chrome) : abonnés, badge,
6+# live en cours, bio + LIENS SOCIAUX AUTO-DÉCLARÉS (instagram/twitter/
7+# youtube/tiktok/facebook/discord → cross_link 0.90 côté crea-ka).
8+# ==============================================================================
9+from __future__ import annotations
10+
11+import asyncio
12+import json
13+
14+from apify import Actor
15+
16+from .net import Fetcher
17+
18+API_URL = "https://kick.com/api/v2/channels/{u}"
19+SOCIAL_KEYS = ("instagram", "twitter", "youtube", "tiktok", "facebook",
20+ "discord")
21+
22+
23+async def main() -> None:
24+ async with Actor:
25+ inp = await Actor.get_input() or {}
26+ usernames = [u.strip().lstrip("@").lower()
27+ for u in (inp.get("usernames") or []) if u and u.strip()]
28+ proxy = await Actor.create_proxy_configuration(
29+ actor_proxy_input=inp.get("proxyConfiguration"))
30+ fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1))
31+ sem = asyncio.Semaphore(int(inp.get("concurrency") or 3))
32+
33+ async def one(u: str) -> None:
34+ async with sem:
35+ miss = {"kind": "profile", "platform": "kick",
36+ "username": u, "found": False}
37+ resp = None
38+ for attempt in range(3): # mur Cloudflare → nouvelle IP
39+ try:
40+ resp = await fetcher.get(
41+ API_URL.format(u=u), session_id=f"{u}k{attempt}",
42+ headers={"Accept": "application/json"})
43+ except Exception as exc:
44+ await Actor.push_data({**miss,
45+ "error": str(exc)[:200]})
46+ return
47+ if resp.status_code == 404:
48+ await Actor.push_data({**miss, "error": "http_404"})
49+ return
50+ if resp.status_code == 200 and \
51+ (resp.text or "").startswith("{"):
52+ break
53+ if resp.status_code != 200 or \
54+ not (resp.text or "").startswith("{"):
55+ await Actor.push_data(
56+ {**miss, "error": f"http_{resp.status_code}"})
57+ return
58+ try:
59+ ch = json.loads(resp.text)
60+ except Exception:
61+ await Actor.push_data({**miss, "error": "bad_json"})
62+ return
63+ user = ch.get("user") or {}
64+ live = ch.get("livestream") or {}
65+ cats = [((c.get("category") or {}).get("name"))
66+ for c in (ch.get("recent_categories") or [])
67+ if isinstance(c, dict)]
68+ socials = {k: (user.get(k) or "").strip()
69+ for k in SOCIAL_KEYS if (user.get(k) or "").strip()}
70+ await Actor.push_data({
71+ "kind": "profile",
72+ "platform": "kick",
73+ "found": True,
74+ "id": ch.get("id"),
75+ "username": ch.get("slug"),
76+ "full_name": user.get("username"),
77+ "biography": user.get("bio"),
78+ "followers": ch.get("followers_count")
79+ or ch.get("followersCount"),
80+ "is_verified": bool(ch.get("verified")),
81+ "is_banned": bool(ch.get("is_banned")),
82+ "subscription_enabled": ch.get("subscription_enabled"),
83+ "is_live_now": bool(live),
84+ "live_viewers": live.get("viewer_count"),
85+ "live_title": (live.get("session_title") or "")[:200]
86+ or None,
87+ "live_thumbnail": ((live.get("thumbnail") or {}).get("url")
88+ if isinstance(live.get("thumbnail"),
89+ dict) else None),
90+ "live_category": next(
91+ (c.get("name") for c in (live.get("categories") or [])
92+ if isinstance(c, dict) and c.get("name")), None),
93+ "vod_enabled": ch.get("vod_enabled"),
94+ "avatar": user.get("profile_pic"),
95+ "banner": ((ch.get("banner_image") or {}).get("url")
96+ if isinstance(ch.get("banner_image"), dict)
97+ else None),
98+ "recent_categories": [c for c in cats if c][:5],
99+ "social_links": socials,
100+ })
101+
102+ await asyncio.gather(*[one(u) for u in usernames])
103+ Actor.log.info(f"terminé : {len(usernames)} chaînes")
added actors/ka-kick/src/net.py +69 −0
@@ -0,0 +1,69 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/net.py
4+# Desc: Fetch commun des acteurs KA — curl_cffi (empreinte TLS Chrome) +
5+# proxy Apify (résidentiel par défaut), throttling poli + retries
6+# ==============================================================================
7+from __future__ import annotations
8+
9+import asyncio
10+import random
11+import re
12+
13+from apify import Actor
14+from curl_cffi import requests as cffi
15+
16+UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
17+ "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
18+
19+
20+class Fetcher:
21+ """GET poli avec proxy Apify + impersonation Chrome + retries 403/429."""
22+
23+ def __init__(self, proxy_configuration, delay: float = 1.0) -> None:
24+ self.proxy_configuration = proxy_configuration
25+ self.delay = delay
26+ self._lock = asyncio.Lock()
27+ self._last = 0.0
28+
29+ async def _throttle(self) -> None:
30+ async with self._lock:
31+ now = asyncio.get_event_loop().time()
32+ wait = self.delay - (now - self._last)
33+ if wait > 0:
34+ await asyncio.sleep(wait)
35+ self._last = asyncio.get_event_loop().time()
36+
37+ async def get(self, url: str, headers: dict | None = None,
38+ retries: int = 3, session_id: str | None = None,
39+ impersonate: str | None = "chrome"):
40+ hdrs = {"User-Agent": UA,
41+ "Accept-Language": "fr-CA,fr;q=0.9,en;q=0.8"}
42+ if headers:
43+ hdrs.update(headers)
44+ last_exc: Exception | None = None
45+ for attempt in range(retries):
46+ await self._throttle()
47+ proxy = None
48+ if self.proxy_configuration:
49+ sid = session_id or f"s{random.randint(1, 999_999)}"
50+ sid = re.sub(r"[^\w._~]", "", sid)[:40] or "s"
51+ proxy = await self.proxy_configuration.new_url(
52+ session_id=f"{sid}r{attempt}")
53+ try:
54+ resp = await asyncio.to_thread(
55+ cffi.get, url, headers=hdrs, impersonate=impersonate,
56+ timeout=60, allow_redirects=True,
57+ proxies={"http": proxy, "https": proxy} if proxy else None)
58+ if resp.status_code in (403, 407, 429, 502) and attempt < retries - 1:
59+ Actor.log.warning(
60+ f"HTTP {resp.status_code} {url} — retry {attempt + 1}")
61+ await asyncio.sleep(1.5 * (attempt + 1))
62+ continue
63+ return resp
64+ except Exception as exc: # réseau/proxy : on retente
65+ last_exc = exc
66+ await asyncio.sleep(1.5 * (attempt + 1))
67+ if last_exc:
68+ raise last_exc
69+ raise RuntimeError(f"échec après {retries} tentatives : {url}")
added actors/ka-onlyfans/.actor/Dockerfile +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: Dockerfile (ka-onlyfans)
4+# Desc: Acteur Apify KA — Dockerfile
5+# ==============================================================================
6+FROM apify/actor-python:3.12
7+COPY requirements.txt ./
8+RUN pip install --no-cache-dir -r requirements.txt
9+COPY . ./
10+CMD ["python3", "-m", "src"]
added actors/ka-onlyfans/.actor/actor.json +10 −0
@@ -0,0 +1,10 @@
1+{
2+ "actorSpecification": 1,
3+ "name": "ka-onlyfans",
4+ "title": "KA OnlyFans — profils publics",
5+ "description": "Profils OnlyFans publics best-effort (posts, photos, vidéos, J'aime, prix d'abonnement, bio, avatar) — EXPÉRIMENTAL.",
6+ "version": "0.1",
7+ "buildTag": "latest",
8+ "input": "./input_schema.json",
9+ "dockerfile": "./Dockerfile"
10+}
\ No newline at end of file
added actors/ka-onlyfans/.actor/input_schema.json +41 −0
@@ -0,0 +1,41 @@
1+{
2+ "title": "ka-onlyfans input",
3+ "type": "object",
4+ "schemaVersion": 1,
5+ "properties": {
6+ "usernames": {
7+ "title": "Handles à enrichir",
8+ "type": "array",
9+ "description": "Handles/slugs à traiter (sans @).",
10+ "editor": "stringList",
11+ "prefill": []
12+ },
13+ "requestDelay": {
14+ "title": "Délai entre requêtes (s)",
15+ "type": "integer",
16+ "default": 1,
17+ "minimum": 0,
18+ "description": "Pause minimale entre deux requêtes sortantes, en secondes."
19+ },
20+ "concurrency": {
21+ "title": "Requêtes simultanées",
22+ "type": "integer",
23+ "default": 3,
24+ "minimum": 1,
25+ "maximum": 10,
26+ "description": "Nombre de requêtes menées en parallèle."
27+ },
28+ "proxyConfiguration": {
29+ "title": "Proxy",
30+ "type": "object",
31+ "editor": "proxy",
32+ "description": "Configuration du proxy Apify.",
33+ "prefill": {
34+ "useApifyProxy": true,
35+ "apifyProxyGroups": [
36+ "RESIDENTIAL"
37+ ]
38+ }
39+ }
40+ }
41+}
\ No newline at end of file
added actors/ka-onlyfans/requirements.txt +7 −0
@@ -0,0 +1,7 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: requirements.txt (ka-onlyfans)
4+# Desc: Acteur Apify KA — requirements.txt
5+# ==============================================================================
6+apify
7+curl_cffi>=0.9
added actors/ka-onlyfans/src/__main__.py +10 −0
@@ -0,0 +1,10 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/__main__.py (ka-onlyfans)
4+# Desc: Point d'entrée de l'acteur (asyncio.run).
5+# ==============================================================================
6+import asyncio
7+
8+from .main import main
9+
10+asyncio.run(main())
added actors/ka-onlyfans/src/main.py +112 −0
@@ -0,0 +1,112 @@
1+# ==============================================================================
2+# Author: Simon-Pierre Boucher <contact@spboucher.ai>
3+# File: src/main.py (ka-onlyfans)
4+# Desc: Profils OnlyFans publics (page de profil hors connexion) — extraction
5+# best-effort des compteurs embarqués : posts, photos, vidéos, J'aime
6+# (favoritedCount), prix d'abonnement, badge, bio, avatar/bannière.
7+# EXPÉRIMENTAL : OnlyFans change souvent son rendu ; found=False n'est
8+# pas une erreur du pipeline.
9+# ==============================================================================
10+from __future__ import annotations
11+
12+import asyncio
13+import html as htmllib
14+import json
15+import re
16+
17+from apify import Actor
18+
19+from .net import Fetcher
20+
21+PROFILE_URL = "https://onlyfans.com/{u}"
22+
23+_NUM_RES = {
24+ "posts_count": re.compile(r'"postsCount"\s*:\s*(\d+)'),
25+ "photos_count": re.compile(r'"photosCount"\s*:\s*(\d+)'),
26+ "videos_count": re.compile(r'"videosCount"\s*:\s*(\d+)'),
27+ "likes": re.compile(r'"favoritedCount"\s*:\s*(\d+)'),
28+ "streams_count": re.compile(r'"finishedStreamsCount"\s*:\s*(\d+)'),
29+}
30+_PRICE_RE = re.compile(r'"subscribePrice"\s*:\s*([\d.]+)')
31+_STR_RES = {
32+ "full_name": re.compile(r'"name"\s*:\s*"((?:[^"\\]|\\.)*)"'),
33+ "biography": re.compile(r'"rawAbout"\s*:\s*"((?:[^"\\]|\\.)*)"'),
34+ "location": re.compile(r'"location"\s*:\s*"((?:[^"\\]|\\.)*)"'),
35+ "website": re.compile(r'"website"\s*:\s*"((?:[^"\\]|\\.)*)"'),
36+ "avatar": re.compile(r'"avatar"\s*:\s*"((?:[^"\\]|\\.)*)"'),
37+ "banner": re.compile(r'"header"\s*:\s*"((?:[^"\\]|\\.)*)"'),
38+}
39+_VERIFIED_RE = re.compile(r'"isVerified"\s*:\s*(true|false)')
40+_OG_TITLE_RE = re.compile(r'<meta property="og:title" content="([^"]*)"')
41+
42+
43+def _dec(raw: str) -> str:
44+ try:
45+ return htmllib.unescape(json.loads(f'"{raw}"'))
46+ except Exception:
47+ return raw
48+
49+
50+async def main() -> None:
51+ async with Actor:
52+ inp = await Actor.get_input() or {}
53+ usernames = [u.strip().lstrip("@").lower()
54+ for u in (inp.get("usernames") or []) if u and u.strip()]
55+ proxy = await Actor.create_proxy_configuration(
56+ actor_proxy_input=inp.get("proxyConfiguration"))
57+ fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 2))
58+ sem = asyncio.Semaphore(int(inp.get("concurrency") or 2))
59+
60+ async def one(u: str) -> None:
61+ async with sem:
62+ miss = {"kind": "profile", "platform": "onlyfans",
63+ "username": u, "found": False}
64+ resp = None
65+ for attempt in range(3): # mur Cloudflare → nouvelle IP
66+ try:
67+ resp = await fetcher.get(PROFILE_URL.format(u=u),
68+ session_id=f"{u}o{attempt}")
69+ except Exception as exc:
70+ await Actor.push_data({**miss,
71+ "error": str(exc)[:200]})
72+ return
73+ if resp.status_code == 404:
74+ await Actor.push_data({**miss, "error": "http_404"})
75+ return
76+ if resp.status_code == 200 and \
77+ "postsCount" in (resp.text or ""):
78+ break
79+ text = resp.text or ""
80+ if resp.status_code != 200 or "postsCount" not in text:
81+ await Actor.push_data(
82+ {**miss, "error": f"wall_{resp.status_code}"})
83+ return
84+ rec: dict = {"kind": "profile", "platform": "onlyfans",
85+ "found": True, "username": u}
86+ for key, rx in _NUM_RES.items():
87+ m = rx.search(text)
88+ rec[key] = int(m.group(1)) if m else None
89+ for key, rx in _STR_RES.items():
90+ m = rx.search(text)
91+ rec[key] = _dec(m.group(1)) if m else None
92+ if rec.get("biography"):
93+ rec["biography"] = re.sub(r"<[^>]+>", " ",
94+ rec["biography"])[:1000].strip()
95+ m = _PRICE_RE.search(text)
96+ rec["subscribe_price_usd"] = float(m.group(1)) if m else None
97+ rec["is_free"] = (rec["subscribe_price_usd"] == 0.0
98+ if rec["subscribe_price_usd"] is not None
99+ else None)
100+ m = _VERIFIED_RE.search(text)
101+ rec["is_verified"] = (m.group(1) == "true") if m else None
102+ if not rec.get("full_name"):
103+ m = _OG_TITLE_RE.search(text)
104+ if m:
105+ rec["full_name"] = htmllib.unescape(
106+ m.group(1)).split(" OnlyFans")[0].strip()
107+ # « followers » au sens crea-ka : le compteur public = J'aime
108+ rec["followers"] = None
109+ await Actor.push_data(rec)
110+
111+ await asyncio.gather(*[one(u) for u in usernames])
112+ Actor.log.info(f"terminé : {len(usernames)} profils")
added actors/ka-onlyfans/src/net.py +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-patreon/.actor/Dockerfile +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-patreon/.actor/actor.json +10 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-patreon/.actor/input_schema.json +41 −0
Large diff (42 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-patreon/requirements.txt +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-patreon/src/__main__.py +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-patreon/src/main.py +114 −0
Large diff (114 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-patreon/src/net.py +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-snapchat/.actor/Dockerfile +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-snapchat/.actor/actor.json +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-snapchat/.actor/input_schema.json +43 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-snapchat/requirements.txt +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-snapchat/src/__main__.py +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-snapchat/src/main.py +121 −0
Large diff (121 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-snapchat/src/net.py +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-threads/.actor/Dockerfile +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-threads/.actor/actor.json +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-threads/.actor/input_schema.json +43 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-threads/requirements.txt +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-threads/src/__main__.py +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-threads/src/main.py +99 −0
Large diff (99 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-threads/src/net.py +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-tiktok/.actor/Dockerfile +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-tiktok/.actor/actor.json +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-tiktok/.actor/input_schema.json +43 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-tiktok/requirements.txt +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-tiktok/src/__main__.py +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-tiktok/src/main.py +228 −0
Large diff (228 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-tiktok/src/net.py +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-twitch/.actor/Dockerfile +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-twitch/.actor/actor.json +10 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-twitch/.actor/input_schema.json +40 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-twitch/requirements.txt +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-twitch/src/__main__.py +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-twitch/src/main.py +158 −0
Large diff (158 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-twitch/src/net.py +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-x/.actor/Dockerfile +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-x/.actor/actor.json +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-x/.actor/input_schema.json +48 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-x/requirements.txt +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-x/src/__main__.py +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-x/src/main.py +217 −0
Large diff (217 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-x/src/net.py +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-youtube/.actor/Dockerfile +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-youtube/.actor/actor.json +10 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-youtube/.actor/input_schema.json +46 −0
Large diff (47 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-youtube/requirements.txt +7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-youtube/src/__main__.py +10 −0
Large diff (10 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-youtube/src/main.py +303 −0
Large diff (303 lines) is not loaded by default to keep the page fast.Load diff
added actors/ka-youtube/src/net.py +69 −0
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
modified creaka/connectors/apify_social.py +53 −3
Large diff (124 lines) is not loaded by default to keep the page fast.Load diff
modified creaka/db.py +56 −0
Large diff (74 lines) is not loaded by default to keep the page fast.Load diff
modified creaka/schema.py +1 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
modified creaka/stats.py +90 −0
Large diff (93 lines) is not loaded by default to keep the page fast.Load diff
modified creaka/web.py +3 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
modified frontend/dist/index.html +102 −5
Large diff (177 lines) is not loaded by default to keep the page fast.Load diff
modified frontend/src/index.template.html +102 −5
Large diff (178 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme