diff --git a/dist/renamer-0.8.11-py3-none-any.whl b/dist/renamer-0.8.11-py3-none-any.whl new file mode 100644 index 0000000..2b6ff24 Binary files /dev/null and b/dist/renamer-0.8.11-py3-none-any.whl differ diff --git a/pyproject.toml b/pyproject.toml index dd26383..24824a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "renamer" -version = "0.8.10" +version = "0.8.11" description = "Terminal-based media file renamer and metadata viewer" readme = "README.md" requires-python = ">=3.11" diff --git a/renamer/constants/__init__.py b/renamer/constants/__init__.py index e06d8bd..5623817 100644 --- a/renamer/constants/__init__.py +++ b/renamer/constants/__init__.py @@ -12,7 +12,11 @@ This package contains constants split into logical modules: """ # Import from all constant modules -from .media_constants import MEDIA_TYPES, META_TYPE_TO_EXTENSIONS +from .media_constants import ( + MEDIA_TYPES, + META_TYPE_TO_EXTENSIONS, + get_extension_from_format +) from .source_constants import SOURCE_DICT from .frame_constants import FRAME_CLASSES, NON_STANDARD_QUALITY_INDICATORS from .moviedb_constants import MOVIE_DB_DICT @@ -25,6 +29,7 @@ __all__ = [ # Media types 'MEDIA_TYPES', 'META_TYPE_TO_EXTENSIONS', + 'get_extension_from_format', # Source types 'SOURCE_DICT', # Frame classes diff --git a/renamer/constants/media_constants.py b/renamer/constants/media_constants.py index 830ba05..99d9908 100644 --- a/renamer/constants/media_constants.py +++ b/renamer/constants/media_constants.py @@ -1,6 +1,7 @@ """Media type constants for supported video formats. This module defines all supported video container formats and their metadata. +Each entry includes the MediaInfo format name for proper detection. """ MEDIA_TYPES = { @@ -8,50 +9,79 @@ MEDIA_TYPES = { "description": "Matroska multimedia container", "meta_type": "Matroska", "mime": "video/x-matroska", + "mediainfo_format": "Matroska", }, "mk3d": { "description": "Matroska 3D multimedia container", "meta_type": "Matroska", "mime": "video/x-matroska", + "mediainfo_format": "Matroska", }, "avi": { "description": "Audio Video Interleave", "meta_type": "AVI", "mime": "video/x-msvideo", + "mediainfo_format": "AVI", }, "mov": { "description": "QuickTime movie", "meta_type": "QuickTime", "mime": "video/quicktime", + "mediainfo_format": "QuickTime", }, "mp4": { "description": "MPEG-4 video container", "meta_type": "MP4", "mime": "video/mp4", + "mediainfo_format": "MPEG-4", }, "wmv": { "description": "Windows Media Video", "meta_type": "ASF", "mime": "video/x-ms-wmv", + "mediainfo_format": "Windows Media", + }, + "flv": { + "description": "Flash Video", + "meta_type": "FLV", + "mime": "video/x-flv", + "mediainfo_format": "Flash Video", }, - "flv": {"description": "Flash Video", "meta_type": "FLV", "mime": "video/x-flv"}, "webm": { "description": "WebM multimedia", "meta_type": "WebM", "mime": "video/webm", + "mediainfo_format": "WebM", + }, + "m4v": { + "description": "MPEG-4 video", + "meta_type": "MP4", + "mime": "video/mp4", + "mediainfo_format": "MPEG-4", + }, + "3gp": { + "description": "3GPP multimedia", + "meta_type": "MP4", + "mime": "video/3gpp", + "mediainfo_format": "MPEG-4", + }, + "ogv": { + "description": "Ogg Video", + "meta_type": "Ogg", + "mime": "video/ogg", + "mediainfo_format": "Ogg", }, - "m4v": {"description": "MPEG-4 video", "meta_type": "MP4", "mime": "video/mp4"}, - "3gp": {"description": "3GPP multimedia", "meta_type": "MP4", "mime": "video/3gpp"}, - "ogv": {"description": "Ogg Video", "meta_type": "Ogg", "mime": "video/ogg"}, "mpg": { "description": "MPEG video", "meta_type": "MPEG-PS", "mime": "video/mpeg", + "mediainfo_format": "MPEG-PS", }, "mpeg": { "description": "MPEG video", "meta_type": "MPEG-PS", "mime": "video/mpeg", + "mediainfo_format": "MPEG-PS", }, } @@ -64,3 +94,31 @@ for ext, info in MEDIA_TYPES.items(): if meta_type not in META_TYPE_TO_EXTENSIONS: META_TYPE_TO_EXTENSIONS[meta_type] = [] META_TYPE_TO_EXTENSIONS[meta_type].append(ext) + +# Reverse mapping: MediaInfo format name -> extension +# Built from MEDIA_TYPES at module load +MEDIAINFO_FORMAT_TO_EXTENSION = {} +for ext, info in MEDIA_TYPES.items(): + mediainfo_format = info.get('mediainfo_format') + if mediainfo_format: + # Store only the first (primary) extension for each format + if mediainfo_format not in MEDIAINFO_FORMAT_TO_EXTENSION: + MEDIAINFO_FORMAT_TO_EXTENSION[mediainfo_format] = ext + + +def get_extension_from_format(format_name: str) -> str | None: + """Get file extension from MediaInfo format name. + + Args: + format_name: Format name as reported by MediaInfo (e.g., "MPEG-4", "Matroska") + + Returns: + File extension (e.g., "mp4", "mkv") or None if format is unknown + + Example: + >>> get_extension_from_format("MPEG-4") + 'mp4' + >>> get_extension_from_format("Matroska") + 'mkv' + """ + return MEDIAINFO_FORMAT_TO_EXTENSION.get(format_name) diff --git a/renamer/extractors/default_extractor.py b/renamer/extractors/default_extractor.py index e717fca..1887a68 100644 --- a/renamer/extractors/default_extractor.py +++ b/renamer/extractors/default_extractor.py @@ -101,7 +101,7 @@ class DefaultExtractor: return None def extract_extension(self) -> Optional[str]: - """Return file extension. Returns None as no extension is available.""" + """Return file extension. Returns 'ext' as default placeholder.""" return "ext" def extract_tmdb_url(self) -> Optional[str]: diff --git a/renamer/extractors/fileinfo_extractor.py b/renamer/extractors/fileinfo_extractor.py index 4faf801..fcdf7f3 100644 --- a/renamer/extractors/fileinfo_extractor.py +++ b/renamer/extractors/fileinfo_extractor.py @@ -83,10 +83,12 @@ class FileInfoExtractor: return str(self._file_path) @cached_method() - def extract_extension(self) -> str: + def extract_extension(self) -> str | None: """Extract file extension without the dot. Returns: - File extension in lowercase without leading dot (e.g., "mkv", "mp4") + File extension in lowercase without leading dot (e.g., "mkv", "mp4"), + or None if no extension exists """ - return self._file_path.suffix.lower().lstrip('.') \ No newline at end of file + ext = self._file_path.suffix.lower().lstrip('.') + return ext if ext else None \ No newline at end of file diff --git a/renamer/extractors/mediainfo_extractor.py b/renamer/extractors/mediainfo_extractor.py index d1d0a65..32bef90 100644 --- a/renamer/extractors/mediainfo_extractor.py +++ b/renamer/extractors/mediainfo_extractor.py @@ -1,7 +1,7 @@ from pathlib import Path from pymediainfo import MediaInfo from collections import Counter -from ..constants import FRAME_CLASSES, META_TYPE_TO_EXTENSIONS +from ..constants import FRAME_CLASSES, get_extension_from_format from ..cache import cached_method, Cache import langcodes import logging @@ -265,23 +265,31 @@ class MediaInfoExtractor: @cached_method() def extract_extension(self) -> str | None: - """Extract file extension based on container format""" + """Extract file extension based on container format. + + Uses MediaInfo's format field to determine the appropriate file extension. + Handles special cases like Matroska 3D (mk3d vs mkv). + + Returns: + File extension (e.g., "mp4", "mkv") or None if format is unknown + """ if not self.media_info: return None general_track = next((t for t in self.media_info.tracks if t.track_type == 'General'), None) if not general_track: return None format_ = getattr(general_track, 'format', None) - if format_ in META_TYPE_TO_EXTENSIONS: - exts = META_TYPE_TO_EXTENSIONS[format_] - if format_ == 'Matroska': - if self.is_3d() and 'mk3d' in exts: - return 'mk3d' - else: - return 'mkv' - else: - return exts[0] if exts else None - return None + if not format_: + return None + + # Use the constants function to get extension from format + ext = get_extension_from_format(format_) + + # Special case: Matroska 3D uses mk3d extension + if ext == 'mkv' and self.is_3d(): + return 'mk3d' + + return ext @cached_method() def extract_3d_layout(self) -> str | None: diff --git a/uv.lock b/uv.lock index e2fa171..17cc825 100644 --- a/uv.lock +++ b/uv.lock @@ -462,7 +462,7 @@ wheels = [ [[package]] name = "renamer" -version = "0.8.10" +version = "0.8.11" source = { editable = "." } dependencies = [ { name = "langcodes" },