feat: restructure renamer package and implement media extraction features

- Updated `pyproject.toml` to reflect new package structure.
- Created `renamer/__init__.py` to initialize the package.
- Implemented `RenamerApp` in `renamer/app.py` for the main application interface.
- Added constants for video extensions in `renamer/constants.py`.
- Developed `MediaExtractor` class in `renamer/extractor.py` for extracting metadata from media files.
- Created various extractor classes in `renamer/extractors/` for handling filename, metadata, and media info extraction.
- Added formatting classes in `renamer/formatters/` for displaying media information and proposed filenames.
- Implemented utility functions in `renamer/utils.py` for detecting file types and extracting media track information.
- Introduced `OpenScreen` in `renamer/screens.py` for user input of directory paths.
- Enhanced error handling and user feedback throughout the application.
This commit is contained in:
sha
2025-12-25 03:10:40 +00:00
parent 9e331e58ce
commit 305dd5f43e
21 changed files with 792 additions and 329 deletions
+1
View File
@@ -0,0 +1 @@
# Formatters package
+10
View File
@@ -0,0 +1,10 @@
from datetime import datetime
class DateFormatter:
"""Class for formatting dates"""
@staticmethod
def format_modification_date(mtime: float) -> str:
"""Format file modification time"""
return datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
+16
View File
@@ -0,0 +1,16 @@
from pathlib import Path
from ..constants import VIDEO_EXT_DESCRIPTIONS
class ExtensionExtractor:
"""Class for extracting extension information"""
@staticmethod
def get_extension_name(file_path: Path) -> str:
"""Get extension name without dot"""
return file_path.suffix.lower().lstrip('.')
@staticmethod
def get_extension_description(ext_name: str) -> str:
"""Get description for extension"""
return VIDEO_EXT_DESCRIPTIONS.get(ext_name, f'Unknown extension .{ext_name}')
+38
View File
@@ -0,0 +1,38 @@
from pathlib import Path
from ..constants import VIDEO_EXT_DESCRIPTIONS
from ..utils import detect_file_type
class ExtensionFormatter:
"""Class for formatting extension information"""
@staticmethod
def check_extension_match(ext_name: str, meta_type: str) -> bool:
"""Check if file extension matches detected type"""
if ext_name.upper() == meta_type:
return True
elif ext_name == 'mkv' and meta_type == 'Matroska':
return True
elif ext_name == 'avi' and meta_type == 'AVI':
return True
elif ext_name == 'mov' and meta_type == 'QuickTime':
return True
elif ext_name == 'wmv' and meta_type == 'ASF':
return True
elif ext_name == 'flv' and meta_type == 'FLV':
return True
elif ext_name == 'webm' and meta_type == 'WebM':
return True
elif ext_name == 'ogv' and meta_type == 'Ogg':
return True
return False
@staticmethod
def format_extension_info(ext_name: str, ext_desc: str, meta_type: str, meta_desc: str, match: bool) -> str:
"""Format extension information with match status"""
if match:
return f"[bold green]Extension:[/bold green] {ext_name} - [grey]{ext_desc}[/grey]"
else:
return (f"[bold yellow]Extension:[/bold yellow] {ext_name} - [grey]{ext_desc}[/grey]\n"
f"[bold red]Meta extension:[/bold red] {meta_type} - [grey]{meta_desc}[/grey]\n"
"[bold red]Warning: Extensions do not match![/bold red]")
+84
View File
@@ -0,0 +1,84 @@
from pathlib import Path
from .size_formatter import SizeFormatter
from .date_formatter import DateFormatter
from .extension_extractor import ExtensionExtractor
from .extension_formatter import ExtensionFormatter
from ..utils import detect_file_type
class MediaFormatter:
"""Class to format media data for display"""
def format_file_info(self, file_path: Path, rename_data: dict) -> str:
"""Format complete file information for display"""
# Get file stats
size_full = SizeFormatter.format_size_full(file_path.stat().st_size)
date_formatted = DateFormatter.format_modification_date(file_path.stat().st_mtime)
# Get extension info
ext_name = ExtensionExtractor.get_extension_name(file_path)
ext_desc = ExtensionExtractor.get_extension_description(ext_name)
meta_type, meta_desc = detect_file_type(file_path)
match = ExtensionFormatter.check_extension_match(ext_name, meta_type)
ext_info = ExtensionFormatter.format_extension_info(ext_name, ext_desc, meta_type, meta_desc, match)
file_name = file_path.name
# Build basic info
full_info = f"[bold blue]Path:[/bold blue] {str(file_path)}\n\n"
full_info += f"[bold green]Size:[/bold green] {size_full}\n"
full_info += f"[bold cyan]File:[/bold cyan] {file_name}\n"
full_info += f"{ext_info}\n"
full_info += f"[bold magenta]Modified:[/bold magenta] {date_formatted}"
# Extra metadata
extra_text = self._format_extra_metadata(rename_data['metadata'])
if extra_text:
full_info += f"\n\n{extra_text}"
return full_info
def format_proposed_name(self, rename_data: dict, ext_name: str) -> str:
"""Format the proposed filename"""
proposed_parts = []
if rename_data['title']:
proposed_parts.append(rename_data['title'])
if rename_data['year']:
proposed_parts.append(f"({rename_data['year']})")
if rename_data['source']:
proposed_parts.append(rename_data['source'])
tags = []
if rename_data['resolution']:
tags.append(rename_data['resolution'])
if rename_data['hdr']:
tags.append(rename_data['hdr'])
if rename_data['audio_langs']:
tags.append(rename_data['audio_langs'])
if tags:
proposed_parts.append(f"[{','.join(tags)}]")
return ' '.join(proposed_parts) + f".{ext_name}"
def format_rename_lines(self, rename_data: dict, proposed_name: str) -> list[str]:
"""Format the rename information lines"""
lines = []
lines.append(f"Movie title: {rename_data['title'] or 'Unknown'}")
lines.append(f"Year: {rename_data['year'] or 'Unknown'}")
lines.append(f"Video source: {rename_data['source'] or 'Unknown'}")
lines.append(f"Resolution: {rename_data['resolution'] or 'Unknown'}")
lines.append(f"HDR: {rename_data['hdr'] or 'No'}")
lines.append(f"Audio langs: {rename_data['audio_langs'] or 'None'}")
lines.append(f"Proposed filename: {proposed_name}")
return lines
def _format_extra_metadata(self, metadata: dict) -> str:
"""Format extra metadata like duration, title, artist"""
extra_info = []
if metadata.get('duration'):
extra_info.append(f"[cyan]Duration:[/cyan] {metadata['duration']:.1f} seconds")
if metadata.get('title'):
extra_info.append(f"[cyan]Title:[/cyan] {metadata['title']}")
if metadata.get('artist'):
extra_info.append(f"[cyan]Artist:[/cyan] {metadata['artist']}")
return "\n".join(extra_info) if extra_info else ""
@@ -0,0 +1,21 @@
class ResolutionFormatter:
"""Class for formatting video resolutions"""
@staticmethod
def format_resolution_p(height: int) -> str:
"""Format resolution as 2160p, 1080p, etc."""
if height >= 2160:
return '2160p'
elif height >= 1080:
return '1080p'
elif height >= 720:
return '720p'
elif height >= 480:
return '480p'
else:
return f'{height}p'
@staticmethod
def format_resolution_dimensions(width: int, height: int) -> str:
"""Format resolution as WIDTHxHEIGHT"""
return f"{width}x{height}"
+17
View File
@@ -0,0 +1,17 @@
class SizeFormatter:
"""Class for formatting file sizes"""
@staticmethod
def format_size(bytes_size: int) -> str:
"""Format bytes to human readable with unit"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_size < 1024:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024
return f"{bytes_size:.1f} TB"
@staticmethod
def format_size_full(bytes_size: int) -> str:
"""Format size with both human readable and bytes"""
size_formatted = SizeFormatter.format_size(bytes_size)
return f"{size_formatted} ({bytes_size:,} bytes)"