chore: apply pending code updates

This commit is contained in:
2026-03-17 11:39:27 +01:00
parent e5fae0a0a2
commit 92bd55ada1
45 changed files with 2236 additions and 2130 deletions

View File

@@ -121,11 +121,11 @@ class SerieList:
def load_series(self) -> None:
"""Populate the in-memory map with metadata discovered on disk."""
logging.info("Scanning anime folders in %s", self.directory)
logger.info("Scanning anime folders in %s", self.directory)
try:
entries: Iterable[str] = os.listdir(self.directory)
except OSError as error:
logging.error(
logger.error(
"Unable to scan directory %s: %s",
self.directory,
error,
@@ -145,7 +145,7 @@ class SerieList:
for anime_folder in entries:
anime_path = os.path.join(self.directory, anime_folder, "data")
if os.path.isfile(anime_path):
logging.debug("Found data file for folder %s", anime_folder)
logger.debug("Found data file for folder %s", anime_folder)
serie = self._load_data(anime_folder, anime_path)
if serie:
@@ -159,7 +159,7 @@ class SerieList:
nfo_stats["with_nfo"] += 1
else:
nfo_stats["without_nfo"] += 1
logging.debug(
logger.debug(
"Series '%s' (key: %s) is missing tvshow.nfo",
serie.name,
serie.key
@@ -173,7 +173,7 @@ class SerieList:
media_stats["with_poster"] += 1
else:
media_stats["without_poster"] += 1
logging.debug(
logger.debug(
"Series '%s' (key: %s) is missing poster.jpg",
serie.name,
serie.key
@@ -184,7 +184,7 @@ class SerieList:
media_stats["with_logo"] += 1
else:
media_stats["without_logo"] += 1
logging.debug(
logger.debug(
"Series '%s' (key: %s) is missing logo.png",
serie.name,
serie.key
@@ -195,7 +195,7 @@ class SerieList:
media_stats["with_fanart"] += 1
else:
media_stats["without_fanart"] += 1
logging.debug(
logger.debug(
"Series '%s' (key: %s) is missing fanart.jpg",
serie.name,
serie.key
@@ -203,20 +203,20 @@ class SerieList:
continue
logging.warning(
logger.warning(
"Skipping folder %s because no metadata file was found",
anime_folder,
)
# Log summary statistics
if nfo_stats["total"] > 0:
logging.info(
logger.info(
"NFO scan complete: %d series total, %d with NFO, %d without NFO",
nfo_stats["total"],
nfo_stats["with_nfo"],
nfo_stats["without_nfo"]
)
logging.info(
logger.info(
"Media scan complete: Poster (%d/%d), Logo (%d/%d), Fanart (%d/%d)",
media_stats["with_poster"],
nfo_stats["total"],
@@ -241,14 +241,14 @@ class SerieList:
serie = Serie.load_from_file(data_path)
# Store by key, not folder
self.keyDict[serie.key] = serie
logging.debug(
logger.debug(
"Successfully loaded metadata for %s (key: %s)",
anime_folder,
serie.key
)
return serie
except (OSError, JSONDecodeError, KeyError, ValueError) as error:
logging.error(
logger.error(
"Failed to load metadata for folder %s from %s: %s",
anime_folder,
data_path,

View File

@@ -64,6 +64,16 @@ class Serie:
f"episodeDict={self.episodeDict}{year_str})"
)
def __repr__(self):
"""Concise developer representation of Serie object."""
season_count = len(self.episodeDict)
episode_count = sum(len(eps) for eps in self.episodeDict.values())
year_str = f", year={self.year}" if self.year else ""
return (
f"Serie(key={self.key!r}, name={self.name!r}"
f"{year_str}, seasons={season_count}, episodes={episode_count})"
)
@property
def key(self) -> str:
"""

View File

@@ -55,7 +55,8 @@ class RecoveryStrategies:
if attempt == max_retries - 1:
raise
logger.warning(
f"Network error on attempt {attempt + 1}, retrying..."
"Network error on attempt %d, retrying...",
attempt + 1,
)
continue
@@ -72,7 +73,8 @@ class RecoveryStrategies:
if attempt == max_retries - 1:
raise
logger.warning(
f"Download error on attempt {attempt + 1}, retrying..."
"Download error on attempt %d, retrying...",
attempt + 1,
)
continue
@@ -92,7 +94,7 @@ class FileCorruptionDetector:
# Video files should be at least 1MB
return file_size > 1024 * 1024
except Exception as e:
logger.error(f"Error checking file validity: {e}")
logger.error("Error checking file validity: %s", e)
return False
@@ -123,13 +125,18 @@ def with_error_recovery(
last_error = e
if attempt < max_retries - 1:
logger.warning(
f"Error in {context} (attempt {attempt + 1}/"
f"{max_retries}): {e}, retrying..."
"Error in %s (attempt %d/%d): %s, retrying...",
context,
attempt + 1,
max_retries,
e,
)
else:
logger.error(
f"Error in {context} failed after {max_retries} "
f"attempts: {e}"
"Error in %s failed after %d attempts: %s",
context,
max_retries,
e,
)
if last_error:

View File

@@ -12,6 +12,8 @@ from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, Optional
logger = logging.getLogger(__name__)
class OperationType(str, Enum):
"""Types of operations that can report progress."""
@@ -313,7 +315,7 @@ class CallbackManager:
callback.on_progress(context)
except Exception as e:
# Log but don't let callback errors break the operation
logging.error(
logger.error(
"Error in progress callback %s: %s",
callback,
e,
@@ -332,7 +334,7 @@ class CallbackManager:
callback.on_error(context)
except Exception as e:
# Log but don't let callback errors break the operation
logging.error(
logger.error(
"Error in error callback %s: %s",
callback,
e,
@@ -351,7 +353,7 @@ class CallbackManager:
callback.on_completion(context)
except Exception as e:
# Log but don't let callback errors break the operation
logging.error(
logger.error(
"Error in completion callback %s: %s",
callback,
e,

File diff suppressed because it is too large Load Diff

View File

@@ -87,7 +87,7 @@ class ProviderConfigManager:
settings: Provider settings to apply.
"""
self._provider_settings[provider_name] = settings
logger.info(f"Updated settings for provider: {provider_name}")
logger.info("Updated settings for provider: %s", provider_name)
def update_provider_settings(
self, provider_name: str, **kwargs
@@ -106,7 +106,7 @@ class ProviderConfigManager:
self._provider_settings[provider_name] = ProviderSettings(
name=provider_name, **kwargs
)
logger.info(f"Created new settings for provider: {provider_name}") # noqa: E501
logger.info("Created new settings for provider: %s", provider_name) # noqa: E501
return True
settings = self._provider_settings[provider_name]
@@ -152,7 +152,7 @@ class ProviderConfigManager:
"""
if provider_name in self._provider_settings:
self._provider_settings[provider_name].enabled = True
logger.info(f"Enabled provider: {provider_name}")
logger.info("Enabled provider: %s", provider_name)
return True
return False
@@ -167,7 +167,7 @@ class ProviderConfigManager:
"""
if provider_name in self._provider_settings:
self._provider_settings[provider_name].enabled = False
logger.info(f"Disabled provider: {provider_name}")
logger.info("Disabled provider: %s", provider_name)
return True
return False
@@ -224,7 +224,7 @@ class ProviderConfigManager:
value: Setting value.
"""
self._global_settings[key] = value
logger.info(f"Updated global setting {key}: {value}")
logger.info("Updated global setting %s: %s", key, value)
def get_all_global_settings(self) -> Dict[str, Any]:
"""Get all global settings.
@@ -307,7 +307,7 @@ class ProviderConfigManager:
with open(config_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
logger.info(f"Saved configuration to {config_path}")
logger.info("Saved configuration to %s", config_path)
return True
except Exception as e:

File diff suppressed because it is too large Load Diff

View File

@@ -207,7 +207,7 @@ class ProviderFailover:
"""
if provider_name not in self._providers:
self._providers.append(provider_name)
logger.info(f"Added provider to failover chain: {provider_name}")
logger.info("Added provider to failover chain: %s", provider_name)
def remove_provider(self, provider_name: str) -> bool:
"""Remove a provider from the failover chain.

View File

@@ -151,7 +151,7 @@ class ProviderHealthMonitor:
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Error in health check loop: {e}", exc_info=True)
logger.exception("Error in health check loop: %s", e)
await asyncio.sleep(self._health_check_interval)
async def _perform_health_checks(self) -> None:
@@ -314,7 +314,7 @@ class ProviderHealthMonitor:
)
best_provider = available[0][0]
logger.debug(f"Best provider selected: {best_provider}")
logger.debug("Best provider selected: %s", best_provider)
return best_provider
def _get_recent_metrics(
@@ -355,7 +355,7 @@ class ProviderHealthMonitor:
provider_name=provider_name
)
self._request_history[provider_name].clear()
logger.info(f"Reset metrics for provider: {provider_name}")
logger.info("Reset metrics for provider: %s", provider_name)
return True
def get_health_summary(self) -> Dict[str, Any]:

View File

@@ -134,21 +134,21 @@ class NFOService:
clean_name, extracted_year = self._extract_year_from_name(serie_name)
if year is None and extracted_year is not None:
year = extracted_year
logger.info(f"Extracted year {year} from series name")
logger.info("Extracted year %s from series name", year)
# Use clean name for search
search_name = clean_name
logger.info(f"Creating NFO for {search_name} (year: {year})")
logger.info("Creating NFO for %s (year: %s)", search_name, year)
folder_path = self.anime_directory / serie_folder
if not folder_path.exists():
logger.info(f"Creating series folder: {folder_path}")
logger.info("Creating series folder: %s", folder_path)
folder_path.mkdir(parents=True, exist_ok=True)
async with self.tmdb_client:
# Search for TV show with clean name (without year)
logger.debug(f"Searching TMDB for: {search_name}")
logger.debug("Searching TMDB for: %s", search_name)
search_results = await self.tmdb_client.search_tv_show(search_name)
if not search_results.get("results"):
@@ -158,7 +158,7 @@ class NFOService:
tv_show = self._find_best_match(search_results["results"], search_name, year)
tv_id = tv_show["id"]
logger.info(f"Found match: {tv_show['name']} (ID: {tv_id})")
logger.info("Found match: %s (ID: %s)", tv_show['name'], tv_id)
# Get detailed information with multi-language image support
details = await self.tmdb_client.get_tv_show_details(
@@ -190,7 +190,7 @@ class NFOService:
# Save NFO file
nfo_path = folder_path / "tvshow.nfo"
nfo_path.write_text(nfo_xml, encoding="utf-8")
logger.info(f"Created NFO: {nfo_path}")
logger.info("Created NFO: %s", nfo_path)
# Download media files
await self._download_media_files(
@@ -227,7 +227,7 @@ class NFOService:
if not nfo_path.exists():
raise FileNotFoundError(f"NFO file not found: {nfo_path}")
logger.info(f"Updating NFO for {serie_folder}")
logger.info("Updating NFO for %s", serie_folder)
# Parse existing NFO to extract TMDB ID
try:
@@ -253,7 +253,7 @@ class NFOService:
f"Delete the NFO and create a new one instead."
)
logger.debug(f"Found TMDB ID: {tmdb_id}")
logger.debug("Found TMDB ID: %s", tmdb_id)
except etree.XMLSyntaxError as e:
raise TMDBAPIError(f"Invalid XML in NFO file: {e}")
@@ -262,7 +262,7 @@ class NFOService:
# Fetch fresh data from TMDB
async with self.tmdb_client:
logger.debug(f"Fetching fresh data for TMDB ID: {tmdb_id}")
logger.debug("Fetching fresh data for TMDB ID: %s", tmdb_id)
details = await self.tmdb_client.get_tv_show_details(
tmdb_id,
append_to_response="credits,external_ids,images"
@@ -286,7 +286,7 @@ class NFOService:
# Save updated NFO file
nfo_path.write_text(nfo_xml, encoding="utf-8")
logger.info(f"Updated NFO: {nfo_path}")
logger.info("Updated NFO: %s", nfo_path)
# Re-download media files if requested
if download_media:
@@ -318,7 +318,7 @@ class NFOService:
result = {"tmdb_id": None, "tvdb_id": None}
if not nfo_path.exists():
logger.debug(f"NFO file not found: {nfo_path}")
logger.debug("NFO file not found: %s", nfo_path)
return result
try:
@@ -375,9 +375,9 @@ class NFOService:
)
except etree.XMLSyntaxError as e:
logger.error(f"Invalid XML in NFO file {nfo_path}: {e}")
logger.error("Invalid XML in NFO file %s: %s", nfo_path, e)
except Exception as e: # pylint: disable=broad-except
logger.error(f"Error parsing NFO file {nfo_path}: {e}")
logger.error("Error parsing NFO file %s: %s", nfo_path, e)
return result
@@ -480,7 +480,7 @@ class NFOService:
for result in results:
first_air_date = result.get("first_air_date", "")
if first_air_date.startswith(str(year)):
logger.debug(f"Found year match: {result['name']} ({first_air_date})")
logger.debug("Found year match: %s (%s)", result['name'], first_air_date)
return result
# Return first result (usually best match)
@@ -545,7 +545,7 @@ class NFOService:
skip_existing=True
)
logger.info(f"Media download results: {results}")
logger.info("Media download results: %s", results)
return results

View File

@@ -136,7 +136,7 @@ class SeriesManagerService:
# If NFO exists, parse IDs and update database
if nfo_exists:
logger.debug(f"Parsing IDs from existing NFO for '{serie_name}'")
logger.debug("Parsing IDs from existing NFO for '%s'", serie_name)
ids = self.nfo_service.parse_nfo_ids(nfo_path)
if ids["tmdb_id"] or ids["tvdb_id"]:
@@ -203,14 +203,14 @@ class SeriesManagerService:
download_logo=self.download_logo,
download_fanart=self.download_fanart
)
logger.info(f"Successfully created NFO for '{serie_name}'")
logger.info("Successfully created NFO for '%s'", serie_name)
elif nfo_exists:
logger.debug(
f"NFO exists for '{serie_name}', skipping download"
)
except TMDBAPIError as e:
logger.error(f"TMDB API error processing '{serie_name}': {e}")
logger.error("TMDB API error processing '%s': %s", serie_name, e)
except Exception as e:
logger.error(
f"Unexpected error processing NFO for '{serie_name}': {e}",
@@ -246,7 +246,7 @@ class SeriesManagerService:
logger.info("No series found in database to process")
return
logger.info(f"Processing NFO for {len(anime_series_list)} series...")
logger.info("Processing NFO for %s series...", len(anime_series_list))
# Create tasks for concurrent processing
# Each task creates its own database session

View File

@@ -107,7 +107,7 @@ class TMDBClient:
# Cache key for deduplication
cache_key = f"{endpoint}:{str(sorted(params.items()))}"
if cache_key in self._cache:
logger.debug(f"Cache hit for {endpoint}")
logger.debug("Cache hit for %s", endpoint)
return self._cache[cache_key]
delay = 1
@@ -121,7 +121,7 @@ class TMDBClient:
if self.session is None:
raise TMDBAPIError("Session is not available")
logger.debug(f"TMDB API request: {endpoint} (attempt {attempt + 1})")
logger.debug("TMDB API request: %s (attempt %s)", endpoint, attempt + 1)
async with self.session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=60)) as resp:
if resp.status == 401:
raise TMDBAPIError("Invalid TMDB API key")
@@ -130,7 +130,7 @@ class TMDBClient:
elif resp.status == 429:
# Rate limit - wait longer
retry_after = int(resp.headers.get('Retry-After', delay * 2))
logger.warning(f"Rate limited, waiting {retry_after}s")
logger.warning("Rate limited, waiting %ss", retry_after)
await asyncio.sleep(retry_after)
continue
@@ -142,26 +142,26 @@ class TMDBClient:
except asyncio.TimeoutError as e:
last_error = e
if attempt < max_retries - 1:
logger.warning(f"Request timeout (attempt {attempt + 1}), retrying in {delay}s")
logger.warning("Request timeout (attempt %s), retrying in %ss", attempt + 1, delay)
await asyncio.sleep(delay)
delay *= 2
else:
logger.error(f"Request timed out after {max_retries} attempts")
logger.error("Request timed out after %s attempts", max_retries)
except (aiohttp.ClientError, AttributeError) as e:
last_error = e
# If connector/session was closed, try to recreate it
if "Connector is closed" in str(e) or isinstance(e, AttributeError):
logger.warning(f"Session issue detected, recreating session: {e}")
logger.warning("Session issue detected, recreating session: %s", e)
self.session = None
await self._ensure_session()
if attempt < max_retries - 1:
logger.warning(f"Request failed (attempt {attempt + 1}): {e}, retrying in {delay}s")
logger.warning("Request failed (attempt %s): %s, retrying in %ss", attempt + 1, e, delay)
await asyncio.sleep(delay)
delay *= 2
else:
logger.error(f"Request failed after {max_retries} attempts: {e}")
logger.error("Request failed after %s attempts: %s", max_retries, e)
raise TMDBAPIError(f"Request failed after {max_retries} attempts: {last_error}")
@@ -275,7 +275,7 @@ class TMDBClient:
url = f"{self.image_base_url}/{size}{image_path}"
try:
logger.debug(f"Downloading image from {url}")
logger.debug("Downloading image from %s", url)
async with self.session.get(url, timeout=aiohttp.ClientTimeout(total=60)) as resp:
resp.raise_for_status()
@@ -286,7 +286,7 @@ class TMDBClient:
with open(local_path, "wb") as f:
f.write(await resp.read())
logger.info(f"Downloaded image to {local_path}")
logger.info("Downloaded image to %s", local_path)
except aiohttp.ClientError as e:
raise TMDBAPIError(f"Failed to download image: {e}")

View File

@@ -125,7 +125,7 @@ class ImageDownloader:
# Check if file already exists
if skip_existing and local_path.exists():
if local_path.stat().st_size >= self.min_file_size:
logger.debug(f"Image already exists: {local_path}")
logger.debug("Image already exists: %s", local_path)
return True
# Ensure parent directory exists
@@ -137,15 +137,16 @@ class ImageDownloader:
for attempt in range(self.max_retries):
try:
logger.debug(
f"Downloading image from {url} "
f"(attempt {attempt + 1})"
"Downloading image from %s (attempt %d)",
url,
attempt + 1,
)
# Use persistent session
session = self._get_session()
async with session.get(url) as resp:
if resp.status == 404:
logger.warning(f"Image not found: {url}")
logger.warning("Image not found: %s", url)
return False
resp.raise_for_status()
@@ -168,21 +169,25 @@ class ImageDownloader:
local_path.unlink(missing_ok=True)
raise ImageDownloadError("Image validation failed")
logger.info(f"Downloaded image to {local_path}")
logger.info("Downloaded image to %s", local_path)
return True
except (aiohttp.ClientError, IOError, ImageDownloadError) as e:
last_error = e
if attempt < self.max_retries - 1:
logger.warning(
f"Download failed (attempt {attempt + 1}): {e}, "
f"retrying in {delay}s"
"Download failed (attempt %d): %s, retrying in %s",
attempt + 1,
e,
delay,
)
await asyncio.sleep(delay)
delay *= 2
else:
logger.error(
f"Download failed after {self.max_retries} attempts: {e}"
"Download failed after %d attempts: %s",
self.max_retries,
e,
)
raise ImageDownloadError(
@@ -211,7 +216,7 @@ class ImageDownloader:
try:
return await self.download_image(url, local_path, skip_existing)
except ImageDownloadError as e:
logger.warning(f"Failed to download poster: {e}")
logger.warning("Failed to download poster: %s", e)
return False
async def download_logo(
@@ -236,7 +241,7 @@ class ImageDownloader:
try:
return await self.download_image(url, local_path, skip_existing)
except ImageDownloadError as e:
logger.warning(f"Failed to download logo: {e}")
logger.warning("Failed to download logo: %s", e)
return False
async def download_fanart(
@@ -261,7 +266,7 @@ class ImageDownloader:
try:
return await self.download_image(url, local_path, skip_existing)
except ImageDownloadError as e:
logger.warning(f"Failed to download fanart: {e}")
logger.warning("Failed to download fanart: %s", e)
return False
def validate_image(self, image_path: Path) -> bool:
@@ -280,13 +285,13 @@ class ImageDownloader:
# Check file size
if image_path.stat().st_size < self.min_file_size:
logger.warning(f"Image file too small: {image_path}")
logger.warning("Image file too small: %s", image_path)
return False
return True
except Exception as e:
logger.warning(f"Image validation failed for {image_path}: {e}")
logger.warning("Image validation failed for %s: %s", image_path, e)
return False
async def download_all_media(
@@ -341,7 +346,7 @@ class ImageDownloader:
for (media_type, _), result in zip(tasks, task_results):
if isinstance(result, Exception):
logger.error(f"Error downloading {media_type}: {result}")
logger.error("Error downloading %s: %s", media_type, result)
results[media_type] = False
else:
results[media_type] = result

View File

@@ -209,5 +209,5 @@ def validate_nfo_xml(xml_string: str) -> bool:
etree.fromstring(xml_string.encode('utf-8'))
return True
except etree.XMLSyntaxError as e:
logger.error(f"Invalid NFO XML: {e}")
logger.error("Invalid NFO XML: %s", e)
return False