Initial commit: homelab scripts (backup, system update, plex recap, cronmaster trim)

This commit is contained in:
2026-05-04 10:35:36 +02:00
commit 4846d9cc48
15 changed files with 1684 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
__pycache__/
*.pyc
*.log
system-update-config.json
+147
View File
@@ -0,0 +1,147 @@
# Récapitulatif hebdomadaire Plex pour Discord
Ce script génère et envoie automatiquement un récapitulatif hebdomadaire des nouveautés de votre serveur Plex sur Discord.
## Installation
Les dépendances Python (`requests` et `urllib3`) sont généralement déjà installées avec Python3. Si ce n'est pas le cas, installez-les :
```bash
# Option 1 : Via apt (recommandé sur Debian/Ubuntu)
apt install python3-pip
pip3 install -r /opt/container/scripts/requirements.txt
# Option 2 : Vérifier si les modules sont déjà disponibles
python3 -c "import requests, urllib3; print('Dépendances OK')"
```
## Configuration
1. **Créer un webhook Discord** :
- Allez dans les paramètres de votre salon Discord
- Intégrations > Webhooks > Nouveau Webhook
- Copiez l'URL du webhook
2. **Configurer le fichier de configuration** :
- Éditez `/opt/container/scripts/plex-recap-config.json`
- Ajoutez l'URL du webhook Discord dans `discord.webhook_url`
- Vérifiez que les URLs et clés API sont correctes
## Utilisation
Le script peut être utilisé de plusieurs façons pour envoyer des récapitulatifs séparés :
### Méthode 1 : Scripts wrapper (recommandé pour cronmaster)
Des scripts wrapper sont disponibles pour simplifier l'utilisation :
**Envoyer uniquement les films** :
```bash
/opt/container/scripts/plex-recap-movies.sh
```
**Envoyer uniquement les séries** :
```bash
/opt/container/scripts/plex-recap-series.sh
```
**Envoyer uniquement les demandes approuvées** :
```bash
/opt/container/scripts/plex-recap-requests.sh
```
### Méthode 2 : Arguments en ligne de commande
**Envoyer uniquement les films** :
```bash
python3 /opt/container/scripts/plex-weekly-recap.py --movies
```
**Envoyer uniquement les séries** :
```bash
python3 /opt/container/scripts/plex-weekly-recap.py --series
```
**Envoyer uniquement les demandes approuvées** :
```bash
python3 /opt/container/scripts/plex-weekly-recap.py --requests
```
**Envoyer tout (films, séries, demandes) - par défaut** :
```bash
python3 /opt/container/scripts/plex-weekly-recap.py --all
# ou simplement
python3 /opt/container/scripts/plex-weekly-recap.py
```
**Personnaliser le nombre de jours** :
```bash
python3 /opt/container/scripts/plex-weekly-recap.py --movies --days 14
```
## Planification
### Option 1 : Via cronmaster (recommandé)
Vous pouvez créer plusieurs tâches séparées dans cronmaster :
**Films uniquement** (chaque dimanche à 18h00) :
- **Commande** : `/opt/container/scripts/plex-recap-movies.sh` (ou `python3 /opt/container/scripts/plex-weekly-recap.py --movies`)
- **Planification** : `0 18 * * 0`
**Séries uniquement** (chaque lundi à 18h00) :
- **Commande** : `/opt/container/scripts/plex-recap-series.sh` (ou `python3 /opt/container/scripts/plex-weekly-recap.py --series`)
- **Planification** : `0 18 * * 1`
**Demandes uniquement** (chaque mardi à 18h00) :
- **Commande** : `/opt/container/scripts/plex-recap-requests.sh` (ou `python3 /opt/container/scripts/plex-weekly-recap.py --requests`)
- **Planification** : `0 18 * * 2`
**Tout** (chaque dimanche à 19h00) :
- **Commande** : `python3 /opt/container/scripts/plex-weekly-recap.py --all`
- **Planification** : `0 19 * * 0`
### Option 2 : Via crontab système
Ajoutez ces lignes dans votre crontab (`crontab -e`) :
```
# Films chaque dimanche à 18h00
0 18 * * 0 /opt/container/scripts/plex-recap-movies.sh
# Séries chaque lundi à 18h00
0 18 * * 1 /opt/container/scripts/plex-recap-series.sh
# Demandes chaque mardi à 18h00
0 18 * * 2 /opt/container/scripts/plex-recap-requests.sh
```
### Option 3 : Test manuel
Pour tester le script manuellement :
```bash
# Tester les films
python3 /opt/container/scripts/plex-weekly-recap.py --movies
# Tester les séries
python3 /opt/container/scripts/plex-weekly-recap.py --series
# Tester les demandes
python3 /opt/container/scripts/plex-weekly-recap.py --requests
```
## Format du message Discord
Le message Discord contiendra :
- 📺 **Nouveaux Films** : Liste des films ajoutés via Radarr
- 📺 **Nouvelles Séries** : Liste des séries ajoutées via Sonarr
-**Demandes approuvées** : Liste des demandes approuvées dans Jellyseerr
## Logs
Les logs sont écrits dans `/opt/container/scripts/plex-recap.log`
## Dépannage
- Vérifiez que les APIs de Jellyseerr, Radarr et Sonarr sont accessibles
- Vérifiez que l'URL du webhook Discord est correcte
- Consultez le fichier de log pour les erreurs détaillées
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
# ============================================================
# backup_immich_usb.sh — Backup Immich → clé USB /backup
# Exclut : postgres (géré séparément), thumbs, encoded-video
# ============================================================
SOURCE="/mnt/serveurNAS/Immich/"
DEST="/backup/immich/"
LOGFILE="/var/log/backup_immich_usb.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$DATE] === Début backup Immich → USB ===" >> "$LOGFILE"
# Monter la clé USB si elle ne l'est pas déjà (l'automount systemd peut être inactif après débranchement)
mount /backup 2>/dev/null || true
# Vérifier que la clé USB est bien montée
if ! mountpoint -q /backup; then
echo "[$DATE] ERREUR: /backup n'est pas monté (clé USB absente ?)" >> "$LOGFILE"
exit 1
fi
# Vérifier espace disponible (alerte si < 5 Go)
AVAIL=$(df /backup | awk 'NR==2 {print $4}')
AVAIL_GB=$((AVAIL / 1024 / 1024))
echo "[$DATE] Espace disponible sur clé USB: ${AVAIL_GB} Go" >> "$LOGFILE"
if [ "$AVAIL_GB" -lt 5 ]; then
echo "[$DATE] AVERTISSEMENT: Moins de 5 Go disponibles sur la clé USB !" >> "$LOGFILE"
fi
# Créer le dossier destination
mkdir -p "$DEST"
# Rsync — copie uniquement les fichiers modifiés
# Exclut : postgres (base de données), thumbs et encoded-video (régénérables)
rsync -av \
--no-perms --no-owner --no-group \
--exclude='postgres/' \
--exclude='thumbs/' \
--exclude='encoded-video/' \
--delete \
--progress \
--stats \
"$SOURCE" "$DEST" >> "$LOGFILE" 2>&1
EXIT_CODE=$?
DATE_END=$(date '+%Y-%m-%d %H:%M:%S')
if [ $EXIT_CODE -eq 0 ]; then
echo "[$DATE_END] ✅ Backup terminé avec succès" >> "$LOGFILE"
# Taille finale
SIZE=$(du -sh "$DEST" 2>/dev/null | cut -f1)
echo "[$DATE_END] Taille backup: $SIZE" >> "$LOGFILE"
else
echo "[$DATE_END] ❌ Backup terminé avec erreurs (code: $EXIT_CODE)" >> "$LOGFILE"
fi
echo "[$DATE_END] === Fin backup ===" >> "$LOGFILE"
echo "" >> "$LOGFILE"
exit $EXIT_CODE
+23
View File
@@ -0,0 +1,23 @@
{
"discord": {
"webhook_url": "https://discord.com/api/webhooks/1463183628157718634/6Tux8xOpA96vm_8mLgP1-izVzgmUV5PWxA5XUjNSSXlxwX8SHu-n4P3ZqGjumjhLcvtS",
"embed_color": 3447003
},
"jellyseerr": {
"url": "http://192.168.1.29:5055",
"api_key": "MTc2ODEzNzUzODQ5N2QyZTJlOTE1LWMzYTMtNGRhYi1iZmQwLWFiZDM1OTc3NzkzNQ=="
},
"radarr": {
"hostname": "192.168.1.29",
"port": 7878,
"api_key": "2208f084bcf94a7ba7020b3b4df2a695",
"use_ssl": false
},
"sonarr": {
"hostname": "192.168.1.29",
"port": 8989,
"api_key": "2998fffae00a43648e67d927d8536ed6",
"use_ssl": false
},
"days": 7
}
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Script wrapper pour envoyer uniquement les films
python3 /opt/container/scripts/plex-weekly-recap.py --movies
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Script wrapper pour envoyer uniquement les demandes approuvées
python3 /opt/container/scripts/plex-weekly-recap.py --requests
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
# Script wrapper pour envoyer uniquement les séries
python3 /opt/container/scripts/plex-weekly-recap.py --series
+797
View File
@@ -0,0 +1,797 @@
#!/usr/bin/env python3
"""
Script de récapitulatif hebdomadaire Plex pour Discord
Récupère les nouveautés de Jellyseerr, Radarr et Sonarr et les envoie sur Discord
Usage:
python3 plex-weekly-recap.py --movies # Envoie uniquement les films
python3 plex-weekly-recap.py --series # Envoie uniquement les séries
python3 plex-weekly-recap.py --requests # Envoie uniquement les demandes
python3 plex-weekly-recap.py --all # Envoie tout (par défaut)
"""
import json
import sys
import os
import argparse
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Configuration du chemin du fichier de config
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'plex-recap-config.json')
LOG_FILE = os.path.join(os.path.dirname(__file__), 'plex-recap.log')
def log(message: str, level: str = "INFO"):
"""Écrit un message dans le fichier de log"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message = f"[{timestamp}] [{level}] {message}\n"
try:
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(log_message)
except Exception as e:
print(f"Erreur lors de l'écriture du log: {e}")
if level == "ERROR":
print(log_message.strip())
def load_config() -> Dict:
"""Charge la configuration depuis le fichier JSON"""
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
return config
except FileNotFoundError:
log(f"Fichier de configuration non trouvé: {CONFIG_FILE}", "ERROR")
sys.exit(1)
except json.JSONDecodeError as e:
log(f"Erreur de parsing JSON dans la configuration: {e}", "ERROR")
sys.exit(1)
def create_session() -> requests.Session:
"""Crée une session requests avec retry automatique"""
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
def get_jellyseerr_requests(config: Dict, days: int = 7) -> List[Dict]:
"""Récupère les demandes approuvées de Jellyseerr des derniers jours"""
jellyseerr_config = config.get('jellyseerr', {})
url = jellyseerr_config.get('url', 'http://localhost:5055')
api_key = jellyseerr_config.get('api_key', '')
if not api_key:
log("Clé API Jellyseerr non configurée", "ERROR")
return []
# Calcul de la date limite
cutoff_date = datetime.now() - timedelta(days=days)
try:
session = create_session()
headers = {'X-Api-Key': api_key}
# Récupération de toutes les demandes disponibles
# On récupère toutes les demandes et on filtre ensuite
response = session.get(
f"{url}/api/v1/request?take=100",
headers=headers,
timeout=10
)
response.raise_for_status()
requests_data = response.json()
results = requests_data.get('results', [])
# Filtrage par date (demandes approuvées/disponibles dans les derniers jours)
filtered_requests = []
for req in results:
# Vérifier que la demande est disponible
status = req.get('status', 0)
# Status 2 = Available, Status 3 = Partially Available
if status in [2, 3]:
# Vérifier la date de modification ou d'approbation
modified = req.get('modified', '')
if modified:
try:
# Gérer différents formats de date
modified_str = modified.replace('Z', '+00:00')
if '.' in modified_str and '+' in modified_str:
# Format ISO avec timezone
modified_date = datetime.fromisoformat(modified_str)
else:
# Format ISO simple
modified_date = datetime.fromisoformat(modified_str.split('+')[0])
if modified_date.replace(tzinfo=None) >= cutoff_date:
filtered_requests.append(req)
except (ValueError, AttributeError) as e:
log(f"Erreur de parsing de date pour demande {req.get('id', 'unknown')}: {e}", "ERROR")
continue
log(f"Récupéré {len(filtered_requests)} demandes Jellyseerr des {days} derniers jours")
return filtered_requests
except requests.exceptions.RequestException as e:
log(f"Erreur lors de la récupération des demandes Jellyseerr: {e}", "ERROR")
return []
def get_radarr_movies(config: Dict, days: int = 7) -> List[Dict]:
"""Récupère les nouveaux films de Radarr des derniers jours"""
radarr_config = config.get('radarr', {})
hostname = radarr_config.get('hostname', 'localhost')
port = radarr_config.get('port', 7878)
api_key = radarr_config.get('api_key', '')
use_ssl = radarr_config.get('use_ssl', False)
if not api_key:
log("Clé API Radarr non configurée", "ERROR")
return []
protocol = 'https' if use_ssl else 'http'
url = f"{protocol}://{hostname}:{port}"
# Calcul de la date limite
cutoff_date = datetime.now() - timedelta(days=days)
try:
session = create_session()
params = {'apikey': api_key}
response = session.get(
f"{url}/api/v3/movie",
params=params,
timeout=10
)
response.raise_for_status()
movies = response.json()
# Filtrage par date d'ajout
new_movies = []
for movie in movies:
added = movie.get('added', '')
if added:
try:
added_date = datetime.fromisoformat(added.replace('Z', '+00:00'))
if added_date.replace(tzinfo=None) >= cutoff_date:
# Vérifier que le film est disponible
if movie.get('hasFile', False):
new_movies.append(movie)
except (ValueError, AttributeError):
pass
log(f"Récupéré {len(new_movies)} nouveaux films Radarr des {days} derniers jours")
return new_movies
except requests.exceptions.RequestException as e:
log(f"Erreur lors de la récupération des films Radarr: {e}", "ERROR")
return []
def get_sonarr_series(config: Dict, days: int = 7) -> List[Dict]:
"""Récupère les séries avec de nouveaux épisodes de Sonarr des derniers jours"""
sonarr_config = config.get('sonarr', {})
hostname = sonarr_config.get('hostname', 'localhost')
port = sonarr_config.get('port', 8989)
api_key = sonarr_config.get('api_key', '')
use_ssl = sonarr_config.get('use_ssl', False)
if not api_key:
log("Clé API Sonarr non configurée", "ERROR")
return []
protocol = 'https' if use_ssl else 'http'
url = f"{protocol}://{hostname}:{port}"
# Calcul de la date limite
cutoff_date = datetime.now() - timedelta(days=days)
log(f"Recherche d'épisodes ajoutés après le {cutoff_date.strftime('%Y-%m-%d %H:%M:%S')}")
try:
session = create_session()
params = {'apikey': api_key}
# D'abord récupérer toutes les séries pour avoir les infos
response_series = session.get(
f"{url}/api/v3/series",
params=params,
timeout=10
)
response_series.raise_for_status()
all_series = response_series.json()
series_map = {s.get('id'): s for s in all_series}
log(f"Récupéré {len(series_map)} séries depuis Sonarr")
# Utiliser l'historique pour trouver les épisodes récemment téléchargés
# L'historique contient les événements de téléchargement avec les dates
cutoff_timestamp = int(cutoff_date.timestamp())
history_params = {
'apikey': api_key,
'page': 1,
'pageSize': 1000,
'sortKey': 'date',
'sortDirection': 'descending'
}
response_history = session.get(
f"{url}/api/v3/history",
params=history_params,
timeout=10
)
response_history.raise_for_status()
history_data = response_history.json()
history_records = history_data.get('records', [])
log(f"Récupéré {len(history_records)} enregistrements d'historique depuis Sonarr")
# Filtrer les épisodes téléchargés récemment
new_episodes_by_series = {}
event_types_found = set()
for record in history_records:
event_type = record.get('eventType', '')
event_types_found.add(event_type)
# Vérifier que c'est un événement de téléchargement d'épisode
# Les types peuvent varier selon la version de Sonarr
if event_type in ['downloadFolderImported', 'grabbed', 'episodeFileDeleted', 'episodeFileRenamed']:
date_str = record.get('date', '')
if date_str:
try:
# Gérer différents formats de date
date_str_clean = date_str.replace('Z', '+00:00')
if '.' in date_str_clean and '+' in date_str_clean:
event_date = datetime.fromisoformat(date_str_clean)
else:
event_date = datetime.fromisoformat(date_str_clean.split('+')[0])
if event_date.replace(tzinfo=None) >= cutoff_date:
# Récupérer les infos de l'épisode depuis l'historique
episode_info = record.get('episode', {})
if not episode_info:
# Parfois l'info est directement dans le record
episode_info = record
series_id = episode_info.get('seriesId', 0)
if series_id and series_id in series_map:
series_info = series_map[series_id]
if series_id not in new_episodes_by_series:
# Récupérer la langue originale
original_language = series_info.get('originalLanguage', {})
language_name = original_language.get('name', '') if isinstance(original_language, dict) else ''
new_episodes_by_series[series_id] = {
'seriesId': series_id,
'title': series_info.get('title', 'Titre inconnu'),
'year': series_info.get('year', ''),
'originalLanguage': language_name,
'episodeCount': 0,
'episodes': []
}
new_episodes_by_series[series_id]['episodeCount'] += 1
except (ValueError, AttributeError) as e:
log(f"Erreur de parsing de date pour historique: {date_str} - {e}", "ERROR")
pass
log(f"Types d'événements trouvés dans l'historique: {', '.join(sorted(event_types_found))}")
log(f"Épisodes trouvés dans l'historique: {len(new_episodes_by_series)} séries")
# Si l'historique ne donne pas assez d'infos, parcourir les séries et leurs épisodes
if len(new_episodes_by_series) == 0:
log("Aucun épisode trouvé dans l'historique, parcours des séries...")
series_checked = 0
episodes_found_in_series = 0
for series in all_series:
series_id = series.get('id', 0)
if not series_id:
continue
series_checked += 1
# Récupérer les épisodes de cette série
episode_params = {'apikey': api_key, 'seriesId': series_id}
try:
response_episodes = session.get(
f"{url}/api/v3/episode",
params=episode_params,
timeout=10
)
response_episodes.raise_for_status()
episodes = response_episodes.json()
# Filtrer les épisodes récemment ajoutés
for episode in episodes:
if episode.get('hasFile', False):
episode_file = episode.get('episodeFile', {})
if episode_file:
date_added = episode_file.get('dateAdded', '')
if date_added:
try:
date_str_clean = date_added.replace('Z', '+00:00')
if '.' in date_str_clean and '+' in date_str_clean:
added_date = datetime.fromisoformat(date_str_clean)
else:
added_date = datetime.fromisoformat(date_str_clean.split('+')[0])
if added_date.replace(tzinfo=None) >= cutoff_date:
episodes_found_in_series += 1
if series_id not in new_episodes_by_series:
# Récupérer la langue originale
original_language = series.get('originalLanguage', {})
language_name = original_language.get('name', '') if isinstance(original_language, dict) else ''
new_episodes_by_series[series_id] = {
'seriesId': series_id,
'title': series.get('title', 'Titre inconnu'),
'year': series.get('year', ''),
'originalLanguage': language_name,
'episodeCount': 0,
'episodes': []
}
new_episodes_by_series[series_id]['episodeCount'] += 1
except (ValueError, AttributeError) as e:
log(f"Erreur de parsing date épisode: {date_added} - {e}", "ERROR")
pass
except requests.exceptions.RequestException as e:
log(f"Erreur lors de la récupération des épisodes pour série {series_id}: {e}", "ERROR")
continue
log(f"Parcours terminé: {series_checked} séries vérifiées, {episodes_found_in_series} épisodes trouvés")
# Convertir en liste
new_series = list(new_episodes_by_series.values())
log(f"Récupéré {len(new_series)} séries avec nouveaux épisodes Sonarr des {days} derniers jours ({sum(s['episodeCount'] for s in new_series)} épisodes au total)")
return new_series
except requests.exceptions.RequestException as e:
log(f"Erreur lors de la récupération des séries Sonarr: {e}", "ERROR")
if hasattr(e, 'response') and e.response is not None:
try:
log(f"Réponse API: {e.response.text[:200]}", "ERROR")
except:
pass
return []
def format_movie_title(movie: Dict) -> str:
"""Formate le titre d'un film avec année et qualité"""
title = movie.get('title', 'Titre inconnu')
year = movie.get('year', '')
quality = ''
# Récupération de la qualité depuis le fichier
movie_file = movie.get('movieFile', {})
if movie_file:
quality_profile = movie_file.get('quality', {})
if quality_profile:
quality_name = quality_profile.get('quality', {}).get('name', '')
if quality_name:
quality = f" ({quality_name})"
if year:
return f"**{title}** ({year}){quality}"
return f"**{title}**{quality}"
def format_series_title(series: Dict) -> str:
"""Formate le titre d'une série avec année et nombre d'épisodes"""
title = series.get('title', 'Titre inconnu')
year = series.get('year', '')
episode_count = series.get('episodeCount', 0)
if year:
return f"**{title}** ({year}) - {episode_count} épisode(s)"
return f"**{title}** - {episode_count} épisode(s)"
def format_request_title(request: Dict) -> str:
"""Formate le titre d'une demande Jellyseerr"""
media = request.get('media', {})
media_type = request.get('type', '')
if media_type == 'movie':
title = media.get('title', 'Titre inconnu')
year = media.get('releaseDate', '')
if year:
year = year[:4] # Extraire l'année
return f"**{title}** ({year})"
return f"**{title}**"
else: # TV show
title = media.get('name', 'Titre inconnu')
season = request.get('seasons', [])
if season:
season_num = season[0].get('seasonNumber', '')
return f"**{title}** - Saison {season_num}"
return f"**{title}**"
def split_text_field(text: str, max_length: int = 1024) -> List[str]:
"""Divise un texte en plusieurs parties si trop long pour Discord"""
if len(text) <= max_length:
return [text]
parts = []
lines = text.split('\n')
current_part = ""
for line in lines:
# Si la ligne seule dépasse la limite, on la tronque
if len(line) > max_length:
if current_part:
parts.append(current_part)
current_part = ""
# Tronquer la ligne
parts.append(line[:max_length-3] + "...")
# Si ajouter la ligne dépasse la limite, sauvegarder la partie actuelle
elif len(current_part) + len(line) + 1 > max_length:
if current_part:
parts.append(current_part)
current_part = line
else:
# Ajouter la ligne à la partie actuelle
if current_part:
current_part += "\n" + line
else:
current_part = line
if current_part:
parts.append(current_part)
return parts
def create_movies_embed(config: Dict, movies: List[Dict], days: int = 7) -> List[Dict]:
"""Crée l'embed Discord pour les films uniquement"""
embed_color = config.get('discord', {}).get('embed_color', 0x00ff00)
# Calcul des dates de la semaine
now = datetime.now()
week_start = now - timedelta(days=days)
week_start_str = week_start.strftime("%d/%m/%Y")
week_end_str = now.strftime("%d/%m/%Y")
embeds = []
if movies:
movies_text = "\n".join([format_movie_title(m) for m in movies])
movies_parts = split_text_field(movies_text, 1024)
for i, part in enumerate(movies_parts):
embed = {
"title": "🎬 Nouveaux Films Plex" if i == 0 else "🎬 Nouveaux Films Plex (suite)",
"description": f"Films ajoutés du {week_start_str} au {week_end_str}" if i == 0 else "",
"color": embed_color,
"timestamp": now.isoformat(),
"fields": [{
"name": f"🎬 Nouveaux Films ({len(movies)})" if i == 0 else f"🎬 Suite ({len(movies)} au total)",
"value": part,
"inline": False
}],
"footer": {
"text": "Récapitulatif automatique Plex"
}
}
embeds.append(embed)
else:
# Aucun film
embed = {
"title": "🎬 Nouveaux Films Plex",
"description": f"Films ajoutés du {week_start_str} au {week_end_str}",
"color": embed_color,
"timestamp": now.isoformat(),
"fields": [{
"name": "️ Aucun nouveau film",
"value": "Aucun nouveau film cette semaine.",
"inline": False
}],
"footer": {
"text": "Récapitulatif automatique Plex"
}
}
embeds.append(embed)
return embeds
def create_series_embed(config: Dict, series: List[Dict], days: int = 7) -> List[Dict]:
"""Crée l'embed Discord pour les séries uniquement, séparées en séries normales et manga"""
embed_color = config.get('discord', {}).get('embed_color', 0x00ff00)
# Calcul des dates de la semaine
now = datetime.now()
week_start = now - timedelta(days=days)
week_start_str = week_start.strftime("%d/%m/%Y")
week_end_str = now.strftime("%d/%m/%Y")
embeds = []
if series:
# Séparer les séries normales et les manga (Japanese/Chinese)
normal_series = []
manga_series = []
for s in series:
original_language = s.get('originalLanguage', '').lower()
if original_language in ['japanese', 'chinese']:
manga_series.append(s)
else:
normal_series.append(s)
# Créer l'embed principal
main_embed = {
"title": "📺 Nouveautés Séries Plex",
"description": f"Séries ajoutées du {week_start_str} au {week_end_str}",
"color": embed_color,
"timestamp": now.isoformat(),
"fields": [],
"footer": {
"text": "Récapitulatif automatique Plex"
}
}
# Section Séries normales
if normal_series:
normal_text = "\n".join([format_series_title(s) for s in normal_series])
normal_parts = split_text_field(normal_text, 1024)
for i, part in enumerate(normal_parts):
if i == 0:
main_embed["fields"].append({
"name": f"📺 Nouveautés Séries ({len(normal_series)})",
"value": part,
"inline": False
})
else:
# Créer un embed supplémentaire pour la suite
if len(embeds) == 0:
embeds.append({
"color": embed_color,
"fields": []
})
embeds[0]["fields"].append({
"name": f"📺 Nouveautés Séries (suite)",
"value": part,
"inline": False
})
# Section Manga
if manga_series:
manga_text = "\n".join([format_series_title(s) for s in manga_series])
manga_parts = split_text_field(manga_text, 1024)
for i, part in enumerate(manga_parts):
if i == 0:
main_embed["fields"].append({
"name": f"🏮 Nouveautés Manga ({len(manga_series)})",
"value": part,
"inline": False
})
else:
# Utiliser le dernier embed ou en créer un nouveau
if len(embeds) == 0:
embeds.append({
"color": embed_color,
"fields": []
})
embeds[0]["fields"].append({
"name": f"🏮 Nouveautés Manga (suite)",
"value": part,
"inline": False
})
# Si aucune série dans aucune catégorie (ne devrait pas arriver)
if not normal_series and not manga_series:
main_embed["fields"].append({
"name": "️ Aucune Nouveautés série",
"value": "Aucune Nouveautés série cette semaine.",
"inline": False
})
embeds.insert(0, main_embed)
else:
# Aucune série
embed = {
"title": "📺 Nouveautés Séries Plex",
"description": f"Séries ajoutées du {week_start_str} au {week_end_str}",
"color": embed_color,
"timestamp": now.isoformat(),
"fields": [{
"name": "️ Aucune Nouveautés série",
"value": "Aucune Nouveautés série cette semaine.",
"inline": False
}],
"footer": {
"text": "Récapitulatif automatique Plex"
}
}
embeds.append(embed)
return embeds
def create_requests_embed(config: Dict, requests: List[Dict], days: int = 7) -> List[Dict]:
"""Crée l'embed Discord pour les demandes approuvées uniquement"""
embed_color = config.get('discord', {}).get('embed_color', 0x00ff00)
# Calcul des dates de la semaine
now = datetime.now()
week_start = now - timedelta(days=days)
week_start_str = week_start.strftime("%d/%m/%Y")
week_end_str = now.strftime("%d/%m/%Y")
embeds = []
if requests:
requests_text = "\n".join([format_request_title(r) for r in requests])
requests_parts = split_text_field(requests_text, 1024)
for i, part in enumerate(requests_parts):
embed = {
"title": "✅ Demandes approuvées Jellyseerr" if i == 0 else "✅ Demandes approuvées Jellyseerr (suite)",
"description": f"Demandes approuvées du {week_start_str} au {week_end_str}" if i == 0 else "",
"color": embed_color,
"timestamp": now.isoformat(),
"fields": [{
"name": f"✅ Demandes approuvées ({len(requests)})" if i == 0 else f"✅ Suite ({len(requests)} au total)",
"value": part,
"inline": False
}],
"footer": {
"text": "Récapitulatif automatique Plex"
}
}
embeds.append(embed)
else:
# Aucune demande
embed = {
"title": "✅ Demandes approuvées Jellyseerr",
"description": f"Demandes approuvées du {week_start_str} au {week_end_str}",
"color": embed_color,
"timestamp": now.isoformat(),
"fields": [{
"name": "️ Aucune demande approuvée",
"value": "Aucune demande approuvée cette semaine.",
"inline": False
}],
"footer": {
"text": "Récapitulatif automatique Plex"
}
}
embeds.append(embed)
return embeds
def send_discord_webhook(config: Dict, embeds: List[Dict]) -> bool:
"""Envoie le message Discord via webhook (peut envoyer plusieurs embeds)"""
discord_config = config.get('discord', {})
webhook_url = discord_config.get('webhook_url', '')
if not webhook_url:
log("URL du webhook Discord non configurée", "ERROR")
return False
# Discord limite à 10 embeds par message
max_embeds_per_message = 10
try:
session = create_session()
# Diviser les embeds en groupes de 10
for i in range(0, len(embeds), max_embeds_per_message):
embeds_batch = embeds[i:i + max_embeds_per_message]
payload = {
"embeds": embeds_batch
}
response = session.post(
webhook_url,
json=payload,
timeout=10
)
response.raise_for_status()
log(f"Message Discord envoyé avec succès ({len(embeds_batch)} embed(s))")
return True
except requests.exceptions.RequestException as e:
log(f"Erreur lors de l'envoi du message Discord: {e}", "ERROR")
if hasattr(e, 'response') and e.response is not None:
try:
error_detail = e.response.json()
log(f"Détails de l'erreur Discord: {error_detail}", "ERROR")
except:
log(f"Réponse brute: {e.response.text[:200]}", "ERROR")
return False
def send_movies(config: Dict, days: int = 7) -> bool:
"""Envoie uniquement les films sur Discord"""
log("Récupération des films...")
movies = get_radarr_movies(config, days)
embeds = create_movies_embed(config, movies, days)
return send_discord_webhook(config, embeds)
def send_series(config: Dict, days: int = 7) -> bool:
"""Envoie uniquement les séries sur Discord"""
log("Récupération des séries...")
series = get_sonarr_series(config, days)
embeds = create_series_embed(config, series, days)
return send_discord_webhook(config, embeds)
def send_requests(config: Dict, days: int = 7) -> bool:
"""Envoie uniquement les demandes approuvées sur Discord"""
log("Récupération des demandes...")
requests = get_jellyseerr_requests(config, days)
embeds = create_requests_embed(config, requests, days)
return send_discord_webhook(config, embeds)
def send_all(config: Dict, days: int = 7) -> bool:
"""Envoie tous les récapitulatifs (films, séries, demandes) sur Discord"""
log("Récupération de toutes les données...")
movies = get_radarr_movies(config, days)
series = get_sonarr_series(config, days)
requests = get_jellyseerr_requests(config, days)
# Envoyer chaque type séparément
success_movies = send_movies(config, days)
success_series = send_series(config, days)
success_requests = send_requests(config, days)
return success_movies and success_series and success_requests
def main():
"""Fonction principale"""
parser = argparse.ArgumentParser(
description='Récapitulatif hebdomadaire Plex pour Discord',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Exemples:
python3 plex-weekly-recap.py --movies # Envoie uniquement les films
python3 plex-weekly-recap.py --series # Envoie uniquement les séries
python3 plex-weekly-recap.py --requests # Envoie uniquement les demandes
python3 plex-weekly-recap.py --all # Envoie tout (par défaut)
"""
)
parser.add_argument('--movies', action='store_true', help='Envoyer uniquement les films')
parser.add_argument('--series', action='store_true', help='Envoyer uniquement les séries')
parser.add_argument('--requests', action='store_true', help='Envoyer uniquement les demandes approuvées')
parser.add_argument('--all', action='store_true', help='Envoyer tout (films, séries, demandes)')
parser.add_argument('--days', type=int, default=None, help='Nombre de jours à considérer (défaut: depuis config)')
args = parser.parse_args()
log("Démarrage du récapitulatif hebdomadaire Plex")
# Chargement de la configuration
config = load_config()
# Nombre de jours à considérer
days = args.days if args.days is not None else config.get('days', 7)
success = False
# Déterminer ce qu'on doit envoyer
if args.movies:
log("Mode: Films uniquement")
success = send_movies(config, days)
elif args.series:
log("Mode: Séries uniquement")
success = send_series(config, days)
elif args.requests:
log("Mode: Demandes uniquement")
success = send_requests(config, days)
else:
# Par défaut, envoyer tout
log("Mode: Tout (films, séries, demandes)")
success = send_all(config, days)
if success:
log("Récapitulatif envoyé avec succès")
sys.exit(0)
else:
log("Échec de l'envoi du récapitulatif", "ERROR")
sys.exit(1)
if __name__ == "__main__":
main()
+2
View File
@@ -0,0 +1,2 @@
requests>=2.31.0
urllib3>=2.0.0
Executable
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# Script pour configurer la tâche cron hebdomadaire
# Ce script ajoute une tâche cron pour exécuter le récapitulatif chaque dimanche à 18h00
SCRIPT_DIR="/opt/container/scripts"
SCRIPT_PATH="$SCRIPT_DIR/plex-weekly-recap.py"
CRON_JOB="0 18 * * 0 python3 $SCRIPT_PATH"
# Vérifier si la tâche existe déjà
if crontab -l 2>/dev/null | grep -q "$SCRIPT_PATH"; then
echo "La tâche cron existe déjà."
echo "Pour la modifier, utilisez: crontab -e"
else
# Ajouter la tâche cron
(crontab -l 2>/dev/null; echo "$CRON_JOB") | crontab -
echo "Tâche cron ajoutée avec succès !"
echo "Le script s'exécutera chaque dimanche à 18h00"
echo ""
echo "Pour voir les tâches cron : crontab -l"
echo "Pour éditer les tâches cron : crontab -e"
fi
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
Script de synchronisation des dépôts Gitea vers GitHub
Synchronise tous les dépôts depuis http://192.168.1.29:3500/perco vers GitHub (percolouco)
"""
import os
import sys
import json
import subprocess
import tempfile
import shutil
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
# Configuration
GITEA_URL = "http://192.168.1.29:3500"
GITEA_USER = "perco"
GITHUB_USER = "percolouco"
# Les tokens doivent être configurés comme secrets dans Cursor Dashboard
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
def log(message):
"""Affiche un message avec timestamp"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] {message}")
def api_request(url, token=None, method="GET", data=None):
"""Effectue une requête HTTP avec gestion du token"""
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"token {token}"
if data:
headers["Content-Type"] = "application/json"
data = json.dumps(data).encode('utf-8')
req = Request(url, headers=headers, data=data, method=method)
try:
with urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except HTTPError as e:
log(f"Erreur HTTP {e.code} pour {url}: {e.read().decode('utf-8')}")
return None
except URLError as e:
log(f"Erreur URL pour {url}: {e.reason}")
return None
def get_gitea_repos():
"""Liste tous les dépôts de l'utilisateur Gitea"""
log(f"Récupération des dépôts Gitea pour {GITEA_USER}...")
url = f"{GITEA_URL}/api/v1/repos/search?limit=50&owner={GITEA_USER}"
repos_data = api_request(url, token=GITEA_TOKEN)
repos = repos_data.get("data", []) if repos_data else None
if repos is None:
log("Erreur lors de la récupération des dépôts Gitea")
return []
log(f"Trouvé {len(repos)} dépôts Gitea")
return repos
def github_repo_exists(repo_name):
"""Vérifie si un dépôt existe sur GitHub"""
url = f"https://api.github.com/repos/{GITHUB_USER}/{repo_name}"
result = api_request(url, token=GITHUB_TOKEN)
return result is not None
def create_github_repo(repo_name, description="", is_private=False):
"""Crée un dépôt sur GitHub"""
log(f"Création du dépôt GitHub: {repo_name}")
url = "https://api.github.com/user/repos"
data = {
"name": repo_name,
"description": description,
"private": is_private,
"auto_init": False
}
result = api_request(url, token=GITHUB_TOKEN, method="POST", data=data)
return result is not None
def sync_repo(gitea_repo):
"""Synchronise un dépôt Gitea vers GitHub en mode mirror"""
repo_name = gitea_repo["name"]
log(f"Début de la synchronisation: {repo_name}")
# Construire l'URL clone avec token
clone_url = gitea_repo["clone_url"]
if GITEA_TOKEN:
# Insérer le token dans l'URL
clone_url = clone_url.replace("http://", f"http://oauth2:{GITEA_TOKEN}@")
# URL GitHub avec token
github_url = f"https://{GITHUB_TOKEN}@github.com/{GITHUB_USER}/{repo_name}.git"
# Créer le dépôt GitHub s'il n'existe pas
if not github_repo_exists(repo_name):
description = gitea_repo.get("description", "")
is_private = gitea_repo.get("private", False)
if not create_github_repo(repo_name, description, is_private):
log(f"Échec de la création du dépôt GitHub: {repo_name}")
return False
# Créer un répertoire temporaire
temp_dir = tempfile.mkdtemp(prefix=f"gitea_sync_{repo_name}_")
try:
log(f"Clone mirror depuis Gitea: {repo_name}")
# Clone en mode mirror
result = subprocess.run(
["git", "clone", "--mirror", clone_url, temp_dir],
capture_output=True,
text=True,
timeout=300
)
if result.returncode != 0:
log(f"Erreur lors du clone: {result.stderr}")
return False
log(f"Push mirror vers GitHub: {repo_name}")
# Push en mode mirror vers GitHub (hors refs/pull/* non supportées par GitHub)
result = subprocess.run(
["git", "push", "--prune",
github_url,
"+refs/heads/*:refs/heads/*",
"+refs/tags/*:refs/tags/*"],
cwd=temp_dir,
capture_output=True,
text=True,
timeout=300
)
if result.returncode != 0:
log(f"Erreur lors du push: {result.stderr}")
return False
log(f"✓ Synchronisation réussie: {repo_name}")
return True
except subprocess.TimeoutExpired:
log(f"Timeout lors de la synchronisation de {repo_name}")
return False
except Exception as e:
log(f"Erreur lors de la synchronisation de {repo_name}: {e}")
return False
finally:
# Nettoyer le répertoire temporaire
try:
shutil.rmtree(temp_dir)
except Exception as e:
log(f"Erreur lors du nettoyage de {temp_dir}: {e}")
def main():
"""Fonction principale"""
log("=== Début de la synchronisation Gitea → GitHub ===")
# Vérifier les tokens
if not GITEA_TOKEN:
log("ATTENTION: GITEA_TOKEN non défini, l'accès aux dépôts privés peut échouer")
if not GITHUB_TOKEN:
log("ERREUR: GITHUB_TOKEN non défini (requis)")
sys.exit(1)
# Récupérer la liste des dépôts
repos = get_gitea_repos()
if not repos:
log("Aucun dépôt à synchroniser")
return
# Synchroniser chaque dépôt
success_count = 0
fail_count = 0
for repo in repos:
if sync_repo(repo):
success_count += 1
else:
fail_count += 1
# Résumé
log("=== Synchronisation terminée ===")
log(f"Réussis: {success_count}/{len(repos)}")
log(f"Échecs: {fail_count}/{len(repos)}")
if fail_count > 0:
sys.exit(1)
if __name__ == "__main__":
main()
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# Script de mise à jour complète : Debian + npm
set -euo pipefail
echo "========================================="
echo "Mise à jour système complète"
echo "Timestamp : $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================="
echo ""
# ===== CAPTURE VERSIONS AVANT =====
echo "📊 Versions AVANT mise à jour :"
echo " npm: $(npm -v 2>/dev/null || echo 'N/A')"
echo ""
# ===== DEBIAN UPDATES =====
echo "🔄 Mise à jour Debian (hôte via nsenter)..."
NSENTER="nsenter --target 1 --mount --uts --ipc --net --pid -- env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
$NSENTER dpkg --configure -a
$NSENTER apt update
$NSENTER apt full-upgrade -y
$NSENTER apt autoremove -y
$NSENTER apt autoclean
echo ""
# ===== NPM UPDATES =====
echo "🔄 Mise à jour npm globaux..."
npm install -g npm@latest || echo "⚠️ npm update skipped (non-fatal)"
echo ""
# ===== CAPTURE VERSIONS APRÈS =====
echo "📊 Versions APRÈS mise à jour :"
NEW_NPM=$(npm -v 2>/dev/null || echo 'N/A')
echo " npm: $NEW_NPM"
echo ""
echo "========================================="
echo "✅ Mise à jour complète terminée"
echo "========================================="
+248
View File
@@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""
Script de récapitulatif DÉTAILLÉ des mises à jour système
Affiche packages Debian mis à jour
"""
import json
import sys
import os
import re
import argparse
from datetime import datetime
from typing import Dict, List, Optional, Tuple
import requests
from pathlib import Path
CRONMASTER_LOGS_DIR = "/opt/container/cronmaster/data/logs"
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'system-update-config.json')
LOG_FILE = os.path.join(os.path.dirname(__file__), 'system-update-recap.log')
def log(message: str, level: str = "INFO"):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message = f"[{timestamp}] [{level}] {message}\n"
try:
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(log_message)
except Exception:
pass
if level in ["ERROR", "INFO"]:
print(log_message.strip())
def load_config() -> Dict:
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
log(f"Erreur config: {e}", "ERROR")
sys.exit(1)
def find_latest_update_job() -> Optional[Tuple[str, str]]:
log("Recherche du dernier job...")
logs_path = Path(CRONMASTER_LOGS_DIR)
if not logs_path.exists():
return None
latest_job = None
latest_time = None
for job_dir in sorted(logs_path.iterdir(), reverse=True):
if not job_dir.is_dir():
continue
for log_file in sorted(job_dir.iterdir(), reverse=True):
if not log_file.suffix == '.log':
continue
try:
try:
with open(log_file, 'r', encoding='utf-8') as f:
content = f.read()
except UnicodeDecodeError:
with open(log_file, 'r', encoding='latin-1') as f:
content = f.read()
if 'apt update' in content or 'apt full-upgrade' in content or 'Mise à jour système complète' in content or 'system-full-update' in content:
timestamp_match = re.search(r'Timestamp\s*:\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})', content)
if timestamp_match:
timestamp_str = timestamp_match.group(1)
timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
if latest_time is None or timestamp > latest_time:
latest_time = timestamp
latest_job = (job_dir.name, str(log_file))
except Exception:
continue
if latest_job:
log(f"Job trouvé: {latest_job[0]}")
return latest_job
def parse_package_upgrades(content: str) -> List[Dict]:
packages = []
for line in content.split('\n'):
unpacking = re.search(r'Unpacking\s+([^\s]+)\s+\(([^)]+)\)\s+over\s+\(([^)]+)\)', line)
if unpacking:
packages.append({
'name': unpacking.group(1),
'old_version': unpacking.group(3),
'new_version': unpacking.group(2)
})
continue
setting_up = re.search(r'Paramétrage de\s+([^\s]+)\s+\(([^)]+)\)', line)
if setting_up:
pkg_name = setting_up.group(1)
pkg_version = setting_up.group(2)
if not any(p['name'] == pkg_name for p in packages):
packages.append({
'name': pkg_name,
'old_version': None,
'new_version': pkg_version
})
return packages
def parse_update_log(log_file_path: str) -> Dict:
log(f"Parsing: {log_file_path}")
try:
try:
with open(log_file_path, 'r', encoding='utf-8') as f:
content = f.read()
except UnicodeDecodeError:
with open(log_file_path, 'r', encoding='latin-1') as f:
content = f.read()
except Exception as e:
log(f"Erreur lecture: {e}", "ERROR")
return {}
result = {
'timestamp': None,
'duration': None,
'exit_code': None,
'status': None,
'packages_upgraded': 0,
'package_details': []
}
timestamp_match = re.search(r'Timestamp\s*:\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})', content)
if timestamp_match:
result['timestamp'] = timestamp_match.group(1)
duration_match = re.search(r'Duration\s*:\s*(\d+)s', content)
if duration_match:
result['duration'] = int(duration_match.group(1))
exit_code_match = re.search(r'Exit Code\s*:\s*(\d+)', content)
if exit_code_match:
result['exit_code'] = int(exit_code_match.group(1))
status_match = re.search(r'Status\s*:\s*(\w+)', content)
if status_match:
result['status'] = status_match.group(1)
result['package_details'] = parse_package_upgrades(content)
result['packages_upgraded'] = len(result['package_details'])
return result
def send_to_discord(config: Dict, update_info: Dict) -> bool:
log("Envoi Discord...")
discord_config = config.get('discord', {})
webhook_url = discord_config.get('webhook_url', '')
if not webhook_url or webhook_url == "WEBHOOK_URL_HERE":
log("Webhook non configuré", "ERROR")
return False
try:
status_emoji = "" if update_info['status'] == 'SUCCESS' else ""
duration_str = f"{update_info['duration']}s" if update_info['duration'] < 60 else f"{update_info['duration']//60}m {update_info['duration']%60}s"
fields = []
fields.append({
"name": "📊 Status",
"value": f"{status_emoji} {update_info['status']}\n⏱️ {duration_str}",
"inline": True
})
pkg_details = update_info.get('package_details', [])
if pkg_details:
pkg_list = []
for pkg in pkg_details[:10]:
if pkg.get('old_version'):
pkg_list.append(f"{pkg['name']}: {pkg['old_version']}{pkg['new_version']}")
else:
pkg_list.append(f"{pkg['name']}: {pkg['new_version']}")
if len(pkg_details) > 10:
pkg_list.append(f"... et {len(pkg_details) - 10} autres")
fields.append({
"name": f"📦 Packages Debian ({len(pkg_details)} mis à jour)",
"value": "\n".join(pkg_list),
"inline": False
})
else:
fields.append({
"name": "📦 Packages Debian",
"value": "Aucune mise à jour",
"inline": False
})
embed = {
"title": f"{status_emoji} Mise à jour système complète",
"description": f"Exécutée le {update_info['timestamp']}",
"color": 5814783,
"fields": fields,
"timestamp": datetime.now().isoformat(),
"footer": {"text": "Récapitulatif détaillé système"}
}
response = requests.post(webhook_url, json={"embeds": [embed]}, timeout=10)
response.raise_for_status()
log("Envoyé ✅")
return True
except Exception as e:
log(f"Erreur Discord: {e}", "ERROR")
return False
def main():
parser = argparse.ArgumentParser(description='Récap détaillé mises à jour')
parser.add_argument('--job-id', type=str)
parser.add_argument('--auto', action='store_true')
args = parser.parse_args()
config = load_config()
if args.job_id:
job_dir = Path(CRONMASTER_LOGS_DIR) / args.job_id
log_files = sorted(job_dir.glob('*.log'), reverse=True)
log_file = str(log_files[0]) if log_files else None
else:
result = find_latest_update_job()
if not result:
log("Aucun job trouvé", "ERROR")
sys.exit(1)
_, log_file = result
if not log_file:
log("Log introuvable", "ERROR")
sys.exit(1)
update_info = parse_update_log(log_file)
if not update_info.get('timestamp'):
log("Parse impossible", "ERROR")
sys.exit(1)
success = send_to_discord(config, update_info)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
Executable
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""
Script de test pour valider la configuration du récapitulatif Plex
"""
import json
import sys
import os
import requests
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'plex-recap-config.json')
def test_config():
"""Teste la configuration"""
print("🔍 Test de la configuration...\n")
# Charger la configuration
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
except FileNotFoundError:
print(f"❌ Fichier de configuration non trouvé: {CONFIG_FILE}")
return False
except json.JSONDecodeError as e:
print(f"❌ Erreur de parsing JSON: {e}")
return False
print("✅ Fichier de configuration chargé\n")
# Test Discord webhook
discord_config = config.get('discord', {})
webhook_url = discord_config.get('webhook_url', '')
if not webhook_url:
print("⚠️ URL du webhook Discord non configurée")
print(" Veuillez ajouter l'URL dans plex-recap-config.json\n")
else:
print(f"✅ Webhook Discord configuré: {webhook_url[:50]}...")
# Test de connexion
try:
response = requests.get(webhook_url, timeout=5)
print("✅ Webhook Discord accessible\n")
except requests.exceptions.RequestException as e:
print(f"⚠️ Impossible de tester le webhook: {e}\n")
# Test Jellyseerr
jellyseerr_config = config.get('jellyseerr', {})
jellyseerr_url = jellyseerr_config.get('url', '')
jellyseerr_key = jellyseerr_config.get('api_key', '')
if not jellyseerr_key:
print("⚠️ Clé API Jellyseerr non configurée\n")
else:
print(f"✅ Jellyseerr configuré: {jellyseerr_url}")
try:
headers = {'X-Api-Key': jellyseerr_key}
response = requests.get(
f"{jellyseerr_url}/api/v1/status",
headers=headers,
timeout=5
)
if response.status_code == 200:
print("✅ Jellyseerr API accessible\n")
else:
print(f"⚠️ Jellyseerr API retourne le code: {response.status_code}\n")
except requests.exceptions.RequestException as e:
print(f"⚠️ Impossible d'accéder à Jellyseerr: {e}\n")
# Test Radarr
radarr_config = config.get('radarr', {})
radarr_host = radarr_config.get('hostname', '')
radarr_port = radarr_config.get('port', 7878)
radarr_key = radarr_config.get('api_key', '')
use_ssl = radarr_config.get('use_ssl', False)
if not radarr_key:
print("⚠️ Clé API Radarr non configurée\n")
else:
protocol = 'https' if use_ssl else 'http'
radarr_url = f"{protocol}://{radarr_host}:{radarr_port}"
print(f"✅ Radarr configuré: {radarr_url}")
try:
params = {'apikey': radarr_key}
response = requests.get(
f"{radarr_url}/api/v3/system/status",
params=params,
timeout=5
)
if response.status_code == 200:
print("✅ Radarr API accessible\n")
else:
print(f"⚠️ Radarr API retourne le code: {response.status_code}\n")
except requests.exceptions.RequestException as e:
print(f"⚠️ Impossible d'accéder à Radarr: {e}\n")
# Test Sonarr
sonarr_config = config.get('sonarr', {})
sonarr_host = sonarr_config.get('hostname', '')
sonarr_port = sonarr_config.get('port', 8989)
sonarr_key = sonarr_config.get('api_key', '')
use_ssl = sonarr_config.get('use_ssl', False)
if not sonarr_key:
print("⚠️ Clé API Sonarr non configurée\n")
else:
protocol = 'https' if use_ssl else 'http'
sonarr_url = f"{protocol}://{sonarr_host}:{sonarr_port}"
print(f"✅ Sonarr configuré: {sonarr_url}")
try:
params = {'apikey': sonarr_key}
response = requests.get(
f"{sonarr_url}/api/v3/system/status",
params=params,
timeout=5
)
if response.status_code == 200:
print("✅ Sonarr API accessible\n")
else:
print(f"⚠️ Sonarr API retourne le code: {response.status_code}\n")
except requests.exceptions.RequestException as e:
print(f"⚠️ Impossible d'accéder à Sonarr: {e}\n")
print("\n✅ Test de configuration terminé")
return True
if __name__ == "__main__":
test_config()
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# Garde uniquement les 50 derniers logs par job cronmaster
LOGS_DIR="/cronmaster-data/logs"
MAX_LOGS=50
for dir in "$LOGS_DIR"/*/; do
[ -d "$dir" ] || continue
count=$(ls "$dir" | wc -l)
if [ "$count" -gt "$MAX_LOGS" ]; then
ls -t "$dir" | tail -n +"$((MAX_LOGS + 1))" | while read f; do
rm -f "$dir$f"
done
fi
done