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:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format bytes to human readable format
|
||||
*/
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes per second to speed
|
||||
*/
|
||||
function formatSpeed(bytesPerSecond) {
|
||||
return formatBytes(bytesPerSecond) + '/s';
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate download status to French
|
||||
*/
|
||||
function translateStatus(status) {
|
||||
const translations = {
|
||||
'pending': 'En attente',
|
||||
'downloading': 'Téléchargement',
|
||||
'paused': 'En pause',
|
||||
'completed': 'Terminé',
|
||||
'failed': 'Échoué',
|
||||
'cancelled': 'Annulé'
|
||||
};
|
||||
return translations[status] || status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract series name from filename (for grouping)
|
||||
*/
|
||||
function extractSeriesName(filename) {
|
||||
let name = filename;
|
||||
|
||||
// Remove file extension
|
||||
name = name.replace(/\.[^/.]+$/, '');
|
||||
|
||||
// Remove episode numbers and patterns
|
||||
name = name
|
||||
.replace(/[-_ ]?(E(?:p)?|Episode|Épisode|Saison|Season)[-_: ]?\d+/gi, '')
|
||||
.replace(/[-_ ]?S\d{2}E\d{2}/gi, '')
|
||||
.replace(/\[.*?\]/g, '')
|
||||
.replace(/\(.*\)/g, '')
|
||||
.replace(/[-_ ]?\d{3,4}p/gi, '')
|
||||
.replace(/[-_ ]?(VOSTFR|VF|MULTI)/gi, '')
|
||||
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
|
||||
.replace(/[-_]+$/, '') // Remove trailing dashes/underscores
|
||||
.trim();
|
||||
|
||||
// If nothing left or too short, use original filename without extension
|
||||
if (!name || name.length < 3) {
|
||||
return filename.replace(/\.[^/.]+$/, '');
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get day string for grouping
|
||||
*/
|
||||
function getDayString(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return "Aujourd'hui";
|
||||
} else if (date.toDateString() === yesterday.toDateString()) {
|
||||
return "Hier";
|
||||
} else {
|
||||
return date.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'short' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user