Commit initial
This commit is contained in:
23
README.md
Normal file
23
README.md
Normal file
@@ -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)
|
||||
387
demo.html
Normal file
387
demo.html
Normal file
@@ -0,0 +1,387 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Torrent Indicator — Démo</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #f6f8fa;
|
||||
color: #24292f;
|
||||
margin: 0;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
.page { max-width: 760px; margin: 0 auto; }
|
||||
h1 { font-size: 1.5rem; margin: 0 0 4px; }
|
||||
.subtitle { color: #57606a; margin: 0 0 40px; }
|
||||
section { margin-bottom: 48px; }
|
||||
h2 {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
color: #57606a;
|
||||
border-bottom: 1px solid #d0d7de;
|
||||
padding-bottom: 6px;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
.description { color: #57606a; font-size: 14px; margin: 0 0 16px; }
|
||||
.widgets-row { display: flex; flex-wrap: wrap; gap: 16px; }
|
||||
pre {
|
||||
background: #161b22;
|
||||
color: #e6edf3;
|
||||
padding: 14px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Formulaire interactif */
|
||||
.try-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.try-form label { font-size: 13px; font-weight: 600; }
|
||||
.try-form input {
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
}
|
||||
.try-form input:focus { outline: 2px solid #0969da; border-color: transparent; }
|
||||
.try-controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.try-form button {
|
||||
padding: 8px 16px;
|
||||
background: #0969da;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.try-form button:hover { background: #0757b8; }
|
||||
.layout-toggle { display: flex; gap: 4px; }
|
||||
.layout-toggle label {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
.layout-toggle input[type=radio] { accent-color: #0969da; }
|
||||
#try-result { margin-top: 16px; }
|
||||
</style>
|
||||
|
||||
<!-- ============================================================
|
||||
CSS + JS du widget — copier depuis ghost-inject.html
|
||||
Adapter API_URL selon votre déploiement.
|
||||
============================================================ -->
|
||||
<style id="ti-styles">
|
||||
.ti-widget{display:inline-flex;flex-direction:column;font-family:system-ui,sans-serif;font-size:13px;border:1px solid #d0d7de;border-radius:8px;overflow:hidden;min-width:180px;max-width:260px;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.08);color:#24292f}
|
||||
.ti-header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f6f8fa;border-bottom:1px solid #d0d7de;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:#57606a}
|
||||
.ti-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:#8c8c8c;transition:background .3s}
|
||||
.ti-body{padding:10px 12px;display:flex;flex-direction:column;gap:6px}
|
||||
.ti-row{display:flex;justify-content:space-between;align-items:center}
|
||||
.ti-label{color:#57606a}
|
||||
.ti-value{font-weight:600;font-variant-numeric:tabular-nums}
|
||||
.ti-badge{display:inline-block;padding:1px 7px;border-radius:12px;font-size:11px;font-weight:600;letter-spacing:.02em}
|
||||
.ti-health-dead{background:#ffeef0;color:#cf222e}
|
||||
.ti-health-poor{background:#fff3cd;color:#9a6700}
|
||||
.ti-health-good,.ti-health-excellent{background:#dafbe1;color:#116329}
|
||||
.ti-dot-dead{background:#cf222e}
|
||||
.ti-dot-poor{background:#d4a900}
|
||||
.ti-dot-good,.ti-dot-excellent{background:#2da44e}
|
||||
.ti-pop-low{background:#f0f0f0;color:#57606a}
|
||||
.ti-pop-moderate{background:#ddf4ff;color:#0550ae}
|
||||
.ti-pop-popular{background:#dbedff;color:#0550ae}
|
||||
.ti-pop-viral{background:#fff0f0;color:#a40000}
|
||||
.ti-footer{padding:5px 12px 7px;font-size:10px;color:#8c959f;border-top:1px solid #f0f0f0;text-align:right}
|
||||
.ti-loading,.ti-error{padding:14px 12px;text-align:center;color:#57606a;font-size:12px}
|
||||
.ti-error{color:#cf222e}
|
||||
.ti-widget--wide{max-width:680px;width:100%;margin:0 auto;min-width:0}
|
||||
.ti-widget--wide .ti-body{flex-direction:row;padding:0;gap:0}
|
||||
.ti-stat{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;flex:1;padding:14px 8px;border-right:1px solid #f0f0f0}
|
||||
.ti-stat:last-child{border-right:none}
|
||||
.ti-stat-label{font-size:10px;color:#8c959f;text-transform:uppercase;letter-spacing:.05em;font-weight:600}
|
||||
.ti-stat-value{font-size:22px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}
|
||||
@media(max-width:480px){
|
||||
.ti-widget--wide .ti-body{flex-wrap:wrap}
|
||||
.ti-stat{flex:1 1 50%;border-bottom:1px solid #f0f0f0}
|
||||
.ti-stat:nth-child(2n){border-right:none}
|
||||
.ti-stat:nth-last-child(-n+2){border-bottom:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
|
||||
<h1>Torrent Indicator</h1>
|
||||
<p class="subtitle">Exemples d'intégration sur une page HTML standard (sans Ghost).</p>
|
||||
|
||||
<!-- ============================================================
|
||||
Cas 1 — Layout compact, via info hash
|
||||
============================================================ -->
|
||||
<section>
|
||||
<h2>1 · Layout compact — via info hash</h2>
|
||||
<p class="description">
|
||||
Usage minimal : un <code>div</code> avec <code>data-hash</code>.
|
||||
Idéal en sidebar ou en ligne dans un article.
|
||||
</p>
|
||||
<div class="widgets-row">
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"></div>
|
||||
|
||||
<div class="torrent-indicator"
|
||||
data-hash="9bb80f655e2a0490b1ed7b19b63a7b2acacffe0e"
|
||||
data-label="Debian 12 netinst"></div>
|
||||
</div>
|
||||
<pre><div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"></div></pre>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
Cas 2 — Layout large, via info hash
|
||||
============================================================ -->
|
||||
<section>
|
||||
<h2>2 · Layout large — centré, pleine largeur</h2>
|
||||
<p class="description">
|
||||
Ajouter <code>data-layout="wide"</code> pour un affichage horizontal
|
||||
qui occupe toute la largeur disponible. Adaptatif sur mobile (grille 2×2).
|
||||
</p>
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"
|
||||
data-layout="wide"></div>
|
||||
<pre><div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"
|
||||
data-layout="wide"></div></pre>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
Cas 3 — Via lien magnet
|
||||
============================================================ -->
|
||||
<section>
|
||||
<h2>3 · Via lien magnet</h2>
|
||||
<p class="description">
|
||||
<code>data-magnet</code> accepte un lien magnet complet.
|
||||
Le hash est extrait automatiquement (hex 40 chars ou base32).
|
||||
</p>
|
||||
<div class="torrent-indicator"
|
||||
data-magnet="magnet:?xt=urn:btih:3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0&dn=ubuntu-24.04-desktop-amd64.iso"
|
||||
data-label="Ubuntu via magnet"
|
||||
data-layout="wide"></div>
|
||||
<pre><div class="torrent-indicator"
|
||||
data-magnet="magnet:?xt=urn:btih:3b245504...&dn=ubuntu..."
|
||||
data-label="Ubuntu via magnet"
|
||||
data-layout="wide"></div></pre>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
Cas 4 — Plusieurs widgets sur la même page
|
||||
============================================================ -->
|
||||
<section>
|
||||
<h2>4 · Plusieurs widgets sur la même page</h2>
|
||||
<p class="description">
|
||||
Les deux layouts coexistent. Le script s'initialise une seule fois
|
||||
et cible tous les <code>.torrent-indicator</code> présents.
|
||||
</p>
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04"
|
||||
data-layout="wide"></div>
|
||||
<br>
|
||||
<div class="widgets-row">
|
||||
<div class="torrent-indicator"
|
||||
data-hash="9bb80f655e2a0490b1ed7b19b63a7b2acacffe0e"
|
||||
data-label="Debian 12"></div>
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
Cas 5 — Formulaire interactif
|
||||
============================================================ -->
|
||||
<section>
|
||||
<h2>5 · Essayer avec votre propre torrent</h2>
|
||||
<p class="description">
|
||||
Entrez un info hash (40 caractères hex) ou un lien magnet.
|
||||
</p>
|
||||
<div class="try-form">
|
||||
<label for="try-input">Hash ou lien magnet</label>
|
||||
<input id="try-input"
|
||||
type="text"
|
||||
placeholder="abc123... ou magnet:?xt=urn:btih:..."
|
||||
value="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0">
|
||||
<div class="try-controls">
|
||||
<button onclick="tryWidget()">Afficher</button>
|
||||
<div class="layout-toggle">
|
||||
<label><input type="radio" name="try-layout" value="" checked> Compact</label>
|
||||
<label><input type="radio" name="try-layout" value="wide"> Large</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="try-result"></div>
|
||||
<pre>// 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);</pre>
|
||||
</section>
|
||||
|
||||
</div><!-- /page -->
|
||||
|
||||
<!-- ============================================================
|
||||
Script du widget — à placer juste avant </body>
|
||||
Adapter API_URL selon votre déploiement.
|
||||
============================================================ -->
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ← Adapter selon votre déploiement
|
||||
var API_URL = 'https://toscr.team4kw.fr';
|
||||
|
||||
var L = {
|
||||
header:'État du torrent', seeders:'Seeders', leechers:'Leechers',
|
||||
health:'Santé', popularity:'Popularité', updated:'Mis à jour',
|
||||
health_dead:'Mort', health_poor:'Faible', health_good:'Bon', health_excellent:'Excellent',
|
||||
pop_low:'Faible', pop_moderate:'Modérée', pop_popular:'Populaire', pop_viral:'Virale',
|
||||
loading:'Chargement…', error:'Données indisponibles', no_data:'Aucun tracker n\'a répondu', stale:'données en cache',
|
||||
};
|
||||
|
||||
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
function fmt(n){ return Number(n).toLocaleString('fr-FR'); }
|
||||
function ts(){ var d=new Date(),p=function(n){return n<10?'0'+n:''+n;}; return p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds()); }
|
||||
function row(label,val){ return '<div class="ti-row"><span class="ti-label">'+esc(label)+'</span>'+val+'</div>'; }
|
||||
|
||||
function footer(data) {
|
||||
return data.stale
|
||||
? L.updated+' à '+ts()+' · <em>'+L.stale+'</em>'
|
||||
: L.updated+' à '+ts();
|
||||
}
|
||||
|
||||
function stat(label, valueHtml) {
|
||||
return '<div class="ti-stat"><span class="ti-stat-label">'+esc(label)+'</span>'+valueHtml+'</div>';
|
||||
}
|
||||
|
||||
function render(el, data) {
|
||||
var lbl = el.getAttribute('data-label') || L.header;
|
||||
var wide = el.getAttribute('data-layout') === 'wide';
|
||||
|
||||
if (data.error) {
|
||||
var cls = wide ? 'ti-widget ti-widget--wide' : 'ti-widget';
|
||||
el.innerHTML='<div class="'+cls+'"><div class="ti-header"><span class="ti-dot"></span>'+esc(lbl)+'</div><div class="ti-error">'+esc(data.error)+'</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var h=data.health||'dead', p=data.popularity||'low';
|
||||
|
||||
if (wide) {
|
||||
el.innerHTML='<div class="ti-widget ti-widget--wide">'
|
||||
+'<div class="ti-header"><span class="ti-dot ti-dot-'+h+'"></span>'+esc(lbl)+'</div>'
|
||||
+'<div class="ti-body">'
|
||||
+stat(L.seeders, '<span class="ti-stat-value" style="color:#2da44e">'+fmt(data.seeders)+'</span>')
|
||||
+stat(L.leechers, '<span class="ti-stat-value" style="color:#cf222e">'+fmt(data.leechers)+'</span>')
|
||||
+stat(L.health, '<span class="ti-badge ti-health-'+h+'">'+esc(L['health_'+h]||h)+'</span>')
|
||||
+stat(L.popularity,'<span class="ti-badge ti-pop-'+p+'">'+esc(L['pop_'+p]||p)+'</span>')
|
||||
+'</div>'
|
||||
+'<div class="ti-footer">'+footer(data)+'</div>'
|
||||
+'</div>';
|
||||
} else {
|
||||
el.innerHTML='<div class="ti-widget">'
|
||||
+'<div class="ti-header"><span class="ti-dot ti-dot-'+h+'"></span>'+esc(lbl)+'</div>'
|
||||
+'<div class="ti-body">'
|
||||
+row(L.seeders, '<span class="ti-value" style="color:#2da44e">'+fmt(data.seeders)+'</span>')
|
||||
+row(L.leechers, '<span class="ti-value" style="color:#cf222e">'+fmt(data.leechers)+'</span>')
|
||||
+row(L.health, '<span class="ti-badge ti-health-'+h+'">'+esc(L['health_'+h]||h)+'</span>')
|
||||
+row(L.popularity,'<span class="ti-badge ti-pop-'+p+'">'+esc(L['pop_'+p]||p)+'</span>')
|
||||
+'</div>'
|
||||
+'<div class="ti-footer">'+footer(data)+'</div>'
|
||||
+'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(el) {
|
||||
var hash=el.getAttribute('data-hash'), magnet=el.getAttribute('data-magnet');
|
||||
if (hash) return API_URL+'?hash='+encodeURIComponent(hash.trim());
|
||||
if (magnet) return API_URL+'?magnet='+encodeURIComponent(magnet.trim());
|
||||
return null;
|
||||
}
|
||||
|
||||
function load(el) {
|
||||
var url=buildUrl(el);
|
||||
if (!url) { render(el,{error:'Attribut data-hash ou data-magnet manquant.'}); return; }
|
||||
var lbl=el.getAttribute('data-label')||L.header;
|
||||
var wide=el.getAttribute('data-layout')==='wide';
|
||||
var cls=wide?'ti-widget ti-widget--wide':'ti-widget';
|
||||
el.innerHTML='<div class="'+cls+'"><div class="ti-header"><span class="ti-dot"></span>'+esc(lbl)+'</div><div class="ti-loading">'+L.loading+'</div></div>';
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.open('GET',url,true); xhr.timeout=15000;
|
||||
xhr.onload=function(){
|
||||
if(xhr.status>=200&&xhr.status<300){
|
||||
try{ var d=JSON.parse(xhr.responseText); if(d.sources===0&&!d.stale) d.error=L.no_data; render(el,d); }
|
||||
catch(e){ render(el,{error:L.error}); }
|
||||
} else { render(el,{error:L.error+' ('+xhr.status+')'}); }
|
||||
};
|
||||
xhr.onerror=xhr.ontimeout=function(){ render(el,{error:L.error}); };
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function init() {
|
||||
var els=document.querySelectorAll('.torrent-indicator');
|
||||
for(var i=0;i<els.length;i++) load(els[i]);
|
||||
}
|
||||
|
||||
document.readyState==='loading'
|
||||
? document.addEventListener('DOMContentLoaded',init)
|
||||
: init();
|
||||
|
||||
window.TorrentIndicator={ refreshAll:init, init:load };
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Formulaire interactif (cas 5)
|
||||
function tryWidget() {
|
||||
var input = document.getElementById('try-input').value.trim();
|
||||
var layout = document.querySelector('input[name="try-layout"]:checked').value;
|
||||
var result = document.getElementById('try-result');
|
||||
|
||||
if (!input) return;
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.className = 'torrent-indicator';
|
||||
el.setAttribute('data-label', 'Résultat');
|
||||
|
||||
var isMagnet = input.startsWith('magnet:');
|
||||
el.setAttribute(isMagnet ? 'data-magnet' : 'data-hash', input);
|
||||
if (layout) el.setAttribute('data-layout', layout);
|
||||
|
||||
result.innerHTML = '';
|
||||
result.appendChild(el);
|
||||
TorrentIndicator.init(el);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
120
docs/installation.md
Normal file
120
docs/installation.md
Normal file
@@ -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
|
||||
```
|
||||
100
docs/standalone.md
Normal file
100
docs/standalone.md
Normal file
@@ -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 `<style>` et le bloc `<script>` depuis `ghost-inject.html` dans votre page.
|
||||
Adapter `API_URL` :
|
||||
|
||||
```html
|
||||
<style id="ti-styles">
|
||||
/* ... contenu de ghost-inject.html ... */
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* ... contenu de ghost-inject.html ... */
|
||||
var API_URL = 'https://torrent-api.monsite.com';
|
||||
</script>
|
||||
```
|
||||
|
||||
Le script se place de préférence juste avant `</body>`.
|
||||
|
||||
### 2. Placer les widgets dans le HTML
|
||||
|
||||
```html
|
||||
<!-- Compact -->
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"></div>
|
||||
|
||||
<!-- Large -->
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"
|
||||
data-layout="wide"></div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cas d'usage
|
||||
|
||||
### Page de téléchargement
|
||||
|
||||
```html
|
||||
<h2>Télécharger</h2>
|
||||
<div class="torrent-indicator"
|
||||
data-magnet="magnet:?xt=urn:btih:3b245504..."
|
||||
data-label="Ubuntu 24.04 LTS"
|
||||
data-layout="wide"></div>
|
||||
<a href="magnet:?xt=urn:btih:3b245504...">Ouvrir le magnet</a>
|
||||
```
|
||||
|
||||
### Tableau comparatif de plusieurs torrents
|
||||
|
||||
```html
|
||||
<div style="display:flex; gap:16px; flex-wrap:wrap;">
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04"></div>
|
||||
|
||||
<div class="torrent-indicator"
|
||||
data-hash="9bb80f655e2a0490b1ed7b19b63a7b2acacffe0e"
|
||||
data-label="Debian 12"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Création dynamique via JavaScript
|
||||
|
||||
```html
|
||||
<button onclick="addWidget()">Vérifier le torrent</button>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
function addWidget() {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'torrent-indicator';
|
||||
el.setAttribute('data-hash', '3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0');
|
||||
el.setAttribute('data-layout', 'wide');
|
||||
document.getElementById('result').appendChild(el);
|
||||
TorrentIndicator.init(el);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 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.
|
||||
96
docs/widget.md
Normal file
96
docs/widget.md
Normal file
@@ -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
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"></div>
|
||||
```
|
||||
|
||||
### Layout large (horizontal, centré)
|
||||
|
||||
```html
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"
|
||||
data-layout="wide"></div>
|
||||
```
|
||||
|
||||
### Via lien magnet
|
||||
|
||||
```html
|
||||
<div class="torrent-indicator"
|
||||
data-magnet="magnet:?xt=urn:btih:3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0&dn=ubuntu"
|
||||
data-label="Ubuntu 24.04 LTS"
|
||||
data-layout="wide"></div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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'));
|
||||
```
|
||||
180
ghost-inject.html
Normal file
180
ghost-inject.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<!--
|
||||
============================================================
|
||||
TORRENT INDICATOR — Ghost Code Injection (Site Header)
|
||||
============================================================
|
||||
|
||||
Coller CE BLOC dans :
|
||||
Ghost Admin → Settings → Code injection → Site Header
|
||||
|
||||
Remplacer l'URL ci-dessous par celle de votre Cloudflare Worker.
|
||||
============================================================
|
||||
-->
|
||||
|
||||
<style id="ti-styles">
|
||||
.ti-widget{display:inline-flex;flex-direction:column;font-family:system-ui,sans-serif;font-size:13px;border:1px solid #d0d7de;border-radius:8px;overflow:hidden;min-width:180px;max-width:260px;background:#fff;box-shadow:0 1px 4px rgba(0,0,0,.08);color:#24292f}
|
||||
.ti-header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f6f8fa;border-bottom:1px solid #d0d7de;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:#57606a}
|
||||
.ti-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:#8c8c8c;transition:background .3s}
|
||||
.ti-body{padding:10px 12px;display:flex;flex-direction:column;gap:6px}
|
||||
.ti-row{display:flex;justify-content:space-between;align-items:center}
|
||||
.ti-label{color:#57606a}
|
||||
.ti-value{font-weight:600;font-variant-numeric:tabular-nums}
|
||||
.ti-badge{display:inline-block;padding:1px 7px;border-radius:12px;font-size:11px;font-weight:600;letter-spacing:.02em}
|
||||
.ti-health-dead{background:#ffeef0;color:#cf222e}
|
||||
.ti-health-poor{background:#fff3cd;color:#9a6700}
|
||||
.ti-health-good,.ti-health-excellent{background:#dafbe1;color:#116329}
|
||||
.ti-dot-dead{background:#cf222e}
|
||||
.ti-dot-poor{background:#d4a900}
|
||||
.ti-dot-good,.ti-dot-excellent{background:#2da44e}
|
||||
.ti-pop-low{background:#f0f0f0;color:#57606a}
|
||||
.ti-pop-moderate{background:#ddf4ff;color:#0550ae}
|
||||
.ti-pop-popular{background:#dbedff;color:#0550ae}
|
||||
.ti-pop-viral{background:#fff0f0;color:#a40000}
|
||||
.ti-footer{padding:5px 12px 7px;font-size:10px;color:#8c959f;border-top:1px solid #f0f0f0;text-align:right}
|
||||
.ti-loading,.ti-error{padding:14px 12px;text-align:center;color:#57606a;font-size:12px}
|
||||
.ti-error{color:#cf222e}
|
||||
/* ── Layout large (data-layout="wide") ── */
|
||||
.ti-widget--wide{max-width:680px;width:100%;margin:0 auto;min-width:0}
|
||||
.ti-widget--wide .ti-body{flex-direction:row;padding:0;gap:0}
|
||||
.ti-stat{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;flex:1;padding:14px 8px;border-right:1px solid #f0f0f0}
|
||||
.ti-stat:last-child{border-right:none}
|
||||
.ti-stat-label{font-size:10px;color:#8c959f;text-transform:uppercase;letter-spacing:.05em;font-weight:600}
|
||||
.ti-stat-value{font-size:22px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}
|
||||
@media(max-width:480px){
|
||||
.ti-widget--wide .ti-body{flex-wrap:wrap}
|
||||
.ti-stat{flex:1 1 50%;border-bottom:1px solid #f0f0f0}
|
||||
.ti-stat:nth-child(2n){border-right:none}
|
||||
.ti-stat:nth-last-child(-n+2){border-bottom:none}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ← Remplacer par l'URL de votre Cloudflare Worker/Service selfhost
|
||||
var API_URL = 'https://toscr.team4kw.fr';
|
||||
|
||||
var L = {
|
||||
header:'État du torrent', seeders:'Seeders', leechers:'Leechers',
|
||||
health:'Santé', popularity:'Popularité', updated:'Mis à jour',
|
||||
health_dead:'Mort', health_poor:'Faible', health_good:'Bon', health_excellent:'Excellent',
|
||||
pop_low:'Faible', pop_moderate:'Modérée', pop_popular:'Populaire', pop_viral:'Virale',
|
||||
loading:'Chargement…', error:'Données indisponibles', no_data:'Aucun tracker n\'a répondu', stale:'données en cache',
|
||||
};
|
||||
|
||||
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
function fmt(n){ return Number(n).toLocaleString('fr-FR'); }
|
||||
function ts(){ var d=new Date(),p=function(n){return n<10?'0'+n:''+n;}; return p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds()); }
|
||||
function row(label,val){ return '<div class="ti-row"><span class="ti-label">'+esc(label)+'</span>'+val+'</div>'; }
|
||||
|
||||
function footer(data) {
|
||||
return data.stale
|
||||
? L.updated+' à '+ts()+' · <em>'+L.stale+'</em>'
|
||||
: L.updated+' à '+ts();
|
||||
}
|
||||
|
||||
function stat(label, valueHtml) {
|
||||
return '<div class="ti-stat"><span class="ti-stat-label">'+esc(label)+'</span>'+valueHtml+'</div>';
|
||||
}
|
||||
|
||||
function render(el, data) {
|
||||
var lbl = el.getAttribute('data-label') || L.header;
|
||||
var wide = el.getAttribute('data-layout') === 'wide';
|
||||
|
||||
if (data.error) {
|
||||
var cls = wide ? 'ti-widget ti-widget--wide' : 'ti-widget';
|
||||
el.innerHTML='<div class="'+cls+'"><div class="ti-header"><span class="ti-dot"></span>'+esc(lbl)+'</div><div class="ti-error">'+esc(data.error)+'</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var h=data.health||'dead', p=data.popularity||'low';
|
||||
|
||||
if (wide) {
|
||||
el.innerHTML='<div class="ti-widget ti-widget--wide">'
|
||||
+'<div class="ti-header"><span class="ti-dot ti-dot-'+h+'"></span>'+esc(lbl)+'</div>'
|
||||
+'<div class="ti-body">'
|
||||
+stat(L.seeders, '<span class="ti-stat-value" style="color:#2da44e">'+fmt(data.seeders)+'</span>')
|
||||
+stat(L.leechers, '<span class="ti-stat-value" style="color:#cf222e">'+fmt(data.leechers)+'</span>')
|
||||
+stat(L.health, '<span class="ti-badge ti-health-'+h+'">'+esc(L['health_'+h]||h)+'</span>')
|
||||
+stat(L.popularity,'<span class="ti-badge ti-pop-'+p+'">'+esc(L['pop_'+p]||p)+'</span>')
|
||||
+'</div>'
|
||||
+'<div class="ti-footer">'+footer(data)+'</div>'
|
||||
+'</div>';
|
||||
} else {
|
||||
el.innerHTML='<div class="ti-widget">'
|
||||
+'<div class="ti-header"><span class="ti-dot ti-dot-'+h+'"></span>'+esc(lbl)+'</div>'
|
||||
+'<div class="ti-body">'
|
||||
+row(L.seeders, '<span class="ti-value" style="color:#2da44e">'+fmt(data.seeders)+'</span>')
|
||||
+row(L.leechers, '<span class="ti-value" style="color:#cf222e">'+fmt(data.leechers)+'</span>')
|
||||
+row(L.health, '<span class="ti-badge ti-health-'+h+'">'+esc(L['health_'+h]||h)+'</span>')
|
||||
+row(L.popularity,'<span class="ti-badge ti-pop-'+p+'">'+esc(L['pop_'+p]||p)+'</span>')
|
||||
+'</div>'
|
||||
+'<div class="ti-footer">'+footer(data)+'</div>'
|
||||
+'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(el) {
|
||||
var hash=el.getAttribute('data-hash'), magnet=el.getAttribute('data-magnet');
|
||||
if (hash) return API_URL+'?hash='+encodeURIComponent(hash.trim());
|
||||
if (magnet) return API_URL+'?magnet='+encodeURIComponent(magnet.trim());
|
||||
return null;
|
||||
}
|
||||
|
||||
function load(el) {
|
||||
var url=buildUrl(el);
|
||||
if (!url) { render(el,{error:'Attribut data-hash ou data-magnet manquant.'}); return; }
|
||||
var lbl=el.getAttribute('data-label')||L.header;
|
||||
el.innerHTML='<div class="ti-widget"><div class="ti-header"><span class="ti-dot"></span>'+esc(lbl)+'</div><div class="ti-loading">'+L.loading+'</div></div>';
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.open('GET',url,true); xhr.timeout=15000;
|
||||
xhr.onload=function(){
|
||||
if(xhr.status>=200&&xhr.status<300){
|
||||
try{ var d=JSON.parse(xhr.responseText); if(d.sources===0&&!d.stale) d.error=L.no_data; render(el,d); }
|
||||
catch(e){ render(el,{error:L.error}); }
|
||||
} else { render(el,{error:L.error+' ('+xhr.status+')'}); }
|
||||
};
|
||||
xhr.onerror=xhr.ontimeout=function(){ render(el,{error:L.error}); };
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function init() {
|
||||
var els=document.querySelectorAll('.torrent-indicator');
|
||||
for(var i=0;i<els.length;i++) load(els[i]);
|
||||
}
|
||||
|
||||
document.readyState==='loading'
|
||||
? document.addEventListener('DOMContentLoaded',init)
|
||||
: init();
|
||||
|
||||
window.TorrentIndicator={ refreshAll:init, init:load };
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
============================================================
|
||||
UTILISATION DANS LES ARTICLES GHOST
|
||||
============================================================
|
||||
|
||||
Dans l'éditeur Ghost, ajouter un bloc "HTML" et coller :
|
||||
|
||||
Layout compact (vertical, défaut) :
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"></div>
|
||||
|
||||
Layout large (horizontal, centré) :
|
||||
<div class="torrent-indicator"
|
||||
data-hash="3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Ubuntu 24.04 LTS"
|
||||
data-layout="wide"></div>
|
||||
|
||||
Via lien magnet (fonctionne dans les deux layouts) :
|
||||
<div class="torrent-indicator"
|
||||
data-magnet="magnet:?xt=urn:btih:3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0"
|
||||
data-label="Mon Torrent"
|
||||
data-layout="wide"></div>
|
||||
|
||||
Plusieurs widgets, layouts différents, sur la même page sont supportés.
|
||||
============================================================
|
||||
-->
|
||||
300
scrape_server.py
Normal file
300
scrape_server.py
Normal file
@@ -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=<magnet_uri>
|
||||
|
||||
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 : i<n>e
|
||||
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 : l<items>e
|
||||
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<key><value>...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 : <longueur>:<données>
|
||||
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é.")
|
||||
24
torrent-scrape.service
Normal file
24
torrent-scrape.service
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user