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:
root
2026-01-25 20:42:29 +00:00
parent 5e50081b58
commit c1c31d7685
17 changed files with 938 additions and 219 deletions
+18 -28
View File
@@ -424,7 +424,7 @@ class AnimeSamaDownloader(BaseAnimeSite):
filename = target_filename if target_filename else temp_filename
print(f"[ANIME-SAMA] Got video: {filename}")
print(f"[ANIME-SAMA] Video URL: {video_url[:100]}...")
print(f"[ANIME-SAMA] Video URL: {video_url[:100] if video_url else 'None'}...")
# Return the direct video URL
# The download_manager will handle the actual download
@@ -432,7 +432,8 @@ class AnimeSamaDownloader(BaseAnimeSite):
except Exception as e:
print(f"[ANIME-SAMA] Lpayer extraction error: {e}")
raise Exception(f"Error extracting from lpayer: {str(e)}")
# Re-raise with clearer message
raise Exception(f"Lpayer player not supported - this video host requires manual download. Try another host (VidMoly, SendVid, Sibnet). Error: {str(e)}")
async def _extract_from_player(self, player_url: str) -> str | None:
"""Try to extract direct video URL from player iframe"""
@@ -783,7 +784,8 @@ class AnimeSamaDownloader(BaseAnimeSite):
print(f"[ANIME-SAMA] Detected format {'A (source-based)' if is_format_a else 'B (episode-based)'} - eps1 has {len(eps1_urls)} URLs")
host_preference = ['sibnet.ru', 'vidmoly', 'sendvid', 'lpayer']
# No more host preference! Just collect all available URLs for each episode
# The download system will automatically detect and use the appropriate downloader
all_episodes_by_number = {}
if is_format_a:
@@ -797,48 +799,36 @@ class AnimeSamaDownloader(BaseAnimeSite):
if episode_num not in all_episodes_by_number:
all_episodes_by_number[episode_num] = []
# Determine host preference score (lower = better)
host_score = len(host_preference)
for i, host in enumerate(host_preference):
if host in url.lower():
host_score = i
break
all_episodes_by_number[episode_num].append((host_score, url))
all_episodes_by_number[episode_num].append(url)
else:
# Format B: Each epsX is an episode, containing multiple sources
for eps_num, urls_text in eps_matches:
episode_num = str(eps_num).zfill(2)
episode_urls = re.findall(r"'(https?://[^']+)'", urls_text)
for url in episode_urls:
if episode_num not in all_episodes_by_number:
all_episodes_by_number[episode_num] = []
if episode_num not in all_episodes_by_number:
all_episodes_by_number[episode_num] = []
# Determine host preference score (lower = better)
host_score = len(host_preference)
for i, host in enumerate(host_preference):
if host in url.lower():
host_score = i
break
all_episodes_by_number[episode_num].extend(episode_urls)
all_episodes_by_number[episode_num].append((host_score, url))
# For each episode, use the best available URL (lowest score = best host)
# For each episode, use the first available URL
# (they are usually already in order of preference on the site)
for episode_num in sorted(all_episodes_by_number.keys()):
sorted_urls = sorted(all_episodes_by_number[episode_num], key=lambda x: x[0])
best_url = sorted_urls[0][1] # Get the URL with lowest score (best host)
available_urls = all_episodes_by_number[episode_num]
# Use the first available URL (the site usually lists them in preference order)
episode_url = available_urls[0]
episode_title = f'Episode {episode_num}'
combined_url = f"{best_url}|{anime_url}|{episode_title}"
combined_url = f"{episode_url}|{anime_url}|{episode_title}"
episodes.append({
'episode': episode_num,
'url': combined_url,
'title': episode_title
'title': episode_title,
'available_hosts': len(available_urls) # Store count of available hosts
})
print(f"[ANIME-SAMA] Found {len(episodes)} episodes (prioritizing {host_preference})")
print(f"[ANIME-SAMA] Found {len(episodes)} episodes")
return episodes
except Exception as e: