Initial commit

This commit is contained in:
2025-11-11 01:48:45 +01:00
commit 06d0e1c426
14 changed files with 568 additions and 0 deletions

20
.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
tests/
.env
instance/
# Fichiers Python compilés
__pycache__/
*.py[cod]
*$py.class
# Environnements virtuels
venv/
env/
.venv/
.env/
# Fichiers de logs
logs/
# Caches de pytest
.cache/

50
backend/__init__.py Normal file
View File

@@ -0,0 +1,50 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
import os
db = SQLAlchemy()
login_manager = LoginManager()
def create_app():
# Frontend path
base_dir = os.path.abspath(os.path.dirname(__file__))
template_dir = os.path.join(base_dir, '..', 'frontend', 'templates')
static_dir = os.path.join(base_dir, '..', 'frontend', 'static')
# DB path
instance_path = os.path.join(base_dir, '..', 'instance')
os.makedirs(instance_path, exist_ok=True)
app = Flask(
__name__,
template_folder=template_dir,
static_folder=static_dir,
instance_path=os.path.abspath(instance_path)
)
app.config['SECRET_KEY'] = 'IddAhxI%$G%2X3joI04i'
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{os.path.join(app.instance_path, 'users.db')}"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Init extensions
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = "login"
# Import models for user_loader
from backend.models import User
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# Import routes and give app
from backend import routes
routes.init_app(app)
# Create DB if needed
with app.app_context():
db.create_all()
return app

15
backend/models.py Normal file
View File

@@ -0,0 +1,15 @@
from . import db
from flask_login import UserMixin
from datetime import datetime
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
class LoginIP(db.Model):
id = db.Column(db.Integer, primary_key=True)
ip = db.Column(db.String(45), unique=True, nullable=False)
count = db.Column(db.Integer, default=0, nullable=False)
blocked_until = db.Column(db.DateTime, nullable=True)
last_attempt = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

246
backend/routes.py Normal file
View File

@@ -0,0 +1,246 @@
# Standard library
from datetime import datetime, timedelta
from io import BytesIO
import time
# Third-party libraries
import requests
from flask import render_template, request, redirect, url_for, session, flash, jsonify
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import check_password_hash
# Project imports
from . import db
from backend.models import User, LoginIP
from backend.utils import format_size, calculate_age
MAX_ATTEMPTS = 5
BLOCK_TIME = timedelta(minutes=15)
MAX_SIZE_BYTES = 5*1024**3
YGG_PASSKEY = "xj1MgNuyzFKCjOtnawGBC2egDOciUg04"
ALLDEBRID_KEY = "mtrQI4h583rHe2ZpvpbC"
def init_app(app):
@app.route('/')
def home():
if 'user' in session:
return redirect(url_for('dashboard'))
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
ip = request.remote_addr or "unknown"
ip_record = LoginIP.query.filter_by(ip=ip).first()
if not ip_record:
ip_record = LoginIP(ip=ip)
db.session.add(ip_record)
db.session.commit()
if ip_record.blocked_until and datetime.utcnow() < ip_record.blocked_until:
remaining = int((ip_record.blocked_until - datetime.utcnow()).total_seconds() // 60) + 1
flash(f"Trop de tentatives depuis votre IP. Réessayez dans {remaining} min.")
return render_template("login.html")
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password, password):
ip_record.count = 0
ip_record.blocked_until = None
db.session.commit()
login_user(user)
session['user'] = user.username
return redirect(url_for("dashboard"))
else:
ip_record.count += 1
ip_record.last_attempt = datetime.utcnow()
if ip_record.count >= MAX_ATTEMPTS:
ip_record.blocked_until = datetime.utcnow() + BLOCK_TIME
msg = f"Trop de tentatives. Blocage pour {BLOCK_TIME.seconds // 60} minutes."
else:
msg = f"Identifiants invalides ({ip_record.count}/{MAX_ATTEMPTS})"
db.session.commit()
flash(msg)
return render_template("login.html")
@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html', user=current_user.username)
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))
@app.route('/search')
@login_required
def search():
query = request.args.get('query')
category_id = request.args.get('category_id')
# Préparer l'URL
url = "https://yggapi.eu/torrents"
params = {
"page": 1,
"q": query,
"category_id" : category_id,
"order_by": "uploaded_at",
"per_page": 100
}
# Appeler l'API
try:
response = requests.get(url, params=params, timeout=5)
response.raise_for_status() # déclenche une exception si erreur HTTP
results = response.json()
except Exception as e:
print("Erreur API Yggtorrent:", e)
results = []
filtered_results = []
for torrent in results:
if torrent['size'] <= MAX_SIZE_BYTES:
torrent['size'] = format_size(torrent['size'])
days, human = calculate_age(torrent['uploaded_at'])
torrent['age_days'] = days # to show
torrent['age_human'] = human # to sort
filtered_results.append(torrent)
return render_template(
'search_results.html',
query=query,
results=filtered_results,
user=current_user.username
)
@app.route("/torrent/<torrent_id>")
@login_required
def torrent_details(torrent_id):
# --- 1er bloc : récupération du torrent ---
try:
url = f"https://yggapi.eu/torrent/{torrent_id}/download"
params = {
"passkey": YGG_PASSKEY,
"tracker_domain": "tracker.p2p-world.net"
}
response = requests.get(url, params=params, timeout=10, allow_redirects=True)
response.raise_for_status()
torrent_file_content = response.content
torrent_filename = f"{torrent_id}.torrent"
# Request error
except requests.exceptions.RequestException as e:
print("Erreur lors de la récupération du torrent depuis Yggtorrent:", e)
return "Erreur récupération torrent", 500
# --- 2e bloc : upload à Alldebrid ---
try:
files = {'files[]': (torrent_filename, BytesIO(torrent_file_content))}
headers = {"Authorization": f"Bearer {ALLDEBRID_KEY}"}
upload_request = requests.post(
"https://api.alldebrid.com/v4/magnet/upload/file",
headers=headers,
files=files,
timeout=20
)
upload_request.raise_for_status()
first_answer = upload_request.json()
debrid_id = first_answer["data"]["files"][0]["id"]
# API error
if first_answer["status"] != "success":
print("API error 1 : upload Alldebrid:", first_answer)
return "API error 1 : upload Alldebrid", 500
# Request error
except requests.exceptions.RequestException as e:
print("Request error 1 : upload Alldebrid:", e)
return "Request error 1 : upload Alldebrid:", 500
print("Alldebrid id:",debrid_id)
time.sleep(2)
# --- 3e bloc : second appel Alldebrid ---
try:
headers = {"Authorization": f"Bearer {ALLDEBRID_KEY}"}
alldebrid_link_request = requests.get(
"https://api.alldebrid.com/v4/magnet/files",
headers=headers,
params={"id[]": debrid_id},
timeout=10
)
alldebrid_link_request.raise_for_status()
second_answer = alldebrid_link_request.json()
# API error
if second_answer.get("status") != "success":
print("API error 2 : get Alldebrid link:", second_answer)
return "API error 2 : get Alldebrid link", 500
alldebrid_links = []
for magnet in second_answer.get("data", {}).get("magnets", []):
for file in magnet.get("files", []):
# Cas où il y a une liste "e"
if "e" in file:
for entry in file["e"]:
if "l" in entry:
alldebrid_links.append(entry["l"])
# Cas où le lien est directement dans "l"
elif "l" in file:
alldebrid_links.append(file["l"])
# Request error
except requests.exceptions.RequestException as e:
print("Request error 2 : get Alldebrid link:", e)
return "Request error 2 : get Alldebrid link", 500
print("Alldebrid links:",alldebrid_links)
# --- 4e bloc : troisieme appel Alldebrid ---
direct_links = []
try:
headers = {"Authorization": f"Bearer {ALLDEBRID_KEY}"}
for link in alldebrid_links:
direct_link_request = requests.get(
"https://api.alldebrid.com/v4/link/unlock",
headers=headers,
params={"link": link},
timeout=10
)
direct_link_request.raise_for_status()
third_anwser = direct_link_request.json()
# API error
if third_anwser.get("status") != "success":
print("API error 3 : get direct link:", third_anwser)
return "API error 3 : get direct link", 500
# Récupérer les liens directs
direct_links.append({
"name": third_anwser["data"]["filename"],
"size": third_anwser["data"]["filesize"],
"link": third_anwser["data"]["link"]
})
# Request error
except requests.exceptions.RequestException as e:
print("API error 3 : get direct link:", e)
return "API error 3 : get direct link", 500
print("Direct links:",direct_links)
# 3. Retour du lien direct au client
return jsonify(direct_links)

26
backend/utils.py Normal file
View File

@@ -0,0 +1,26 @@
from datetime import datetime, timezone
def format_size(bytes_size):
"""Convertit une taille en octets en format lisible (Ko, Mo, Go, ...)."""
for unit in ['o', 'Ko', 'Mo', 'Go', 'To']:
if bytes_size < 1024:
return f"{bytes_size:.2f} {unit}"
bytes_size /= 1024
return f"{bytes_size:.2f} Po"
def calculate_age(uploaded_at_iso):
uploaded_date = datetime.fromisoformat(uploaded_at_iso.rstrip('Z')).replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
delta_days = (now.date() - uploaded_date.date()).days
# human
if delta_days < 30:
age_human = f"{delta_days} j"
elif delta_days < 365:
months = delta_days // 30
age_human = f"{months} mois"
else:
years = delta_days // 365
age_human = f"{years} ans"
return delta_days, age_human

16
create_user.py Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env python3
from backend import create_app, db
from backend.models import User
from werkzeug.security import generate_password_hash
app = create_app()
with app.app_context():
username = input("Nom d'utilisateur : ")
password = input("Mot de passe : ")
if User.query.filter_by(username=username).first():
print(f"Utilisateur '{username}' existe déjà !")
else:
user = User(username=username, password=generate_password_hash(password))
db.session.add(user)
db.session.commit()
print(f"Utilisateur '{username}' créé avec succès !")

View File

@@ -0,0 +1,18 @@
body {
font-family: Arial, sans-serif;
margin: 20px;
}
header {
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 5px 10px;
}
th {
background-color: #f0f0f0;
}

View File

@@ -0,0 +1,14 @@
// Exemple pour polling du download si nécessaire
function checkDownloadStatus(downloadId) {
fetch(`/download_status/${downloadId}`)
.then(response => response.json())
.then(data => {
if(data.status === 'ready') {
// Mettre à jour le lien sur la page
document.querySelector(`#link-${downloadId}`).innerHTML =
`<a href="${data.link}">Télécharger</a>`;
} else {
setTimeout(() => checkDownloadStatus(downloadId), 5000);
}
});
}

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{% block title %}Yggtorrent App{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header>
<h1>Les films de Lulu</h1>
{% if user %}
<p>Connecté en tant que {{ user }}</p>
<form action="{{ url_for('logout') }}" method="GET" style="display:inline">
<button type="submit">Déconnexion</button>
</form>
{% endif %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% block title %}Tableau de bord{% endblock %}
{% block content %}
<form id="search-form" action="/search" method="GET">
<label>Catégorie</label>
<select name="category_id">
<option value="2178">Films danimation</option>
<option value="2179">Séries danimation / Mangas</option>
<option value="2181">Documentaires</option>
<option value="2182">Émissions TV</option>
<option value="2183">Films</option>
<option value="2184">Séries</option>
</select>
<label>Recherche</label>
<input type="text" name="query" required>
<button type="submit">Rechercher</button>
</form>
<div id="results">
<!-- Les résultats de recherche seront injectés ici -->
</div>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block title %}Connexion{% endblock %}
{% block content %}
<form action="/login" method="POST">
<label>Nom d'utilisateur</label>
<input type="text" name="username" required>
<label>Mot de passe</label>
<input type="password" name="password" required>
<button type="submit">Se connecter</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% block content %}
<h2>Résultats pour "{{ query }}"</h2>
<table id="results-table">
<thead>
<tr>
<th onclick="sortTable(0)">Nom &#x21C5;</th>
<th onclick="sortTable(1)">Taille &#x21C5;</th>
<th onclick="sortTable(2)">Âge &#x21C5;</th>
<th>Lien</th>
</tr>
</thead>
<tbody>
{% for item in results %}
<tr>
<td>{{ item.title }}</td>
<td>{{ item.size }}</td>
<td data-age="{{ item.age_days }}">{{ item.age_human }}</td>
<td>
<button onclick="getTorrentLink('{{ item.id }}', this)">Obtenir le lien</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
function parseSize(sizeStr) {
// Convertit "6.2 Go", "700 Mo" en octets
const units = { "o": 1, "Ko": 1024, "Mo": 1024**2, "Go": 1024**3, "To": 1024**4 };
const match = sizeStr.match(/([\d\.]+)\s*(o|Ko|Mo|Go|To)/);
if (!match) return 0;
return parseFloat(match[1]) * (units[match[2]] || 1);
}
function sortTable(colIndex) {
const table = document.getElementById("results-table");
const tbody = table.tBodies[0];
const rows = Array.from(tbody.rows);
const asc = table.asc = !table.asc;
rows.sort((a, b) => {
let aText = a.cells[colIndex].innerText.trim();
let bText = b.cells[colIndex].innerText.trim();
if (colIndex === 1) { // Taille
return asc ? parseSize(aText) - parseSize(bText) : parseSize(bText) - parseSize(aText);
} else if (colIndex === 2) { // Âge
const aVal = parseInt(a.cells[colIndex].dataset.age || 0);
const bVal = parseInt(b.cells[colIndex].dataset.age || 0);
return asc ? aVal - bVal : bVal - aVal;
} else { // Nom
return asc ? aText.localeCompare(bText) : bText.localeCompare(aText);
}
});
rows.forEach(row => tbody.appendChild(row));
}
async function getTorrentLink(torrentId, btn) {
btn.disabled = true;
btn.innerText = "Chargement...";
try {
const response = await fetch(`/torrent/${torrentId}`);
const links = await response.json(); // <-- lire comme JSON
if (links && links.length > 0) {
// Construire une liste HTML
const listHTML = links.map(link =>
`<li><a href="${link.link}" target="_blank">${link.name} (${(link.size / 1024 / 1024).toFixed(2)} Mo)</a></li>`
).join("");
btn.innerHTML = `<ul>${listHTML}</ul>`;
} else {
btn.innerText = "Aucun fichier trouvé";
}
} catch (err) {
btn.innerText = "Erreur";
console.error(err);
}
}
</script>
{% endblock %}

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
Flask
Flask-Login
Flask-SQLAlchemy
requests
feedparser

7
run.py Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env python3
from backend import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)