Fix Code Duplication 4: Create media utilities module

- Created src/server/utils/media.py with reusable media file functions
- Functions: check_media_files(), get_media_file_paths(), has_all_images(), count_video_files(), has_video_files()
- Defined standard filename constants: POSTER_FILENAME, LOGO_FILENAME, FANART_FILENAME, NFO_FILENAME
- Defined VIDEO_EXTENSIONS set for media player compatibility
- Refactored src/server/api/nfo.py (7 locations) to use utility functions
- Refactored src/server/services/background_loader_service.py to use utility
- Functions accept both str and Path for compatibility
- Marked Code Duplications 1, 3, 4 as RESOLVED in instructions.md
- Updated Further Considerations as RESOLVED (addressed in Issues 7, 9, 10)
This commit is contained in:
2026-01-24 21:34:43 +01:00
parent 4abaf8def7
commit 46271a9845
6 changed files with 390 additions and 80 deletions

229
src/server/utils/media.py Normal file
View File

@@ -0,0 +1,229 @@
"""Media file utilities for AniWorld.
This module provides utilities for checking and validating media files
(videos, images, NFO files) in the anime directory.
"""
from pathlib import Path
from typing import Dict, Optional, Union
# Standard media file names as defined by Kodi/Plex conventions
POSTER_FILENAME = "poster.jpg"
LOGO_FILENAME = "logo.png"
FANART_FILENAME = "fanart.jpg"
NFO_FILENAME = "tvshow.nfo"
# Video file extensions supported by most media players
VIDEO_EXTENSIONS = {".mp4", ".mkv", ".avi", ".webm", ".mov", ".m4v", ".flv", ".wmv"}
def check_media_files(
series_folder: Union[str, Path],
check_poster: bool = True,
check_logo: bool = True,
check_fanart: bool = True,
check_nfo: bool = True
) -> Dict[str, bool]:
"""Check existence of standard media files for a series.
Checks for standard Kodi/Plex media files in the series folder:
- poster.jpg: Series poster image
- logo.png: Series logo/clearlogo
- fanart.jpg: Series fanart/background image
- tvshow.nfo: Series NFO metadata file
Args:
series_folder: Path to the series folder (string or Path object)
check_poster: Whether to check for poster.jpg
check_logo: Whether to check for logo.png
check_fanart: Whether to check for fanart.jpg
check_nfo: Whether to check for tvshow.nfo
Returns:
Dict mapping file types to existence status:
{
"poster": bool,
"logo": bool,
"fanart": bool,
"nfo": bool
}
Example:
>>> from pathlib import Path
>>> series_path = Path("/anime/Attack on Titan (2013)")
>>> status = check_media_files(series_path)
>>> print(status["poster"]) # True if poster.jpg exists
"""
# Convert to Path object if string
folder_path = Path(series_folder) if isinstance(series_folder, str) else series_folder
result = {}
if check_poster:
poster_path = folder_path / POSTER_FILENAME
result["poster"] = poster_path.exists()
if check_logo:
logo_path = folder_path / LOGO_FILENAME
result["logo"] = logo_path.exists()
if check_fanart:
fanart_path = folder_path / FANART_FILENAME
result["fanart"] = fanart_path.exists()
if check_nfo:
nfo_path = folder_path / NFO_FILENAME
result["nfo"] = nfo_path.exists()
return result
def get_media_file_paths(
series_folder: Union[str, Path],
include_poster: bool = True,
include_logo: bool = True,
include_fanart: bool = True,
include_nfo: bool = True
) -> Dict[str, Optional[Path]]:
"""Get paths to standard media files for a series.
Returns paths only if the files exist. Useful for operations that need
the actual file paths (e.g., reading, copying, moving).
Args:
series_folder: Path to the series folder (string or Path object)
include_poster: Whether to include poster.jpg path
include_logo: Whether to include logo.png path
include_fanart: Whether to include fanart.jpg path
include_nfo: Whether to include tvshow.nfo path
Returns:
Dict mapping file types to paths (None if file doesn't exist):
{
"poster": Optional[Path],
"logo": Optional[Path],
"fanart": Optional[Path],
"nfo": Optional[Path]
}
Example:
>>> from pathlib import Path
>>> series_path = Path("/anime/Attack on Titan (2013)")
>>> paths = get_media_file_paths(series_path)
>>> if paths["poster"]:
... print(f"Poster found at: {paths['poster']}")
"""
# Convert to Path object if string
folder_path = Path(series_folder) if isinstance(series_folder, str) else series_folder
result = {}
if include_poster:
poster_path = folder_path / POSTER_FILENAME
result["poster"] = poster_path if poster_path.exists() else None
if include_logo:
logo_path = folder_path / LOGO_FILENAME
result["logo"] = logo_path if logo_path.exists() else None
if include_fanart:
fanart_path = folder_path / FANART_FILENAME
result["fanart"] = fanart_path if fanart_path.exists() else None
if include_nfo:
nfo_path = folder_path / NFO_FILENAME
result["nfo"] = nfo_path if nfo_path.exists() else None
return result
def has_all_images(series_folder: Union[str, Path]) -> bool:
"""Check if series has all standard image files (poster, logo, fanart).
Args:
series_folder: Path to the series folder (string or Path object)
Returns:
True if all image files exist, False otherwise
Example:
>>> from pathlib import Path
>>> series_path = Path("/anime/Attack on Titan (2013)")
>>> if has_all_images(series_path):
... print("Series has complete image set")
"""
# Convert to Path object if string
folder_path = Path(series_folder) if isinstance(series_folder, str) else series_folder
poster_path = folder_path / POSTER_FILENAME
logo_path = folder_path / LOGO_FILENAME
fanart_path = folder_path / FANART_FILENAME
return (
poster_path.exists()
and logo_path.exists()
and fanart_path.exists()
)
def count_video_files(series_folder: Union[str, Path], recursive: bool = True) -> int:
"""Count video files in the series folder.
Counts files with standard video extensions (.mp4, .mkv, .avi, etc.).
Args:
series_folder: Path to the series folder (string or Path object)
recursive: Whether to search subdirectories (for season folders)
Returns:
Number of video files found
Example:
>>> from pathlib import Path
>>> series_path = Path("/anime/Attack on Titan (2013)")
>>> video_count = count_video_files(series_path)
>>> print(f"Found {video_count} episodes")
"""
# Convert to Path object if string
folder_path = Path(series_folder) if isinstance(series_folder, str) else series_folder
if not folder_path.exists():
return 0
count = 0
pattern = "**/*" if recursive else "*"
for file_path in folder_path.glob(pattern):
if file_path.is_file() and file_path.suffix.lower() in VIDEO_EXTENSIONS:
count += 1
return count
def has_video_files(series_folder: Union[str, Path]) -> bool:
"""Check if series folder contains any video files.
Args:
series_folder: Path to the series folder (string or Path object)
Returns:
True if at least one video file exists, False otherwise
Example:
>>> from pathlib import Path
>>> series_path = Path("/anime/Attack on Titan (2013)")
>>> if not has_video_files(series_path):
... print("No episodes found")
"""
# Convert to Path object if string
folder_path = Path(series_folder) if isinstance(series_folder, str) else series_folder
if not folder_path.exists():
return False
for file_path in folder_path.rglob("*"):
if file_path.is_file() and file_path.suffix.lower() in VIDEO_EXTENSIONS:
return True
return False