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]>
214 lines
7.3 KiB
JavaScript
214 lines
7.3 KiB
JavaScript
/**
|
|
* Main initialization and event handlers
|
|
*/
|
|
|
|
// Initialize on DOM load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initializeForms();
|
|
loadProviders();
|
|
loadDownloads();
|
|
setInterval(loadDownloads, 1000);
|
|
|
|
// Load home content (recommendations & releases)
|
|
loadHomeContent();
|
|
});
|
|
|
|
/**
|
|
* Initialize form event listeners
|
|
*/
|
|
function initializeForms() {
|
|
// Search form
|
|
document.getElementById('searchInput').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
handleSearch();
|
|
}
|
|
});
|
|
|
|
// Direct download form
|
|
document.getElementById('downloadForm').addEventListener('submit', handleDirectDownload);
|
|
}
|
|
|
|
/**
|
|
* Load providers dynamically
|
|
*/
|
|
async function loadProviders() {
|
|
try {
|
|
const data = await getProvidersInfo();
|
|
|
|
// Update anime tabs
|
|
const animeTabsContainer = document.querySelector('.tabs');
|
|
const existingTabs = animeTabsContainer.querySelectorAll('.tab[data-tab-type="anime"]');
|
|
existingTabs.forEach(tab => tab.remove());
|
|
|
|
// Add anime provider tabs
|
|
Object.entries(data.anime_providers).forEach(([id, provider]) => {
|
|
// Check if tab doesn't exist
|
|
if (!document.querySelector(`.tab[data-provider="${id}"]`)) {
|
|
const button = document.createElement('button');
|
|
button.className = 'tab';
|
|
button.setAttribute('data-tab-type', 'anime');
|
|
button.setAttribute('data-provider', id);
|
|
button.innerHTML = `${provider.icon} ${provider.name}`;
|
|
button.onclick = () => switchTab(`anime-${id}`);
|
|
animeTabsContainer.appendChild(button);
|
|
|
|
// Create corresponding tab content
|
|
const tabContent = document.createElement('div');
|
|
tabContent.id = `tab-anime-${id}`;
|
|
tabContent.className = 'tab-content';
|
|
tabContent.innerHTML = createAnimeTabContent(id, provider);
|
|
document.querySelector('.container').insertBefore(
|
|
tabContent,
|
|
document.getElementById('downloadsList')
|
|
);
|
|
}
|
|
});
|
|
|
|
// Update supported hosts badges
|
|
const hostsContainer = document.querySelector('.supported-hosts');
|
|
hostsContainer.innerHTML = '';
|
|
|
|
Object.values(data.file_hosts).forEach(host => {
|
|
const badge = document.createElement('span');
|
|
badge.className = 'host-badge';
|
|
badge.textContent = `${host.icon} ${host.name}`;
|
|
hostsContainer.appendChild(badge);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading providers:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create anime provider tab content
|
|
*/
|
|
function createAnimeTabContent(providerId, provider) {
|
|
return `
|
|
<div class="url-form">
|
|
<div class="anime-input-group">
|
|
<input
|
|
type="text"
|
|
id="${providerId}UrlInput"
|
|
placeholder="URL de l'anime (ex: ${provider.url_pattern})"
|
|
>
|
|
<button type="button" class="btn-primary" onclick="handleLoadProviderEpisodes('${providerId}')">
|
|
<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>
|
|
Charger
|
|
</button>
|
|
</div>
|
|
|
|
<div id="${providerId}EpisodeSelector" class="anime-select-group" style="display:none;">
|
|
<select id="${providerId}EpisodeSelect">
|
|
<option value="">Sélectionner un épisode</option>
|
|
</select>
|
|
<button type="button" class="btn-primary" onclick="handleDownloadProviderEpisode('${providerId}')">
|
|
<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>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Handle load provider episodes
|
|
*/
|
|
async function handleLoadProviderEpisodes(providerId) {
|
|
const animeUrl = document.getElementById(`${providerId}UrlInput`).value;
|
|
if (!animeUrl) {
|
|
alert('Veuillez entrer une URL d\'anime');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = await loadEpisodes(animeUrl, null);
|
|
|
|
if (data.episodes && data.episodes.length > 0) {
|
|
const select = document.getElementById(`${providerId}EpisodeSelect`);
|
|
select.innerHTML = '<option value="">Sélectionner un épisode</option>';
|
|
|
|
data.episodes.forEach(ep => {
|
|
const option = document.createElement('option');
|
|
option.value = ep.url;
|
|
option.textContent = `Épisode ${ep.episode}`;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
document.getElementById(`${providerId}EpisodeSelector`).style.display = 'flex';
|
|
} else {
|
|
alert('Aucun épisode trouvé');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading episodes:', error);
|
|
alert('Erreur lors du chargement des épisodes');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle download provider episode
|
|
*/
|
|
async function handleDownloadProviderEpisode(providerId) {
|
|
const episodeUrl = document.getElementById(`${providerId}EpisodeSelect`).value;
|
|
if (!episodeUrl) {
|
|
alert('Veuillez sélectionner un épisode');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await downloadEpisode(episodeUrl);
|
|
document.getElementById(`${providerId}EpisodeSelect`).value = '';
|
|
loadDownloads();
|
|
} catch (error) {
|
|
console.error('Download error:', error);
|
|
alert('Erreur lors du téléchargement');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Switch between tabs
|
|
*/
|
|
function switchTab(tabName) {
|
|
// Hide all tabs
|
|
document.querySelectorAll('.tab-content').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
});
|
|
document.querySelectorAll('.tab').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
|
|
// Show selected tab
|
|
const tabElement = document.getElementById(`tab-${tabName}`);
|
|
if (tabElement) {
|
|
tabElement.classList.add('active');
|
|
}
|
|
|
|
// Find and activate the button
|
|
const buttons = document.querySelectorAll('.tab');
|
|
buttons.forEach(btn => {
|
|
const tabType = btn.getAttribute('data-tab-type');
|
|
|
|
if (tabType === 'home' && tabName === 'home') {
|
|
btn.classList.add('active');
|
|
} else if (tabType === 'search' && tabName === 'search') {
|
|
btn.classList.add('active');
|
|
} else if (tabType === 'direct' && tabName === 'direct') {
|
|
btn.classList.add('active');
|
|
} else if (tabType === 'anime' && btn.getAttribute('data-provider') === tabName.replace('anime-', '')) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
|
|
// Load home content when switching to home tab
|
|
if (tabName === 'home') {
|
|
// Content is already loaded on init, but you can reload if needed
|
|
if (typeof loadHomeContent === 'function' && !document.getElementById('recommendationsList').hasChildNodes()) {
|
|
loadHomeContent();
|
|
}
|
|
}
|
|
}
|