fix: resolve line length violations (80+ characters)
- refactor src/cli/Main.py: split long logging config, user prompts, and method calls - refactor src/config/settings.py: break long Field definitions into multiple lines - refactor src/core/providers/enhanced_provider.py: split provider lists, headers, and long f-strings - refactor src/core/providers/streaming/voe.py: format HTTP header setup - update QualityTODO.md: mark all line length violations as completed All files now comply with 88-character line limit. Code readability improved with better-structured multi-line statements and intermediate variables for complex expressions.
This commit is contained in:
parent
68c2f9bda2
commit
80507119b7
@ -80,18 +80,14 @@ conda run -n AniWorld python -m pytest tests/ -v -s
|
|||||||
|
|
||||||
#### Line Length Violations (80+ characters)
|
#### Line Length Violations (80+ characters)
|
||||||
|
|
||||||
- [ ] `src/cli/Main.py` line 14 - Logging configuration exceeds 100 chars
|
✅ **COMPLETED** - All line length violations have been fixed:
|
||||||
- [ ] `src/cli/Main.py` line 80 - User input prompt exceeds 88 chars
|
|
||||||
- [ ] `src/cli/Main.py` line 91 - List comprehension exceeds 100 chars
|
- ✅ `src/cli/Main.py` - Refactored long lines 14, 80, 91, 118, 122, 127, 133, 155, 157, 159, 175, 184, 197, 206, 227
|
||||||
- [ ] `src/cli/Main.py` line 118 - Nested sum with comprehension exceeds 100 chars
|
- ✅ `src/config/settings.py` - Fixed lines 9, 11, 12, 18
|
||||||
- [ ] `src/cli/Main.py` line 133 - Method call with many arguments exceeds 100 chars
|
- ✅ `src/core/providers/enhanced_provider.py` - Refactored multiple long lines with headers and logging messages
|
||||||
- [ ] `src/config/settings.py` line 9 - Field definition exceeds 100 chars
|
- ✅ `src/core/providers/streaming/voe.py` - Fixed line 52
|
||||||
- [ ] `src/config/settings.py` line 18 - Field definition exceeds 100 chars
|
- ✅ `src/server/utils/dependencies.py` - No violations found (line 260 was already compliant)
|
||||||
- [ ] `src/server/utils/dependencies.py` line 260 - Conditional check exceeds 100 chars
|
- ✅ `src/server/database/models.py` - No violations found
|
||||||
- [ ] `src/core/providers/enhanced_provider.py` line 45 - List of providers exceeds 88 chars
|
|
||||||
- [ ] `src/core/providers/enhanced_provider.py` line 48-61 - Header dict values exceed 88 chars (10+ instances)
|
|
||||||
- [ ] `src/core/providers/streaming/voe.py` line 52 - HTTP header setup exceeds 100 chars
|
|
||||||
- [ ] `src/server/database/models.py` - Check field documentation lengths
|
|
||||||
|
|
||||||
#### Naming Convention Issues
|
#### Naming Convention Issues
|
||||||
|
|
||||||
|
|||||||
154
src/cli/Main.py
154
src/cli/Main.py
@ -1,22 +1,22 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
from ..core.providers import aniworld_provider
|
import os
|
||||||
|
import sys
|
||||||
from rich.progress import Progress
|
|
||||||
from ..core.entities import SerieList
|
|
||||||
from ..core.SerieScanner import SerieScanner
|
|
||||||
from ..core.providers.provider_factory import Loaders
|
|
||||||
from ..core.entities.series import Serie
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from rich.progress import Progress
|
||||||
|
|
||||||
|
from ..core.entities import SerieList
|
||||||
|
from ..core.entities.series import Serie
|
||||||
|
from ..core.providers import aniworld_provider
|
||||||
|
from ..core.providers.provider_factory import Loaders
|
||||||
|
from ..core.SerieScanner import SerieScanner
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.FATAL, format='%(asctime)s - %(levelname)s - %(funcName)s - %(message)s')
|
log_format = "%(asctime)s - %(levelname)s - %(funcName)s - %(message)s"
|
||||||
|
logging.basicConfig(level=logging.FATAL, format=log_format)
|
||||||
console_handler = logging.StreamHandler()
|
console_handler = logging.StreamHandler()
|
||||||
console_handler.setLevel(logging.ERROR)
|
console_handler.setLevel(logging.ERROR)
|
||||||
console_handler.setFormatter(logging.Formatter(
|
console_handler.setFormatter(logging.Formatter(log_format))
|
||||||
"%(asctime)s - %(levelname)s - %(funcName)s - %(message)s")
|
|
||||||
)
|
|
||||||
for h in logging.root.handlers:
|
for h in logging.root.handlers:
|
||||||
logging.root.removeHandler(h)
|
logging.root.removeHandler(h)
|
||||||
|
|
||||||
@ -76,8 +76,11 @@ class SeriesApp:
|
|||||||
"""Handle user input for selecting series."""
|
"""Handle user input for selecting series."""
|
||||||
self.display_series()
|
self.display_series()
|
||||||
while True:
|
while True:
|
||||||
selection = input(
|
prompt = (
|
||||||
"\nSelect series by number (e.g. '1', '1,2' or 'all') or type 'exit' to return: ").strip().lower()
|
"\nSelect series by number (e.g. '1', '1,2' or 'all') "
|
||||||
|
"or type 'exit' to return: "
|
||||||
|
)
|
||||||
|
selection = input(prompt).strip().lower()
|
||||||
|
|
||||||
if selection == "exit":
|
if selection == "exit":
|
||||||
return None
|
return None
|
||||||
@ -87,17 +90,31 @@ class SeriesApp:
|
|||||||
selected_series = self.series_list
|
selected_series = self.series_list
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
indexes = [int(num) - 1 for num in selection.split(",")]
|
indexes = [
|
||||||
selected_series = [self.series_list[i] for i in indexes if 0 <= i < len(self.series_list)]
|
int(num) - 1 for num in selection.split(",")
|
||||||
|
]
|
||||||
|
selected_series = [
|
||||||
|
self.series_list[i]
|
||||||
|
for i in indexes
|
||||||
|
if 0 <= i < len(self.series_list)
|
||||||
|
]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("Invalid selection. Going back to the result display.")
|
msg = (
|
||||||
|
"Invalid selection. "
|
||||||
|
"Going back to the result display."
|
||||||
|
)
|
||||||
|
print(msg)
|
||||||
self.display_series()
|
self.display_series()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if selected_series:
|
if selected_series:
|
||||||
return selected_series
|
return selected_series
|
||||||
else:
|
else:
|
||||||
print("No valid series selected. Going back to the result display.")
|
msg = (
|
||||||
|
"No valid series selected. "
|
||||||
|
"Going back to the result display."
|
||||||
|
)
|
||||||
|
print(msg)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -107,7 +124,6 @@ class SeriesApp:
|
|||||||
func(*args, **kwargs)
|
func(*args, **kwargs)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
||||||
print(e)
|
print(e)
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
return False
|
return False
|
||||||
@ -115,22 +131,45 @@ class SeriesApp:
|
|||||||
def download_series(self, series):
|
def download_series(self, series):
|
||||||
"""Simulate the downloading process with a progress bar."""
|
"""Simulate the downloading process with a progress bar."""
|
||||||
total_downloaded = 0
|
total_downloaded = 0
|
||||||
total_episodes = sum(sum(len(ep) for ep in serie.episodeDict.values()) for serie in series)
|
total_episodes = sum(
|
||||||
|
sum(len(ep) for ep in serie.episodeDict.values())
|
||||||
|
for serie in series
|
||||||
|
)
|
||||||
self.progress = Progress()
|
self.progress = Progress()
|
||||||
task1 = self.progress.add_task("[red]Processing...", total=total_episodes)
|
task1 = self.progress.add_task(
|
||||||
task2 = self.progress.add_task(f"[green]...", total=0)
|
"[red]Processing...", total=total_episodes
|
||||||
self.task3 = self.progress.add_task(f"[Gray]...", total=100) # Setze total auf 100 für Prozentanzeige
|
)
|
||||||
|
task2 = self.progress.add_task("[green]...", total=0)
|
||||||
|
# Set total to 100 for percentage display
|
||||||
|
self.task3 = self.progress.add_task("[Gray]...", total=100)
|
||||||
self.progress.start()
|
self.progress.start()
|
||||||
|
|
||||||
for serie in series:
|
for serie in series:
|
||||||
serie_episodes = sum(len(ep) for ep in serie.episodeDict.values())
|
serie_episodes = sum(
|
||||||
self.progress.update(task2, description=f"[green]{serie.folder}", total=serie_episodes)
|
len(ep) for ep in serie.episodeDict.values()
|
||||||
|
)
|
||||||
|
self.progress.update(
|
||||||
|
task2,
|
||||||
|
description=f"[green]{serie.folder}",
|
||||||
|
total=serie_episodes,
|
||||||
|
)
|
||||||
downloaded = 0
|
downloaded = 0
|
||||||
for season, episodes in serie.episodeDict.items():
|
for season, episodes in serie.episodeDict.items():
|
||||||
for episode in episodes:
|
for episode in episodes:
|
||||||
loader = self.Loaders.GetLoader(key="aniworld.to")
|
loader = self.Loaders.GetLoader(key="aniworld.to")
|
||||||
if loader.IsLanguage(season, episode, serie.key):
|
if loader.IsLanguage(season, episode, serie.key):
|
||||||
self.retry(loader.Download, 3, 1, self.directory_to_search, serie.folder, season, episode, serie.key, "German Dub",self.print_Download_Progress)
|
self.retry(
|
||||||
|
loader.Download,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
self.directory_to_search,
|
||||||
|
serie.folder,
|
||||||
|
season,
|
||||||
|
episode,
|
||||||
|
serie.key,
|
||||||
|
"German Dub",
|
||||||
|
self.print_Download_Progress,
|
||||||
|
)
|
||||||
|
|
||||||
downloaded += 1
|
downloaded += 1
|
||||||
total_downloaded += 1
|
total_downloaded += 1
|
||||||
@ -143,20 +182,29 @@ class SeriesApp:
|
|||||||
self.progress = None
|
self.progress = None
|
||||||
|
|
||||||
def print_Download_Progress(self, d):
|
def print_Download_Progress(self, d):
|
||||||
# Nutze self.progress und self.task3 für Fortschrittsanzeige
|
"""Update download progress in the UI."""
|
||||||
if self.progress is None or not hasattr(self, 'task3'):
|
# Use self.progress and self.task3 to display progress
|
||||||
|
if self.progress is None or not hasattr(self, "task3"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if d['status'] == 'downloading':
|
if d["status"] == "downloading":
|
||||||
total = d.get('total_bytes') or d.get('total_bytes_estimate')
|
total = d.get("total_bytes") or d.get("total_bytes_estimate")
|
||||||
downloaded = d.get('downloaded_bytes', 0)
|
downloaded = d.get("downloaded_bytes", 0)
|
||||||
if total:
|
if total:
|
||||||
percent = downloaded / total * 100
|
percent = downloaded / total * 100
|
||||||
self.progress.update(self.task3, completed=percent, description=f"[gray]Download: {percent:.1f}%")
|
desc = f"[gray]Download: {percent:.1f}%"
|
||||||
|
self.progress.update(
|
||||||
|
self.task3, completed=percent, description=desc
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.progress.update(self.task3, description=f"[gray]{downloaded/1024/1024:.2f}MB geladen")
|
mb_downloaded = downloaded / 1024 / 1024
|
||||||
elif d['status'] == 'finished':
|
desc = f"[gray]{mb_downloaded:.2f}MB geladen"
|
||||||
self.progress.update(self.task3, completed=100, description="[gray]Download abgeschlossen.")
|
self.progress.update(self.task3, description=desc)
|
||||||
|
elif d["status"] == "finished":
|
||||||
|
desc = "[gray]Download abgeschlossen."
|
||||||
|
self.progress.update(
|
||||||
|
self.task3, completed=100, description=desc
|
||||||
|
)
|
||||||
|
|
||||||
def search_mode(self):
|
def search_mode(self):
|
||||||
"""Search for a series and allow user to select an option."""
|
"""Search for a series and allow user to select an option."""
|
||||||
@ -172,7 +220,10 @@ class SeriesApp:
|
|||||||
print(f"{i}. {result.get('name')}")
|
print(f"{i}. {result.get('name')}")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
selection = input("\nSelect an option by number or type '<enter>' to return: ").strip().lower()
|
prompt = (
|
||||||
|
"\nSelect an option by number or type '<enter>' to return: "
|
||||||
|
)
|
||||||
|
selection = input(prompt).strip().lower()
|
||||||
|
|
||||||
if selection == "":
|
if selection == "":
|
||||||
return
|
return
|
||||||
@ -181,7 +232,14 @@ class SeriesApp:
|
|||||||
index = int(selection) - 1
|
index = int(selection) - 1
|
||||||
if 0 <= index < len(results):
|
if 0 <= index < len(results):
|
||||||
chosen_name = results[index]
|
chosen_name = results[index]
|
||||||
self.List.add(Serie(chosen_name["link"], chosen_name["name"], "aniworld.to", chosen_name["link"], {}))
|
serie = Serie(
|
||||||
|
chosen_name["link"],
|
||||||
|
chosen_name["name"],
|
||||||
|
"aniworld.to",
|
||||||
|
chosen_name["link"],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
self.List.add(serie)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
print("Invalid selection. Try again.")
|
print("Invalid selection. Try again.")
|
||||||
@ -194,16 +252,22 @@ class SeriesApp:
|
|||||||
def run(self):
|
def run(self):
|
||||||
"""Main function to run the app."""
|
"""Main function to run the app."""
|
||||||
while True:
|
while True:
|
||||||
action = input("\nChoose action ('s' for search, 'i' for init or 'd' for download): ").strip().lower()
|
prompt = (
|
||||||
|
"\nChoose action ('s' for search, 'i' for init "
|
||||||
|
"or 'd' for download): "
|
||||||
|
)
|
||||||
|
action = input(prompt).strip().lower()
|
||||||
|
|
||||||
if action == "s":
|
if action == "s":
|
||||||
self.search_mode()
|
self.search_mode()
|
||||||
if action == "i":
|
if action == "i":
|
||||||
|
|
||||||
print("\nRescanning series...\n")
|
print("\nRescanning series...\n")
|
||||||
|
|
||||||
self.progress = Progress()
|
self.progress = Progress()
|
||||||
self.task1 = self.progress.add_task("[red]items processed...", total=300)
|
task1 = self.progress.add_task(
|
||||||
|
"[red]items processed...", total=300
|
||||||
|
)
|
||||||
|
self.task1 = task1
|
||||||
self.progress.start()
|
self.progress.start()
|
||||||
|
|
||||||
self.SerieScanner.Reinit()
|
self.SerieScanner.Reinit()
|
||||||
@ -220,10 +284,12 @@ class SeriesApp:
|
|||||||
if selected_series:
|
if selected_series:
|
||||||
self.download_series(selected_series)
|
self.download_series(selected_series)
|
||||||
|
|
||||||
# Run the app
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
# Read the base directory from an environment variable
|
# Read the base directory from an environment variable
|
||||||
directory_to_search = os.getenv("ANIME_DIRECTORY", "\\\\sshfs.r\\ubuntu@192.168.178.43\\media\\serien\\Serien")
|
default_dir = (
|
||||||
|
"\\\\sshfs.r\\ubuntu@192.168.178.43\\media\\serien\\Serien"
|
||||||
|
)
|
||||||
|
directory_to_search = os.getenv("ANIME_DIRECTORY", default_dir)
|
||||||
app = SeriesApp(directory_to_search)
|
app = SeriesApp(directory_to_search)
|
||||||
app.run()
|
app.run()
|
||||||
@ -6,19 +6,32 @@ from pydantic_settings import BaseSettings
|
|||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
"""Application settings from environment variables."""
|
"""Application settings from environment variables."""
|
||||||
jwt_secret_key: str = Field(default="your-secret-key-here", env="JWT_SECRET_KEY")
|
jwt_secret_key: str = Field(
|
||||||
|
default="your-secret-key-here", env="JWT_SECRET_KEY"
|
||||||
|
)
|
||||||
password_salt: str = Field(default="default-salt", env="PASSWORD_SALT")
|
password_salt: str = Field(default="default-salt", env="PASSWORD_SALT")
|
||||||
master_password_hash: Optional[str] = Field(default=None, env="MASTER_PASSWORD_HASH")
|
master_password_hash: Optional[str] = Field(
|
||||||
master_password: Optional[str] = Field(default=None, env="MASTER_PASSWORD") # For development
|
default=None, env="MASTER_PASSWORD_HASH"
|
||||||
token_expiry_hours: int = Field(default=24, env="SESSION_TIMEOUT_HOURS")
|
)
|
||||||
|
# For development
|
||||||
|
master_password: Optional[str] = Field(
|
||||||
|
default=None, env="MASTER_PASSWORD"
|
||||||
|
)
|
||||||
|
token_expiry_hours: int = Field(
|
||||||
|
default=24, env="SESSION_TIMEOUT_HOURS"
|
||||||
|
)
|
||||||
anime_directory: str = Field(default="", env="ANIME_DIRECTORY")
|
anime_directory: str = Field(default="", env="ANIME_DIRECTORY")
|
||||||
log_level: str = Field(default="INFO", env="LOG_LEVEL")
|
log_level: str = Field(default="INFO", env="LOG_LEVEL")
|
||||||
|
|
||||||
# Additional settings from .env
|
# Additional settings from .env
|
||||||
database_url: str = Field(default="sqlite:///./data/aniworld.db", env="DATABASE_URL")
|
database_url: str = Field(
|
||||||
|
default="sqlite:///./data/aniworld.db", env="DATABASE_URL"
|
||||||
|
)
|
||||||
cors_origins: str = Field(default="*", env="CORS_ORIGINS")
|
cors_origins: str = Field(default="*", env="CORS_ORIGINS")
|
||||||
api_rate_limit: int = Field(default=100, env="API_RATE_LIMIT")
|
api_rate_limit: int = Field(default=100, env="API_RATE_LIMIT")
|
||||||
default_provider: str = Field(default="aniworld.to", env="DEFAULT_PROVIDER")
|
default_provider: str = Field(
|
||||||
|
default="aniworld.to", env="DEFAULT_PROVIDER"
|
||||||
|
)
|
||||||
provider_timeout: int = Field(default=30, env="PROVIDER_TIMEOUT")
|
provider_timeout: int = Field(default=30, env="PROVIDER_TIMEOUT")
|
||||||
retry_attempts: int = Field(default=3, env="RETRY_ATTEMPTS")
|
retry_attempts: int = Field(default=3, env="RETRY_ATTEMPTS")
|
||||||
|
|
||||||
|
|||||||
@ -5,35 +5,35 @@ This module extends the original AniWorldLoader with comprehensive
|
|||||||
error handling, retry mechanisms, and recovery strategies.
|
error handling, retry mechanisms, and recovery strategies.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import html
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import logging
|
import shutil
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import html
|
|
||||||
from urllib.parse import quote
|
|
||||||
import time
|
import time
|
||||||
import hashlib
|
from typing import Any, Callable, Dict, Optional
|
||||||
from typing import Optional, Dict, Any, Callable
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
from error_handler import (
|
||||||
|
DownloadError,
|
||||||
|
NetworkError,
|
||||||
|
NonRetryableError,
|
||||||
|
RetryableError,
|
||||||
|
file_corruption_detector,
|
||||||
|
recovery_strategies,
|
||||||
|
with_error_recovery,
|
||||||
|
)
|
||||||
from fake_useragent import UserAgent
|
from fake_useragent import UserAgent
|
||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
from urllib3.util.retry import Retry
|
from urllib3.util.retry import Retry
|
||||||
from yt_dlp import YoutubeDL
|
from yt_dlp import YoutubeDL
|
||||||
import shutil
|
|
||||||
|
|
||||||
from .base_provider import Loader
|
|
||||||
from ..interfaces.providers import Providers
|
from ..interfaces.providers import Providers
|
||||||
from error_handler import (
|
from .base_provider import Loader
|
||||||
with_error_recovery,
|
|
||||||
recovery_strategies,
|
|
||||||
NetworkError,
|
|
||||||
DownloadError,
|
|
||||||
RetryableError,
|
|
||||||
NonRetryableError,
|
|
||||||
file_corruption_detector
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EnhancedAniWorldLoader(Loader):
|
class EnhancedAniWorldLoader(Loader):
|
||||||
@ -42,15 +42,32 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
self.SUPPORTED_PROVIDERS = ["VOE", "Doodstream", "Vidmoly", "Vidoza", "SpeedFiles", "Streamtape", "Luluvdo"]
|
providers = [
|
||||||
|
"VOE",
|
||||||
|
"Doodstream",
|
||||||
|
"Vidmoly",
|
||||||
|
"Vidoza",
|
||||||
|
"SpeedFiles",
|
||||||
|
"Streamtape",
|
||||||
|
"Luluvdo",
|
||||||
|
]
|
||||||
|
self.SUPPORTED_PROVIDERS = providers
|
||||||
|
|
||||||
self.AniworldHeaders = {
|
self.AniworldHeaders = {
|
||||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
"accept": (
|
||||||
|
"text/html,application/xhtml+xml,application/xml;q=0.9,"
|
||||||
|
"image/avif,image/webp,image/apng,*/*;q=0.8"
|
||||||
|
),
|
||||||
"accept-encoding": "gzip, deflate, br, zstd",
|
"accept-encoding": "gzip, deflate, br, zstd",
|
||||||
"accept-language": "de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
"accept-language": (
|
||||||
|
"de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"
|
||||||
|
),
|
||||||
"cache-control": "max-age=0",
|
"cache-control": "max-age=0",
|
||||||
"priority": "u=0, i",
|
"priority": "u=0, i",
|
||||||
"sec-ch-ua": '"Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"',
|
"sec-ch-ua": (
|
||||||
|
'"Chromium";v="136", "Microsoft Edge";v="136", '
|
||||||
|
'"Not.A/Brand";v="99"'
|
||||||
|
),
|
||||||
"sec-ch-ua-mobile": "?0",
|
"sec-ch-ua-mobile": "?0",
|
||||||
"sec-ch-ua-platform": '"Windows"',
|
"sec-ch-ua-platform": '"Windows"',
|
||||||
"sec-fetch-dest": "document",
|
"sec-fetch-dest": "document",
|
||||||
@ -58,12 +75,32 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
"sec-fetch-site": "none",
|
"sec-fetch-site": "none",
|
||||||
"sec-fetch-user": "?1",
|
"sec-fetch-user": "?1",
|
||||||
"upgrade-insecure-requests": "1",
|
"upgrade-insecure-requests": "1",
|
||||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
|
"user-agent": (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.INVALID_PATH_CHARS = ['<', '>', ':', '"', '/', '\\', '|', '?', '*', '&']
|
invalid_chars = [
|
||||||
|
"<",
|
||||||
|
">",
|
||||||
|
":",
|
||||||
|
'"',
|
||||||
|
"/",
|
||||||
|
"\\",
|
||||||
|
"|",
|
||||||
|
"?",
|
||||||
|
"*",
|
||||||
|
"&",
|
||||||
|
]
|
||||||
|
self.INVALID_PATH_CHARS = invalid_chars
|
||||||
self.RANDOM_USER_AGENT = UserAgent().random
|
self.RANDOM_USER_AGENT = UserAgent().random
|
||||||
self.LULUVDO_USER_AGENT = "Mozilla/5.0 (Android 15; Mobile; rv:132.0) Gecko/132.0 Firefox/132.0"
|
android_ua = (
|
||||||
|
"Mozilla/5.0 (Android 15; Mobile; rv:132.0) "
|
||||||
|
"Gecko/132.0 Firefox/132.0"
|
||||||
|
)
|
||||||
|
self.LULUVDO_USER_AGENT = android_ua
|
||||||
|
|
||||||
self.PROVIDER_HEADERS = {
|
self.PROVIDER_HEADERS = {
|
||||||
"Vidmoly": ['Referer: "https://vidmoly.to"'],
|
"Vidmoly": ['Referer: "https://vidmoly.to"'],
|
||||||
@ -71,10 +108,10 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
"VOE": [f'User-Agent: {self.RANDOM_USER_AGENT}'],
|
"VOE": [f'User-Agent: {self.RANDOM_USER_AGENT}'],
|
||||||
"Luluvdo": [
|
"Luluvdo": [
|
||||||
f'User-Agent: {self.LULUVDO_USER_AGENT}',
|
f'User-Agent: {self.LULUVDO_USER_AGENT}',
|
||||||
'Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
|
"Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||||
'Origin: "https://luluvdo.com"',
|
'Origin: "https://luluvdo.com"',
|
||||||
'Referer: "https://luluvdo.com/"'
|
'Referer: "https://luluvdo.com/"',
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ANIWORLD_TO = "https://aniworld.to"
|
self.ANIWORLD_TO = "https://aniworld.to"
|
||||||
@ -224,64 +261,99 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
try:
|
try:
|
||||||
decoded_data = strategy(clean_text)
|
decoded_data = strategy(clean_text)
|
||||||
if isinstance(decoded_data, list):
|
if isinstance(decoded_data, list):
|
||||||
self.logger.debug(f"Successfully parsed anime response with strategy {i + 1}")
|
msg = (
|
||||||
|
f"Successfully parsed anime response with "
|
||||||
|
f"strategy {i + 1}"
|
||||||
|
)
|
||||||
|
self.logger.debug(msg)
|
||||||
return decoded_data
|
return decoded_data
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Strategy {i + 1} returned non-list data: {type(decoded_data)}")
|
msg = (
|
||||||
|
f"Strategy {i + 1} returned non-list data: "
|
||||||
|
f"{type(decoded_data)}"
|
||||||
|
)
|
||||||
|
self.logger.warning(msg)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
self.logger.debug(f"Parsing strategy {i + 1} failed: {e}")
|
msg = f"Parsing strategy {i + 1} failed: {e}"
|
||||||
|
self.logger.debug(msg)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raise ValueError("Could not parse anime search response with any strategy")
|
raise ValueError(
|
||||||
|
"Could not parse anime search response with any strategy"
|
||||||
|
)
|
||||||
|
|
||||||
def _GetLanguageKey(self, language: str) -> int:
|
def _GetLanguageKey(self, language: str) -> int:
|
||||||
"""Get numeric language code."""
|
"""Get numeric language code."""
|
||||||
language_map = {
|
language_map = {
|
||||||
"German Dub": 1,
|
"German Dub": 1,
|
||||||
"English Sub": 2,
|
"English Sub": 2,
|
||||||
"German Sub": 3
|
"German Sub": 3,
|
||||||
}
|
}
|
||||||
return language_map.get(language, 0)
|
return language_map.get(language, 0)
|
||||||
|
|
||||||
@with_error_recovery(max_retries=2, context="language_check")
|
@with_error_recovery(max_retries=2, context="language_check")
|
||||||
def IsLanguage(self, season: int, episode: int, key: str, language: str = "German Dub") -> bool:
|
def IsLanguage(
|
||||||
"""Check if episode is available in specified language with error handling."""
|
self,
|
||||||
|
season: int,
|
||||||
|
episode: int,
|
||||||
|
key: str,
|
||||||
|
language: str = "German Dub",
|
||||||
|
) -> bool:
|
||||||
|
"""Check if episode is available in specified language."""
|
||||||
try:
|
try:
|
||||||
languageCode = self._GetLanguageKey(language)
|
languageCode = self._GetLanguageKey(language)
|
||||||
if languageCode == 0:
|
if languageCode == 0:
|
||||||
raise ValueError(f"Unknown language: {language}")
|
raise ValueError(f"Unknown language: {language}")
|
||||||
|
|
||||||
episode_response = self._GetEpisodeHTML(season, episode, key)
|
episode_response = self._GetEpisodeHTML(season, episode, key)
|
||||||
soup = BeautifulSoup(episode_response.content, 'html.parser')
|
soup = BeautifulSoup(episode_response.content, "html.parser")
|
||||||
|
|
||||||
change_language_box_div = soup.find('div', class_='changeLanguageBox')
|
lang_box = soup.find("div", class_="changeLanguageBox")
|
||||||
if not change_language_box_div:
|
if not lang_box:
|
||||||
self.logger.debug(f"No language box found for {key} S{season}E{episode}")
|
debug_msg = (
|
||||||
|
f"No language box found for {key} S{season}E{episode}"
|
||||||
|
)
|
||||||
|
self.logger.debug(debug_msg)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
img_tags = change_language_box_div.find_all('img')
|
img_tags = lang_box.find_all("img")
|
||||||
available_languages = []
|
available_languages = []
|
||||||
|
|
||||||
for img in img_tags:
|
for img in img_tags:
|
||||||
lang_key = img.get('data-lang-key')
|
lang_key = img.get("data-lang-key")
|
||||||
if lang_key and lang_key.isdigit():
|
if lang_key and lang_key.isdigit():
|
||||||
available_languages.append(int(lang_key))
|
available_languages.append(int(lang_key))
|
||||||
|
|
||||||
is_available = languageCode in available_languages
|
is_available = languageCode in available_languages
|
||||||
self.logger.debug(f"Language check for {key} S{season}E{episode} - "
|
debug_msg = (
|
||||||
f"Requested: {languageCode}, Available: {available_languages}, "
|
f"Language check for {key} S{season}E{episode}: "
|
||||||
f"Result: {is_available}")
|
f"Requested={languageCode}, "
|
||||||
|
f"Available={available_languages}, "
|
||||||
|
f"Result={is_available}"
|
||||||
|
)
|
||||||
|
self.logger.debug(debug_msg)
|
||||||
|
|
||||||
return is_available
|
return is_available
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Language check failed for {key} S{season}E{episode}: {e}")
|
error_msg = (
|
||||||
|
f"Language check failed for {key} S{season}E{episode}: {e}"
|
||||||
|
)
|
||||||
|
self.logger.error(error_msg)
|
||||||
raise RetryableError(f"Language check failed: {e}") from e
|
raise RetryableError(f"Language check failed: {e}") from e
|
||||||
|
|
||||||
def Download(self, baseDirectory: str, serieFolder: str, season: int, episode: int,
|
def Download(
|
||||||
key: str, language: str = "German Dub", progress_callback: Callable = None) -> bool:
|
self,
|
||||||
"""Download episode with comprehensive error handling and recovery."""
|
baseDirectory: str,
|
||||||
self.download_stats['total_downloads'] += 1
|
serieFolder: str,
|
||||||
|
season: int,
|
||||||
|
episode: int,
|
||||||
|
key: str,
|
||||||
|
language: str = "German Dub",
|
||||||
|
progress_callback: Optional[Callable] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Download episode with comprehensive error handling."""
|
||||||
|
self.download_stats["total_downloads"] += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Validate inputs
|
# Validate inputs
|
||||||
@ -292,8 +364,10 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
raise ValueError("Season and episode must be non-negative")
|
raise ValueError("Season and episode must be non-negative")
|
||||||
|
|
||||||
# Prepare file paths
|
# Prepare file paths
|
||||||
sanitized_anime_title = ''.join(
|
sanitized_anime_title = "".join(
|
||||||
char for char in self.GetTitle(key) if char not in self.INVALID_PATH_CHARS
|
char
|
||||||
|
for char in self.GetTitle(key)
|
||||||
|
if char not in self.INVALID_PATH_CHARS
|
||||||
)
|
)
|
||||||
|
|
||||||
if not sanitized_anime_title:
|
if not sanitized_anime_title:
|
||||||
@ -301,26 +375,43 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
|
|
||||||
# Generate output filename
|
# Generate output filename
|
||||||
if season == 0:
|
if season == 0:
|
||||||
output_file = f"{sanitized_anime_title} - Movie {episode:02} - ({language}).mp4"
|
output_file = (
|
||||||
|
f"{sanitized_anime_title} - Movie {episode:02} - "
|
||||||
|
f"({language}).mp4"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
output_file = f"{sanitized_anime_title} - S{season:02}E{episode:03} - ({language}).mp4"
|
output_file = (
|
||||||
|
f"{sanitized_anime_title} - S{season:02}E{episode:03} - "
|
||||||
|
f"({language}).mp4"
|
||||||
|
)
|
||||||
|
|
||||||
# Create directory structure
|
# Create directory structure
|
||||||
folder_path = os.path.join(baseDirectory, serieFolder, f"Season {season}")
|
folder_path = os.path.join(
|
||||||
|
baseDirectory, serieFolder, f"Season {season}"
|
||||||
|
)
|
||||||
output_path = os.path.join(folder_path, output_file)
|
output_path = os.path.join(folder_path, output_file)
|
||||||
|
|
||||||
# Check if file already exists and is valid
|
# Check if file already exists and is valid
|
||||||
if os.path.exists(output_path):
|
if os.path.exists(output_path):
|
||||||
if file_corruption_detector.is_valid_video_file(output_path):
|
if file_corruption_detector.is_valid_video_file(output_path):
|
||||||
self.logger.info(f"File already exists and is valid: {output_file}")
|
msg = (
|
||||||
self.download_stats['successful_downloads'] += 1
|
f"File already exists and is valid: "
|
||||||
|
f"{output_file}"
|
||||||
|
)
|
||||||
|
self.logger.info(msg)
|
||||||
|
self.download_stats["successful_downloads"] += 1
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Existing file appears corrupted, removing: {output_path}")
|
warning_msg = (
|
||||||
|
f"Existing file appears corrupted, removing: "
|
||||||
|
f"{output_path}"
|
||||||
|
)
|
||||||
|
self.logger.warning(warning_msg)
|
||||||
try:
|
try:
|
||||||
os.remove(output_path)
|
os.remove(output_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to remove corrupted file: {e}")
|
error_msg = f"Failed to remove corrupted file: {e}"
|
||||||
|
self.logger.error(error_msg)
|
||||||
|
|
||||||
os.makedirs(folder_path, exist_ok=True)
|
os.makedirs(folder_path, exist_ok=True)
|
||||||
|
|
||||||
@ -331,59 +422,84 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
|
|
||||||
# Attempt download with recovery strategies
|
# Attempt download with recovery strategies
|
||||||
success = self._download_with_recovery(
|
success = self._download_with_recovery(
|
||||||
season, episode, key, language, temp_path, output_path, progress_callback
|
season,
|
||||||
|
episode,
|
||||||
|
key,
|
||||||
|
language,
|
||||||
|
temp_path,
|
||||||
|
output_path,
|
||||||
|
progress_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self.download_stats['successful_downloads'] += 1
|
self.download_stats["successful_downloads"] += 1
|
||||||
self.logger.info(f"Successfully downloaded: {output_file}")
|
success_msg = f"Successfully downloaded: {output_file}"
|
||||||
|
self.logger.info(success_msg)
|
||||||
else:
|
else:
|
||||||
self.download_stats['failed_downloads'] += 1
|
self.download_stats["failed_downloads"] += 1
|
||||||
self.download_error_logger.error(
|
fail_msg = (
|
||||||
f"Download failed for {key} S{season}E{episode} ({language})"
|
f"Download failed for {key} S{season}E{episode} "
|
||||||
|
f"({language})"
|
||||||
)
|
)
|
||||||
|
self.download_error_logger.error(fail_msg)
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.download_stats['failed_downloads'] += 1
|
self.download_stats["failed_downloads"] += 1
|
||||||
self.download_error_logger.error(
|
err_msg = (
|
||||||
f"Download error for {key} S{season}E{episode}: {e}", exc_info=True
|
f"Download error for {key} S{season}E{episode}: {e}"
|
||||||
)
|
)
|
||||||
|
self.download_error_logger.error(err_msg, exc_info=True)
|
||||||
raise DownloadError(f"Download failed: {e}") from e
|
raise DownloadError(f"Download failed: {e}") from e
|
||||||
finally:
|
finally:
|
||||||
self.ClearCache()
|
self.ClearCache()
|
||||||
|
|
||||||
def _download_with_recovery(self, season: int, episode: int, key: str, language: str,
|
def _download_with_recovery(
|
||||||
temp_path: str, output_path: str, progress_callback: Callable) -> bool:
|
self,
|
||||||
"""Attempt download with multiple providers and recovery strategies."""
|
season: int,
|
||||||
|
episode: int,
|
||||||
|
key: str,
|
||||||
|
language: str,
|
||||||
|
temp_path: str,
|
||||||
|
output_path: str,
|
||||||
|
progress_callback: Optional[Callable],
|
||||||
|
) -> bool:
|
||||||
|
"""Attempt download with multiple providers and recovery."""
|
||||||
|
|
||||||
for provider_name in self.SUPPORTED_PROVIDERS:
|
for provider_name in self.SUPPORTED_PROVIDERS:
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"Attempting download with provider: {provider_name}")
|
info_msg = f"Attempting download with provider: {provider_name}"
|
||||||
|
self.logger.info(info_msg)
|
||||||
|
|
||||||
# Get download link and headers for provider
|
# Get download link and headers for provider
|
||||||
link, headers = recovery_strategies.handle_network_failure(
|
link, headers = recovery_strategies.handle_network_failure(
|
||||||
self._get_direct_link_from_provider,
|
self._get_direct_link_from_provider,
|
||||||
season, episode, key, language
|
season,
|
||||||
|
episode,
|
||||||
|
key,
|
||||||
|
language,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not link:
|
if not link:
|
||||||
self.logger.warning(f"No download link found for provider: {provider_name}")
|
warn_msg = (
|
||||||
|
f"No download link found for provider: "
|
||||||
|
f"{provider_name}"
|
||||||
|
)
|
||||||
|
self.logger.warning(warn_msg)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Configure yt-dlp options
|
# Configure yt-dlp options
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
'fragment_retries': float('inf'),
|
"fragment_retries": float("inf"),
|
||||||
'outtmpl': temp_path,
|
"outtmpl": temp_path,
|
||||||
'quiet': True,
|
"quiet": True,
|
||||||
'no_warnings': True,
|
"no_warnings": True,
|
||||||
'progress_with_newline': False,
|
"progress_with_newline": False,
|
||||||
'nocheckcertificate': True,
|
"nocheckcertificate": True,
|
||||||
'socket_timeout': self.download_timeout,
|
"socket_timeout": self.download_timeout,
|
||||||
'http_chunk_size': 1024 * 1024, # 1MB chunks
|
"http_chunk_size": 1024 * 1024, # 1MB chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
if headers:
|
if headers:
|
||||||
ydl_opts['http_headers'] = headers
|
ydl_opts['http_headers'] = headers
|
||||||
|
|
||||||
@ -408,11 +524,16 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
try:
|
try:
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Failed to remove temp file: {e}")
|
warn_msg = f"Failed to remove temp file: {e}"
|
||||||
|
self.logger.warning(warn_msg)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Downloaded file failed validation: {temp_path}")
|
warn_msg = (
|
||||||
|
f"Downloaded file failed validation: "
|
||||||
|
f"{temp_path}"
|
||||||
|
)
|
||||||
|
self.logger.warning(warn_msg)
|
||||||
try:
|
try:
|
||||||
os.remove(temp_path)
|
os.remove(temp_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -425,7 +546,9 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _perform_ytdl_download(self, ydl_opts: Dict[str, Any], link: str) -> bool:
|
def _perform_ytdl_download(
|
||||||
|
self, ydl_opts: Dict[str, Any], link: str
|
||||||
|
) -> bool:
|
||||||
"""Perform actual download using yt-dlp."""
|
"""Perform actual download using yt-dlp."""
|
||||||
try:
|
try:
|
||||||
with YoutubeDL(ydl_opts) as ydl:
|
with YoutubeDL(ydl_opts) as ydl:
|
||||||
@ -476,16 +599,21 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
self.nokey_logger.error(f"Anime key not found: {key}")
|
msg = f"Anime key not found: {key}"
|
||||||
raise NonRetryableError(f"Anime key not found: {key}")
|
self.nokey_logger.error(msg)
|
||||||
|
raise NonRetryableError(msg)
|
||||||
else:
|
else:
|
||||||
raise RetryableError(f"HTTP error {response.status_code} for key {key}")
|
err_msg = (
|
||||||
|
f"HTTP error {response.status_code} for key {key}"
|
||||||
|
)
|
||||||
|
raise RetryableError(err_msg)
|
||||||
|
|
||||||
self._KeyHTMLDict[key] = response
|
self._KeyHTMLDict[key] = response
|
||||||
return self._KeyHTMLDict[key]
|
return self._KeyHTMLDict[key]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to get HTML for key {key}: {e}")
|
error_msg = f"Failed to get HTML for key {key}: {e}"
|
||||||
|
self.logger.error(error_msg)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@with_error_recovery(max_retries=2, context="get_episode_html")
|
@with_error_recovery(max_retries=2, context="get_episode_html")
|
||||||
@ -496,68 +624,114 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
return self._EpisodeHTMLDict[cache_key]
|
return self._EpisodeHTMLDict[cache_key]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = f"{self.ANIWORLD_TO}/anime/stream/{key}/staffel-{season}/episode-{episode}"
|
url = (
|
||||||
|
f"{self.ANIWORLD_TO}/anime/stream/{key}/"
|
||||||
|
f"staffel-{season}/episode-{episode}"
|
||||||
|
)
|
||||||
response = recovery_strategies.handle_network_failure(
|
response = recovery_strategies.handle_network_failure(
|
||||||
self.session.get,
|
self.session.get, url, timeout=self.DEFAULT_REQUEST_TIMEOUT
|
||||||
url,
|
|
||||||
timeout=self.DEFAULT_REQUEST_TIMEOUT
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
raise NonRetryableError(f"Episode not found: {key} S{season}E{episode}")
|
err_msg = (
|
||||||
|
f"Episode not found: {key} S{season}E{episode}"
|
||||||
|
)
|
||||||
|
raise NonRetryableError(err_msg)
|
||||||
else:
|
else:
|
||||||
raise RetryableError(f"HTTP error {response.status_code} for episode")
|
err_msg = (
|
||||||
|
f"HTTP error {response.status_code} for episode"
|
||||||
|
)
|
||||||
|
raise RetryableError(err_msg)
|
||||||
|
|
||||||
self._EpisodeHTMLDict[cache_key] = response
|
self._EpisodeHTMLDict[cache_key] = response
|
||||||
return self._EpisodeHTMLDict[cache_key]
|
return self._EpisodeHTMLDict[cache_key]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to get episode HTML for {key} S{season}E{episode}: {e}")
|
error_msg = (
|
||||||
|
f"Failed to get episode HTML for {key} "
|
||||||
|
f"S{season}E{episode}: {e}"
|
||||||
|
)
|
||||||
|
self.logger.error(error_msg)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _get_provider_from_html(self, season: int, episode: int, key: str) -> dict:
|
def _get_provider_from_html(
|
||||||
|
self, season: int, episode: int, key: str
|
||||||
|
) -> dict:
|
||||||
"""Extract providers from HTML with error handling."""
|
"""Extract providers from HTML with error handling."""
|
||||||
try:
|
try:
|
||||||
soup = BeautifulSoup(self._GetEpisodeHTML(season, episode, key).content, 'html.parser')
|
episode_html = self._GetEpisodeHTML(season, episode, key)
|
||||||
providers = {}
|
soup = BeautifulSoup(episode_html.content, "html.parser")
|
||||||
|
providers: dict[str, dict] = {}
|
||||||
|
|
||||||
episode_links = soup.find_all(
|
episode_links = soup.find_all(
|
||||||
'li', class_=lambda x: x and x.startswith('episodeLink')
|
"li", class_=lambda x: x and x.startswith("episodeLink")
|
||||||
)
|
)
|
||||||
|
|
||||||
if not episode_links:
|
if not episode_links:
|
||||||
self.logger.warning(f"No episode links found for {key} S{season}E{episode}")
|
warn_msg = (
|
||||||
|
f"No episode links found for {key} S{season}E{episode}"
|
||||||
|
)
|
||||||
|
self.logger.warning(warn_msg)
|
||||||
return providers
|
return providers
|
||||||
|
|
||||||
for link in episode_links:
|
for link in episode_links:
|
||||||
provider_name_tag = link.find('h4')
|
provider_name_tag = link.find("h4")
|
||||||
provider_name = provider_name_tag.text.strip() if provider_name_tag else None
|
provider_name = (
|
||||||
|
provider_name_tag.text.strip()
|
||||||
|
if provider_name_tag
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
redirect_link_tag = link.find('a', class_='watchEpisode')
|
redirect_link_tag = link.find("a", class_="watchEpisode")
|
||||||
redirect_link = redirect_link_tag['href'] if redirect_link_tag else None
|
redirect_link = (
|
||||||
|
redirect_link_tag["href"]
|
||||||
|
if redirect_link_tag
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
lang_key = link.get('data-lang-key')
|
lang_key = link.get("data-lang-key")
|
||||||
lang_key = int(lang_key) if lang_key and lang_key.isdigit() else None
|
lang_key = (
|
||||||
|
int(lang_key)
|
||||||
|
if lang_key and lang_key.isdigit()
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
if provider_name and redirect_link and lang_key:
|
if provider_name and redirect_link and lang_key:
|
||||||
if provider_name not in providers:
|
if provider_name not in providers:
|
||||||
providers[provider_name] = {}
|
providers[provider_name] = {}
|
||||||
providers[provider_name][lang_key] = f"{self.ANIWORLD_TO}{redirect_link}"
|
providers[provider_name][lang_key] = (
|
||||||
|
f"{self.ANIWORLD_TO}{redirect_link}"
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.debug(f"Found {len(providers)} providers for {key} S{season}E{episode}")
|
debug_msg = (
|
||||||
|
f"Found {len(providers)} providers for "
|
||||||
|
f"{key} S{season}E{episode}"
|
||||||
|
)
|
||||||
|
self.logger.debug(debug_msg)
|
||||||
return providers
|
return providers
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to parse providers from HTML: {e}")
|
error_msg = f"Failed to parse providers from HTML: {e}"
|
||||||
|
self.logger.error(error_msg)
|
||||||
raise RetryableError(f"Provider parsing failed: {e}") from e
|
raise RetryableError(f"Provider parsing failed: {e}") from e
|
||||||
|
|
||||||
def _get_redirect_link(self, season: int, episode: int, key: str, language: str = "German Dub"):
|
def _get_redirect_link(
|
||||||
|
self,
|
||||||
|
season: int,
|
||||||
|
episode: int,
|
||||||
|
key: str,
|
||||||
|
language: str = "German Dub",
|
||||||
|
):
|
||||||
"""Get redirect link for episode with error handling."""
|
"""Get redirect link for episode with error handling."""
|
||||||
languageCode = self._GetLanguageKey(language)
|
languageCode = self._GetLanguageKey(language)
|
||||||
|
|
||||||
if not self.IsLanguage(season, episode, key, language):
|
if not self.IsLanguage(season, episode, key, language):
|
||||||
raise NonRetryableError(f"Language {language} not available for {key} S{season}E{episode}")
|
err_msg = (
|
||||||
|
f"Language {language} not available for "
|
||||||
|
f"{key} S{season}E{episode}"
|
||||||
|
)
|
||||||
|
raise NonRetryableError(err_msg)
|
||||||
|
|
||||||
providers = self._get_provider_from_html(season, episode, key)
|
providers = self._get_provider_from_html(season, episode, key)
|
||||||
|
|
||||||
@ -565,30 +739,51 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
if languageCode in lang_dict:
|
if languageCode in lang_dict:
|
||||||
return lang_dict[languageCode], provider_name
|
return lang_dict[languageCode], provider_name
|
||||||
|
|
||||||
raise NonRetryableError(f"No provider found for {language} in {key} S{season}E{episode}")
|
err_msg = (
|
||||||
|
f"No provider found for {language} in "
|
||||||
|
f"{key} S{season}E{episode}"
|
||||||
|
)
|
||||||
|
raise NonRetryableError(err_msg)
|
||||||
|
|
||||||
def _get_embeded_link(self, season: int, episode: int, key: str, language: str = "German Dub"):
|
def _get_embeded_link(
|
||||||
|
self,
|
||||||
|
season: int,
|
||||||
|
episode: int,
|
||||||
|
key: str,
|
||||||
|
language: str = "German Dub",
|
||||||
|
):
|
||||||
"""Get embedded link with error handling."""
|
"""Get embedded link with error handling."""
|
||||||
try:
|
try:
|
||||||
redirect_link, provider_name = self._get_redirect_link(season, episode, key, language)
|
redirect_link, provider_name = self._get_redirect_link(
|
||||||
|
season, episode, key, language
|
||||||
|
)
|
||||||
|
|
||||||
response = recovery_strategies.handle_network_failure(
|
response = recovery_strategies.handle_network_failure(
|
||||||
self.session.get,
|
self.session.get,
|
||||||
redirect_link,
|
redirect_link,
|
||||||
timeout=self.DEFAULT_REQUEST_TIMEOUT,
|
timeout=self.DEFAULT_REQUEST_TIMEOUT,
|
||||||
headers={'User-Agent': self.RANDOM_USER_AGENT}
|
headers={"User-Agent": self.RANDOM_USER_AGENT},
|
||||||
)
|
)
|
||||||
|
|
||||||
return response.url
|
return response.url
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to get embedded link: {e}")
|
error_msg = f"Failed to get embedded link: {e}"
|
||||||
|
self.logger.error(error_msg)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _get_direct_link_from_provider(self, season: int, episode: int, key: str, language: str = "German Dub"):
|
def _get_direct_link_from_provider(
|
||||||
"""Get direct download link from provider with error handling."""
|
self,
|
||||||
|
season: int,
|
||||||
|
episode: int,
|
||||||
|
key: str,
|
||||||
|
language: str = "German Dub",
|
||||||
|
):
|
||||||
|
"""Get direct download link from provider."""
|
||||||
try:
|
try:
|
||||||
embedded_link = self._get_embeded_link(season, episode, key, language)
|
embedded_link = self._get_embeded_link(
|
||||||
|
season, episode, key, language
|
||||||
|
)
|
||||||
if not embedded_link:
|
if not embedded_link:
|
||||||
raise NonRetryableError("No embedded link found")
|
raise NonRetryableError("No embedded link found")
|
||||||
|
|
||||||
@ -597,10 +792,13 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
if not provider:
|
if not provider:
|
||||||
raise NonRetryableError("VOE provider not available")
|
raise NonRetryableError("VOE provider not available")
|
||||||
|
|
||||||
return provider.GetLink(embedded_link, self.DEFAULT_REQUEST_TIMEOUT)
|
return provider.GetLink(
|
||||||
|
embedded_link, self.DEFAULT_REQUEST_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to get direct link from provider: {e}")
|
error_msg = f"Failed to get direct link from provider: {e}"
|
||||||
|
self.logger.error(error_msg)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@with_error_recovery(max_retries=2, context="get_season_episode_count")
|
@with_error_recovery(max_retries=2, context="get_season_episode_count")
|
||||||
@ -611,29 +809,35 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
response = recovery_strategies.handle_network_failure(
|
response = recovery_strategies.handle_network_failure(
|
||||||
requests.get,
|
requests.get,
|
||||||
base_url,
|
base_url,
|
||||||
timeout=self.DEFAULT_REQUEST_TIMEOUT
|
timeout=self.DEFAULT_REQUEST_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
soup = BeautifulSoup(response.content, 'html.parser')
|
soup = BeautifulSoup(response.content, "html.parser")
|
||||||
|
|
||||||
season_meta = soup.find('meta', itemprop='numberOfSeasons')
|
season_meta = soup.find("meta", itemprop="numberOfSeasons")
|
||||||
number_of_seasons = int(season_meta['content']) if season_meta else 0
|
number_of_seasons = (
|
||||||
|
int(season_meta["content"]) if season_meta else 0
|
||||||
|
)
|
||||||
|
|
||||||
episode_counts = {}
|
episode_counts = {}
|
||||||
|
|
||||||
for season in range(1, number_of_seasons + 1):
|
for season in range(1, number_of_seasons + 1):
|
||||||
season_url = f"{base_url}staffel-{season}"
|
season_url = f"{base_url}staffel-{season}"
|
||||||
season_response = recovery_strategies.handle_network_failure(
|
season_response = (
|
||||||
|
recovery_strategies.handle_network_failure(
|
||||||
requests.get,
|
requests.get,
|
||||||
season_url,
|
season_url,
|
||||||
timeout=self.DEFAULT_REQUEST_TIMEOUT
|
timeout=self.DEFAULT_REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
season_soup = BeautifulSoup(season_response.content, 'html.parser')
|
season_soup = BeautifulSoup(
|
||||||
|
season_response.content, "html.parser"
|
||||||
|
)
|
||||||
|
|
||||||
episode_links = season_soup.find_all('a', href=True)
|
episode_links = season_soup.find_all("a", href=True)
|
||||||
unique_links = set(
|
unique_links = set(
|
||||||
link['href']
|
link["href"]
|
||||||
for link in episode_links
|
for link in episode_links
|
||||||
if f"staffel-{season}/episode-" in link['href']
|
if f"staffel-{season}/episode-" in link['href']
|
||||||
)
|
)
|
||||||
@ -668,4 +872,5 @@ class EnhancedAniWorldLoader(Loader):
|
|||||||
# For backward compatibility, create wrapper that uses enhanced loader
|
# For backward compatibility, create wrapper that uses enhanced loader
|
||||||
class AniworldLoader(EnhancedAniWorldLoader):
|
class AniworldLoader(EnhancedAniWorldLoader):
|
||||||
"""Backward compatibility wrapper for the enhanced loader."""
|
"""Backward compatibility wrapper for the enhanced loader."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
@ -1,12 +1,13 @@
|
|||||||
import re
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from requests.adapters import HTTPAdapter
|
|
||||||
from urllib3.util.retry import Retry
|
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from fake_useragent import UserAgent
|
from fake_useragent import UserAgent
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
|
|
||||||
from .Provider import Provider
|
from .Provider import Provider
|
||||||
|
|
||||||
# Compile regex patterns once for better performance
|
# Compile regex patterns once for better performance
|
||||||
@ -49,7 +50,9 @@ class VOE(Provider):
|
|||||||
parts = redirect_url.strip().split("/")
|
parts = redirect_url.strip().split("/")
|
||||||
self.Header["Referer"] = f"{parts[0]}//{parts[2]}/"
|
self.Header["Referer"] = f"{parts[0]}//{parts[2]}/"
|
||||||
|
|
||||||
response = self.session.get(redirect_url, headers={'User-Agent': self.RANDOM_USER_AGENT})
|
response = self.session.get(
|
||||||
|
redirect_url, headers={"User-Agent": self.RANDOM_USER_AGENT}
|
||||||
|
)
|
||||||
html = response.content
|
html = response.content
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user