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:
229
src/server/utils/media.py
Normal file
229
src/server/utils/media.py
Normal 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
|
||||
Reference in New Issue
Block a user