diff --git a/main.py b/main.py index 012edf5..0abd44e 100644 --- a/main.py +++ b/main.py @@ -199,4 +199,33 @@ async def remove_rating(request: Request, trakt_id: int): return {"success": True} +@app.delete("/api/rates/all10") +async def remove_all_10(request: Request): + token = request.session.get("access_token") + if not token: + raise HTTPException(401, "Not authenticated") + + trakt = TraktClient(TRAKT_CLIENT_ID, token) + _, ratings = await trakt.get_watched_and_ratings() + ids = [{"ids": r["movie"]["ids"]} for r in ratings if r["rating"] == 10] + + if not ids: + return {"deleted": 0} + + import asyncio, httpx + async with httpx.AsyncClient(timeout=60) as client: + tasks = [ + client.post( + f"{TraktClient.BASE_URL}/sync/ratings/remove", + headers=trakt.headers, + json={"movies": ids[i:i + 100]}, + ) + for i in range(0, len(ids), 100) + ] + responses = await asyncio.gather(*tasks) + + cache_del(f"movies_{token[:16]}") + return {"deleted": len(ids)} + + app.mount("/static", StaticFiles(directory="static"), name="static") diff --git a/remove_10s.py b/remove_10s.py new file mode 100644 index 0000000..bd6823e --- /dev/null +++ b/remove_10s.py @@ -0,0 +1,102 @@ +""" +One-shot script : supprime toutes les notes 10/10 de ton compte Trakt. +Usage : python remove_10s.py +""" +import httpx +import os +import sys +from dotenv import load_dotenv + +load_dotenv() + +CLIENT_ID = os.environ["TRAKT_CLIENT_ID"] +CLIENT_SECRET = os.environ["TRAKT_CLIENT_SECRET"] +REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" # mode "device" — pas besoin de serveur +BASE = "https://api.trakt.tv" + +HEADERS = { + "Content-Type": "application/json", + "trakt-api-version": "2", + "trakt-api-key": CLIENT_ID, +} + + +def get_token() -> str: + # 1. Demander un code device + r = httpx.post(f"{BASE}/oauth/device/code", json={"client_id": CLIENT_ID}) + r.raise_for_status() + data = r.json() + + print(f"\nOuvre cette URL dans ton navigateur :\n {data['verification_url']}") + print(f"\nSaisis ce code : {data['user_code']}") + print("\nAttente de l'autorisation…") + + # 2. Polling automatique jusqu'à autorisation ou expiration + import time + interval = data.get("interval", 5) + expires_in = data.get("expires_in", 600) + deadline = time.time() + expires_in + + while time.time() < deadline: + time.sleep(interval) + r = httpx.post(f"{BASE}/oauth/device/token", json={ + "code": data["device_code"], + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + }) + if r.status_code == 200: + print("Autorisé !") + return r.json()["access_token"] + if r.status_code == 400: + continue # pending + if r.status_code == 409: + continue # slow down + break + + print("Autorisation échouée ou expirée.") + sys.exit(1) + + +def main(): + token = get_token() + auth_headers = {**HEADERS, "Authorization": f"Bearer {token}"} + + # 3. Récupérer toutes les notes + print("Récupération des notes…") + r = httpx.get(f"{BASE}/sync/ratings/movies", headers=auth_headers, timeout=60) + r.raise_for_status() + all_ratings = r.json() + + movies_10 = [ + {"ids": m["movie"]["ids"]} + for m in all_ratings + if m["rating"] == 10 + ] + + if not movies_10: + print("Aucun film noté 10/10.") + return + + print(f"{len(movies_10)} films notés 10/10 trouvés. Suppression en cours…") + + # 4. Supprimer par batch de 100 + BATCH = 100 + total = 0 + for i in range(0, len(movies_10), BATCH): + batch = movies_10[i:i + BATCH] + r = httpx.post( + f"{BASE}/sync/ratings/remove", + headers=auth_headers, + json={"movies": batch}, + timeout=30, + ) + r.raise_for_status() + deleted = r.json().get("deleted", {}).get("movies", 0) + total += deleted + print(f" batch {i // BATCH + 1} — {deleted} supprimés") + + print(f"\nTerminé. {total} notes 10/10 supprimées.") + + +if __name__ == "__main__": + main() diff --git a/static/app.js b/static/app.js index 13375b7..e3fc717 100644 --- a/static/app.js +++ b/static/app.js @@ -63,6 +63,7 @@ async function init() { document.getElementById('filter-select').addEventListener('change', e => { currentFilter = e.target.value; currentPage = 1; + document.getElementById('remove-all-10-btn').classList.toggle('hidden', currentFilter !== '10'); loadMovies(); }); document.getElementById('sort-select').addEventListener('change', e => { @@ -219,6 +220,28 @@ function buildRow(movie) { return row; } +/* ── Remove all 10/10 ──────────────────────────────────── */ +async function removeAll10() { + const btn = document.getElementById('remove-all-10-btn'); + const total = document.getElementById('count-badge').textContent; + if (!confirm(`Supprimer les notes de ${total} films notés 10/10 ?`)) return; + + btn.disabled = true; + btn.textContent = 'Suppression…'; + try { + const r = await fetch('/api/rates/all10', { method: 'DELETE' }); + if (!r.ok) throw new Error(); + const { deleted } = await r.json(); + showToast(`${deleted} notes 10/10 supprimées`); + loadMovies(); + } catch { + showToast('Erreur lors de la suppression', true); + } finally { + btn.disabled = false; + btn.textContent = 'Supprimer tous les 10/10'; + } +} + /* ── Remove rating ──────────────────────────────────────── */ async function removeRating(movie, row) { row.style.pointerEvents = 'none'; diff --git a/static/index.html b/static/index.html index 284fc67..815f9d4 100644 --- a/static/index.html +++ b/static/index.html @@ -50,6 +50,7 @@ + Déconnexion diff --git a/static/style.css b/static/style.css index ba22e1c..4511785 100644 --- a/static/style.css +++ b/static/style.css @@ -163,6 +163,20 @@ select:hover, select:focus { border-color: var(--accent); } } .btn-ghost:hover { color: #f43f5e; border-color: rgba(244,63,94,.4); } +.btn-danger { + color: #f87191; + font-size: .78rem; + font-family: inherit; + padding: .3rem .65rem; + border: 1px solid rgba(244,63,94,.35); + border-radius: 7px; + background: rgba(244,63,94,.08); + cursor: pointer; + transition: background .15s, border-color .15s; +} +.btn-danger:hover { background: rgba(244,63,94,.15); border-color: rgba(244,63,94,.6); } +.btn-danger:disabled { opacity: .5; cursor: not-allowed; } + /* ── Main ─────────────────────────────────────────────── */ main { max-width: 1420px;