feat: Add series TV support with Vidzy HLS downloads and duplicate prevention
Major improvements: - Series TV support via FS7 provider with dedicated search endpoint - Vidzy downloader now uses Playwright for JS obfuscation and ffmpeg for HLS streams - Episode filenames properly named (Series Title - Episode X) instead of master.m3u8.mp4 - Duplicate download prevention: checks existing tasks before creating new ones - Removed host preference system in favor of intelligent URL-based detection Technical changes: - Vidzy: Added Playwright extraction and M3U8→MP4 conversion with ffmpeg - FS7: Episodes now use pipe format (video_url|series_url|episode_title) - DownloadManager: Extract target_filename from pipe URL and prevent duplicates - UI: New Series tab with search, recommendations, and releases sections - Anime-Sama: Removed hardcoded host preferences, uses site's URL order Generated with [Claude Code](https://claude.com/claude-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:
@@ -338,8 +338,21 @@ async function handleSearch() {
|
||||
await searchAnimeDetails(query);
|
||||
}
|
||||
|
||||
// Handle anime search (new dedicated function)
|
||||
async function handleAnimeSearch() {
|
||||
const searchInput = document.getElementById('animeSearchInput') || document.getElementById('searchInput');
|
||||
if (!searchInput) return;
|
||||
|
||||
const query = searchInput.value.trim();
|
||||
if (!query) return;
|
||||
|
||||
// Use the new anime details search
|
||||
await searchAnimeDetails(query);
|
||||
}
|
||||
|
||||
// Ensure global scope
|
||||
window.handleSearch = handleSearch;
|
||||
window.handleAnimeSearch = handleAnimeSearch;
|
||||
|
||||
/**
|
||||
* Handle direct download form submission
|
||||
|
||||
@@ -149,3 +149,15 @@ async function cancelDownload(id) {
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Make functions available globally
|
||||
window.getProvidersInfo = getProvidersInfo;
|
||||
window.searchAnime = searchAnime;
|
||||
window.loadEpisodes = loadEpisodes;
|
||||
window.downloadEpisode = downloadEpisode;
|
||||
window.downloadSeason = downloadSeason;
|
||||
window.startDownload = startDownload;
|
||||
window.getDownloads = getDownloads;
|
||||
window.pauseDownload = pauseDownload;
|
||||
window.resumeDownload = resumeDownload;
|
||||
window.cancelDownload = cancelDownload;
|
||||
|
||||
+15
-9
@@ -17,12 +17,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
* Initialize form event listeners
|
||||
*/
|
||||
function initializeForms() {
|
||||
// Search form
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keypress', (e) => {
|
||||
// Anime search form
|
||||
const animeSearchInput = document.getElementById('animeSearchInput');
|
||||
if (animeSearchInput) {
|
||||
animeSearchInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearch();
|
||||
handleAnimeSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Series search form
|
||||
const seriesSearchInput = document.getElementById('seriesSearchInput');
|
||||
if (seriesSearchInput) {
|
||||
seriesSearchInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSeriesSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -209,10 +219,6 @@ function switchTab(tabName) {
|
||||
|
||||
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' && tabName === 'anime') {
|
||||
// Static anime tab
|
||||
btn.classList.add('active');
|
||||
|
||||
@@ -253,20 +253,20 @@ function getRatingColor(score) {
|
||||
return 'linear-gradient(45deg, #666, #888)';
|
||||
}
|
||||
|
||||
// Search anime on providers (redirects to search tab)
|
||||
// Search anime on providers (redirects to anime tab)
|
||||
function searchAnimeOnProviders(title) {
|
||||
// Switch to search tab
|
||||
switchTab('search');
|
||||
// Switch to anime tab
|
||||
switchTab('anime');
|
||||
|
||||
// Fill search input
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchInput = document.getElementById('animeSearchInput');
|
||||
if (searchInput) {
|
||||
searchInput.value = title;
|
||||
|
||||
// Trigger search
|
||||
setTimeout(() => {
|
||||
if (typeof searchAnime === 'function') {
|
||||
searchAnime();
|
||||
if (typeof handleAnimeSearch === 'function') {
|
||||
handleAnimeSearch();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Series search functionality for FS7
|
||||
*/
|
||||
|
||||
// Handle series search
|
||||
async function handleSeriesSearch() {
|
||||
const searchInput = document.getElementById('seriesSearchInput');
|
||||
const resultsContainer = document.getElementById('seriesSearchResults');
|
||||
|
||||
if (!searchInput || !resultsContainer) return;
|
||||
|
||||
const query = searchInput.value.trim();
|
||||
if (!query) {
|
||||
alert('Veuillez entrer un nom de série');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resultsContainer.innerHTML = '<div class="loading-spinner">Recherche de séries TV en cours...</div>';
|
||||
|
||||
// Search on series providers using the dedicated endpoint
|
||||
const response = await fetch(`${API_BASE}/series/search?q=${encodeURIComponent(query)}&lang=vf`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.results && data.results['fs7'] && data.results['fs7'].length > 0) {
|
||||
const series = data.results['fs7'];
|
||||
let html = `
|
||||
<div class="streaming-results-header">
|
||||
<h3>📺 Résultats pour "${escapeHtml(query)}"</h3>
|
||||
</div>
|
||||
<div class="search-results" style="margin-top: 20px;">
|
||||
`;
|
||||
|
||||
series.forEach(s => {
|
||||
let coverImage = s.cover_image || '';
|
||||
|
||||
// Convert relative poster.php URLs to absolute URLs
|
||||
if (coverImage.startsWith('/poster.php?url=')) {
|
||||
const actualUrl = coverImage.replace('/poster.php?url=', '');
|
||||
coverImage = actualUrl;
|
||||
} else if (coverImage.startsWith('/')) {
|
||||
coverImage = 'https://fs7.lol' + coverImage;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="anime-card" id="series-fs7-${encodeURIComponent(s.url)}">
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${escapeHtml(s.title)}</div>
|
||||
<div class="anime-card-provider">📺 French Stream</div>
|
||||
</div>
|
||||
${coverImage ? `
|
||||
<div style="text-align: center; margin: 10px 0;">
|
||||
<img src="${escapeHtml(coverImage)}" alt="" style="max-width: 200px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.3);" onerror="this.style.display='none'">
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="anime-card-actions">
|
||||
<button class="btn-secondary btn-small" onclick="window.open('${escapeHtml(s.url)}', '_blank')">
|
||||
🔗 Voir sur FS7
|
||||
</button>
|
||||
<button class="btn-primary btn-small" onclick="loadSeriesEpisodesDirect('${escapeHtml(s.url)}', '${escapeHtml(s.title)}')">
|
||||
📥 Voir les épisodes
|
||||
</button>
|
||||
</div>
|
||||
<div id="episodes-fs7-${encodeURIComponent(s.url)}" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
resultsContainer.innerHTML = html;
|
||||
} else {
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p>❌ Aucune série trouvée pour "${escapeHtml(query)}"</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; opacity: 0.7;">
|
||||
Essayez avec un autre titre ou vérifiez l'orthographe
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error searching series:', error);
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p>❌ Erreur lors de la recherche</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Load series episodes directly without redirecting to search
|
||||
async function loadSeriesEpisodesDirect(url, title) {
|
||||
const episodesContainer = document.getElementById(`episodes-fs7-${encodeURIComponent(url)}`);
|
||||
|
||||
if (!episodesContainer) return;
|
||||
|
||||
try {
|
||||
episodesContainer.innerHTML = '<div class="loading-spinner">Chargement des épisodes...</div>';
|
||||
|
||||
const response = await fetch(`${API_BASE}/anime/episodes?url=${encodeURIComponent(url)}&lang=vf`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.episodes && data.episodes.length > 0) {
|
||||
let html = `
|
||||
<div style="margin-top: 15px;">
|
||||
<label style="font-size: 12px; color: #00d9ff; margin-bottom: 5px; display: block;">
|
||||
📺 Sélectionner un épisode:
|
||||
</label>
|
||||
<select id="select-episodes-${encodeURIComponent(url)}" style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid rgba(0, 217, 255, 0.3); background: rgba(0, 0, 0, 0.3); color: white;">
|
||||
<option value="">Sélectionner un épisode</option>
|
||||
${data.episodes.map(ep => `
|
||||
<option value="${escapeHtml(ep.url)}">Épisode ${escapeHtml(ep.episode)}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
<button class="btn-primary" style="margin-top: 10px; width: 100%;" onclick="downloadSeriesEpisode('${escapeHtml(url)}', '${escapeHtml(title)}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;margin-right:4px;">
|
||||
<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 l'épisode
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
episodesContainer.innerHTML = html;
|
||||
} else {
|
||||
episodesContainer.innerHTML = '<div class="no-results" style="margin-top: 10px;">Aucun épisode disponible</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading episodes:', error);
|
||||
episodesContainer.innerHTML = `<div class="no-results" style="margin-top: 10px; color: #ff6b6b;">Erreur: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Download series episode
|
||||
async function downloadSeriesEpisode(url, title) {
|
||||
const select = document.getElementById(`select-episodes-${encodeURIComponent(url)}`);
|
||||
if (!select || !select.value) {
|
||||
alert('Veuillez sélectionner un épisode');
|
||||
return;
|
||||
}
|
||||
|
||||
const episodeUrl = select.value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/anime/download?url=${encodeURIComponent(episodeUrl)}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert(`✅ Téléchargement démarré pour "${title}"`);
|
||||
// Refresh downloads
|
||||
if (typeof loadDownloads === 'function') {
|
||||
loadDownloads();
|
||||
}
|
||||
} else {
|
||||
const error = await response.json();
|
||||
const errorMessage = error.detail
|
||||
? (typeof error.detail === 'string' ? error.detail : JSON.stringify(error.detail))
|
||||
: 'Impossible de démarrer le téléchargement';
|
||||
alert(`❌ Erreur: ${errorMessage}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Download error:', error);
|
||||
alert(`❌ Erreur lors du téléchargement: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Make functions available globally
|
||||
window.handleSeriesSearch = handleSeriesSearch;
|
||||
window.loadSeriesEpisodesDirect = loadSeriesEpisodesDirect;
|
||||
window.downloadSeriesEpisode = downloadSeriesEpisode;
|
||||
+204
-9
@@ -2,6 +2,158 @@
|
||||
* New tabs functionality
|
||||
*/
|
||||
|
||||
// Render series recommendation card (same design as anime recommendations)
|
||||
function renderSeriesRecommendationCard(series) {
|
||||
let coverImage = series.cover_image || '';
|
||||
|
||||
// Convert relative poster.php URLs to absolute URLs
|
||||
if (coverImage.startsWith('/poster.php?url=')) {
|
||||
// Extract the actual image URL from the poster.php URL
|
||||
const actualUrl = coverImage.replace('/poster.php?url=', '');
|
||||
coverImage = actualUrl;
|
||||
}
|
||||
// If it's a relative path, make it absolute using FS7 base URL
|
||||
else if (coverImage.startsWith('/')) {
|
||||
coverImage = 'https://fs7.lol' + coverImage;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="anime-card-horizontal recommendation-card">
|
||||
<div class="recommendation-badge">🎺 Série TV populaire</div>
|
||||
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${escapeHtml(series.title)}</div>
|
||||
</div>
|
||||
|
||||
<div class="anime-card-content">
|
||||
${coverImage ? `<img src="${escapeHtml(coverImage)}" alt="" class="anime-card-image" onerror="this.style.display='none'">` : ''}
|
||||
|
||||
<div class="anime-card-info">
|
||||
<div class="anime-card-meta">
|
||||
📺 Série TV
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="anime-card-actions">
|
||||
<button class="btn-secondary btn-small" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
|
||||
🔗 Voir sur FS7
|
||||
</button>
|
||||
<button class="btn-primary btn-small" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
|
||||
📥 Voir les épisodes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Load series episodes (redirects to series tab with search)
|
||||
async function loadSeriesEpisodes(url, title) {
|
||||
// Switch to series tab
|
||||
switchTab('series');
|
||||
|
||||
// Fill search input with the series title
|
||||
const searchInput = document.getElementById('seriesSearchInput');
|
||||
if (searchInput) {
|
||||
searchInput.value = title;
|
||||
|
||||
// Trigger search
|
||||
setTimeout(() => {
|
||||
if (typeof handleSeriesSearch === 'function') {
|
||||
handleSeriesSearch();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Render series release card (same design as anime releases)
|
||||
function renderSeriesReleaseCard(series) {
|
||||
let coverImage = series.cover_image || '';
|
||||
|
||||
// Convert relative poster.php URLs to absolute URLs
|
||||
if (coverImage.startsWith('/poster.php?url=')) {
|
||||
// Extract the actual image URL from the poster.php URL
|
||||
const actualUrl = coverImage.replace('/poster.php?url=', '');
|
||||
coverImage = actualUrl;
|
||||
}
|
||||
// If it's a relative path, make it absolute using FS7 base URL
|
||||
else if (coverImage.startsWith('/')) {
|
||||
coverImage = 'https://fs7.lol' + coverImage;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="anime-card-horizontal release-card">
|
||||
<div class="anime-card-header">
|
||||
<div class="anime-card-title">${escapeHtml(series.title)}</div>
|
||||
</div>
|
||||
|
||||
<div class="anime-card-content">
|
||||
${coverImage ? `<img src="${escapeHtml(coverImage)}" alt="" class="anime-card-image" onerror="this.style.display='none'">` : ''}
|
||||
|
||||
<div class="anime-card-info">
|
||||
<div class="anime-card-meta">
|
||||
📺 Série TV • Nouveau
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="anime-card-actions">
|
||||
<button class="btn-secondary btn-small" onclick="window.open('${escapeHtml(series.url)}', '_blank')">
|
||||
🔗 Voir sur FS7
|
||||
</button>
|
||||
<button class="btn-primary btn-small" onclick="loadSeriesEpisodes('${escapeHtml(series.url)}', '${escapeHtml(series.title)}')">
|
||||
📥 Voir les épisodes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Load series recommendations for the Series tab
|
||||
async function loadSeriesRecommendations() {
|
||||
try {
|
||||
const container = document.getElementById('seriesRecommendationsList');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="loading-spinner">Chargement des recommandations séries...</div>';
|
||||
|
||||
// Search for popular series from all providers (including FS7)
|
||||
const searchTerms = ['Breaking Bad', 'Game of Thrones', 'Stranger Things', 'The Walking Dead', 'The Last of Us', 'Wednesday', 'Squid Game', 'Peaky Blinders', 'House of the Dragon', 'The Witcher'];
|
||||
const allSeries = [];
|
||||
|
||||
for (const term of searchTerms) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/series/search?q=${encodeURIComponent(term)}&lang=vf`);
|
||||
const data = await response.json();
|
||||
|
||||
// Collect results from all providers, especially fs7
|
||||
if (data.results) {
|
||||
// Prioritize fs7 results
|
||||
if (data.results['fs7'] && data.results['fs7'].length > 0) {
|
||||
allSeries.push(...data.results['fs7'].slice(0, 2));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error searching for ${term}:`, error);
|
||||
}
|
||||
|
||||
if (allSeries.length >= 12) break; // Limit to 12 series total
|
||||
}
|
||||
|
||||
if (allSeries.length > 0) {
|
||||
container.innerHTML = `<div class="recommendations-carousel">${allSeries.map(series =>
|
||||
renderSeriesRecommendationCard(series)
|
||||
).join('')}</div>`;
|
||||
} else {
|
||||
container.innerHTML = '<div class="no-results">Aucune recommandation trouvée</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading series recommendations:', error);
|
||||
const container = document.getElementById('seriesRecommendationsList');
|
||||
if (container) container.innerHTML = '<div class="no-results">Erreur lors du chargement</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load anime releases for the Anime tab
|
||||
async function loadAnimeReleases() {
|
||||
try {
|
||||
@@ -34,23 +186,63 @@ async function loadSeriesReleases() {
|
||||
const container = document.getElementById('seriesReleasesList');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="loading-spinner">Chargement des dernières sorties séries...</div>';
|
||||
container.innerHTML = '<div class="loading-spinner">Chargement des dernières séries TV...</div>';
|
||||
|
||||
// For series, we'll show the same releases but could filter later
|
||||
const response = await fetch(`${API_BASE}/releases/latest?limit=12`);
|
||||
const data = await response.json();
|
||||
// Search for popular series from all providers (including FS7)
|
||||
const searchTerms = ['Breaking Bad', 'Game of Thrones', 'Stranger Things', 'The Walking Dead', 'The Last of Us', 'Wednesday', 'Squid Game', 'Peaky Blinders'];
|
||||
const allSeries = [];
|
||||
|
||||
if (data.releases && data.releases.length > 0) {
|
||||
container.innerHTML = `<div class="releases-carousel">${data.releases.map(anime =>
|
||||
renderReleaseCard({...anime, title: anime.title + ' [Série]'})
|
||||
for (const term of searchTerms) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/series/search?q=${encodeURIComponent(term)}&lang=vf`);
|
||||
const data = await response.json();
|
||||
|
||||
// Collect results from all providers, especially fs7
|
||||
if (data.results) {
|
||||
// Prioritize fs7 results
|
||||
if (data.results['fs7'] && data.results['fs7'].length > 0) {
|
||||
allSeries.push(...data.results['fs7'].slice(0, 2));
|
||||
}
|
||||
// Add results from other providers if needed
|
||||
for (const [provider, results] of Object.entries(data.results)) {
|
||||
if (provider !== 'fs7' && results.length > 0 && allSeries.length < 12) {
|
||||
allSeries.push(...results.slice(0, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error searching for ${term}:`, error);
|
||||
}
|
||||
|
||||
if (allSeries.length >= 12) break; // Limit to 12 series total
|
||||
}
|
||||
|
||||
if (allSeries.length > 0) {
|
||||
container.innerHTML = `<div class="releases-carousel">${allSeries.map(series =>
|
||||
renderSeriesReleaseCard(series)
|
||||
).join('')}</div>`;
|
||||
} else {
|
||||
container.innerHTML = '<div class="no-results">Aucune sortie trouvée</div>';
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p>Aucune série trouvée</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; opacity: 0.7;">
|
||||
Utilisez l'onglet "Recherche" pour trouver des séries spécifiques
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading series releases:', error);
|
||||
const container = document.getElementById('seriesReleasesList');
|
||||
if (container) container.innerHTML = '<div class="no-results">Erreur lors du chargement</div>';
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="no-results">
|
||||
<p>❌ Erreur lors du chargement des séries</p>
|
||||
<p style="font-size: 12px; margin-top: 10px; color: #ff6b6b;">${error.message}</p>
|
||||
<button class="btn-secondary btn-small" onclick="loadSeriesReleases()" style="margin-top: 10px;">
|
||||
🔄 Réessayer
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +376,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
} else if (tabName === 'series') {
|
||||
if (!window.seriesTabLoaded) {
|
||||
loadSeriesRecommendations();
|
||||
loadSeriesReleases();
|
||||
window.seriesTabLoaded = true;
|
||||
}
|
||||
@@ -200,6 +393,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
// Make functions available globally
|
||||
window.loadSeriesEpisodes = loadSeriesEpisodes;
|
||||
window.loadSeriesRecommendations = loadSeriesRecommendations;
|
||||
window.loadAnimeReleases = loadAnimeReleases;
|
||||
window.loadSeriesReleases = loadSeriesReleases;
|
||||
window.loadProvidersGrid = loadProvidersGrid;
|
||||
|
||||
Reference in New Issue
Block a user