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:
@@ -18,6 +18,7 @@ dependencies = [
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"mypy>=1.0.0",
|
"mypy>=1.0.0",
|
||||||
|
"types-requests>=2.31.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
+10
-11
@@ -6,6 +6,7 @@ from textual.command import Provider, Hit
|
|||||||
from rich.markup import escape
|
from rich.markup import escape
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from typing import TYPE_CHECKING, cast, Any
|
||||||
import threading
|
import threading
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ class CacheCommandProvider(Provider):
|
|||||||
yield Hit(
|
yield Hit(
|
||||||
score,
|
score,
|
||||||
matcher.highlight(display_name),
|
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
|
help=help_text
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -208,11 +209,11 @@ class MomaApp(App):
|
|||||||
|
|
||||||
# File type icons
|
# File type icons
|
||||||
icons = {
|
icons = {
|
||||||
'mkv': '', # Video camera for MKV
|
'mkv': '🎥', # Video camera for MKV
|
||||||
'mk3d': '', # Clapper board for 3D
|
'mk3d': '🕹️', # Clapper board for 3D
|
||||||
'mp4': '', # Video camera
|
'mp4': '🎥', # Video camera
|
||||||
'mov': '', # Video camera
|
'mov': '🎥', # Video camera
|
||||||
'webm': '', # Video camera
|
'webm': '🎥', # Video camera
|
||||||
'avi': '💿', # Film frames for AVI
|
'avi': '💿', # Film frames for AVI
|
||||||
'wmv': '📀', # Video camera
|
'wmv': '📀', # Video camera
|
||||||
'm4v': '📹', # Video camera
|
'm4v': '📹', # Video camera
|
||||||
@@ -308,14 +309,12 @@ class MomaApp(App):
|
|||||||
extractor = MediaExtractor(file_path)
|
extractor = MediaExtractor(file_path)
|
||||||
|
|
||||||
mode = self.settings.get("mode")
|
mode = self.settings.get("mode")
|
||||||
poster_content = ""
|
poster_content: Any = ""
|
||||||
|
|
||||||
if mode == "technical":
|
if mode == "technical":
|
||||||
formatter = MediaPanelView(extractor)
|
full_info = MediaPanelView(extractor).file_info_panel()
|
||||||
full_info = formatter.file_info_panel()
|
|
||||||
else: # catalog
|
else: # catalog
|
||||||
formatter = CatalogFormatter(extractor, self.settings)
|
full_info, poster_content = CatalogFormatter(extractor, self.settings).format_catalog_info()
|
||||||
full_info, poster_content = formatter.format_catalog_info()
|
|
||||||
|
|
||||||
# Update UI
|
# Update UI
|
||||||
self.call_later(
|
self.call_later(
|
||||||
|
|||||||
Vendored
+1
@@ -16,6 +16,7 @@ class Cache:
|
|||||||
|
|
||||||
_instance: Optional['Cache'] = None
|
_instance: Optional['Cache'] = None
|
||||||
_lock_init = threading.Lock()
|
_lock_init = threading.Lock()
|
||||||
|
_initialized: bool
|
||||||
|
|
||||||
def __new__(cls, cache_dir: Optional[Path] = None):
|
def __new__(cls, cache_dir: Optional[Path] = None):
|
||||||
"""Create or return singleton instance."""
|
"""Create or return singleton instance."""
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from .media_constants import (
|
|||||||
get_extension_from_format
|
get_extension_from_format
|
||||||
)
|
)
|
||||||
from .source_constants import SOURCE_DICT
|
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 .moviedb_constants import MOVIE_DB_DICT
|
||||||
from .edition_constants import SPECIAL_EDITIONS
|
from .edition_constants import SPECIAL_EDITIONS
|
||||||
from .lang_constants import SKIP_WORDS
|
from .lang_constants import SKIP_WORDS
|
||||||
@@ -35,6 +35,7 @@ __all__ = [
|
|||||||
# Frame classes
|
# Frame classes
|
||||||
'FRAME_CLASSES',
|
'FRAME_CLASSES',
|
||||||
'NON_STANDARD_QUALITY_INDICATORS',
|
'NON_STANDARD_QUALITY_INDICATORS',
|
||||||
|
'FrameClassInfo',
|
||||||
# Movie databases
|
# Movie databases
|
||||||
'MOVIE_DB_DICT',
|
'MOVIE_DB_DICT',
|
||||||
# Special editions
|
# Special editions
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ Also includes non-standard quality indicators that appear in filenames but don't
|
|||||||
represent specific resolutions.
|
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
|
# 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
|
# 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
|
# 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
|
# the exact resolution, so we treat them as non-standard indicators
|
||||||
NON_STANDARD_QUALITY_INDICATORS = ['SD', 'LQ', 'HD', 'QHD', 'FHD', 'FullHD', '4K', '8K']
|
NON_STANDARD_QUALITY_INDICATORS = ['SD', 'LQ', 'HD', 'QHD', 'FHD', 'FullHD', '4K', '8K']
|
||||||
|
|
||||||
FRAME_CLASSES = {
|
FRAME_CLASSES: dict[str, FrameClassInfo] = {
|
||||||
"480p": {
|
"480p": {
|
||||||
"nominal_height": 480,
|
"nominal_height": 480,
|
||||||
"typical_widths": [640, 704, 720],
|
"typical_widths": [640, 704, 720],
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ MEDIA_TYPES = {
|
|||||||
|
|
||||||
# Reverse mapping: meta_type -> list of extensions
|
# Reverse mapping: meta_type -> list of extensions
|
||||||
# Built once at module load instead of rebuilding in every extractor instance
|
# 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():
|
for ext, info in MEDIA_TYPES.items():
|
||||||
meta_type = info.get('meta_type')
|
meta_type = info.get('meta_type')
|
||||||
if meta_type:
|
if meta_type:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pymediainfo import MediaInfo
|
from pymediainfo import MediaInfo
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
from typing import Any
|
||||||
from ..constants import FRAME_CLASSES, get_extension_from_format
|
from ..constants import FRAME_CLASSES, get_extension_from_format
|
||||||
from ..cache import cached_method, Cache
|
from ..cache import cached_method, Cache
|
||||||
import langcodes
|
import langcodes
|
||||||
@@ -52,7 +53,7 @@ class MediaInfoExtractor:
|
|||||||
Cache() if use_cache else None
|
Cache() if use_cache else None
|
||||||
) # Singleton cache for @cached_method decorator
|
) # Singleton cache for @cached_method decorator
|
||||||
self.settings = None # Will be set by Settings singleton if needed
|
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()
|
@cached_method()
|
||||||
def _get_media_info(self) -> MediaInfo | None:
|
def _get_media_info(self) -> MediaInfo | None:
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import hashlib
|
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -17,7 +13,7 @@ class TMDBExtractor:
|
|||||||
self.cache = Cache() if use_cache else None # Singleton cache
|
self.cache = Cache() if use_cache else None # Singleton cache
|
||||||
self.settings = Settings() # Singleton settings
|
self.settings = Settings() # Singleton settings
|
||||||
self.ttl_seconds = self.settings.get("cache_ttl_extractors", 21600)
|
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]]:
|
def _get_cached_data(self, cache_key: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Get data from cache if valid"""
|
"""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)
|
local_path = self.cache.set_image(cache_key, image_data, self.ttl_seconds)
|
||||||
return str(local_path) if local_path else None
|
return str(local_path) if local_path else None
|
||||||
except requests.RequestException as e:
|
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
|
return None
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ should inherit from. This ensures a consistent interface and enables type checki
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
|
||||||
class Formatter(ABC):
|
class Formatter(ABC):
|
||||||
@@ -106,7 +106,7 @@ class MarkupFormatter(Formatter):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CompositeFormatter(Formatter):
|
class CompositeFormatter:
|
||||||
"""Formatter that applies multiple formatters in sequence.
|
"""Formatter that applies multiple formatters in sequence.
|
||||||
|
|
||||||
This class allows chaining multiple formatters together in a specific order.
|
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
|
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.
|
"""Initialize the composite formatter.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from .text_formatter import TextFormatter
|
from .text_formatter import TextFormatter
|
||||||
from src.views.posters import AsciiPosterRenderer, ViuPosterRenderer, RichPixelsPosterRenderer
|
from src.views.posters import AsciiPosterRenderer, ViuPosterRenderer, RichPixelsPosterRenderer
|
||||||
from typing import Union
|
from typing import Union, Any
|
||||||
|
from io import StringIO
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ class CatalogFormatter:
|
|||||||
self.extractor = extractor
|
self.extractor = extractor
|
||||||
self.settings = settings
|
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.
|
"""Format catalog information for display.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -66,18 +67,18 @@ class CatalogFormatter:
|
|||||||
text_content = "\n\n".join(lines) if lines else "No catalog information available"
|
text_content = "\n\n".join(lines) if lines else "No catalog information available"
|
||||||
|
|
||||||
from rich.console import Console
|
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)
|
console.print(text_content, markup=True)
|
||||||
rendered_text = console.file.getvalue()
|
rendered_text = sio.getvalue()
|
||||||
|
|
||||||
# Get poster separately
|
# Get poster separately
|
||||||
poster_content = self.get_poster()
|
poster_content = self.get_poster()
|
||||||
|
|
||||||
return rendered_text, poster_content
|
return rendered_text, poster_content
|
||||||
|
|
||||||
def get_poster(self) -> Union[str, object]:
|
def get_poster(self) -> Any:
|
||||||
"""Get poster content for separate display.
|
"""Get poster content for separate display.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -99,7 +100,7 @@ class CatalogFormatter:
|
|||||||
return f"{TextFormatter.bold('Poster:')} {poster_path} (not cached yet)"
|
return f"{TextFormatter.bold('Poster:')} {poster_path} (not cached yet)"
|
||||||
return ""
|
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.
|
"""Display poster image based on mode setting.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -113,6 +114,7 @@ class CatalogFormatter:
|
|||||||
return f"Image file not found: {image_path}"
|
return f"Image file not found: {image_path}"
|
||||||
|
|
||||||
# Select renderer based on mode
|
# Select renderer based on mode
|
||||||
|
renderer: Union[ViuPosterRenderer, AsciiPosterRenderer, RichPixelsPosterRenderer]
|
||||||
if mode == "viu":
|
if mode == "viu":
|
||||||
renderer = ViuPosterRenderer()
|
renderer = ViuPosterRenderer()
|
||||||
elif mode == "pseudo":
|
elif mode == "pseudo":
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ class SizeFormatter:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def format_size(bytes_size: int) -> str:
|
def format_size(bytes_size: int) -> str:
|
||||||
"""Format bytes to human readable with unit"""
|
"""Format bytes to human readable with unit"""
|
||||||
|
size: float = bytes_size
|
||||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||||
if bytes_size < 1024:
|
if size < 1024:
|
||||||
return f"{bytes_size:.1f} {unit}"
|
return f"{size:.1f} {unit}"
|
||||||
bytes_size /= 1024
|
size /= 1024
|
||||||
return f"{bytes_size:.1f} TB"
|
return f"{size:.1f} TB"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_size_full(bytes_size: int) -> str:
|
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}")
|
logger.debug(f"Expanded languages from '{audio_langs_str}' to: {langs}")
|
||||||
|
|
||||||
# Map to tracks (pad with None if needed)
|
# Map to tracks (pad with None if needed)
|
||||||
result = []
|
result: List[Optional[str]] = []
|
||||||
for i in range(audio_track_count):
|
for i in range(audio_track_count):
|
||||||
if i < len(langs):
|
if i < len(langs):
|
||||||
result.append(langs[i])
|
result.append(langs[i])
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ This service manages the extraction of metadata from media files with:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Callable
|
from typing import Optional, Callable, Any
|
||||||
from concurrent.futures import ThreadPoolExecutor, Future
|
from concurrent.futures import ThreadPoolExecutor, Future
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
@@ -169,12 +169,11 @@ class MetadataService:
|
|||||||
mode = self.settings.get("mode")
|
mode = self.settings.get("mode")
|
||||||
|
|
||||||
# Format based on mode
|
# Format based on mode
|
||||||
|
formatted_info: Any
|
||||||
if mode == "technical":
|
if mode == "technical":
|
||||||
formatter = MediaPanelView(extractor)
|
formatted_info = MediaPanelView(extractor).file_info_panel()
|
||||||
formatted_info = formatter.file_info_panel()
|
|
||||||
else: # catalog
|
else: # catalog
|
||||||
formatter = CatalogFormatter(extractor, self.settings)
|
formatted_info = CatalogFormatter(extractor, self.settings).format_catalog_info()
|
||||||
formatted_info = formatter.format_catalog_info()
|
|
||||||
|
|
||||||
# Generate proposed name
|
# Generate proposed name
|
||||||
proposed_formatter = ProposedFilenameView(extractor)
|
proposed_formatter = ProposedFilenameView(extractor)
|
||||||
|
|||||||
@@ -247,13 +247,13 @@ class RenameService:
|
|||||||
is_valid, error = self.validate_filename(sanitized_filename)
|
is_valid, error = self.validate_filename(sanitized_filename)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
logger.error(f"Invalid filename: {error}")
|
logger.error(f"Invalid filename: {error}")
|
||||||
return False, error
|
return False, error or "Invalid filename"
|
||||||
|
|
||||||
# Check for conflicts
|
# Check for conflicts
|
||||||
has_conflict, conflict_msg = self.check_name_conflict(source_path, sanitized_filename)
|
has_conflict, conflict_msg = self.check_name_conflict(source_path, sanitized_filename)
|
||||||
if has_conflict:
|
if has_conflict:
|
||||||
logger.warning(f"Name conflict: {conflict_msg}")
|
logger.warning(f"Name conflict: {conflict_msg}")
|
||||||
return False, conflict_msg
|
return False, conflict_msg or "Name conflict"
|
||||||
|
|
||||||
# Build the new path
|
# Build the new path
|
||||||
new_path = source_path.parent / sanitized_filename
|
new_path = source_path.parent / sanitized_filename
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class Settings:
|
|||||||
|
|
||||||
_instance: Optional['Settings'] = None
|
_instance: Optional['Settings'] = None
|
||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
|
_initialized: bool
|
||||||
|
|
||||||
def __new__(cls, config_dir: Path | None = None):
|
def __new__(cls, config_dir: Path | None = None):
|
||||||
"""Create or return singleton instance."""
|
"""Create or return singleton instance."""
|
||||||
|
|||||||
@@ -540,3 +540,4 @@ class MediaPanelProperties:
|
|||||||
@track_decorators.subtitle_track()
|
@track_decorators.subtitle_track()
|
||||||
def subtitle_track(self, track, index) -> str:
|
def subtitle_track(self, track, index) -> str:
|
||||||
"""Get subtitle track info formatted with label."""
|
"""Get subtitle track info formatted with label."""
|
||||||
|
return track
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"""ASCII art poster renderer."""
|
"""ASCII art poster renderer."""
|
||||||
|
|
||||||
from .base import PosterRenderer
|
from .base import PosterRenderer
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class AsciiPosterRenderer(PosterRenderer):
|
class AsciiPosterRenderer(PosterRenderer):
|
||||||
"""Render posters as ASCII art using PIL."""
|
"""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.
|
"""Render poster as ASCII art.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -28,7 +29,7 @@ class AsciiPosterRenderer(PosterRenderer):
|
|||||||
from PIL import Image, ImageEnhance
|
from PIL import Image, ImageEnhance
|
||||||
|
|
||||||
# Open image
|
# Open image
|
||||||
img = Image.open(image_path)
|
img: Any = Image.open(image_path)
|
||||||
|
|
||||||
# Enhance contrast for better detail
|
# Enhance contrast for better detail
|
||||||
enhancer = ImageEnhance.Contrast(img)
|
enhancer = ImageEnhance.Contrast(img)
|
||||||
@@ -57,7 +58,7 @@ class AsciiPosterRenderer(PosterRenderer):
|
|||||||
|
|
||||||
# Map pixel brightness to character
|
# Map pixel brightness to character
|
||||||
# Invert: 0 (black) -> dark char, 255 (white) -> light char
|
# 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]
|
char = ascii_chars[char_index]
|
||||||
row.append(char)
|
row.append(char)
|
||||||
ascii_art.append(''.join(row))
|
ascii_art.append(''.join(row))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Base class for poster renderers."""
|
"""Base class for poster renderers."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
@@ -8,15 +9,15 @@ class PosterRenderer(ABC):
|
|||||||
"""Abstract base class for poster rendering implementations."""
|
"""Abstract base class for poster rendering implementations."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def render(self, image_path: str, width: int = 40) -> str:
|
def render(self, image_path: str, width: int = 40) -> Any:
|
||||||
"""Render a poster image to a string.
|
"""Render a poster image.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
image_path: Path to the poster image file
|
image_path: Path to the poster image file
|
||||||
width: Desired width in characters
|
width: Desired width in characters
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Rendered poster as a string
|
Rendered poster as a string or Rich Renderable
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"""Rich-pixels renderer for high-quality terminal image display."""
|
"""Rich-pixels renderer for high-quality terminal image display."""
|
||||||
|
|
||||||
from .base import PosterRenderer
|
from .base import PosterRenderer
|
||||||
from typing import Union
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class RichPixelsPosterRenderer(PosterRenderer):
|
class RichPixelsPosterRenderer(PosterRenderer):
|
||||||
"""Render posters using rich-pixels library for high-quality display."""
|
"""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.
|
"""Render poster using rich-pixels.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ dependencies = [
|
|||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
|
{ name = "types-requests" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -237,6 +238,7 @@ requires-dist = [
|
|||||||
{ name = "requests", specifier = ">=2.31.0" },
|
{ name = "requests", specifier = ">=2.31.0" },
|
||||||
{ name = "rich-pixels", specifier = ">=1.0.0" },
|
{ name = "rich-pixels", specifier = ">=1.0.0" },
|
||||||
{ name = "textual", specifier = ">=6.11.0" },
|
{ name = "textual", specifier = ">=6.11.0" },
|
||||||
|
{ name = "types-requests", marker = "extra == 'dev'", specifier = ">=2.31.0" },
|
||||||
]
|
]
|
||||||
provides-extras = ["dev"]
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
@@ -501,6 +503,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b5/fc/5e2988590ff2e0128eea6446806c904445a44e17256c67141573ea16b5a5/textual-6.11.0-py3-none-any.whl", hash = "sha256:9e663b73ed37123a9b13c16a0c85e09ef917a4cfded97814361ed5cccfa40f89", size = 714886, upload-time = "2025-12-18T10:48:36.269Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/fc/5e2988590ff2e0128eea6446806c904445a44e17256c67141573ea16b5a5/textual-6.11.0-py3-none-any.whl", hash = "sha256:9e663b73ed37123a9b13c16a0c85e09ef917a4cfded97814361ed5cccfa40f89", size = 714886, upload-time = "2025-12-18T10:48:36.269Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-requests"
|
||||||
|
version = "2.33.0.20260408"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/69/6a/749dc53a54a3f35842c1f8197b3ca6b54af6d7458a1bfc75f6629b6da666/types_requests-2.33.0.20260408.tar.gz", hash = "sha256:95b9a86376807a216b2fb412b47617b202091c3ea7c078f47cc358d5528ccb7b", size = 23882, upload-time = "2026-04-08T04:34:49.33Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl", hash = "sha256:81f31d5ea4acb39f03be7bc8bed569ba6d5a9c5d97e89f45ac43d819b68ca50f", size = 20739, upload-time = "2026-04-08T04:34:48.325Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user