From d5b00998db70598c45ddc7b5dd053f100386ab3f Mon Sep 17 00:00:00 2001 From: chwps Date: Thu, 26 Mar 2026 18:38:03 +0100 Subject: [PATCH 01/23] Nettoyage --- RemoveFromWebhook.py => archive/RemoveFromWebhook_deprecated.py | 0 web_onboard.py => archive/web_onboard_deprecated.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename RemoveFromWebhook.py => archive/RemoveFromWebhook_deprecated.py (100%) rename web_onboard.py => archive/web_onboard_deprecated.py (100%) diff --git a/RemoveFromWebhook.py b/archive/RemoveFromWebhook_deprecated.py similarity index 100% rename from RemoveFromWebhook.py rename to archive/RemoveFromWebhook_deprecated.py diff --git a/web_onboard.py b/archive/web_onboard_deprecated.py similarity index 100% rename from web_onboard.py rename to archive/web_onboard_deprecated.py From 2c501f4c3149669a8c3bcc6ae67eee3cdeb3e78b Mon Sep 17 00:00:00 2001 From: chwps Date: Thu, 26 Mar 2026 19:31:53 +0100 Subject: [PATCH 02/23] =?UTF-8?q?Mise=20=C3=A0=20jour=20CI/CD=20:=20ajout?= =?UTF-8?q?=20du=20support=20de=20la=20branche=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85c5f0e..06a4729 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,9 @@ name: Build & Push on: push: - branches: [main] + branches: + - main + - dev jobs: docker: @@ -18,9 +20,19 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Définir le tag selon la branche + id: vars + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + echo "TAG=latest" >> $GITHUB_ENV + else + echo "TAG=dev" >> $GITHUB_ENV + fi + - name: Build & push uses: docker/build-push-action@v5 with: context: . push: true - tags: ghcr.io/${{ github.repository }}:latest + tags: ghcr.io/${{ github.repository }}:${{ env.TAG }} \ No newline at end of file From 37b2308289a55e64a543f6f0d06998c62e971922 Mon Sep 17 00:00:00 2001 From: chwps Date: Thu, 26 Mar 2026 19:55:16 +0100 Subject: [PATCH 03/23] =?UTF-8?q?Ajout=20du=20Webhook=20pour=20g=C3=A9rer?= =?UTF-8?q?=20les=20notes=200,5/5=20via=20collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 1 + 2 files changed, 49 insertions(+) diff --git a/app.py b/app.py index 8b4a1ea..69e6a28 100644 --- a/app.py +++ b/app.py @@ -40,6 +40,7 @@ ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") # si défini, on considérera ce compte comme admin PLEX_URL = os.getenv("PLEX_URL", "http://localhost:32400") COLLECTIONS = [c.strip() for c in os.getenv("COLLECTIONS", "").split(",") if c.strip()] +WEBHOOK_COLLECTION = os.getenv("WEBHOOK_COLLECTION", "Demande de suppression") # ------------------------------------------------------------------ # UTILS stockage / client id @@ -309,6 +310,53 @@ def run_sync_endpoint(): logging.exception("Erreur lors du run_sync") return f"error: {e}", 500 + +@app.route('/webhook', methods=['POST']) +def webhook(): + """Écoute les webhooks de Plex pour ajouter un média noté 0.5 à une collection""" + + payload_str = request.form.get('payload') + + if not payload_str: + data = request.get_json(silent=True) or {} + else: + try: + data = json.loads(payload_str) + except Exception as e: + logging.error("Erreur de lecture du Webhook: %s", e) + return "JSON invalide", 400 + + if data.get('event') == 'media.rate': + rating = data.get('Rating', {}).get('value') + username = data.get('Account', {}).get('title', 'Inconnu') + + # 1 = 0.5 étoile sur l'interface Plex + if rating == 1: + rating_key = data.get('Metadata', {}).get('ratingKey') + title = data.get('Metadata', {}).get('title', 'Titre inconnu') + + logging.info("L'utilisateur %s a mis 0,5 étoile à '%s'. Ajout à la collection...", username, title) + + token = get_admin_token() + if not token: + logging.error("Impossible de modifier : aucun token admin en cache.") + return "Erreur token admin", 500 + + try: + server = PlexServer(PLEX_URL, token=token) + item = server.fetchItem(int(rating_key)) + + item.addCollection(WEBHOOK_COLLECTION) + + logging.info("Succès : '%s' a été ajouté à la collection '%s' !", title, WEBHOOK_COLLECTION) + return "Média ajouté à la collection", 200 + + except Exception as e: + logging.error("Erreur lors de l'ajout de '%s' à la collection : %s", title, e) + return f"Erreur d'ajout : {e}", 500 + + return "Événement ignoré", 200 + # ------------------------------------------------------------------ # DÉMARRAGE # ------------------------------------------------------------------ diff --git a/docker-compose.yml b/docker-compose.yml index 943d0fc..bbc0e09 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: PLEX_URL: "http://localhost:32400" ADMIN_USERNAME: "adminUser" COLLECTIONS: "Collection1,Collection2,Collection3" #No collection limit + WEBHOOK_COLLECTION: "Asking For Deletion" CRON_SCHEDULE: "0 */1 * * *" # every hour RUN_SYNC_AT_STARTUP: "true" #decide if it syncs directly or wait for cron, "true" or "false" volumes: From c0590935b47d215bc1076507c85b7cf3b9c5c0b3 Mon Sep 17 00:00:00 2001 From: Chwps <106549456+chwps@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:56:29 +0100 Subject: [PATCH 04/23] Add WEBHOOK_COLLECTION to environment variables Add WEBHOOK_COLLECTION environment variable for deletion requests --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 06dd38f..cbcb28b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ services: PLEX_URL: "http://localhost:32400" ADMIN_USERNAME: "adminUser" COLLECTIONS: "Collection1,Collection2,Collection3" #No collection limit + WEBHOOK_COLLECTION: "Asking For Deletion" #Media that users rated 0.5 start out of 5 will be added to this collection CRON_SCHEDULE: "0 */1 * * *" # every hour RUN_SYNC_AT_STARTUP: "true" #decide if it syncs directly or wait for cron, "true" or "false" volumes: From 0408ecfa4c27bb446161b013336bda06f4618d01 Mon Sep 17 00:00:00 2001 From: chwps Date: Thu, 26 Mar 2026 20:05:26 +0100 Subject: [PATCH 05/23] =?UTF-8?q?Ajout=20du=20Webhook=20pour=20g=C3=A9rer?= =?UTF-8?q?=20les=20notes=200,5/5=20via=20collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 69e6a28..4c52d25 100644 --- a/app.py +++ b/app.py @@ -314,7 +314,7 @@ def run_sync_endpoint(): @app.route('/webhook', methods=['POST']) def webhook(): """Écoute les webhooks de Plex pour ajouter un média noté 0.5 à une collection""" - + logging.info("--- Webhook reçu ! ---") payload_str = request.form.get('payload') if not payload_str: From 7b658f5193849f0545193f8cdc139bb38a6c7cfc Mon Sep 17 00:00:00 2001 From: chwps Date: Thu, 26 Mar 2026 20:06:28 +0100 Subject: [PATCH 06/23] =?UTF-8?q?Ajout=20du=20Webhook=20pour=20g=C3=A9rer?= =?UTF-8?q?=20les=20notes=200,5/5=20via=20collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index cbcb28b..3ae79e8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ Checks for new media added to collections automatically. ### Exemple of Docker Compose yml : ```yaml -version: "3.9" services: plex-watchlist-cleaner: image: ghcr.io/chwps/plex-watchlist-cleaner:latest From 61527686a13b2edec82f1ad03dd495a3f8f2f119 Mon Sep 17 00:00:00 2001 From: chwps Date: Thu, 26 Mar 2026 20:21:17 +0100 Subject: [PATCH 07/23] debug --- README.md | 2 +- app.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3ae79e8..1f53f56 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ services: PLEX_URL: "http://localhost:32400" ADMIN_USERNAME: "adminUser" COLLECTIONS: "Collection1,Collection2,Collection3" #No collection limit - WEBHOOK_COLLECTION: "Asking For Deletion" #Media that users rated 0.5 start out of 5 will be added to this collection + WEBHOOK_COLLECTION: "Asking For Deletion" #Media that users rated 0.5 stars out of 5 will be added to this collection CRON_SCHEDULE: "0 */1 * * *" # every hour RUN_SYNC_AT_STARTUP: "true" #decide if it syncs directly or wait for cron, "true" or "false" volumes: diff --git a/app.py b/app.py index 4c52d25..24d45aa 100644 --- a/app.py +++ b/app.py @@ -41,6 +41,7 @@ PLEX_URL = os.getenv("PLEX_URL", "http://localhost:32400") COLLECTIONS = [c.strip() for c in os.getenv("COLLECTIONS", "").split(",") if c.strip()] WEBHOOK_COLLECTION = os.getenv("WEBHOOK_COLLECTION", "Demande de suppression") +DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL") #pour notifications Discord # ------------------------------------------------------------------ # UTILS stockage / client id @@ -101,6 +102,13 @@ def get_admin_token(): logging.warning("Aucun token admin disponible en cache ni dans user_tokens.json.") return None +def send_to_discord(message): + if DISCORD_WEBHOOK_URL: + try: + requests.post(DISCORD_WEBHOOK_URL, json={"content": message}, timeout=5) + except Exception as e: + logging.error("Erreur Discord : %s", e) + # ------------------------------------------------------------------ # PIN / Auth App flow (onboarding web) # ------------------------------------------------------------------ @@ -314,7 +322,8 @@ def run_sync_endpoint(): @app.route('/webhook', methods=['POST']) def webhook(): """Écoute les webhooks de Plex pour ajouter un média noté 0.5 à une collection""" - logging.info("--- Webhook reçu ! ---") + send_to_discord("🔔 Un Webhook vient d'arriver sur le NAS !") + payload_str = request.form.get('payload') if not payload_str: @@ -334,6 +343,7 @@ def webhook(): if rating == 1: rating_key = data.get('Metadata', {}).get('ratingKey') title = data.get('Metadata', {}).get('title', 'Titre inconnu') + send_to_discord(f"✅ Suppression demandée pour : {title}") logging.info("L'utilisateur %s a mis 0,5 étoile à '%s'. Ajout à la collection...", username, title) From f2651f3c575ce7c105879d45fbc8a8aeb51b8a16 Mon Sep 17 00:00:00 2001 From: chwps Date: Thu, 26 Mar 2026 21:44:12 +0100 Subject: [PATCH 08/23] debug --- app.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index 24d45aa..082fa4b 100644 --- a/app.py +++ b/app.py @@ -335,16 +335,35 @@ def webhook(): logging.error("Erreur de lecture du Webhook: %s", e) return "JSON invalide", 400 + # DEBUG : Affiche tout le contenu du webhook dans les logs de ton conteneur Docker + logging.info("=== PAYLOAD PLEX REÇU ===") + logging.info(json.dumps(data, indent=2)) + logging.info("=========================") + if data.get('event') == 'media.rate': - rating = data.get('Rating', {}).get('value') + # On vérifie si la note est à la racine (Rating) ou dans les Metadata + rating_obj = data.get('Rating', {}) + metadata_obj = data.get('Metadata', {}) + + # On tente de récupérer la note à deux endroits possibles + rating = rating_obj.get('value') or metadata_obj.get('userRating') + username = data.get('Account', {}).get('title', 'Inconnu') - # 1 = 0.5 étoile sur l'interface Plex - if rating == 1: - rating_key = data.get('Metadata', {}).get('ratingKey') - title = data.get('Metadata', {}).get('title', 'Titre inconnu') - send_to_discord(f"✅ Suppression demandée pour : {title}") + # On convertit en float pour éviter les problèmes entre 1 et 1.0 ou "1" + try: + rating_val = float(rating) if rating is not None else None + except ValueError: + rating_val = None + + logging.info("Événement media.rate détecté. Utilisateur: %s, Note lue: %s", username, rating_val) + + # 1.0 = 0.5 étoile sur l'interface Plex + if rating_val == 1.0: + rating_key = metadata_obj.get('ratingKey') + title = metadata_obj.get('title', 'Titre inconnu') + send_to_discord(f"✅ Suppression demandée pour : {title}") logging.info("L'utilisateur %s a mis 0,5 étoile à '%s'. Ajout à la collection...", username, title) token = get_admin_token() @@ -364,6 +383,8 @@ def webhook(): except Exception as e: logging.error("Erreur lors de l'ajout de '%s' à la collection : %s", title, e) return f"Erreur d'ajout : {e}", 500 + else: + logging.info("La note (%s) n'est pas égale à 1.0 (0.5 étoile). On ignore.", rating_val) return "Événement ignoré", 200 From 4e59c0f2dfcdb0683f24ad2f20863d9e07ca83c7 Mon Sep 17 00:00:00 2001 From: chwps Date: Thu, 26 Mar 2026 21:48:02 +0100 Subject: [PATCH 09/23] debug --- app.py | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/app.py b/app.py index 082fa4b..2cbea90 100644 --- a/app.py +++ b/app.py @@ -322,8 +322,6 @@ def run_sync_endpoint(): @app.route('/webhook', methods=['POST']) def webhook(): """Écoute les webhooks de Plex pour ajouter un média noté 0.5 à une collection""" - send_to_discord("🔔 Un Webhook vient d'arriver sur le NAS !") - payload_str = request.form.get('payload') if not payload_str: @@ -335,36 +333,27 @@ def webhook(): logging.error("Erreur de lecture du Webhook: %s", e) return "JSON invalide", 400 - # DEBUG : Affiche tout le contenu du webhook dans les logs de ton conteneur Docker - logging.info("=== PAYLOAD PLEX REÇU ===") - logging.info(json.dumps(data, indent=2)) - logging.info("=========================") - + # On ne réagit qu'aux événements de notation if data.get('event') == 'media.rate': - # On vérifie si la note est à la racine (Rating) ou dans les Metadata - rating_obj = data.get('Rating', {}) - metadata_obj = data.get('Metadata', {}) - - # On tente de récupérer la note à deux endroits possibles - rating = rating_obj.get('value') or metadata_obj.get('userRating') - + # Optimisation : On cible exactement les clés vues dans tes logs + rating = data.get('rating') or data.get('Metadata', {}).get('userRating') username = data.get('Account', {}).get('title', 'Inconnu') - # On convertit en float pour éviter les problèmes entre 1 et 1.0 ou "1" try: rating_val = float(rating) if rating is not None else None except ValueError: rating_val = None - logging.info("Événement media.rate détecté. Utilisateur: %s, Note lue: %s", username, rating_val) - # 1.0 = 0.5 étoile sur l'interface Plex if rating_val == 1.0: + metadata_obj = data.get('Metadata', {}) rating_key = metadata_obj.get('ratingKey') title = metadata_obj.get('title', 'Titre inconnu') - send_to_discord(f"✅ Suppression demandée pour : {title}") logging.info("L'utilisateur %s a mis 0,5 étoile à '%s'. Ajout à la collection...", username, title) + + # Notification Discord uniquement quand la note est de 0.5 + send_to_discord(f"✅ Demande de suppression reçue par {username} pour le film : {title}") token = get_admin_token() if not token: @@ -383,8 +372,6 @@ def webhook(): except Exception as e: logging.error("Erreur lors de l'ajout de '%s' à la collection : %s", title, e) return f"Erreur d'ajout : {e}", 500 - else: - logging.info("La note (%s) n'est pas égale à 1.0 (0.5 étoile). On ignore.", rating_val) return "Événement ignoré", 200 From c702867d31503248eb9bcece923a27be458b63f9 Mon Sep 17 00:00:00 2001 From: chwps Date: Thu, 26 Mar 2026 21:59:11 +0100 Subject: [PATCH 10/23] debug --- app.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/app.py b/app.py index 2cbea90..736b525 100644 --- a/app.py +++ b/app.py @@ -321,7 +321,7 @@ def run_sync_endpoint(): @app.route('/webhook', methods=['POST']) def webhook(): - """Écoute les webhooks de Plex pour ajouter un média noté 0.5 à une collection""" + """Écoute les webhooks de Plex pour gérer les collections selon les notes""" payload_str = request.form.get('payload') if not payload_str: @@ -335,7 +335,6 @@ def webhook(): # On ne réagit qu'aux événements de notation if data.get('event') == 'media.rate': - # Optimisation : On cible exactement les clés vues dans tes logs rating = data.get('rating') or data.get('Metadata', {}).get('userRating') username = data.get('Account', {}).get('title', 'Inconnu') @@ -344,17 +343,13 @@ def webhook(): except ValueError: rating_val = None - # 1.0 = 0.5 étoile sur l'interface Plex - if rating_val == 1.0: + # Si la note est 1.0 (0.5 étoile) OU >= 8.0 (4 étoiles et plus) + if rating_val == 1.0 or (rating_val is not None and rating_val >= 8.0): metadata_obj = data.get('Metadata', {}) rating_key = metadata_obj.get('ratingKey') title = metadata_obj.get('title', 'Titre inconnu') - logging.info("L'utilisateur %s a mis 0,5 étoile à '%s'. Ajout à la collection...", username, title) - - # Notification Discord uniquement quand la note est de 0.5 - send_to_discord(f"✅ Demande de suppression reçue par {username} pour le film : {title}") - + # Connexion à Plex (commune aux deux actions) token = get_admin_token() if not token: logging.error("Impossible de modifier : aucun token admin en cache.") @@ -364,14 +359,27 @@ def webhook(): server = PlexServer(PLEX_URL, token=token) item = server.fetchItem(int(rating_key)) - item.addCollection(WEBHOOK_COLLECTION) - - logging.info("Succès : '%s' a été ajouté à la collection '%s' !", title, WEBHOOK_COLLECTION) - return "Média ajouté à la collection", 200 + # CAS 1 : Note de 0.5 étoile -> On ajoute à la collection + if rating_val == 1.0: + logging.info("L'utilisateur %s a mis 0,5 étoile à '%s'. Ajout à la collection...", username, title) + item.addCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🗑️ **Demande de suppression** reçue par {username} pour le film : **{title}**") + logging.info("Succès : '%s' a été ajouté à la collection '%s' !", title, WEBHOOK_COLLECTION) + return "Média ajouté à la collection", 200 + # CAS 2 : Note de 4 étoiles ou plus -> On retire de la collection + elif rating_val >= 8.0: + # On calcule la note sur 5 pour l'affichage Discord + note_sur_5 = rating_val / 2 + logging.info("L'utilisateur %s a mis %s/5 à '%s'. Retrait de la collection...", username, note_sur_5, title) + item.removeCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🛡️ **Annulation de la suppression** par {username} pour le film : **{title}** (Note modifiée : {note_sur_5}/5)") + logging.info("Succès : '%s' a été retiré de la collection '%s' !", title, WEBHOOK_COLLECTION) + return "Média retiré de la collection", 200 + except Exception as e: - logging.error("Erreur lors de l'ajout de '%s' à la collection : %s", title, e) - return f"Erreur d'ajout : {e}", 500 + logging.error("Erreur lors de la modification de '%s' : %s", title, e) + return f"Erreur de modification : {e}", 500 return "Événement ignoré", 200 From 71588df93600531a72a0a267432698d427374046 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 17:41:01 +0100 Subject: [PATCH 11/23] Debug pour app Plex - Sync par token --- app.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/app.py b/app.py index 736b525..e3e8e4b 100644 --- a/app.py +++ b/app.py @@ -257,7 +257,69 @@ def remove_batch(guids): except Exception as e: logging.exception("Erreur pour %s : %s", user["username"], e) +def sync_ratings(): + """Vérifie les dernières notes de tous les utilisateurs et met à jour la collection""" + logging.info("Démarrage de la vérification des notes (Polling) pour tous les utilisateurs...") + + admin_token = get_admin_token() + if not admin_token: + logging.error("Pas de token admin, impossible de modifier les collections.") + return + + admin_server = PlexServer(PLEX_URL, token=admin_token) + users = list_all_users() + + for u in users: + username = u["username"] + token = u["token"] + + try: + # 1. On se connecte à Plex en tant que cet utilisateur spécifique + user_server = PlexServer(PLEX_URL, token=token) + + for section in user_server.library.sections(): + if section.type not in {"movie", "show"}: + continue + + # 2. On cherche les 50 derniers médias notés par cet utilisateur + try: + recent_rated = section.search(sort="lastRatedAt:desc", limit=50) + except Exception: + # Sécurité si le tri lastRatedAt n'est pas supporté sur une vieille version + continue + + for item in recent_rated: + rating = item.userRating + if rating is None: + continue + + # On convertit en float par sécurité + rating_val = float(rating) + + # On récupère l'objet côté Admin pour pouvoir modifier ses collections + admin_item = admin_server.fetchItem(item.ratingKey) + current_collections = [c.tag for c in admin_item.collections] + + # CAS 1 : Note = 0.5 (1.0) et pas encore dans la collection + if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: + logging.info("Synchro : %s a noté 0.5 '%s'. Ajout à la collection...", username, item.title) + admin_item.addCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username} sur mobile/autre)") + + # CAS 2 : Note >= 4 (8.0) et présent dans la collection + elif rating_val >= 8.0 and WEBHOOK_COLLECTION in current_collections: + note_sur_5 = rating_val / 2 + logging.info("Synchro : %s a noté %s/5 '%s'. Retrait de la collection...", username, note_sur_5, item.title) + admin_item.removeCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🛡️ **Synchro** : Annulation de suppression pour **{item.title}** ({username} a changé sa note à {note_sur_5}/5)") + + except Exception as e: + logging.error("Erreur lors de la vérification des notes pour %s : %s", username, e) + def sync_collections_once(): + # === NOUVEAU : On lance d'abord la vérification des notes === + sync_ratings() + if not COLLECTIONS: logging.warning("Aucune collection configurée (env COLLECTIONS).") return From c1ada8a266f385ad9c4283fc2f3ddac8487c0577 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 17:53:29 +0100 Subject: [PATCH 12/23] Debug pour app Plex - Sync par token --- app.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/app.py b/app.py index e3e8e4b..f3d88cc 100644 --- a/app.py +++ b/app.py @@ -266,26 +266,39 @@ def sync_ratings(): logging.error("Pas de token admin, impossible de modifier les collections.") return - admin_server = PlexServer(PLEX_URL, token=admin_token) users = list_all_users() + logging.info("Nombre d'utilisateurs trouvés : %d", len(users)) + + if not users: + logging.warning("Aucun utilisateur à vérifier.") + return + + try: + admin_server = PlexServer(PLEX_URL, token=admin_token) + except Exception as e: + logging.error("Erreur lors de la connexion admin au serveur : %s", e) + return for u in users: username = u["username"] token = u["token"] + logging.info("--- Analyse du compte : %s ---", username) try: - # 1. On se connecte à Plex en tant que cet utilisateur spécifique user_server = PlexServer(PLEX_URL, token=token) for section in user_server.library.sections(): if section.type not in {"movie", "show"}: continue - # 2. On cherche les 50 derniers médias notés par cet utilisateur + logging.info("Recherche dans la bibliothèque : %s", section.title) + try: + # On tente de récupérer les 50 derniers médias notés recent_rated = section.search(sort="lastRatedAt:desc", limit=50) - except Exception: - # Sécurité si le tri lastRatedAt n'est pas supporté sur une vieille version + logging.info("Trouvé %d médias récemment notés dans '%s'.", len(recent_rated), section.title) + except Exception as e: + logging.warning("Impossible d'utiliser le tri lastRatedAt sur '%s' : %s", section.title, e) continue for item in recent_rated: @@ -293,33 +306,30 @@ def sync_ratings(): if rating is None: continue - # On convertit en float par sécurité rating_val = float(rating) + logging.info("-> Film trouvé : '%s' noté %s par %s", item.title, rating_val, username) - # On récupère l'objet côté Admin pour pouvoir modifier ses collections admin_item = admin_server.fetchItem(item.ratingKey) current_collections = [c.tag for c in admin_item.collections] - # CAS 1 : Note = 0.5 (1.0) et pas encore dans la collection if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: - logging.info("Synchro : %s a noté 0.5 '%s'. Ajout à la collection...", username, item.title) + logging.info("Action : Ajout de '%s' à la collection.", item.title) admin_item.addCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username} sur mobile/autre)") + send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username})") - # CAS 2 : Note >= 4 (8.0) et présent dans la collection elif rating_val >= 8.0 and WEBHOOK_COLLECTION in current_collections: note_sur_5 = rating_val / 2 - logging.info("Synchro : %s a noté %s/5 '%s'. Retrait de la collection...", username, note_sur_5, item.title) + logging.info("Action : Retrait de '%s' de la collection.", item.title) admin_item.removeCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🛡️ **Synchro** : Annulation de suppression pour **{item.title}** ({username} a changé sa note à {note_sur_5}/5)") + send_to_discord(f"🛡️ **Synchro** : Annulation de suppression pour **{item.title}** ({username} a mis {note_sur_5}/5)") except Exception as e: - logging.error("Erreur lors de la vérification des notes pour %s : %s", username, e) + logging.error("Erreur générale pour l'utilisateur %s : %s", username, e) def sync_collections_once(): # === NOUVEAU : On lance d'abord la vérification des notes === sync_ratings() - + if not COLLECTIONS: logging.warning("Aucune collection configurée (env COLLECTIONS).") return From 28abdad596bb444933f23a004953b7aad075fdd6 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 17:56:10 +0100 Subject: [PATCH 13/23] Debug pour app Plex - Sync par token --- app.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app.py b/app.py index f3d88cc..fc4824b 100644 --- a/app.py +++ b/app.py @@ -258,7 +258,7 @@ def remove_batch(guids): logging.exception("Erreur pour %s : %s", user["username"], e) def sync_ratings(): - """Vérifie les dernières notes de tous les utilisateurs et met à jour la collection""" + """Vérifie les notes de tous les utilisateurs et met à jour la collection""" logging.info("Démarrage de la vérification des notes (Polling) pour tous les utilisateurs...") admin_token = get_admin_token() @@ -294,32 +294,33 @@ def sync_ratings(): logging.info("Recherche dans la bibliothèque : %s", section.title) try: - # On tente de récupérer les 50 derniers médias notés - recent_rated = section.search(sort="lastRatedAt:desc", limit=50) - logging.info("Trouvé %d médias récemment notés dans '%s'.", len(recent_rated), section.title) + # CORRECTION : On demande tous les médias qui ont été notés par l'utilisateur + rated_items = section.search(filters={'userRating>>': 0}) + logging.info("Trouvé %d médias notés dans '%s'.", len(rated_items), section.title) except Exception as e: - logging.warning("Impossible d'utiliser le tri lastRatedAt sur '%s' : %s", section.title, e) + logging.warning("Impossible de chercher les notes dans '%s' : %s", section.title, e) continue - for item in recent_rated: + for item in rated_items: rating = item.userRating if rating is None: continue rating_val = float(rating) - logging.info("-> Film trouvé : '%s' noté %s par %s", item.title, rating_val, username) - + admin_item = admin_server.fetchItem(item.ratingKey) current_collections = [c.tag for c in admin_item.collections] + # Note = 0.5 (1.0) et pas encore dans la collection if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: - logging.info("Action : Ajout de '%s' à la collection.", item.title) + logging.info("Action : Ajout de '%s' à la collection (noté 0.5 par %s).", item.title, username) admin_item.addCollection(WEBHOOK_COLLECTION) send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username})") + # Note >= 4 (8.0) et présent dans la collection elif rating_val >= 8.0 and WEBHOOK_COLLECTION in current_collections: note_sur_5 = rating_val / 2 - logging.info("Action : Retrait de '%s' de la collection.", item.title) + logging.info("Action : Retrait de '%s' de la collection (noté %s/5 par %s).", item.title, note_sur_5, username) admin_item.removeCollection(WEBHOOK_COLLECTION) send_to_discord(f"🛡️ **Synchro** : Annulation de suppression pour **{item.title}** ({username} a mis {note_sur_5}/5)") From 8a034672b9751700ddfe81f8f35d7d681c18b2d6 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 18:03:20 +0100 Subject: [PATCH 14/23] Debug pour app Plex - Sync par token --- app.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app.py b/app.py index fc4824b..ed87763 100644 --- a/app.py +++ b/app.py @@ -294,19 +294,21 @@ def sync_ratings(): logging.info("Recherche dans la bibliothèque : %s", section.title) try: - # CORRECTION : On demande tous les médias qui ont été notés par l'utilisateur + # On récupère le "pool" global des médias notés rated_items = section.search(filters={'userRating>>': 0}) - logging.info("Trouvé %d médias notés dans '%s'.", len(rated_items), section.title) + + # On compte uniquement ceux que CET utilisateur a noté + user_actual_ratings = [item for item in rated_items if item.userRating is not None] + + if user_actual_ratings: + logging.info("Trouvé %d média(s) noté(s) par %s dans '%s'.", len(user_actual_ratings), username, section.title) + except Exception as e: logging.warning("Impossible de chercher les notes dans '%s' : %s", section.title, e) continue - for item in rated_items: - rating = item.userRating - if rating is None: - continue - - rating_val = float(rating) + for item in user_actual_ratings: + rating_val = float(item.userRating) admin_item = admin_server.fetchItem(item.ratingKey) current_collections = [c.tag for c in admin_item.collections] From 3627a12780a588d6a86ed23e2d173aedda1841c2 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 18:06:52 +0100 Subject: [PATCH 15/23] Debug pour app Plex - Sync par token --- app.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app.py b/app.py index ed87763..fc4824b 100644 --- a/app.py +++ b/app.py @@ -294,21 +294,19 @@ def sync_ratings(): logging.info("Recherche dans la bibliothèque : %s", section.title) try: - # On récupère le "pool" global des médias notés + # CORRECTION : On demande tous les médias qui ont été notés par l'utilisateur rated_items = section.search(filters={'userRating>>': 0}) - - # On compte uniquement ceux que CET utilisateur a noté - user_actual_ratings = [item for item in rated_items if item.userRating is not None] - - if user_actual_ratings: - logging.info("Trouvé %d média(s) noté(s) par %s dans '%s'.", len(user_actual_ratings), username, section.title) - + logging.info("Trouvé %d médias notés dans '%s'.", len(rated_items), section.title) except Exception as e: logging.warning("Impossible de chercher les notes dans '%s' : %s", section.title, e) continue - for item in user_actual_ratings: - rating_val = float(item.userRating) + for item in rated_items: + rating = item.userRating + if rating is None: + continue + + rating_val = float(rating) admin_item = admin_server.fetchItem(item.ratingKey) current_collections = [c.tag for c in admin_item.collections] From 6db7fb80724e731e92d58b9a0f2d200a67cd039a Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 18:14:15 +0100 Subject: [PATCH 16/23] Debug pour app Plex - Sync par token --- app.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/app.py b/app.py index fc4824b..dfe3708 100644 --- a/app.py +++ b/app.py @@ -258,7 +258,7 @@ def remove_batch(guids): logging.exception("Erreur pour %s : %s", user["username"], e) def sync_ratings(): - """Vérifie les notes de tous les utilisateurs et met à jour la collection""" + """Vérifie les notes de tous les utilisateurs et met à jour la collection (Polling optimisé)""" logging.info("Démarrage de la vérification des notes (Polling) pour tous les utilisateurs...") admin_token = get_admin_token() @@ -274,6 +274,7 @@ def sync_ratings(): return try: + # Connexion globale en tant qu'admin pour modifier les collections admin_server = PlexServer(PLEX_URL, token=admin_token) except Exception as e: logging.error("Erreur lors de la connexion admin au serveur : %s", e) @@ -285,6 +286,7 @@ def sync_ratings(): logging.info("--- Analyse du compte : %s ---", username) try: + # Connexion locale en tant que l'utilisateur pour lire SES notes user_server = PlexServer(PLEX_URL, token=token) for section in user_server.library.sections(): @@ -294,30 +296,42 @@ def sync_ratings(): logging.info("Recherche dans la bibliothèque : %s", section.title) try: - # CORRECTION : On demande tous les médias qui ont été notés par l'utilisateur - rated_items = section.search(filters={'userRating>>': 0}) - logging.info("Trouvé %d médias notés dans '%s'.", len(rated_items), section.title) + # 1. On cherche UNIQUEMENT les médias notés 0.5 étoile (1.0) par CET utilisateur + bad_items = section.search(userRating=1.0) + + # 2. On cherche UNIQUEMENT les médias notés 4 étoiles ou plus (>= 8.0) par CET utilisateur + good_items = section.search(userRating__gte=8.0) + + # On regroupe les deux listes de résultats + user_actual_ratings = bad_items + good_items + + if user_actual_ratings: + logging.info("Trouvé %d média(s) pertinent(s) pour %s dans '%s'.", len(user_actual_ratings), username, section.title) + else: + # On passe silencieusement au suivant si rien ne correspond (Zéro charge pour le NAS) + continue + except Exception as e: - logging.warning("Impossible de chercher les notes dans '%s' : %s", section.title, e) + logging.warning("Impossible de chercher les notes exactes dans '%s' : %s", section.title, e) continue - for item in rated_items: - rating = item.userRating - if rating is None: + for item in user_actual_ratings: + if item.userRating is None: continue + + rating_val = float(item.userRating) - rating_val = float(rating) - + # On récupère l'objet côté Admin pour pouvoir modifier ses collections admin_item = admin_server.fetchItem(item.ratingKey) current_collections = [c.tag for c in admin_item.collections] - # Note = 0.5 (1.0) et pas encore dans la collection + # CAS 1 : Note = 0.5 (1.0) et pas encore dans la collection if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: logging.info("Action : Ajout de '%s' à la collection (noté 0.5 par %s).", item.title, username) admin_item.addCollection(WEBHOOK_COLLECTION) send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username})") - # Note >= 4 (8.0) et présent dans la collection + # CAS 2 : Note >= 4 (8.0) et présent dans la collection elif rating_val >= 8.0 and WEBHOOK_COLLECTION in current_collections: note_sur_5 = rating_val / 2 logging.info("Action : Retrait de '%s' de la collection (noté %s/5 par %s).", item.title, note_sur_5, username) From 92156419ed69d1ea3c6b668614533d3fe20ea3ce Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 18:19:18 +0100 Subject: [PATCH 17/23] Debug pour app Plex - Sync par token --- app.py | 70 ++++++++++++++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/app.py b/app.py index dfe3708..e8a0bc5 100644 --- a/app.py +++ b/app.py @@ -258,7 +258,7 @@ def remove_batch(guids): logging.exception("Erreur pour %s : %s", user["username"], e) def sync_ratings(): - """Vérifie les notes de tous les utilisateurs et met à jour la collection (Polling optimisé)""" + """Vérifie les notes de tous les utilisateurs et met à jour la collection (Tri via Python)""" logging.info("Démarrage de la vérification des notes (Polling) pour tous les utilisateurs...") admin_token = get_admin_token() @@ -270,11 +270,9 @@ def sync_ratings(): logging.info("Nombre d'utilisateurs trouvés : %d", len(users)) if not users: - logging.warning("Aucun utilisateur à vérifier.") return try: - # Connexion globale en tant qu'admin pour modifier les collections admin_server = PlexServer(PLEX_URL, token=admin_token) except Exception as e: logging.error("Erreur lors de la connexion admin au serveur : %s", e) @@ -286,57 +284,51 @@ def sync_ratings(): logging.info("--- Analyse du compte : %s ---", username) try: - # Connexion locale en tant que l'utilisateur pour lire SES notes user_server = PlexServer(PLEX_URL, token=token) for section in user_server.library.sections(): if section.type not in {"movie", "show"}: continue - logging.info("Recherche dans la bibliothèque : %s", section.title) - try: - # 1. On cherche UNIQUEMENT les médias notés 0.5 étoile (1.0) par CET utilisateur - bad_items = section.search(userRating=1.0) - - # 2. On cherche UNIQUEMENT les médias notés 4 étoiles ou plus (>= 8.0) par CET utilisateur - good_items = section.search(userRating__gte=8.0) + # 1. On demande la liste basique (très léger pour le NAS) + all_items = section.all() - # On regroupe les deux listes de résultats - user_actual_ratings = bad_items + good_items + # 2. Python filtre instantanément les médias qui ont vraiment été notés par l'utilisateur + user_actual_ratings = [item for item in all_items if item.userRating is not None] - if user_actual_ratings: - logging.info("Trouvé %d média(s) pertinent(s) pour %s dans '%s'.", len(user_actual_ratings), username, section.title) - else: - # On passe silencieusement au suivant si rien ne correspond (Zéro charge pour le NAS) - continue - except Exception as e: - logging.warning("Impossible de chercher les notes exactes dans '%s' : %s", section.title, e) + logging.warning("Impossible de lire la bibliothèque '%s' : %s", section.title, e) + continue + + if not user_actual_ratings: + # On passe en silence s'il n'y a aucune note continue + logging.info("Trouvé %d média(s) noté(s) par %s dans '%s'.", len(user_actual_ratings), username, section.title) + for item in user_actual_ratings: - if item.userRating is None: - continue - rating_val = float(item.userRating) - # On récupère l'objet côté Admin pour pouvoir modifier ses collections - admin_item = admin_server.fetchItem(item.ratingKey) - current_collections = [c.tag for c in admin_item.collections] - - # CAS 1 : Note = 0.5 (1.0) et pas encore dans la collection - if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: - logging.info("Action : Ajout de '%s' à la collection (noté 0.5 par %s).", item.title, username) - admin_item.addCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username})") - - # CAS 2 : Note >= 4 (8.0) et présent dans la collection - elif rating_val >= 8.0 and WEBHOOK_COLLECTION in current_collections: - note_sur_5 = rating_val / 2 - logging.info("Action : Retrait de '%s' de la collection (noté %s/5 par %s).", item.title, note_sur_5, username) - admin_item.removeCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🛡️ **Synchro** : Annulation de suppression pour **{item.title}** ({username} a mis {note_sur_5}/5)") + # On ne traite que les notes qui nous intéressent (0.5 ou >= 4 étoiles) + if rating_val == 1.0 or rating_val >= 8.0: + logging.info("-> Examen de '%s' (Note trouvée : %s/10)", item.title, rating_val) + + admin_item = admin_server.fetchItem(item.ratingKey) + current_collections = [c.tag for c in admin_item.collections] + + # CAS 1 : Note = 0.5 (1.0) et pas encore dans la collection + if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: + logging.info("Action : Ajout de '%s' à la collection.", item.title) + admin_item.addCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username})") + + # CAS 2 : Note >= 4 (8.0) et présent dans la collection + elif rating_val >= 8.0 and WEBHOOK_COLLECTION in current_collections: + note_sur_5 = rating_val / 2 + logging.info("Action : Retrait de '%s' de la collection.", item.title) + admin_item.removeCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🛡️ **Synchro** : Annulation de suppression pour **{item.title}** ({username} a mis {note_sur_5}/5)") except Exception as e: logging.error("Erreur générale pour l'utilisateur %s : %s", username, e) From b85ff924c78064aad9a8fe9bf8624a7d106f078e Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 18:30:34 +0100 Subject: [PATCH 18/23] Debug pour app Plex - Sync par token --- app.py | 66 +++++++++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/app.py b/app.py index e8a0bc5..5b3f260 100644 --- a/app.py +++ b/app.py @@ -258,8 +258,8 @@ def remove_batch(guids): logging.exception("Erreur pour %s : %s", user["username"], e) def sync_ratings(): - """Vérifie les notes de tous les utilisateurs et met à jour la collection (Tri via Python)""" - logging.info("Démarrage de la vérification des notes (Polling) pour tous les utilisateurs...") + """Vérifie les notes de tous les utilisateurs via l'API Plex et met à jour la collection""" + logging.info("Démarrage de la vérification des notes (Polling via API) pour tous les utilisateurs...") admin_token = get_admin_token() if not admin_token: @@ -286,49 +286,59 @@ def sync_ratings(): try: user_server = PlexServer(PLEX_URL, token=token) + # Ligne de contrôle : On s'assure que Plex nous voit bien comme le bon utilisateur + try: + plex_identity = user_server.myPlexAccount().title + logging.info("Vérification identité Plex : connecté en tant que '%s'", plex_identity) + except: + logging.info("Vérification identité Plex : impossible de récupérer le nom du compte") + for section in user_server.library.sections(): if section.type not in {"movie", "show"}: continue try: - # 1. On demande la liste basique (très léger pour le NAS) - all_items = section.all() + # 1. On demande à Plex UNIQUEMENT les médias notés 0.5 étoile (1.0) par ce compte + bad_items = section.search(userRating=1.0) + + # 2. On demande à Plex UNIQUEMENT les médias notés 4 étoiles ou plus (>= 8.0) par ce compte + good_items = section.search(userRating__gte=8.0) - # 2. Python filtre instantanément les médias qui ont vraiment été notés par l'utilisateur - user_actual_ratings = [item for item in all_items if item.userRating is not None] + # On fusionne les deux listes + user_actual_ratings = bad_items + good_items except Exception as e: - logging.warning("Impossible de lire la bibliothèque '%s' : %s", section.title, e) + logging.warning("Impossible de filtrer les notes via l'API dans '%s' : %s", section.title, e) continue if not user_actual_ratings: - # On passe en silence s'il n'y a aucune note + # Rien de pertinent trouvé, on passe au suivant en silence continue - logging.info("Trouvé %d média(s) noté(s) par %s dans '%s'.", len(user_actual_ratings), username, section.title) + logging.info("Trouvé %d média(s) pertinent(s) pour %s dans '%s'.", len(user_actual_ratings), username, section.title) for item in user_actual_ratings: + if item.userRating is None: + continue + rating_val = float(item.userRating) + logging.info("-> Examen de '%s' (Note trouvée : %s/10)", item.title, rating_val) - # On ne traite que les notes qui nous intéressent (0.5 ou >= 4 étoiles) - if rating_val == 1.0 or rating_val >= 8.0: - logging.info("-> Examen de '%s' (Note trouvée : %s/10)", item.title, rating_val) - - admin_item = admin_server.fetchItem(item.ratingKey) - current_collections = [c.tag for c in admin_item.collections] - - # CAS 1 : Note = 0.5 (1.0) et pas encore dans la collection - if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: - logging.info("Action : Ajout de '%s' à la collection.", item.title) - admin_item.addCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username})") - - # CAS 2 : Note >= 4 (8.0) et présent dans la collection - elif rating_val >= 8.0 and WEBHOOK_COLLECTION in current_collections: - note_sur_5 = rating_val / 2 - logging.info("Action : Retrait de '%s' de la collection.", item.title) - admin_item.removeCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🛡️ **Synchro** : Annulation de suppression pour **{item.title}** ({username} a mis {note_sur_5}/5)") + admin_item = admin_server.fetchItem(item.ratingKey) + current_collections = [c.tag for c in admin_item.collections] + + # CAS 1 : Note = 0.5 (1.0) et pas encore dans la collection + if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: + logging.info("Action : Ajout de '%s' à la collection.", item.title) + admin_item.addCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username})") + + # CAS 2 : Note >= 4 (8.0) et présent dans la collection + elif rating_val >= 8.0 and WEBHOOK_COLLECTION in current_collections: + note_sur_5 = rating_val / 2 + logging.info("Action : Retrait de '%s' de la collection.", item.title) + admin_item.removeCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🛡️ **Synchro** : Annulation de suppression pour **{item.title}** ({username} a mis {note_sur_5}/5)") except Exception as e: logging.error("Erreur générale pour l'utilisateur %s : %s", username, e) From 26d4685858f3aef0ca0f29feaae7f385eb4cd2e8 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 18:40:01 +0100 Subject: [PATCH 19/23] Debug pour app Plex - Sync par token --- app.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app.py b/app.py index 5b3f260..b874122 100644 --- a/app.py +++ b/app.py @@ -273,7 +273,10 @@ def sync_ratings(): return try: + # Connexion admin au serveur local admin_server = PlexServer(PLEX_URL, token=admin_token) + # On récupère le nom exact de ton serveur (ex: "NAS") pour que les utilisateurs puissent le trouver + server_name = admin_server.friendlyName except Exception as e: logging.error("Erreur lors de la connexion admin au serveur : %s", e) return @@ -284,7 +287,14 @@ def sync_ratings(): logging.info("--- Analyse du compte : %s ---", username) try: - user_server = PlexServer(PLEX_URL, token=token) + # NOUVELLE MÉTHODE DE CONNEXION POUR LES UTILISATEURS + if token == admin_token: + # Si c'est l'admin, on peut se connecter directement en local (plus rapide) + user_server = PlexServer(PLEX_URL, token=token) + else: + # Si c'est un ami, on se connecte via Plex.tv puis on cible le serveur + account = MyPlexAccount(token=token) + user_server = account.resource(server_name).connect() # Ligne de contrôle : On s'assure que Plex nous voit bien comme le bon utilisateur try: From 1ead6e96167716eaf911ee8341ddfdc3458f8793 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 18:53:30 +0100 Subject: [PATCH 20/23] Debug pour app Plex - Sync par token --- app.py | 353 ++++++++++++++++++++++----------------------------------- 1 file changed, 134 insertions(+), 219 deletions(-) diff --git a/app.py b/app.py index b874122..7393fe6 100644 --- a/app.py +++ b/app.py @@ -5,6 +5,7 @@ - stockage tokens utilisateurs (/data/user_tokens.json) - si l'utilisateur connecté est l'admin (ADMIN_USERNAME), on met aussi à jour /data/plex_token.json - routine de sync des collections (fonction sync_collections_once) + - Logique de consensus (une note > 0.5 annule la suppression) """ import os @@ -12,6 +13,7 @@ import time import secrets import logging +import threading from urllib.parse import urlencode import requests @@ -33,18 +35,15 @@ CLIENT_ID_FILE = os.getenv("CLIENT_ID_FILE", "/data/client_id.txt") PLEX_API = "https://plex.tv/api/v2" -# TTL config (env: TOKEN_TTL_HOURS) default 24 hours TOKEN_TTL = int(os.getenv("TOKEN_TTL_HOURS", "24")) * 3600 - -# Admin account name if you want to auto-detect ("Tristan.Brn") -ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") # si défini, on considérera ce compte comme admin +ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") PLEX_URL = os.getenv("PLEX_URL", "http://localhost:32400") COLLECTIONS = [c.strip() for c in os.getenv("COLLECTIONS", "").split(",") if c.strip()] WEBHOOK_COLLECTION = os.getenv("WEBHOOK_COLLECTION", "Demande de suppression") -DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL") #pour notifications Discord +DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL") # ------------------------------------------------------------------ -# UTILS stockage / client id +# UTILS # ------------------------------------------------------------------ def load_json(path): if os.path.exists(path): @@ -66,7 +65,6 @@ def get_client_id(): open(CLIENT_ID_FILE, "w").write(cid) return cid -# user tokens helpers def load_user_tokens(): return load_json(TOKENS_FILE) or {} @@ -76,30 +74,20 @@ def save_user_token(username, token): save_json(TOKENS_FILE, tokens) logging.info("Token utilisateur enregistré pour %s", username) -# admin token helpers (cached token used to access PlexServer) def cache_admin_token(token): save_json(TOKEN_FILE, {"token": token, "ts": time.time()}) logging.info("Token admin mis en cache") def get_admin_token(): - # 1) vérifier cache TOKEN_FILE d = load_json(TOKEN_FILE) if d.get("token") and d.get("ts") and (time.time() - d["ts"] < TOKEN_TTL): - logging.info("Token admin récupéré depuis le cache.") return d["token"] - - # 2) si ADMIN_USERNAME est défini, vérifier si on a le token dans user_tokens.json if ADMIN_USERNAME: tokens = load_user_tokens() admin_token = tokens.get(ADMIN_USERNAME) if admin_token: - logging.info("Token admin récupéré depuis user_tokens.json (admin connecté via onboarding).") - # on met en cache pour accélérer les lectures suivantes cache_admin_token(admin_token) return admin_token - - # 3) pas de token admin disponible - logging.warning("Aucun token admin disponible en cache ni dans user_tokens.json.") return None def send_to_discord(message): @@ -148,40 +136,24 @@ def index(): @app.route("/login") def login(): - """Crée un PIN et redirige l'utilisateur vers Plex Auth App""" client_id = get_client_id() - resp = requests.post( f"{PLEX_API}/pins", headers={"accept": "application/json"}, - data={ - "strong": "true", - "X-Plex-Product": APP_NAME, - "X-Plex-Client-Identifier": client_id, - }, + data={"strong": "true", "X-Plex-Product": APP_NAME, "X-Plex-Client-Identifier": client_id}, timeout=10, ) if not resp.ok: return f"Erreur création PIN : {resp.status_code} {resp.text}", 400 pin = resp.json() - pin_id = pin["id"] - pin_code = pin["code"] - - # forwardUrl: callback avec pin info (on utilisera ces params pour vérifier) - forward_url = build_redirect_uri() + f"?pin_id={pin_id}&pin_code={pin_code}" - params = { - "clientID": client_id, - "code": pin_code, - "forwardUrl": forward_url, - "context[device][product]": APP_NAME, - } + forward_url = build_redirect_uri() + f"?pin_id={pin['id']}&pin_code={pin['code']}" + params = {"clientID": client_id, "code": pin['code'], "forwardUrl": forward_url, "context[device][product]": APP_NAME} auth_url = "https://app.plex.tv/auth#?" + urlencode(params) return redirect(auth_url) @app.route("/callback") def callback(): - """Page de retour de Plex après authent""" pin_id = request.args.get("pin_id") pin_code = request.args.get("pin_code") if not pin_id or not pin_code: @@ -191,10 +163,7 @@ def callback(): resp = requests.get( f"{PLEX_API}/pins/{pin_id}", headers={"accept": "application/json"}, - data={ - "code": pin_code, - "X-Plex-Client-Identifier": client_id, - }, + data={"code": pin_code, "X-Plex-Client-Identifier": client_id}, timeout=10, ) if not resp.ok: @@ -203,46 +172,26 @@ def callback(): data = resp.json() token = data.get("authToken") if not token: - return "Authentification non terminée. Veuillez réessayer après avoir cliqué sur 'Authorize'.", 400 + return "Authentification non terminée.", 400 - # récupérer username via l'API user pour connaître le nom du compte user_resp = requests.get( f"{PLEX_API}/user", - headers={ - "accept": "application/json", - "X-Plex-Product": APP_NAME, - "X-Plex-Client-Identifier": client_id, - "X-Plex-Token": token, - }, + headers={"accept": "application/json", "X-Plex-Product": APP_NAME, "X-Plex-Client-Identifier": client_id, "X-Plex-Token": token}, timeout=10, ) - if not user_resp.ok: - logging.warning("Impossible de récupérer infos user après connexion : %s", user_resp.text) - username = "unknown" - else: - username = user_resp.json().get("username", "unknown") + username = user_resp.json().get("username", "unknown") if user_resp.ok else "unknown" - # enregister token utilisateur (remplace si existant -> évite doublons) save_user_token(username, token) - - # si c'est le compte admin configuré, on met aussi à jour le cache admin if ADMIN_USERNAME and username == ADMIN_USERNAME: cache_admin_token(token) - logging.info("Compte admin connecté via la page web. Token admin mis à jour.") - return f""" -

Merci {username} !

-

Le token a été enregistré. Vous pouvez fermer cette fenêtre.

- - """ + return f"

Merci {username} !

Le token a été enregistré. Vous pouvez fermer cette fenêtre.

" # ------------------------------------------------------------------ -# LOGIQUE de sync (identique à ton code mais réutilisable ici) +# LOGIQUE de sync # ------------------------------------------------------------------ def list_all_users(): tokens = load_user_tokens() - if not tokens: - logging.warning("Aucun token utilisateur enregistré.") return [{"username": u, "token": t} for u, t in tokens.items()] def remove_batch(guids): @@ -255,184 +204,182 @@ def remove_batch(guids): acc.removeFromWatchlist(watchlist[g]) logging.info("Retiré %s pour %s", watchlist[g].title, user["username"]) except Exception as e: - logging.exception("Erreur pour %s : %s", user["username"], e) + logging.error("Erreur pour %s : %s", user["username"], e) def sync_ratings(): - """Vérifie les notes de tous les utilisateurs via l'API Plex et met à jour la collection""" - logging.info("Démarrage de la vérification des notes (Polling via API) pour tous les utilisateurs...") + """Vérifie les notes, agrège les données, et applique un consensus global""" + logging.info("Démarrage de la vérification des notes (Polling avec Consensus)...") admin_token = get_admin_token() - if not admin_token: - logging.error("Pas de token admin, impossible de modifier les collections.") - return - + if not admin_token: return users = list_all_users() - logging.info("Nombre d'utilisateurs trouvés : %d", len(users)) - - if not users: - return + if not users: return try: - # Connexion admin au serveur local admin_server = PlexServer(PLEX_URL, token=admin_token) - # On récupère le nom exact de ton serveur (ex: "NAS") pour que les utilisateurs puissent le trouver - server_name = admin_server.friendlyName + server_name = admin_server.friendlyName except Exception as e: - logging.error("Erreur lors de la connexion admin au serveur : %s", e) + logging.error("Erreur connexion admin : %s", e) return + # Dictionnaire mémoire pour agréger les notes de tout le monde avant d'agir + media_state = {} + for u in users: username = u["username"] token = u["token"] - logging.info("--- Analyse du compte : %s ---", username) - try: - # NOUVELLE MÉTHODE DE CONNEXION POUR LES UTILISATEURS if token == admin_token: - # Si c'est l'admin, on peut se connecter directement en local (plus rapide) - user_server = PlexServer(PLEX_URL, token=token) + user_server = admin_server else: - # Si c'est un ami, on se connecte via Plex.tv puis on cible le serveur account = MyPlexAccount(token=token) user_server = account.resource(server_name).connect() - - # Ligne de contrôle : On s'assure que Plex nous voit bien comme le bon utilisateur - try: - plex_identity = user_server.myPlexAccount().title - logging.info("Vérification identité Plex : connecté en tant que '%s'", plex_identity) - except: - logging.info("Vérification identité Plex : impossible de récupérer le nom du compte") for section in user_server.library.sections(): - if section.type not in {"movie", "show"}: - continue + if section.type not in {"movie", "show"}: continue try: - # 1. On demande à Plex UNIQUEMENT les médias notés 0.5 étoile (1.0) par ce compte + # On identifie les demandes de suppression (0.5 étoile = 1.0) bad_items = section.search(userRating=1.0) - - # 2. On demande à Plex UNIQUEMENT les médias notés 4 étoiles ou plus (>= 8.0) par ce compte - good_items = section.search(userRating__gte=8.0) - - # On fusionne les deux listes - user_actual_ratings = bad_items + good_items - - except Exception as e: - logging.warning("Impossible de filtrer les notes via l'API dans '%s' : %s", section.title, e) - continue - - if not user_actual_ratings: - # Rien de pertinent trouvé, on passe au suivant en silence - continue - - logging.info("Trouvé %d média(s) pertinent(s) pour %s dans '%s'.", len(user_actual_ratings), username, section.title) - - for item in user_actual_ratings: - if item.userRating is None: - continue - - rating_val = float(item.userRating) - logging.info("-> Examen de '%s' (Note trouvée : %s/10)", item.title, rating_val) - - admin_item = admin_server.fetchItem(item.ratingKey) - current_collections = [c.tag for c in admin_item.collections] - - # CAS 1 : Note = 0.5 (1.0) et pas encore dans la collection - if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: - logging.info("Action : Ajout de '%s' à la collection.", item.title) - admin_item.addCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🔄 **Synchro** : Demande de suppression trouvée pour **{item.title}** (Noté par {username})") - - # CAS 2 : Note >= 4 (8.0) et présent dans la collection - elif rating_val >= 8.0 and WEBHOOK_COLLECTION in current_collections: - note_sur_5 = rating_val / 2 - logging.info("Action : Retrait de '%s' de la collection.", item.title) - admin_item.removeCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🛡️ **Synchro** : Annulation de suppression pour **{item.title}** ({username} a mis {note_sur_5}/5)") + # On identifie TOUT média protégé (plus de 0.5 étoile = >1.0) + protected_items = section.search(userRating__gt=1.0) + + for item in bad_items: + rk = item.ratingKey + if rk not in media_state: media_state[rk] = {'title': item.title, 'is_bad': False, 'bad_users': [], 'is_protected': False, 'protectors': []} + media_state[rk]['is_bad'] = True + media_state[rk]['bad_users'].append(username) + + for item in protected_items: + rk = item.ratingKey + if rk not in media_state: media_state[rk] = {'title': item.title, 'is_bad': False, 'bad_users': [], 'is_protected': False, 'protectors': []} + media_state[rk]['is_protected'] = True + media_state[rk]['protectors'].append(f"{username} ({float(item.userRating)/2}/5)") + + except Exception: + pass + except Exception as e: + logging.error("Erreur générale pour %s : %s", username, e) + + # Une fois qu'on a l'avis de tout le monde, on prend les décisions + for rk, state in media_state.items(): + try: + admin_item = admin_server.fetchItem(int(rk)) + current_collections = [c.tag for c in admin_item.collections] + + if state['is_protected']: + # Le média est sauvé par au moins une personne + if WEBHOOK_COLLECTION in current_collections: + logging.info("Consensus : Retrait de '%s' car protégé par %s", state['title'], state['protectors']) + admin_item.removeCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🛡️ **Maintien sur le serveur** : La suppression de **{state['title']}** a été annulée grâce aux notes de : {', '.join(state['protectors'])}") + + elif state['is_bad']: + # Uniquement des notes de 0.5, aucune protection + if WEBHOOK_COLLECTION not in current_collections: + logging.info("Consensus : Ajout de '%s' (noté 0.5 par %s)", state['title'], state['bad_users']) + admin_item.addCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🗑️ **Demande de suppression** validée pour **{state['title']}** (noté 0.5 par {', '.join(state['bad_users'])})") except Exception as e: - logging.error("Erreur générale pour l'utilisateur %s : %s", username, e) + logging.error("Erreur lors de l'application du consensus pour %s : %s", rk, e) def sync_collections_once(): - # === NOUVEAU : On lance d'abord la vérification des notes === sync_ratings() - if not COLLECTIONS: - logging.warning("Aucune collection configurée (env COLLECTIONS).") - return - - logging.info("Collections à surveiller : %s", ", ".join(COLLECTIONS)) + if not COLLECTIONS: return token = get_admin_token() - if not token: - logging.error("Pas de token admin disponible — impossible de se connecter au serveur Plex local.") - return - + if not token: return server = PlexServer(PLEX_URL, token=token) - logging.info("Connecté au serveur Plex local.") current = set() for name in COLLECTIONS: - found = False for lib in server.library.sections(): - if lib.type not in {"movie", "show"}: - continue + if lib.type not in {"movie", "show"}: continue try: coll = next(c for c in lib.collections() if c.title == name) - nb_items = len(coll.items()) - logging.info("Collection '%s' trouvée dans %s (%d élément(s))", name, lib.title, nb_items) current.update(item.guid for item in coll.items()) - found = True break except StopIteration: - logging.debug("Collection '%s' absente de la bibliothèque %s", name, lib.title) - if not found: - logging.warning("Collection '%s' introuvable dans toutes les bibliothèques.", name) + pass previous = set(load_json(STATE_FILE) or []) new_guids = current - previous - logging.info("GUID présents dans les collections : %d", len(current)) - logging.info("GUID déjà connus : %d", len(previous)) - logging.info("Nouveaux GUID à retirer : %d", len(new_guids)) - if new_guids: remove_batch(new_guids) - else: - logging.info("Rien à retirer, watchlist déjà synchronisée.") save_json(STATE_FILE, list(current)) - logging.info("État sauvegardé dans %s", STATE_FILE) -# Expose un endpoint pour déclencher manuellement (utile pour debug/cron) -# **ATTENTION** : si exposé en prod, protège cet endpoint (token, IP, etc.) +# ------------------------------------------------------------------ +# THREAD WEBHOOK (Arrière-plan) +# ------------------------------------------------------------------ +def process_webhook_rating(rating_val, rating_key, title, username): + """Vérifie le consensus en arrière-plan suite à un webhook pour ne pas bloquer Plex""" + logging.info("Webhook reçu pour '%s'. Vérification globale...", title) + + admin_token = get_admin_token() + if not admin_token: return + try: + admin_server = PlexServer(PLEX_URL, token=admin_token) + server_name = admin_server.friendlyName + except Exception: return + + is_protected = False + protectors = [] + + # On interroge les autres utilisateurs + for u in list_all_users(): + try: + if u["token"] == admin_token: + user_server = admin_server + else: + account = MyPlexAccount(token=u["token"]) + user_server = account.resource(server_name).connect() + + item = user_server.fetchItem(int(rating_key)) + if item.userRating is not None and float(item.userRating) > 1.0: + is_protected = True + protectors.append(f"{u['username']} ({float(item.userRating)/2}/5)") + except Exception: + pass + + # Application de la décision + try: + admin_item = admin_server.fetchItem(int(rating_key)) + current_collections = [c.tag for c in admin_item.collections] + + if is_protected: + if WEBHOOK_COLLECTION in current_collections: + admin_item.removeCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🛡️ **Maintien sur le serveur** : La suppression de **{title}** est bloquée/annulée grâce aux notes de : {', '.join(protectors)}") + elif rating_val == 1.0: + logging.info("Webhook : %s a noté 0.5, mais '%s' est protégé par %s. Ignoré.", username, title, protectors) + else: + if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: + admin_item.addCollection(WEBHOOK_COLLECTION) + send_to_discord(f"🗑️ **Demande de suppression** via Webhook reçue par {username} pour : **{title}**") + + except Exception as e: + logging.error("Erreur Thread Webhook : %s", e) + +# ------------------------------------------------------------------ +# ROUTES API +# ------------------------------------------------------------------ @app.route("/run_sync", methods=["POST"]) def run_sync_endpoint(): - # Optional: vérifier header X-Admin-Token ou IP whitelist - # Ici on exécute synchro et retourne OK try: sync_collections_once() return "ok", 200 except Exception as e: - logging.exception("Erreur lors du run_sync") return f"error: {e}", 500 - @app.route('/webhook', methods=['POST']) def webhook(): - """Écoute les webhooks de Plex pour gérer les collections selon les notes""" payload_str = request.form.get('payload') - - if not payload_str: - data = request.get_json(silent=True) or {} - else: - try: - data = json.loads(payload_str) - except Exception as e: - logging.error("Erreur de lecture du Webhook: %s", e) - return "JSON invalide", 400 + data = json.loads(payload_str) if payload_str else (request.get_json(silent=True) or {}) - # On ne réagit qu'aux événements de notation if data.get('event') == 'media.rate': rating = data.get('rating') or data.get('Metadata', {}).get('userRating') username = data.get('Account', {}).get('title', 'Inconnu') @@ -442,43 +389,15 @@ def webhook(): except ValueError: rating_val = None - # Si la note est 1.0 (0.5 étoile) OU >= 8.0 (4 étoiles et plus) - if rating_val == 1.0 or (rating_val is not None and rating_val >= 8.0): + if rating_val is not None: metadata_obj = data.get('Metadata', {}) rating_key = metadata_obj.get('ratingKey') title = metadata_obj.get('title', 'Titre inconnu') - # Connexion à Plex (commune aux deux actions) - token = get_admin_token() - if not token: - logging.error("Impossible de modifier : aucun token admin en cache.") - return "Erreur token admin", 500 - - try: - server = PlexServer(PLEX_URL, token=token) - item = server.fetchItem(int(rating_key)) - - # CAS 1 : Note de 0.5 étoile -> On ajoute à la collection - if rating_val == 1.0: - logging.info("L'utilisateur %s a mis 0,5 étoile à '%s'. Ajout à la collection...", username, title) - item.addCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🗑️ **Demande de suppression** reçue par {username} pour le film : **{title}**") - logging.info("Succès : '%s' a été ajouté à la collection '%s' !", title, WEBHOOK_COLLECTION) - return "Média ajouté à la collection", 200 - - # CAS 2 : Note de 4 étoiles ou plus -> On retire de la collection - elif rating_val >= 8.0: - # On calcule la note sur 5 pour l'affichage Discord - note_sur_5 = rating_val / 2 - logging.info("L'utilisateur %s a mis %s/5 à '%s'. Retrait de la collection...", username, note_sur_5, title) - item.removeCollection(WEBHOOK_COLLECTION) - send_to_discord(f"🛡️ **Annulation de la suppression** par {username} pour le film : **{title}** (Note modifiée : {note_sur_5}/5)") - logging.info("Succès : '%s' a été retiré de la collection '%s' !", title, WEBHOOK_COLLECTION) - return "Média retiré de la collection", 200 - - except Exception as e: - logging.error("Erreur lors de la modification de '%s' : %s", title, e) - return f"Erreur de modification : {e}", 500 + # On lance le travail de vérification en arrière-plan (Thread) + # pour renvoyer tout de suite "202 Accepted" à Plex et éviter un Timeout + threading.Thread(target=process_webhook_rating, args=(rating_val, rating_key, title, username)).start() + return "Vérification de la note en arrière-plan", 202 return "Événement ignoré", 200 @@ -486,12 +405,8 @@ def webhook(): # DÉMARRAGE # ------------------------------------------------------------------ if __name__ == "__main__": - logging.info("==== Démarrage combiné plex-watchlist-cleaner (web + sync) ====") - # Ne lance pas sync automatiquement ici — laisse le scheduler (cron) le faire, - # ou utilise /run_sync pour déclenchement manuel. + logging.info("==== Démarrage combiné plex-watchlist-cleaner ====") if os.getenv("RUN_SYNC_AT_STARTUP", "false").lower() in {"1", "true", "yes"}: - logging.info("Lancement initial de la synchro car RUN_SYNC_AT_STARTUP est activé.") sync_collections_once() - # Lancer le serveur Flask (il servira la page d'onboarding) - app.run(host="0.0.0.0", port=5000, debug=False) + app.run(host="0.0.0.0", port=5000, debug=False) \ No newline at end of file From ed0e90584917080200701555ebf7500775c3ccd4 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 19:10:03 +0100 Subject: [PATCH 21/23] Debug pour app Plex - Sync par token --- app.py | 65 +++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/app.py b/app.py index 7393fe6..cbcbaea 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,7 @@ - stockage tokens utilisateurs (/data/user_tokens.json) - si l'utilisateur connecté est l'admin (ADMIN_USERNAME), on met aussi à jour /data/plex_token.json - routine de sync des collections (fonction sync_collections_once) - - Logique de consensus (une note > 0.5 annule la suppression) + - Logique de consensus (une note >= 1/5 annule la suppression) """ import os @@ -228,6 +228,8 @@ def sync_ratings(): for u in users: username = u["username"] token = u["token"] + logging.info("--- Analyse du compte : %s ---", username) + try: if token == admin_token: user_server = admin_server @@ -239,28 +241,45 @@ def sync_ratings(): if section.type not in {"movie", "show"}: continue try: - # On identifie les demandes de suppression (0.5 étoile = 1.0) + # On demande les notes = 0.5 étoile bad_items = section.search(userRating=1.0) - # On identifie TOUT média protégé (plus de 0.5 étoile = >1.0) - protected_items = section.search(userRating__gt=1.0) + + # On demande TOUTES les notes >= 1 étoile (API Plex accepte mieux __gte que __gt) + good_items = section.search(userRating__gte=2.0) + + user_actual_ratings = bad_items + good_items + + if user_actual_ratings: + logging.info("Trouvé %d média(s) noté(s) pertinent(s) pour %s dans '%s'.", len(user_actual_ratings), username, section.title) + + for item in user_actual_ratings: + if item.userRating is None: continue + + rating_val = float(item.userRating) + logging.info("-> Examen de '%s' (Note trouvée : %s/10 par %s)", item.title, rating_val, username) - for item in bad_items: rk = item.ratingKey - if rk not in media_state: media_state[rk] = {'title': item.title, 'is_bad': False, 'bad_users': [], 'is_protected': False, 'protectors': []} - media_state[rk]['is_bad'] = True - media_state[rk]['bad_users'].append(username) + if rk not in media_state: + media_state[rk] = {'title': item.title, 'is_bad': False, 'bad_users': [], 'is_protected': False, 'protectors': []} - for item in protected_items: - rk = item.ratingKey - if rk not in media_state: media_state[rk] = {'title': item.title, 'is_bad': False, 'bad_users': [], 'is_protected': False, 'protectors': []} - media_state[rk]['is_protected'] = True - media_state[rk]['protectors'].append(f"{username} ({float(item.userRating)/2}/5)") + if rating_val == 1.0: + media_state[rk]['is_bad'] = True + media_state[rk]['bad_users'].append(username) + elif rating_val >= 2.0: + media_state[rk]['is_protected'] = True + media_state[rk]['protectors'].append(f"{username} ({rating_val/2}/5)") + + except Exception as e: + logging.warning("Erreur lors de la recherche dans %s : %s", section.title, e) - except Exception: - pass except Exception as e: logging.error("Erreur générale pour %s : %s", username, e) + logging.info("--- Phase de décision (Consensus global) ---") + if not media_state: + logging.info("Aucun média noté 0.5 ou protégé n'a été trouvé.") + return + # Une fois qu'on a l'avis de tout le monde, on prend les décisions for rk, state in media_state.items(): try: @@ -273,6 +292,8 @@ def sync_ratings(): logging.info("Consensus : Retrait de '%s' car protégé par %s", state['title'], state['protectors']) admin_item.removeCollection(WEBHOOK_COLLECTION) send_to_discord(f"🛡️ **Maintien sur le serveur** : La suppression de **{state['title']}** a été annulée grâce aux notes de : {', '.join(state['protectors'])}") + elif state['is_bad']: + logging.info("Consensus : '%s' demandé en suppression par %s, MAIS protégé par %s. Action ignorée.", state['title'], state['bad_users'], state['protectors']) elif state['is_bad']: # Uniquement des notes de 0.5, aucune protection @@ -282,7 +303,7 @@ def sync_ratings(): send_to_discord(f"🗑️ **Demande de suppression** validée pour **{state['title']}** (noté 0.5 par {', '.join(state['bad_users'])})") except Exception as e: - logging.error("Erreur lors de l'application du consensus pour %s : %s", rk, e) + logging.error("Erreur lors de l'application du consensus pour '%s' : %s", state['title'], e) def sync_collections_once(): sync_ratings() @@ -317,7 +338,7 @@ def sync_collections_once(): # ------------------------------------------------------------------ def process_webhook_rating(rating_val, rating_key, title, username): """Vérifie le consensus en arrière-plan suite à un webhook pour ne pas bloquer Plex""" - logging.info("Webhook reçu pour '%s'. Vérification globale...", title) + logging.info("Webhook reçu pour '%s' (Note: %s/10). Vérification globale...", title, rating_val) admin_token = get_admin_token() if not admin_token: return @@ -339,9 +360,11 @@ def process_webhook_rating(rating_val, rating_key, title, username): user_server = account.resource(server_name).connect() item = user_server.fetchItem(int(rating_key)) - if item.userRating is not None and float(item.userRating) > 1.0: - is_protected = True - protectors.append(f"{u['username']} ({float(item.userRating)/2}/5)") + if item.userRating is not None: + r_val = float(item.userRating) + if r_val >= 2.0: # >= 1 étoile + is_protected = True + protectors.append(f"{u['username']} ({r_val/2}/5)") except Exception: pass @@ -355,7 +378,7 @@ def process_webhook_rating(rating_val, rating_key, title, username): admin_item.removeCollection(WEBHOOK_COLLECTION) send_to_discord(f"🛡️ **Maintien sur le serveur** : La suppression de **{title}** est bloquée/annulée grâce aux notes de : {', '.join(protectors)}") elif rating_val == 1.0: - logging.info("Webhook : %s a noté 0.5, mais '%s' est protégé par %s. Ignoré.", username, title, protectors) + logging.info("Webhook : %s a noté 0.5, mais '%s' est protégé par %s. Action ignorée.", username, title, protectors) else: if rating_val == 1.0 and WEBHOOK_COLLECTION not in current_collections: admin_item.addCollection(WEBHOOK_COLLECTION) From 6112413336e184e610eb50ce3835187424c006b5 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 19:18:39 +0100 Subject: [PATCH 22/23] Debug pour app Plex - Sync par token --- app.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/app.py b/app.py index cbcbaea..2f10ef2 100644 --- a/app.py +++ b/app.py @@ -207,8 +207,8 @@ def remove_batch(guids): logging.error("Erreur pour %s : %s", user["username"], e) def sync_ratings(): - """Vérifie les notes, agrège les données, et applique un consensus global""" - logging.info("Démarrage de la vérification des notes (Polling avec Consensus)...") + """Vérifie les notes, agrège les données, et applique un consensus global en temps réel""" + logging.info("Démarrage de la vérification des notes (Polling Temps Réel avec Consensus)...") admin_token = get_admin_token() if not admin_token: return @@ -241,20 +241,19 @@ def sync_ratings(): if section.type not in {"movie", "show"}: continue try: - # On demande les notes = 0.5 étoile - bad_items = section.search(userRating=1.0) + # CORRECTION : On contourne l'index de recherche de Plex pour avoir du temps réel + all_items = section.all() - # On demande TOUTES les notes >= 1 étoile (API Plex accepte mieux __gte que __gt) - good_items = section.search(userRating__gte=2.0) - - user_actual_ratings = bad_items + good_items + # On filtre instantanément avec Python + user_actual_ratings = [ + item for item in all_items + if item.userRating is not None and (float(item.userRating) == 1.0 or float(item.userRating) >= 2.0) + ] if user_actual_ratings: logging.info("Trouvé %d média(s) noté(s) pertinent(s) pour %s dans '%s'.", len(user_actual_ratings), username, section.title) for item in user_actual_ratings: - if item.userRating is None: continue - rating_val = float(item.userRating) logging.info("-> Examen de '%s' (Note trouvée : %s/10 par %s)", item.title, rating_val, username) From 9c16d9d8bc0716777a8dc9ee4c38b1c1eb183a60 Mon Sep 17 00:00:00 2001 From: chwps Date: Sat, 28 Mar 2026 19:22:03 +0100 Subject: [PATCH 23/23] Debug pour app Plex - Sync par token --- app.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app.py b/app.py index 2f10ef2..cbcbaea 100644 --- a/app.py +++ b/app.py @@ -207,8 +207,8 @@ def remove_batch(guids): logging.error("Erreur pour %s : %s", user["username"], e) def sync_ratings(): - """Vérifie les notes, agrège les données, et applique un consensus global en temps réel""" - logging.info("Démarrage de la vérification des notes (Polling Temps Réel avec Consensus)...") + """Vérifie les notes, agrège les données, et applique un consensus global""" + logging.info("Démarrage de la vérification des notes (Polling avec Consensus)...") admin_token = get_admin_token() if not admin_token: return @@ -241,19 +241,20 @@ def sync_ratings(): if section.type not in {"movie", "show"}: continue try: - # CORRECTION : On contourne l'index de recherche de Plex pour avoir du temps réel - all_items = section.all() + # On demande les notes = 0.5 étoile + bad_items = section.search(userRating=1.0) - # On filtre instantanément avec Python - user_actual_ratings = [ - item for item in all_items - if item.userRating is not None and (float(item.userRating) == 1.0 or float(item.userRating) >= 2.0) - ] + # On demande TOUTES les notes >= 1 étoile (API Plex accepte mieux __gte que __gt) + good_items = section.search(userRating__gte=2.0) + + user_actual_ratings = bad_items + good_items if user_actual_ratings: logging.info("Trouvé %d média(s) noté(s) pertinent(s) pour %s dans '%s'.", len(user_actual_ratings), username, section.title) for item in user_actual_ratings: + if item.userRating is None: continue + rating_val = float(item.userRating) logging.info("-> Examen de '%s' (Note trouvée : %s/10 par %s)", item.title, rating_val, username)