Files
ohm_streaming/static/js/main.js
T
root 1fe7392063 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 <[email protected]>
Co-Authored-By: Happy <[email protected]>
2026-01-24 21:25:47 +00:00

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();
}
}
}