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:
@@ -259,8 +259,9 @@ async def search_anime_unified(q: str, lang: str = "vostfr", include_metadata: b
|
||||
"""
|
||||
import time
|
||||
import asyncio
|
||||
from app.providers import get_anime_providers
|
||||
from app.providers import get_anime_providers, get_series_providers
|
||||
from app.downloaders import AnimeSamaDownloader, AnimeUltimeDownloader, NekoSamaDownloader, VostfreeDownloader
|
||||
from app.downloaders.series_sites import FS7Downloader
|
||||
|
||||
print(f"\n[SEARCH] Starting search for '{q}' in {lang} (metadata={include_metadata})")
|
||||
start_time = time.time()
|
||||
@@ -275,7 +276,12 @@ async def search_anime_unified(q: str, lang: str = "vostfr", include_metadata: b
|
||||
"vostfree": VostfreeDownloader()
|
||||
}
|
||||
|
||||
# Search across all providers in parallel with timeout
|
||||
# Create series downloader instances
|
||||
series_downloaders = {
|
||||
"fs7": FS7Downloader()
|
||||
}
|
||||
|
||||
# Search across all anime providers in parallel with timeout
|
||||
search_tasks = []
|
||||
provider_ids = []
|
||||
|
||||
@@ -286,6 +292,14 @@ async def search_anime_unified(q: str, lang: str = "vostfr", include_metadata: b
|
||||
search_tasks.append(downloader.search_anime(q, lang, include_metadata=include_metadata))
|
||||
provider_ids.append(provider_id)
|
||||
|
||||
# Search across all series providers in parallel with timeout
|
||||
for provider_id, provider in get_series_providers().items():
|
||||
if provider_id in series_downloaders:
|
||||
downloader = series_downloaders[provider_id]
|
||||
print(f"[SEARCH] Queueing search on {provider_id} (series)...")
|
||||
search_tasks.append(downloader.search_anime(q, lang))
|
||||
provider_ids.append(provider_id)
|
||||
|
||||
# Wait for all searches to complete with a timeout per provider
|
||||
print(f"[SEARCH] Waiting for {len(search_tasks)} searches...")
|
||||
search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
|
||||
@@ -311,6 +325,65 @@ async def search_anime_unified(q: str, lang: str = "vostfr", include_metadata: b
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/series/search")
|
||||
async def search_series_unified(q: str, lang: str = "vf"):
|
||||
"""
|
||||
Search across all TV series providers (FS7, etc.)
|
||||
|
||||
Args:
|
||||
q: Search query
|
||||
lang: Language preference (vf, vostfr)
|
||||
"""
|
||||
import time
|
||||
import asyncio
|
||||
from app.providers import get_series_providers
|
||||
from app.downloaders.series_sites import FS7Downloader
|
||||
|
||||
print(f"\n[SERIES SEARCH] Starting search for '{q}' in {lang}")
|
||||
start_time = time.time()
|
||||
|
||||
results = {}
|
||||
|
||||
# Create series downloader instances
|
||||
series_downloaders = {
|
||||
"fs7": FS7Downloader()
|
||||
}
|
||||
|
||||
# Search across all series providers in parallel
|
||||
search_tasks = []
|
||||
provider_ids = []
|
||||
|
||||
for provider_id, provider in get_series_providers().items():
|
||||
if provider_id in series_downloaders:
|
||||
downloader = series_downloaders[provider_id]
|
||||
print(f"[SERIES SEARCH] Queueing search on {provider_id}...")
|
||||
search_tasks.append(downloader.search_anime(q, lang))
|
||||
provider_ids.append(provider_id)
|
||||
|
||||
# Wait for all searches to complete with a timeout per provider
|
||||
print(f"[SERIES SEARCH] Waiting for {len(search_tasks)} searches...")
|
||||
search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
|
||||
|
||||
# Combine results
|
||||
for provider_id, result in zip(provider_ids, search_results):
|
||||
if isinstance(result, Exception):
|
||||
print(f"[SERIES SEARCH] {provider_id} error: {str(result)}")
|
||||
elif result:
|
||||
print(f"[SERIES SEARCH] {provider_id} found {len(result)} results")
|
||||
results[provider_id] = result
|
||||
else:
|
||||
print(f"[SERIES SEARCH] {provider_id} no results")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"[SERIES SEARCH] Completed in {elapsed:.2f}s - Total results: {sum(len(r) for r in results.values())}\n")
|
||||
|
||||
return {
|
||||
"query": q,
|
||||
"lang": lang,
|
||||
"results": results
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/anime/metadata")
|
||||
async def get_anime_metadata(url: str):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user