mirror of
https://github.com/shadoll/moma.git
synced 2026-08-28 03:27:34 +00:00
feat: Add type hints and improve type safety across multiple modules
This commit is contained in:
+10
-11
@@ -6,6 +6,7 @@ from textual.command import Provider, Hit
|
||||
from rich.markup import escape
|
||||
from pathlib import Path
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, cast, Any
|
||||
import threading
|
||||
import logging
|
||||
|
||||
@@ -43,7 +44,7 @@ class CacheCommandProvider(Provider):
|
||||
yield Hit(
|
||||
score,
|
||||
matcher.highlight(display_name),
|
||||
partial(self.app.action_cache_command, command_name),
|
||||
partial(cast('MomaApp', self.app).action_cache_command, command_name),
|
||||
help=help_text
|
||||
)
|
||||
|
||||
@@ -208,11 +209,11 @@ class MomaApp(App):
|
||||
|
||||
# File type icons
|
||||
icons = {
|
||||
'mkv': '', # Video camera for MKV
|
||||
'mk3d': '', # Clapper board for 3D
|
||||
'mp4': '', # Video camera
|
||||
'mov': '', # Video camera
|
||||
'webm': '', # Video camera
|
||||
'mkv': '🎥', # Video camera for MKV
|
||||
'mk3d': '🕹️', # Clapper board for 3D
|
||||
'mp4': '🎥', # Video camera
|
||||
'mov': '🎥', # Video camera
|
||||
'webm': '🎥', # Video camera
|
||||
'avi': '💿', # Film frames for AVI
|
||||
'wmv': '📀', # Video camera
|
||||
'm4v': '📹', # Video camera
|
||||
@@ -308,14 +309,12 @@ class MomaApp(App):
|
||||
extractor = MediaExtractor(file_path)
|
||||
|
||||
mode = self.settings.get("mode")
|
||||
poster_content = ""
|
||||
poster_content: Any = ""
|
||||
|
||||
if mode == "technical":
|
||||
formatter = MediaPanelView(extractor)
|
||||
full_info = formatter.file_info_panel()
|
||||
full_info = MediaPanelView(extractor).file_info_panel()
|
||||
else: # catalog
|
||||
formatter = CatalogFormatter(extractor, self.settings)
|
||||
full_info, poster_content = formatter.format_catalog_info()
|
||||
full_info, poster_content = CatalogFormatter(extractor, self.settings).format_catalog_info()
|
||||
|
||||
# Update UI
|
||||
self.call_later(
|
||||
|
||||
Vendored
+1
@@ -16,6 +16,7 @@ class Cache:
|
||||
|
||||
_instance: Optional['Cache'] = None
|
||||
_lock_init = threading.Lock()
|
||||
_initialized: bool
|
||||
|
||||
def __new__(cls, cache_dir: Optional[Path] = None):
|
||||
"""Create or return singleton instance."""
|
||||
|
||||
@@ -18,7 +18,7 @@ from .media_constants import (
|
||||
get_extension_from_format
|
||||
)
|
||||
from .source_constants import SOURCE_DICT
|
||||
from .frame_constants import FRAME_CLASSES, NON_STANDARD_QUALITY_INDICATORS
|
||||
from .frame_constants import FRAME_CLASSES, NON_STANDARD_QUALITY_INDICATORS, FrameClassInfo
|
||||
from .moviedb_constants import MOVIE_DB_DICT
|
||||
from .edition_constants import SPECIAL_EDITIONS
|
||||
from .lang_constants import SKIP_WORDS
|
||||
@@ -35,6 +35,7 @@ __all__ = [
|
||||
# Frame classes
|
||||
'FRAME_CLASSES',
|
||||
'NON_STANDARD_QUALITY_INDICATORS',
|
||||
'FrameClassInfo',
|
||||
# Movie databases
|
||||
'MOVIE_DB_DICT',
|
||||
# Special editions
|
||||
|
||||
@@ -7,6 +7,15 @@ Also includes non-standard quality indicators that appear in filenames but don't
|
||||
represent specific resolutions.
|
||||
"""
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class FrameClassInfo(TypedDict):
|
||||
"""Information about a video frame class."""
|
||||
nominal_height: int
|
||||
typical_widths: list[int]
|
||||
description: str
|
||||
|
||||
# Non-standard quality indicators that don't have specific resolution values
|
||||
# These are used in filenames to indicate quality but aren't proper frame classes
|
||||
# When found, we return None instead of trying to classify them
|
||||
@@ -15,7 +24,7 @@ represent specific resolutions.
|
||||
# the exact resolution, so we treat them as non-standard indicators
|
||||
NON_STANDARD_QUALITY_INDICATORS = ['SD', 'LQ', 'HD', 'QHD', 'FHD', 'FullHD', '4K', '8K']
|
||||
|
||||
FRAME_CLASSES = {
|
||||
FRAME_CLASSES: dict[str, FrameClassInfo] = {
|
||||
"480p": {
|
||||
"nominal_height": 480,
|
||||
"typical_widths": [640, 704, 720],
|
||||
|
||||
@@ -87,7 +87,7 @@ MEDIA_TYPES = {
|
||||
|
||||
# Reverse mapping: meta_type -> list of extensions
|
||||
# Built once at module load instead of rebuilding in every extractor instance
|
||||
META_TYPE_TO_EXTENSIONS = {}
|
||||
META_TYPE_TO_EXTENSIONS: dict[str, list[str]] = {}
|
||||
for ext, info in MEDIA_TYPES.items():
|
||||
meta_type = info.get('meta_type')
|
||||
if meta_type:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pathlib import Path
|
||||
from pymediainfo import MediaInfo
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
from ..constants import FRAME_CLASSES, get_extension_from_format
|
||||
from ..cache import cached_method, Cache
|
||||
import langcodes
|
||||
@@ -52,7 +53,7 @@ class MediaInfoExtractor:
|
||||
Cache() if use_cache else None
|
||||
) # Singleton cache for @cached_method decorator
|
||||
self.settings = None # Will be set by Settings singleton if needed
|
||||
self._cache = {} # Internal cache for method results
|
||||
self._cache: dict[str, Any] = {} # Internal cache for method results
|
||||
|
||||
@cached_method()
|
||||
def _get_media_info(self) -> MediaInfo | None:
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -17,7 +13,7 @@ class TMDBExtractor:
|
||||
self.cache = Cache() if use_cache else None # Singleton cache
|
||||
self.settings = Settings() # Singleton settings
|
||||
self.ttl_seconds = self.settings.get("cache_ttl_extractors", 21600)
|
||||
self._movie_db_info = None
|
||||
self._movie_db_info: Optional[Dict[str, Any]] = None
|
||||
|
||||
def _get_cached_data(self, cache_key: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get data from cache if valid"""
|
||||
@@ -301,5 +297,5 @@ class TMDBExtractor:
|
||||
local_path = self.cache.set_image(cache_key, image_data, self.ttl_seconds)
|
||||
return str(local_path) if local_path else None
|
||||
except requests.RequestException as e:
|
||||
logging.warning(f"Failed to download poster from {poster_url}: {e}")
|
||||
logging.warning(f"Failed to download poster from {url}: {e}")
|
||||
return None
|
||||
|
||||
@@ -5,7 +5,7 @@ should inherit from. This ensures a consistent interface and enables type checki
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class Formatter(ABC):
|
||||
@@ -106,7 +106,7 @@ class MarkupFormatter(Formatter):
|
||||
pass
|
||||
|
||||
|
||||
class CompositeFormatter(Formatter):
|
||||
class CompositeFormatter:
|
||||
"""Formatter that applies multiple formatters in sequence.
|
||||
|
||||
This class allows chaining multiple formatters together in a specific order.
|
||||
@@ -122,7 +122,7 @@ class CompositeFormatter(Formatter):
|
||||
formatters: List of formatter functions to apply in order
|
||||
"""
|
||||
|
||||
def __init__(self, formatters: list[callable]):
|
||||
def __init__(self, formatters: list[Callable[..., Any]]):
|
||||
"""Initialize the composite formatter.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from .text_formatter import TextFormatter
|
||||
from src.views.posters import AsciiPosterRenderer, ViuPosterRenderer, RichPixelsPosterRenderer
|
||||
from typing import Union
|
||||
from typing import Union, Any
|
||||
from io import StringIO
|
||||
import os
|
||||
|
||||
|
||||
@@ -11,7 +12,7 @@ class CatalogFormatter:
|
||||
self.extractor = extractor
|
||||
self.settings = settings
|
||||
|
||||
def format_catalog_info(self) -> tuple[str, Union[str, object]]:
|
||||
def format_catalog_info(self) -> tuple[str, Any]:
|
||||
"""Format catalog information for display.
|
||||
|
||||
Returns:
|
||||
@@ -66,18 +67,18 @@ class CatalogFormatter:
|
||||
text_content = "\n\n".join(lines) if lines else "No catalog information available"
|
||||
|
||||
from rich.console import Console
|
||||
from io import StringIO
|
||||
|
||||
console = Console(file=StringIO(), width=120, legacy_windows=False)
|
||||
sio = StringIO()
|
||||
console = Console(file=sio, width=120, legacy_windows=False)
|
||||
console.print(text_content, markup=True)
|
||||
rendered_text = console.file.getvalue()
|
||||
rendered_text = sio.getvalue()
|
||||
|
||||
# Get poster separately
|
||||
poster_content = self.get_poster()
|
||||
|
||||
return rendered_text, poster_content
|
||||
|
||||
def get_poster(self) -> Union[str, object]:
|
||||
def get_poster(self) -> Any:
|
||||
"""Get poster content for separate display.
|
||||
|
||||
Returns:
|
||||
@@ -99,7 +100,7 @@ class CatalogFormatter:
|
||||
return f"{TextFormatter.bold('Poster:')} {poster_path} (not cached yet)"
|
||||
return ""
|
||||
|
||||
def _display_poster(self, image_path: str, mode: str) -> Union[str, object]:
|
||||
def _display_poster(self, image_path: str, mode: str) -> Any:
|
||||
"""Display poster image based on mode setting.
|
||||
|
||||
Args:
|
||||
@@ -113,6 +114,7 @@ class CatalogFormatter:
|
||||
return f"Image file not found: {image_path}"
|
||||
|
||||
# Select renderer based on mode
|
||||
renderer: Union[ViuPosterRenderer, AsciiPosterRenderer, RichPixelsPosterRenderer]
|
||||
if mode == "viu":
|
||||
renderer = ViuPosterRenderer()
|
||||
elif mode == "pseudo":
|
||||
|
||||
@@ -4,11 +4,12 @@ class SizeFormatter:
|
||||
@staticmethod
|
||||
def format_size(bytes_size: int) -> str:
|
||||
"""Format bytes to human readable with unit"""
|
||||
size: float = bytes_size
|
||||
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"
|
||||
if size < 1024:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
@staticmethod
|
||||
def format_size_full(bytes_size: int) -> str:
|
||||
|
||||
@@ -287,7 +287,7 @@ class ConversionService:
|
||||
logger.debug(f"Expanded languages from '{audio_langs_str}' to: {langs}")
|
||||
|
||||
# Map to tracks (pad with None if needed)
|
||||
result = []
|
||||
result: List[Optional[str]] = []
|
||||
for i in range(audio_track_count):
|
||||
if i < len(langs):
|
||||
result.append(langs[i])
|
||||
|
||||
@@ -9,7 +9,7 @@ This service manages the extraction of metadata from media files with:
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable
|
||||
from typing import Optional, Callable, Any
|
||||
from concurrent.futures import ThreadPoolExecutor, Future
|
||||
from threading import Lock
|
||||
|
||||
@@ -169,12 +169,11 @@ class MetadataService:
|
||||
mode = self.settings.get("mode")
|
||||
|
||||
# Format based on mode
|
||||
formatted_info: Any
|
||||
if mode == "technical":
|
||||
formatter = MediaPanelView(extractor)
|
||||
formatted_info = formatter.file_info_panel()
|
||||
formatted_info = MediaPanelView(extractor).file_info_panel()
|
||||
else: # catalog
|
||||
formatter = CatalogFormatter(extractor, self.settings)
|
||||
formatted_info = formatter.format_catalog_info()
|
||||
formatted_info = CatalogFormatter(extractor, self.settings).format_catalog_info()
|
||||
|
||||
# Generate proposed name
|
||||
proposed_formatter = ProposedFilenameView(extractor)
|
||||
|
||||
@@ -247,13 +247,13 @@ class RenameService:
|
||||
is_valid, error = self.validate_filename(sanitized_filename)
|
||||
if not is_valid:
|
||||
logger.error(f"Invalid filename: {error}")
|
||||
return False, error
|
||||
return False, error or "Invalid filename"
|
||||
|
||||
# Check for conflicts
|
||||
has_conflict, conflict_msg = self.check_name_conflict(source_path, sanitized_filename)
|
||||
if has_conflict:
|
||||
logger.warning(f"Name conflict: {conflict_msg}")
|
||||
return False, conflict_msg
|
||||
return False, conflict_msg or "Name conflict"
|
||||
|
||||
# Build the new path
|
||||
new_path = source_path.parent / sanitized_filename
|
||||
|
||||
@@ -22,6 +22,7 @@ class Settings:
|
||||
|
||||
_instance: Optional['Settings'] = None
|
||||
_lock = threading.Lock()
|
||||
_initialized: bool
|
||||
|
||||
def __new__(cls, config_dir: Path | None = None):
|
||||
"""Create or return singleton instance."""
|
||||
|
||||
@@ -540,3 +540,4 @@ class MediaPanelProperties:
|
||||
@track_decorators.subtitle_track()
|
||||
def subtitle_track(self, track, index) -> str:
|
||||
"""Get subtitle track info formatted with label."""
|
||||
return track
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""ASCII art poster renderer."""
|
||||
|
||||
from .base import PosterRenderer
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AsciiPosterRenderer(PosterRenderer):
|
||||
"""Render posters as ASCII art using PIL."""
|
||||
|
||||
def render(self, image_path: str, width: int = 35) -> str:
|
||||
def render(self, image_path: str, width: int = 35) -> Any:
|
||||
"""Render poster as ASCII art.
|
||||
|
||||
Args:
|
||||
@@ -28,7 +29,7 @@ class AsciiPosterRenderer(PosterRenderer):
|
||||
from PIL import Image, ImageEnhance
|
||||
|
||||
# Open image
|
||||
img = Image.open(image_path)
|
||||
img: Any = Image.open(image_path)
|
||||
|
||||
# Enhance contrast for better detail
|
||||
enhancer = ImageEnhance.Contrast(img)
|
||||
@@ -57,7 +58,7 @@ class AsciiPosterRenderer(PosterRenderer):
|
||||
|
||||
# Map pixel brightness to character
|
||||
# Invert: 0 (black) -> dark char, 255 (white) -> light char
|
||||
char_index = (255 - avg) * (len(ascii_chars) - 1) // 255
|
||||
char_index = int((255 - avg) * (len(ascii_chars) - 1) // 255)
|
||||
char = ascii_chars[char_index]
|
||||
row.append(char)
|
||||
ascii_art.append(''.join(row))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Base class for poster renderers."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
import os
|
||||
|
||||
|
||||
@@ -8,15 +9,15 @@ class PosterRenderer(ABC):
|
||||
"""Abstract base class for poster rendering implementations."""
|
||||
|
||||
@abstractmethod
|
||||
def render(self, image_path: str, width: int = 40) -> str:
|
||||
"""Render a poster image to a string.
|
||||
def render(self, image_path: str, width: int = 40) -> Any:
|
||||
"""Render a poster image.
|
||||
|
||||
Args:
|
||||
image_path: Path to the poster image file
|
||||
width: Desired width in characters
|
||||
|
||||
Returns:
|
||||
Rendered poster as a string
|
||||
Rendered poster as a string or Rich Renderable
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Rich-pixels renderer for high-quality terminal image display."""
|
||||
|
||||
from .base import PosterRenderer
|
||||
from typing import Union
|
||||
from typing import Any
|
||||
|
||||
|
||||
class RichPixelsPosterRenderer(PosterRenderer):
|
||||
"""Render posters using rich-pixels library for high-quality display."""
|
||||
|
||||
def render(self, image_path: str, width: int = 40) -> Union[str, object]:
|
||||
def render(self, image_path: str, width: int = 40) -> Any:
|
||||
"""Render poster using rich-pixels.
|
||||
|
||||
Args:
|
||||
|
||||
Reference in New Issue
Block a user