Refactor code structure and remove redundant code blocks for improved readability and maintainability

This commit is contained in:
sha
2026-01-02 07:14:33 +00:00
parent 262c0a7b7d
commit 7c7e9ab1e1
9 changed files with 636 additions and 101 deletions
+71 -25
View File
@@ -1,71 +1,117 @@
class DefaultExtractor:
"""Extractor that provides default fallback values"""
"""Default extractor providing fallback values.
def extract_title(self):
This module provides a minimal implementation of the DataExtractor protocol
that returns default/empty values for all extraction methods. Used as a
fallback when no specific extractor is available.
"""
from typing import Optional
class DefaultExtractor:
"""Extractor that provides default fallback values for all extraction methods.
This class implements the DataExtractor protocol by returning sensible
defaults (None, empty strings, empty lists) for all extraction operations.
It's used as a final fallback in the extractor chain when no other
extractor can provide data.
All methods return None or empty values, making it safe to use when
no actual data extraction is possible.
"""
def extract_title(self) -> Optional[str]:
"""Return default title.
Returns:
Default title string "Unknown Title"
"""
return "Unknown Title"
def extract_year(self):
def extract_year(self) -> Optional[str]:
"""Return year. Returns None as no year information is available."""
return None
def extract_source(self):
def extract_source(self) -> Optional[str]:
"""Return video source. Returns None as no source information is available."""
return None
def extract_order(self):
def extract_order(self) -> Optional[str]:
"""Return sequence order. Returns None as no order information is available."""
return None
def extract_resolution(self):
def extract_resolution(self) -> Optional[str]:
"""Return resolution. Returns None as no resolution information is available."""
return None
def extract_hdr(self):
def extract_hdr(self) -> Optional[str]:
"""Return HDR information. Returns None as no HDR information is available."""
return None
def extract_movie_db(self):
def extract_movie_db(self) -> list[str] | None:
"""Return movie database ID. Returns None as no database information is available."""
return None
def extract_special_info(self):
def extract_special_info(self) -> Optional[str]:
"""Return special edition info. Returns None as no special info is available."""
return None
def extract_audio_langs(self):
def extract_audio_langs(self) -> Optional[str]:
"""Return audio languages. Returns None as no language information is available."""
return None
def extract_meta_type(self):
def extract_meta_type(self) -> Optional[str]:
"""Return metadata type. Returns None as no type information is available."""
return None
def extract_size(self):
def extract_size(self) -> Optional[int]:
"""Return file size. Returns None as no size information is available."""
return None
def extract_modification_time(self):
def extract_modification_time(self) -> Optional[float]:
"""Return modification time. Returns None as no timestamp is available."""
return None
def extract_file_name(self):
def extract_file_name(self) -> Optional[str]:
"""Return file name. Returns None as no filename is available."""
return None
def extract_file_path(self):
def extract_file_path(self) -> Optional[str]:
"""Return file path. Returns None as no file path is available."""
return None
def extract_frame_class(self):
def extract_frame_class(self) -> Optional[str]:
"""Return frame class. Returns None as no frame class information is available."""
return None
def extract_video_tracks(self):
def extract_video_tracks(self) -> list[dict]:
"""Return video tracks. Returns empty list as no video tracks are available."""
return []
def extract_audio_tracks(self):
def extract_audio_tracks(self) -> list[dict]:
"""Return audio tracks. Returns empty list as no audio tracks are available."""
return []
def extract_subtitle_tracks(self):
def extract_subtitle_tracks(self) -> list[dict]:
"""Return subtitle tracks. Returns empty list as no subtitle tracks are available."""
return []
def extract_anamorphic(self):
def extract_anamorphic(self) -> Optional[str]:
"""Return anamorphic info. Returns None as no anamorphic information is available."""
return None
def extract_extension(self):
def extract_extension(self) -> Optional[str]:
"""Return file extension. Returns None as no extension is available."""
return None
def extract_tmdb_url(self):
def extract_tmdb_url(self) -> Optional[str]:
"""Return TMDB URL. Returns None as no TMDB URL is available."""
return None
def extract_tmdb_id(self):
def extract_tmdb_id(self) -> Optional[str]:
"""Return TMDB ID. Returns None as no TMDB ID is available."""
return None
def extract_original_title(self):
def extract_original_title(self) -> Optional[str]:
"""Return original title. Returns None as no original title is available."""
return None
+54 -2
View File
@@ -1,3 +1,11 @@
"""Media metadata extraction coordinator.
This module provides the MediaExtractor class which coordinates multiple
specialized extractors to gather comprehensive metadata about media files.
It implements a priority-based extraction system where data is retrieved
from the most appropriate source.
"""
from pathlib import Path
from .filename_extractor import FilenameExtractor
from .metadata_extractor import MetadataExtractor
@@ -8,7 +16,34 @@ from .default_extractor import DefaultExtractor
class MediaExtractor:
"""Class to extract various metadata from media files using specialized extractors"""
"""Coordinator for extracting metadata from media files using multiple specialized extractors.
This class manages a collection of specialized extractors and provides a unified
interface for retrieving metadata. It implements a priority-based system where
each type of data is retrieved from the most appropriate source.
The extraction priority order varies by data type:
- Title: TMDB → Metadata → Filename → Default
- Year: Filename → Default
- Technical info: MediaInfo → Default
- File info: FileInfo → Default
Attributes:
file_path: Path to the media file
filename_extractor: Extracts metadata from filename patterns
metadata_extractor: Extracts embedded metadata tags
mediainfo_extractor: Extracts technical media information
fileinfo_extractor: Extracts basic file system information
tmdb_extractor: Fetches metadata from The Movie Database API
default_extractor: Provides fallback default values
Example:
>>> from pathlib import Path
>>> extractor = MediaExtractor(Path("Movie (2020) [1080p].mkv"))
>>> title = extractor.get("title")
>>> year = extractor.get("year")
>>> tracks = extractor.get("video_tracks")
"""
def __init__(self, file_path: Path):
self.file_path = file_path
@@ -168,7 +203,24 @@ class MediaExtractor:
}
def get(self, key: str, source: str | None = None):
"""Get extracted data by key, optionally from specific source"""
"""Get metadata value by key, optionally from a specific source.
Retrieves metadata using a priority-based system. If a source is specified,
only that extractor is used. Otherwise, extractors are tried in priority
order until a non-None value is found.
Args:
key: The metadata key to retrieve (e.g., "title", "year", "resolution")
source: Optional specific extractor to use ("TMDB", "MediaInfo", "Filename", etc.)
Returns:
The extracted metadata value, or None if not found
Example:
>>> extractor = MediaExtractor(Path("movie.mkv"))
>>> title = extractor.get("title") # Try all sources in priority order
>>> year = extractor.get("year", source="Filename") # Use only filename
"""
if source:
# Specific source requested - find the extractor and call the method directly
for extractor_name, extractor in self._extractors.items():
+58 -8
View File
@@ -1,3 +1,9 @@
"""File system information extractor.
This module provides the FileInfoExtractor class for extracting basic
file system metadata such as size, timestamps, paths, and extensions.
"""
from pathlib import Path
import logging
import os
@@ -5,45 +11,89 @@ from ..decorators import cached_method
# Set up logging conditionally
if os.getenv('FORMATTER_LOG', '0') == '1':
logging.basicConfig(filename='formatter.log', level=logging.INFO,
logging.basicConfig(filename='formatter.log', level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
else:
logging.basicConfig(level=logging.CRITICAL) # Disable logging
class FileInfoExtractor:
"""Class to extract file information"""
"""Extractor for basic file system information.
This class extracts file system metadata including size, modification time,
file name, path, and extension. All extraction methods are cached for
performance.
Attributes:
file_path: Path object pointing to the file
_size: Cached file size in bytes
_modification_time: Cached modification timestamp
_file_name: Cached file name
_file_path: Cached full file path as string
_cache: Internal cache for method results
Example:
>>> from pathlib import Path
>>> extractor = FileInfoExtractor(Path("movie.mkv"))
>>> size = extractor.extract_size() # Returns size in bytes
>>> name = extractor.extract_file_name() # Returns "movie.mkv"
"""
def __init__(self, file_path: Path):
"""Initialize the FileInfoExtractor.
Args:
file_path: Path object pointing to the file to extract info from
"""
self.file_path = file_path
self._size = file_path.stat().st_size
self._modification_time = file_path.stat().st_mtime
self._file_name = file_path.name
self._file_path = str(file_path)
self._cache = {} # Internal cache for method results
self._cache: dict[str, any] = {} # Internal cache for method results
logging.info(f"FileInfoExtractor: file_name={self._file_name!r}, file_path={self._file_path!r}")
@cached_method()
def extract_size(self) -> int:
"""Extract file size in bytes"""
"""Extract file size in bytes.
Returns:
File size in bytes as an integer
"""
return self._size
@cached_method()
def extract_modification_time(self) -> float:
"""Extract file modification time"""
"""Extract file modification time.
Returns:
Unix timestamp (seconds since epoch) as a float
"""
return self._modification_time
@cached_method()
def extract_file_name(self) -> str:
"""Extract file name"""
"""Extract file name (basename).
Returns:
File name including extension (e.g., "movie.mkv")
"""
return self._file_name
@cached_method()
def extract_file_path(self) -> str:
"""Extract full file path as string"""
"""Extract full file path as string.
Returns:
Absolute file path as a string
"""
return self._file_path
@cached_method()
def extract_extension(self) -> str:
"""Extract file extension without the dot"""
"""Extract file extension without the dot.
Returns:
File extension in lowercase without leading dot (e.g., "mkv", "mp4")
"""
return self.file_path.suffix.lower().lstrip('.')
+8 -17
View File
@@ -9,6 +9,7 @@ from ..constants import (
CYRILLIC_TO_ENGLISH
)
from ..decorators import cached_method
from ..utils.pattern_utils import PatternExtractor
import langcodes
logger = logging.getLogger(__name__)
@@ -25,6 +26,9 @@ class FilenameExtractor:
self.file_path = file_path
self.file_name = file_path.name
# Initialize utility helper
self._pattern_extractor = PatternExtractor()
def _normalize_cyrillic(self, text: str) -> str:
"""Normalize Cyrillic characters to English equivalents for parsing"""
for cyr, eng in CYRILLIC_TO_ENGLISH.items():
@@ -222,23 +226,10 @@ class FilenameExtractor:
@cached_method()
def extract_movie_db(self) -> list[str] | None:
"""Extract movie database identifier from filename"""
# Look for patterns at the end of filename in brackets or braces
# Patterns: [tmdbid-123] {imdb-tt123} [imdbid-tt123] etc.
# Match patterns like [tmdbid-123456] or {imdb-tt1234567}
pattern = r'[\[\{]([a-zA-Z]+(?:id)?)[-\s]*([a-zA-Z0-9]+)[\]\}]'
matches = re.findall(pattern, self.file_name)
if matches:
# Take the last match (closest to end of filename)
db_type, db_id = matches[-1]
# Normalize database type
db_type_lower = db_type.lower()
for db_key, db_info in MOVIE_DB_DICT.items():
if any(db_type_lower.startswith(pattern.rstrip('-')) for pattern in db_info['patterns']):
return [db_key, db_id]
# Use PatternExtractor utility to avoid code duplication
db_info = self._pattern_extractor.extract_movie_db_ids(self.file_name)
if db_info:
return [db_info['type'], db_info['id']]
return None
@cached_method()
+60 -7
View File
@@ -1,3 +1,9 @@
"""Embedded metadata extractor using Mutagen.
This module provides the MetadataExtractor class for reading embedded
metadata tags from media files using the Mutagen library.
"""
import mutagen
import logging
from pathlib import Path
@@ -8,11 +14,32 @@ logger = logging.getLogger(__name__)
class MetadataExtractor:
"""Class to extract information from file metadata"""
"""Extractor for embedded metadata tags from media files.
This class uses the Mutagen library to read embedded metadata tags
such as title, artist, and duration. Falls back to MIME type detection
when Mutagen cannot read the file.
Attributes:
file_path: Path object pointing to the file
info: Mutagen file info object, or None if file cannot be read
_cache: Internal cache for method results
Example:
>>> from pathlib import Path
>>> extractor = MetadataExtractor(Path("movie.mkv"))
>>> title = extractor.extract_title()
>>> duration = extractor.extract_duration()
"""
def __init__(self, file_path: Path):
"""Initialize the MetadataExtractor.
Args:
file_path: Path object pointing to the media file
"""
self.file_path = file_path
self._cache = {} # Internal cache for method results
self._cache: dict[str, any] = {} # Internal cache for method results
try:
self.info = mutagen.File(file_path) # type: ignore
except Exception as e:
@@ -21,34 +48,60 @@ class MetadataExtractor:
@cached_method()
def extract_title(self) -> str | None:
"""Extract title from metadata"""
"""Extract title from embedded metadata tags.
Returns:
Title string if found in metadata, None otherwise
"""
if self.info:
return getattr(self.info, 'title', None) or getattr(self.info, 'get', lambda x, default=None: default)('title', [None])[0] # type: ignore
return None
@cached_method()
def extract_duration(self) -> float | None:
"""Extract duration from metadata"""
"""Extract duration from metadata.
Returns:
Duration in seconds as a float, or None if not available
"""
if self.info:
return getattr(self.info, 'length', None)
return None
@cached_method()
def extract_artist(self) -> str | None:
"""Extract artist from metadata"""
"""Extract artist from embedded metadata tags.
Returns:
Artist string if found in metadata, None otherwise
"""
if self.info:
return getattr(self.info, 'artist', None) or getattr(self.info, 'get', lambda x, default=None: default)('artist', [None])[0] # type: ignore
return None
@cached_method()
def extract_meta_type(self) -> str:
"""Extract meta type from metadata"""
"""Extract metadata container type.
Returns the Mutagen class name (e.g., "FLAC", "MP4") if available,
otherwise falls back to MIME type detection.
Returns:
Container type name, or "Unknown" if cannot be determined
"""
if self.info:
return type(self.info).__name__
return self._detect_by_mime()
def _detect_by_mime(self) -> str:
"""Detect meta type by MIME"""
"""Detect metadata type by MIME type.
Uses python-magic library to detect file MIME type and maps it
to a metadata container type.
Returns:
Container type name based on MIME type, or "Unknown" if detection fails
"""
try:
import magic
mime = magic.from_file(str(self.file_path), mime=True)