From c5f42cf95848981fb340eff005aa4d4f3c03729d Mon Sep 17 00:00:00 2001 From: kawa Date: Sun, 8 Mar 2026 01:33:56 +0100 Subject: [PATCH] Commit initial --- README.md | 23 +++ demo.html | 387 +++++++++++++++++++++++++++++++++++++++++ docs/installation.md | 120 +++++++++++++ docs/standalone.md | 100 +++++++++++ docs/widget.md | 96 ++++++++++ ghost-inject.html | 180 +++++++++++++++++++ scrape_server.py | 300 ++++++++++++++++++++++++++++++++ torrent-scrape.service | 24 +++ 8 files changed, 1230 insertions(+) create mode 100644 README.md create mode 100644 demo.html create mode 100644 docs/installation.md create mode 100644 docs/standalone.md create mode 100644 docs/widget.md create mode 100644 ghost-inject.html create mode 100644 scrape_server.py create mode 100644 torrent-scrape.service diff --git a/README.md b/README.md new file mode 100644 index 0000000..8bbfdce --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Torrent Indicator + +Widget d'état de torrent. Affiche seeders, leechers, santé et popularité en temps réel à chaque chargement de page. + +## Fichiers utiles + +| Fichier | Rôle | +|---|---| +| `scrape_server.py` | Serveur Python — interroge les trackers BitTorrent | +| `torrent-scrape.service` | Service systemd pour démarrage automatique | +| `ghost-inject.html` | Snippet à coller dans Ghost (CSS + JS du widget) | + +## Démarrage rapide + +```bash +# 1. Lancer le serveur (test) +python3 scrape_server.py + +# 2. Tester l'API +curl "http://127.0.0.1:8765/?hash=3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0" +``` + +Documentation complète → [docs/](docs/) · Démo interactive → [demo.html](demo.html) diff --git a/demo.html b/demo.html new file mode 100644 index 0000000..133b387 --- /dev/null +++ b/demo.html @@ -0,0 +1,387 @@ + + + + + + Torrent Indicator — Démo + + + + + + +
+ +

Torrent Indicator

+

Exemples d'intégration sur une page HTML standard (sans Ghost).

+ + +
+

1 · Layout compact — via info hash

+

+ Usage minimal : un div avec data-hash. + Idéal en sidebar ou en ligne dans un article. +

+
+
+ +
+
+
<div class="torrent-indicator"
+     data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
+     data-label="Ubuntu 24.04 LTS"></div>
+
+ + +
+

2 · Layout large — centré, pleine largeur

+

+ Ajouter data-layout="wide" pour un affichage horizontal + qui occupe toute la largeur disponible. Adaptatif sur mobile (grille 2×2). +

+
+
<div class="torrent-indicator"
+     data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
+     data-label="Ubuntu 24.04 LTS"
+     data-layout="wide"></div>
+
+ + +
+

3 · Via lien magnet

+

+ data-magnet accepte un lien magnet complet. + Le hash est extrait automatiquement (hex 40 chars ou base32). +

+
+
<div class="torrent-indicator"
+     data-magnet="magnet:?xt=urn:btih:3b245504...&dn=ubuntu..."
+     data-label="Ubuntu via magnet"
+     data-layout="wide"></div>
+
+ + +
+

4 · Plusieurs widgets sur la même page

+

+ Les deux layouts coexistent. Le script s'initialise une seule fois + et cible tous les .torrent-indicator présents. +

+
+
+
+
+
+
+
+ + +
+

5 · Essayer avec votre propre torrent

+

+ Entrez un info hash (40 caractères hex) ou un lien magnet. +

+
+ + +
+ +
+ + +
+
+
+
+
// Créer un widget dynamiquement via l'API JS
+var el = document.createElement('div');
+el.className = 'torrent-indicator';
+el.setAttribute('data-hash', 'abc123...');
+el.setAttribute('data-layout', 'wide'); // optionnel
+document.body.appendChild(el);
+TorrentIndicator.init(el);
+
+ +
+ + + + + + + + diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..5e5a03e --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,120 @@ +# Installation + +## Prérequis + +- VPS Linux avec Python 3.8+ +- nginx +- Certbot + +--- + +## 1. Déployer le serveur Python + +```bash +sudo mkdir -p /var/www/torrent-indicator +sudo cp scrape_server.py /var/www/torrent-indicator/ +``` + +### Activer le service systemd + +```bash +sudo cp torrent-scrape.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable --now torrent-scrape +``` + +Vérifier : + +```bash +sudo systemctl status torrent-scrape +# ● torrent-scrape.service - Torrent Tracker Scrape Server +# Active: active (running) +``` + +--- + +## 2. Obtenir le certificat SSL + +```bash +sudo certbot certonly --nginx -d torrent-api.monsite.com +``` + +--- + +## 3. Configurer nginx + +Ajouter dans `/etc/nginx/sites-available/votresite` : + +```nginx +server { + server_name torrent-api.monsite.com; + + location / { + proxy_pass http://127.0.0.1:8765/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 15s; + } + + listen 443 ssl; + ssl_certificate /etc/letsencrypt/live/torrent-api.monsite.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/torrent-api.monsite.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; +} + +server { + if ($host = torrent-api.monsite.com) { + return 301 https://$host$request_uri; + } + listen 80; + server_name torrent-api.monsite.com; + return 404; +} +``` + +```bash +sudo nginx -t && sudo nginx -s reload +``` + +--- + +## 4. Vérifier l'API publique + +```bash +curl "https://torrent-api.monsite.com/?hash=3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0" +``` + +Réponse attendue : + +```json +{ + "seeders": 1234, + "leechers": 56, + "health": "excellent", + "popularity": "popular", + "sources": 4 +} +``` + +--- + +## Configuration + +Les paramètres se trouvent en haut de `scrape_server.py` : + +| Variable | Défaut | Description | +|---|---|---| +| `HOST` | `127.0.0.1` | Interface d'écoute | +| `PORT` | `8765` | Port du serveur | +| `TIMEOUT` | `7` | Timeout par tracker (secondes) | +| `CACHE_TTL` | `300` | Durée du cache (secondes) | +| `TRACKERS` | *(liste)* | Trackers HTTP interrogés | + +Après modification, redémarrer le service : + +```bash +sudo systemctl restart torrent-scrape +``` diff --git a/docs/standalone.md b/docs/standalone.md new file mode 100644 index 0000000..7c97c72 --- /dev/null +++ b/docs/standalone.md @@ -0,0 +1,100 @@ +# Intégration hors Ghost + +Le widget fonctionne sur n'importe quelle page HTML. Voir `demo.html` pour des exemples concrets. + +## Intégration en 2 étapes + +### 1. Ajouter le CSS et le JS + +Copier le bloc ` + + +``` + +Le script se place de préférence juste avant ``. + +### 2. Placer les widgets dans le HTML + +```html + +
+ + +
+``` + +--- + +## Cas d'usage + +### Page de téléchargement + +```html +

Télécharger

+
+Ouvrir le magnet +``` + +### Tableau comparatif de plusieurs torrents + +```html +
+
+ +
+
+``` + +### Création dynamique via JavaScript + +```html + +
+ + +``` + +### Rafraîchissement manuel + +```js +// Rafraîchir tous les widgets de la page +TorrentIndicator.refreshAll(); +``` + +--- + +## Compatibilité + +- Navigateurs modernes (Chrome, Firefox, Safari, Edge) +- Aucune dépendance externe +- Fonctionne avec : sites statiques, WordPress, Joomla, Drupal, forums, etc. diff --git a/docs/widget.md b/docs/widget.md new file mode 100644 index 0000000..16cd5f3 --- /dev/null +++ b/docs/widget.md @@ -0,0 +1,96 @@ +# Widget Ghost + +## 1. Code Injection (une seule fois) + +Dans **Ghost Admin → Settings → Code injection → Site Header**, coller tout le contenu de `ghost-inject.html`. + +Mettre à jour l'URL de l'API en haut du script : + +```js +var API_URL = 'https://torrent-api.monsite.com'; +``` + +--- + +## 2. Utilisation dans les articles + +Dans l'éditeur Ghost, insérer un bloc **HTML** et coller l'un des snippets suivants. + +### Layout compact (vertical) + +```html +
+``` + +### Layout large (horizontal, centré) + +```html +
+``` + +### Via lien magnet + +```html +
+``` + +--- + +## Attributs disponibles + +| Attribut | Requis | Description | +|---|---|---| +| `data-hash` | Oui* | Info hash hexadécimal (40 caractères) | +| `data-magnet` | Oui* | Lien magnet complet | +| `data-label` | Non | Titre affiché dans l'en-tête du widget | +| `data-layout` | Non | `wide` pour le layout horizontal | + +*`data-hash` ou `data-magnet`, l'un des deux est obligatoire. + +--- + +## Valeurs retournées + +### Santé (`health`) + +| Valeur | Condition | +|---|---| +| `dead` | 0 seeder | +| `poor` | ratio seeders/(seeders+leechers) < 20 % | +| `good` | ratio ≥ 20 % | +| `excellent` | ratio ≥ 50 % | + +### Popularité (`popularity`) + +| Valeur | Total seeders + leechers | +|---|---| +| `low` | < 10 | +| `moderate` | 10 – 99 | +| `popular` | 100 – 999 | +| `viral` | ≥ 1 000 | + +--- + +## Cache et données périmées + +Le serveur met les résultats en cache 5 minutes. Si les trackers sont temporairement inaccessibles, le widget affiche les dernières données connues avec la mention *données en cache* dans le pied de page, plutôt qu'un message d'erreur. + +--- + +## API publique JavaScript + +```js +// Rafraîchir tous les widgets de la page +TorrentIndicator.refreshAll(); + +// Initialiser un élément spécifique +TorrentIndicator.init(document.querySelector('.torrent-indicator')); +``` diff --git a/ghost-inject.html b/ghost-inject.html new file mode 100644 index 0000000..a3e15d3 --- /dev/null +++ b/ghost-inject.html @@ -0,0 +1,180 @@ + + + + + + + diff --git a/scrape_server.py b/scrape_server.py new file mode 100644 index 0000000..a7c369d --- /dev/null +++ b/scrape_server.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +""" +Torrent Tracker Scrape Server +Remplacement auto-hébergé de scrape.php / du Cloudflare Worker. +Dépendances : aucune (stdlib Python 3.8+) + +Démarrage rapide : + python3 scrape_server.py + +Avec systemd : voir torrent-scrape.service + +Usage : GET http://127.0.0.1:8765/?hash=<40_hex_chars> + GET http://127.0.0.1:8765/?magnet= + +Réponse JSON : + {"seeders": n, "leechers": n, "health": "...", "popularity": "...", "sources": n} +""" + +import json +import re +import time +import urllib.request +import urllib.parse +from http.server import HTTPServer, BaseHTTPRequestHandler +from concurrent.futures import ThreadPoolExecutor, as_completed + +# --------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------- + +HOST = '127.0.0.1' # Écouter uniquement en local (nginx fait le proxy) +PORT = 8765 +WORKERS = 10 # Requêtes parallèles vers les trackers +TIMEOUT = 7 # Secondes par tracker +CACHE_TTL = 300 # Durée du cache en secondes (5 min) + +TRACKERS = [ + 'http://tracker.opentrackr.org:1337/scrape', + 'http://open.tracker.cl:1337/scrape', + 'http://tracker.openbittorrent.com:80/scrape', + 'http://tracker.torrent.eu.org:451/scrape', + 'http://tracker.tiny-vps.com:6969/scrape', + 'http://tracker.files.fm:6969/scrape', + 'http://tracker1.bt.moack.co.kr:80/scrape', + 'http://tracker.leechersparadise.org:6969/scrape', + 'http://open.stealth.si:80/scrape', + 'http://tracker4.itzmx.com:2710/scrape', +] + +# --------------------------------------------------------------- +# Cache en mémoire { hash_hex: (timestamp, data_dict) } +# Renvoie les dernières données connues si les trackers sont muets. +# --------------------------------------------------------------- + +_cache: dict = {} + +# --------------------------------------------------------------- +# Décodeur bencoding (format réponse tracker) +# --------------------------------------------------------------- + +def bdecode(data: bytearray, pos: list) -> object: + c = data[pos[0]] + + # Entier : ie + if c == ord('i'): + pos[0] += 1 + end = data.index(ord('e'), pos[0]) + n = int(data[pos[0]:end]) + pos[0] = end + 1 + return n + + # Liste : le + if c == ord('l'): + pos[0] += 1 + lst = [] + while data[pos[0]] != ord('e'): + lst.append(bdecode(data, pos)) + pos[0] += 1 + return lst + + # Dictionnaire : d...e + if c == ord('d'): + pos[0] += 1 + d = {} + while data[pos[0]] != ord('e'): + key = bdecode(data, pos) + val = bdecode(data, pos) + # Clé binaire (ex. info hash 20 octets) → hex string + if isinstance(key, (bytes, bytearray)): + key = key.hex() + d[str(key)] = val + pos[0] += 1 + return d + + # Chaîne : : + if chr(c).isdigit(): + colon = data.index(ord(':'), pos[0]) + length = int(data[pos[0]:colon]) + pos[0] = colon + 1 + raw = data[pos[0]:pos[0] + length] + pos[0] += length + try: + # Texte ASCII → str ; données binaires → bytes + decoded = raw.decode('ascii') + return decoded + except (UnicodeDecodeError, ValueError): + return bytes(raw) + + return None + +# --------------------------------------------------------------- +# Scrape d'un tracker +# --------------------------------------------------------------- + +def scrape_tracker(tracker_url: str, hash_hex: str) -> dict | None: + hash_bytes = bytes.fromhex(hash_hex) + encoded = urllib.parse.quote(hash_bytes, safe='') + url = f"{tracker_url}?info_hash={encoded}" + + try: + req = urllib.request.Request( + url, + headers={'User-Agent': 'TorrentIndicator/1.0'} + ) + with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: + raw = resp.read() + except Exception: + return None + + try: + parsed = bdecode(bytearray(raw), [0]) + except Exception: + return None + + if not isinstance(parsed, dict) or 'files' not in parsed: + return None + + files = parsed['files'] + if not isinstance(files, dict): + return None + + for file_data in files.values(): + if isinstance(file_data, dict): + return { + 'seeders': int(file_data.get('complete', 0) or 0), + 'leechers': int(file_data.get('incomplete', 0) or 0), + } + + return None + +# --------------------------------------------------------------- +# Parsing du magnet link +# --------------------------------------------------------------- + +def extract_hash(magnet: str) -> str: + # Hex 40 chars + m = re.search(r'xt=urn:btih:([0-9a-fA-F]{40})', magnet, re.I) + if m: + return m.group(1).lower() + + # Base32 32 chars + m = re.search(r'xt=urn:btih:([A-Z2-7]{32})', magnet, re.I) + if m: + return _base32_to_hex(m.group(1).upper()) + + return '' + +def _base32_to_hex(s: str) -> str: + alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' + buf, bits, out = 0, 0, [] + for c in s: + val = alphabet.find(c) + if val < 0: + continue + buf = (buf << 5) | val + bits += 5 + if bits >= 8: + bits -= 8 + out.append((buf >> bits) & 0xFF) + return bytes(out).hex() + +# --------------------------------------------------------------- +# Calculs santé / popularité +# --------------------------------------------------------------- + +def compute_health(seeders: int, leechers: int) -> str: + if seeders == 0: + return 'dead' + ratio = seeders / max(1, seeders + leechers) + if ratio >= 0.5: + return 'excellent' + if ratio >= 0.2: + return 'good' + return 'poor' + +def compute_popularity(total: int) -> str: + if total >= 1000: return 'viral' + if total >= 100: return 'popular' + if total >= 10: return 'moderate' + return 'low' + +# --------------------------------------------------------------- +# Serveur HTTP +# --------------------------------------------------------------- + +class ScrapeHandler(BaseHTTPRequestHandler): + + def log_message(self, *args): + pass # Désactiver les logs par défaut (gérer via systemd journal) + + def do_OPTIONS(self): + self.send_response(204) + self._add_cors() + self.end_headers() + + def do_GET(self): + parsed = urllib.parse.urlparse(self.path) + params = urllib.parse.parse_qs(parsed.query) + + hash_hex = params.get('hash', [''])[0].strip().lower() + magnet = params.get('magnet', [''])[0].strip() + + if not hash_hex and magnet: + hash_hex = extract_hash(urllib.parse.unquote(magnet)) + + if not re.fullmatch(r'[0-9a-f]{40}', hash_hex): + self._send_json({'error': 'Hash invalide. Fournissez ?hash= (40 hex) ou ?magnet=.'}, 400) + return + + # Vérifier le cache avant d'interroger les trackers + cached = _cache.get(hash_hex) + if cached and (time.time() - cached[0]) < CACHE_TTL: + self._send_json(cached[1]) + return + + best_seeders = 0 + best_leechers = 0 + sources = 0 + + with ThreadPoolExecutor(max_workers=WORKERS) as executor: + futures = { + executor.submit(scrape_tracker, tracker, hash_hex): tracker + for tracker in TRACKERS + } + for future in as_completed(futures): + result = future.result() + if result: + if result['seeders'] > best_seeders: best_seeders = result['seeders'] + if result['leechers'] > best_leechers: best_leechers = result['leechers'] + sources += 1 + + if sources == 0 and cached: + # Aucun tracker n'a répondu : renvoyer le cache même expiré + # plutôt qu'une erreur visible + stale = dict(cached[1]) + stale['stale'] = True + self._send_json(stale) + return + + data = { + 'seeders': best_seeders, + 'leechers': best_leechers, + 'health': compute_health(best_seeders, best_leechers), + 'popularity': compute_popularity(best_seeders + best_leechers), + 'sources': sources, + } + + # Mettre en cache uniquement si au moins un tracker a répondu + if sources > 0: + _cache[hash_hex] = (time.time(), data) + + self._send_json(data) + + def _send_json(self, data: dict, status: int = 200): + body = json.dumps(data).encode('utf-8') + self.send_response(status) + self.send_header('Content-Type', 'application/json; charset=utf-8') + self.send_header('Content-Length', str(len(body))) + self.send_header('Cache-Control', 'no-store') + self._add_cors() + self.end_headers() + self.wfile.write(body) + + def _add_cors(self): + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type') + +# --------------------------------------------------------------- +# Point d'entrée +# --------------------------------------------------------------- + +if __name__ == '__main__': + server = HTTPServer((HOST, PORT), ScrapeHandler) + print(f"Torrent scrape server → http://{HOST}:{PORT}") + print("Arrêt : Ctrl+C") + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nServeur arrêté.") diff --git a/torrent-scrape.service b/torrent-scrape.service new file mode 100644 index 0000000..0f0dc59 --- /dev/null +++ b/torrent-scrape.service @@ -0,0 +1,24 @@ +# Fichier systemd pour lancer le serveur scrape au démarrage. +# +# Installation : +# sudo cp torrent-scrape.service /etc/systemd/system/ +# sudo systemctl daemon-reload +# sudo systemctl enable --now torrent-scrape +# +# Logs : +# sudo journalctl -u torrent-scrape -f + +[Unit] +Description=Torrent Tracker Scrape Server +After=network.target + +[Service] +Type=simple +# Adapter le chemin et l'utilisateur selon votre configuration +User=www-data +ExecStart=/usr/bin/python3 /var/www/torrent-indicator/scrape_server.py +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target