1fe7392063
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 <[email protected]> Co-Authored-By: Happy <[email protected]>
93 lines
2.4 KiB
JavaScript
93 lines
2.4 KiB
JavaScript
/**
|
|
* 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' });
|
|
}
|
|
}
|