feat: Complete Sonarr integration with security enhancements

This commit adds comprehensive Sonarr webhook integration and implements
critical security improvements identified in code review.

## Sonarr Integration
- Full webhook support for Grab, Download, Rename, Delete, and Test events
- HMAC SHA256 signature verification for webhook authentication
- Series mapping system (Sonarr TVDB ID → Anime Provider URL)
- 11 new API endpoints for configuration, mappings, search, and downloads
- Comprehensive test suite (31 tests, all passing)
- Complete documentation in docs/SONARR_INTEGRATION.md

## Security Enhancements
- CORS restricted to specific origins (user's IP: 192.168.1.204:3000)
- Path traversal prevention via sanitize_filename() and is_safe_filename()
- Structured logging infrastructure (replaced all print() statements)
- Environment-based configuration with .env support
- Filename sanitization prevents malicious path attacks

## New Features
- Lpayer and Sibnet downloader support
- Kitsu API integration for anime metadata
- Recommendation engine based on download history
- Latest releases endpoint for new anime
- Modular web interface with component-based templates

## Configuration
- Centralized settings via app/config.py with pydantic-settings
- Sonarr config auto-created in config/ directory
- Example configurations provided for easy setup

## Tests
- 31 Sonarr integration tests (23 functionality + 9 security)
- 100+ tests passing in core test files
- Security utilities fully tested

## Documentation
- Updated CLAUDE.md with Sonarr and testing info
- Added IMPROVEMENTS_2024-01-24.md analysis
- Added SONARR_IMPLEMENTATION.md technical summary

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
root
2026-01-24 21:25:47 +00:00
parent 92ef76ed2a
commit 1fe7392063
49 changed files with 8651 additions and 2110 deletions
@@ -0,0 +1,32 @@
{# Template pour un onglet de provider anime spécifique #}
{# Variables disponibles: provider_id, provider_info #}
<div id="tab-anime-{{ provider_id }}" class="tab-content">
<div class="url-form">
<div class="anime-input-group">
<input
type="text"
id="searchInput-{{ provider_id }}"
placeholder="Rechercher un anime sur {{ provider_info.name }}..."
onkeypress="if(event.key === 'Enter') searchAnimeProvider('{{ provider_id }}')"
>
<select id="langSelect-{{ provider_id }}" style="max-width: 120px;">
<option value="vostfr">VOSTFR</option>
<option value="vf">VF</option>
</select>
<button type="button" class="btn-primary" onclick="searchAnimeProvider('{{ provider_id }}')">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Rechercher
</button>
</div>
<div style="display: flex; align-items: center; gap: 10px; margin-top: 10px; font-size: 13px; color: #888;">
<input type="checkbox" id="includeMetadata-{{ provider_id }}" style="width: auto; margin: 0;">
<label for="includeMetadata-{{ provider_id }}" style="cursor: pointer; user-select: none;">
📊 Inclure les métadonnées
</label>
</div>
</div>
<div id="searchResults-{{ provider_id }}" class="search-results"></div>
</div>
+28
View File
@@ -0,0 +1,28 @@
<!-- Direct Download Tab -->
<div id="tab-direct" class="tab-content">
<div class="url-form">
<form id="downloadForm">
<div class="input-group">
<input
type="text"
id="urlInput"
placeholder="Collez le lien de téléchargement ici..."
required
>
<button type="submit" class="btn-primary">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
</svg>
Télécharger
</button>
</div>
</form>
<div class="supported-hosts">
<span class="host-badge">1fichier</span>
<span class="host-badge">Doodstream</span>
<span class="host-badge">Rapidfile</span>
<span class="host-badge">Anime-Sama</span>
<span class="host-badge">Anime-Ultime</span>
</div>
</div>
</div>
@@ -0,0 +1,63 @@
<!-- Downloads Section with Filters -->
<div class="section-header">
<h2>Téléchargements</h2>
<div class="downloads-stats" id="downloadsStats"></div>
</div>
<!-- Filters and Controls -->
<div class="downloads-controls">
<div class="filter-group">
<label>Statut:</label>
<select id="statusFilter" onchange="filterDownloads()">
<option value="all">Tous</option>
<option value="downloading">En cours</option>
<option value="paused">En pause</option>
<option value="completed">Terminés</option>
<option value="cancelled">Annulés</option>
<option value="failed">Échoués</option>
</select>
</div>
<div class="filter-group">
<label>Tri par:</label>
<select id="sortBy" onchange="filterDownloads()">
<option value="date">Date (récent)</option>
<option value="date_asc">Date (ancien)</option>
<option value="name">Nom (A-Z)</option>
<option value="name_desc">Nom (Z-A)</option>
<option value="size">Taille</option>
</select>
</div>
<div class="filter-group">
<label>Regroupement:</label>
<select id="groupBy" onchange="filterDownloads()">
<option value="none">Aucun</option>
<option value="series">Par série</option>
<option value="status">Par statut</option>
<option value="day">Par jour</option>
</select>
</div>
<div class="filter-group search-group">
<input type="text" id="searchDownloads" placeholder="🔍 Rechercher..." oninput="filterDownloads()">
</div>
<div class="actions-group">
<button class="btn-small btn-secondary" onclick="clearCompleted()" title="Supprimer annulés, échoués et terminés">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
Nettoyer
</button>
</div>
</div>
<div id="downloadsList" class="downloads-list">
<div class="empty-state">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path>
</svg>
<p>Aucun téléchargement pour le moment</p>
</div>
</div>
+25
View File
@@ -0,0 +1,25 @@
<h1>⚡ Ohm Stream Downloader</h1>
<p class="subtitle">Téléchargez vos vidéos et animes depuis vos hébergeurs préférés</p>
<!-- Tabs (Anime providers will be added dynamically) -->
<div class="tabs">
<button class="tab active" data-tab-type="home" onclick="switchTab('home')">
<svg style="width:16px;height:16px;vertical-align:middle;margin-right:5px" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
Accueil
</button>
<button class="tab" data-tab-type="search" onclick="switchTab('search')">
<svg style="width:16px;height:16px;vertical-align:middle;margin-right:5px" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Recherche
</button>
<button class="tab" data-tab-type="direct" onclick="switchTab('direct')">
<svg style="width:16px;height:16px;vertical-align:middle;margin-right:5px" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
</svg>
Lien direct
</button>
<!-- Anime provider tabs will be loaded dynamically -->
</div>
+33
View File
@@ -0,0 +1,33 @@
<!-- Home Section: Recommendations & Latest Releases -->
<div id="tab-home" class="tab-content active">
<!-- Loading State -->
<div id="homeLoading" class="loading-spinner">Chargement des recommandations...</div>
<!-- Recommendations Section -->
<div id="recommendationsSection" style="display: none;">
<div class="section-header">
<h2>🎯 Recommandé pour vous</h2>
<button class="btn-small btn-secondary" onclick="loadRecommendations()">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Actualiser
</button>
</div>
<div id="recommendationsList" class="search-results"></div>
</div>
<!-- Latest Releases Section -->
<div id="releasesSection" style="display: none; margin-top: 40px;">
<div class="section-header">
<h2>🔥 Dernières sorties de la saison</h2>
<button class="btn-small btn-secondary" onclick="loadLatestReleases()">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Actualiser
</button>
</div>
<div id="releasesList" class="search-results"></div>
</div>
</div>
+24
View File
@@ -0,0 +1,24 @@
<!-- Search Tab -->
<div id="tab-search" class="tab-content">
<div class="url-form">
<div class="input-group">
<input
type="text"
id="searchInput"
placeholder="Rechercher un anime (ex: Frieren, One Piece...)"
>
<button type="button" class="btn-primary" onclick="handleSearch()">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Rechercher
</button>
</div>
<div style="margin-top: 10px; padding: 10px; background: rgba(0, 217, 255, 0.1); border-radius: 8px; font-size: 12px; color: #aaa;">
💡 <strong>Info:</strong> La recherche utilise MyAnimeList pour afficher la fiche complète de l'anime (synopsis, saisons, etc.) et trouve les sources de streaming disponibles.
</div>
</div>
<!-- Anime details and streaming results -->
<div id="animeSearchResults"></div>
</div>