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:
+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