3afad41d46
This commit implements a complete reorganization of the downloader system with a clear distinction between anime streaming sites and video hosting services. ## Structure Changes **New Organization:** - `app/downloaders/anime_sites/` - Anime streaming sites (catalogs + metadata) - `app/downloaders/video_players/` - Video hosting services (file downloads) **Base Classes:** - `BaseAnimeSite` - For anime providers (search, episodes, metadata) - `BaseVideoPlayer` - For video players (download link extraction) **Migrated Downloaders:** Anime Sites (4): - AnimeSama, NekoSama, AnimeUltime, Vostfree Video Players (8): - Doodstream, Sibnet, VidMoly, SendVid, Lpayer, 1fichier, Uptobox, Rapidfile ## Key Improvements 1. **Clear Separation**: Distinct base classes for different use cases 2. **Preserved Functionality**: All existing features maintained - VidMoly: M3U8 support, Playwright, multi-domains, target_filename param - SendVid: target_filename parameter support - All others: No behavioral changes 3. **Better Organization**: - Anime sites: search_anime(), get_episodes(), get_anime_metadata() - Video players: get_download_link(url, target_filename=None) 4. **Fixed Imports**: Updated cross-imports in AnimeSama - from ..video_players.vidmoly import - from ..video_players.sendvid import - from ..video_players.sibnet import - from ..video_players.lpayer import 5. **Updated Tests**: All test imports use new structure 6. **Updated Providers**: Added 4 missing file hosts to providers.py ## Backward Compatibility ✅ Main API unchanged: get_downloader() works identically ✅ All 23 tests passing ✅ Frontend fully functional ✅ No breaking changes for users ## Documentation - RESTRUCTURATION_SUMMARY.md - Technical details - FIX_IMPORT_ERROR.md - Import error resolution - IMPORT_VERIFICATION_REPORT.md - Complete import verification - FRONTEND_VERIFICATION_FINAL.md - Frontend validation Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
192 lines
7.5 KiB
Python
192 lines
7.5 KiB
Python
from .base import BaseVideoPlayer
|
|
from bs4 import BeautifulSoup
|
|
import re
|
|
import asyncio
|
|
|
|
|
|
class LpayerDownloader(BaseVideoPlayer):
|
|
"""Downloader for lpayer.embed4me.com video player"""
|
|
|
|
def can_handle(self, url: str) -> bool:
|
|
return 'lpayer.embed4me.com' in url.lower()
|
|
|
|
async def get_download_link(self, url: str) -> tuple[str, str]:
|
|
"""
|
|
Extract download link from Lpayer video page
|
|
Lpayer uses a React app with dynamic JavaScript - requires Playwright
|
|
"""
|
|
try:
|
|
print(f"[LPAYER] Extracting link from: {url}")
|
|
|
|
# Try using Playwright to extract video URL
|
|
video_url = await self._extract_with_playwright(url)
|
|
|
|
if not video_url:
|
|
raise Exception("Could not find video URL in Lpayer page")
|
|
|
|
print(f"[LPAYER] Found video URL: {video_url[:80]}...")
|
|
|
|
# Generate filename
|
|
filename = "lpayer_video.mp4"
|
|
|
|
return video_url, filename
|
|
|
|
except Exception as e:
|
|
raise Exception(f"Error extracting Lpayer link: {str(e)}")
|
|
|
|
async def _extract_with_playwright(self, url: str) -> str | None:
|
|
"""Extract video URL using Playwright with network interception"""
|
|
try:
|
|
from playwright.async_api import async_playwright
|
|
|
|
print("[LPAYER] Launching browser with network interception...")
|
|
|
|
video_urls = []
|
|
|
|
async with async_playwright() as p:
|
|
browser = await p.chromium.launch(
|
|
headless=True,
|
|
args=['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
|
|
)
|
|
|
|
context = await browser.new_context(
|
|
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'
|
|
)
|
|
|
|
page = await context.new_page()
|
|
|
|
# Set up request interception
|
|
async def handle_request(route):
|
|
req_url = route.request.url
|
|
|
|
# Look for video files
|
|
if any(ext in req_url.lower() for ext in ['.m3u8', '.mp4', '.mkv']):
|
|
if 'lpayer' not in req_url.lower():
|
|
print(f"[LPAYER] 🎥 Captured video URL: {req_url[:100]}...")
|
|
video_urls.append(req_url)
|
|
|
|
await route.continue_()
|
|
|
|
await page.route('**', handle_request)
|
|
|
|
print("[LPAYER] Navigating to page...")
|
|
|
|
try:
|
|
await page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
|
except Exception as e:
|
|
print(f"[LPAYER] Navigation warning: {e}")
|
|
|
|
# Wait for page to load
|
|
print("[LPAYER] Waiting for video player to load...")
|
|
await asyncio.sleep(5)
|
|
|
|
# Try to find and click play button
|
|
try:
|
|
play_selectors = [
|
|
'button[aria-label="Play"]',
|
|
'.play-button',
|
|
'video',
|
|
]
|
|
|
|
for selector in play_selectors:
|
|
try:
|
|
element = await page.query_selector(selector)
|
|
if element:
|
|
print(f"[LPAYER] Found element: {selector}")
|
|
if 'button' in selector:
|
|
await element.click()
|
|
await asyncio.sleep(3)
|
|
break
|
|
except:
|
|
continue
|
|
except Exception as e:
|
|
print(f"[LPAYER] Play button interaction: {e}")
|
|
|
|
# Wait more for network requests
|
|
await asyncio.sleep(3)
|
|
|
|
# Try JavaScript extraction
|
|
try:
|
|
js_result = await page.evaluate("""
|
|
() => {
|
|
// Check all video elements
|
|
const videos = document.querySelectorAll('video');
|
|
for (let v of videos) {
|
|
if (v.src) {
|
|
return v.src;
|
|
}
|
|
const sources = v.querySelectorAll('source');
|
|
for (let s of sources) {
|
|
if (s.src && (s.src.includes('.m3u8') || s.src.includes('.mp4'))) {
|
|
return s.src;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check window object for video URLs
|
|
for (let key in window) {
|
|
if (typeof window[key] === 'string') {
|
|
const str = window[key];
|
|
if ((str.includes('.m3u8') || str.includes('.mp4')) && str.startsWith('http')) {
|
|
return str;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
""")
|
|
|
|
if js_result and ('.m3u8' in js_result or '.mp4' in js_result):
|
|
print(f"[LPAYER] Found video URL via JavaScript")
|
|
video_urls.append(js_result)
|
|
except Exception as e:
|
|
print(f"[LPAYER] JS extraction error: {e}")
|
|
|
|
# Parse page HTML for video URLs
|
|
try:
|
|
content = await page.content()
|
|
patterns = [
|
|
r'"file"\s*:\s*"([^"]+\.m3u8[^"]*)"',
|
|
r'"file"\s*:\s*"([^"]+\.mp4[^"]*)"',
|
|
r'(https?://[^\s"\'<>]+\.m3u8[^\s"\'<>]*)',
|
|
r'(https?://[^\s"\'<>]+\.mp4[^\s"\'<>]*)',
|
|
]
|
|
|
|
for pattern in patterns:
|
|
matches = re.findall(pattern, content)
|
|
for match in matches:
|
|
match = match.replace('\\', '').replace('\/', '/')
|
|
if 'http' in match and 'lpayer' not in match:
|
|
print(f"[LPAYER] Found in HTML: {match[:100]}...")
|
|
video_urls.append(match)
|
|
except Exception as e:
|
|
print(f"[LPAYER] HTML parsing error: {e}")
|
|
|
|
await browser.close()
|
|
|
|
# Return first valid video URL
|
|
if video_urls:
|
|
seen = set()
|
|
unique_urls = []
|
|
for url in video_urls:
|
|
if url not in seen:
|
|
seen.add(url)
|
|
unique_urls.append(url)
|
|
|
|
if unique_urls:
|
|
print(f"[LPAYER] ✅ Found {len(unique_urls)} video URL(s)")
|
|
return unique_urls[0]
|
|
|
|
print("[LPAYER] ❌ No video URLs found")
|
|
return None
|
|
|
|
except ImportError:
|
|
print("[LPAYER] Playwright not installed")
|
|
return None
|
|
except Exception as e:
|
|
print(f"[LPAYER] Playwright error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return None
|