Refactor code structure for improved readability and maintainability
This commit is contained in:
BIN
dist/renamer-0.7.3-py3-none-any.whl
vendored
Normal file
BIN
dist/renamer-0.7.3-py3-none-any.whl
vendored
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "renamer"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
description = "Terminal-based media file renamer and metadata viewer"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -137,7 +137,7 @@ class RenamerApp(App):
|
||||
"Select a file to view details", id="details_technical", markup=True
|
||||
)
|
||||
yield Static(
|
||||
"", id="details_catalog", markup=False
|
||||
"", id="details_catalog", markup=True
|
||||
)
|
||||
yield Static("", id="proposed", markup=True)
|
||||
yield Footer()
|
||||
@@ -255,7 +255,7 @@ class RenamerApp(App):
|
||||
formatter = MediaPanelView(extractor)
|
||||
full_info = formatter.file_info_panel()
|
||||
else: # catalog
|
||||
formatter = CatalogFormatter(extractor)
|
||||
formatter = CatalogFormatter(extractor, self.settings)
|
||||
full_info = formatter.format_catalog_info()
|
||||
|
||||
# Update UI
|
||||
|
||||
@@ -5,8 +5,9 @@ import os
|
||||
class CatalogFormatter:
|
||||
"""Formatter for catalog mode display"""
|
||||
|
||||
def __init__(self, extractor):
|
||||
def __init__(self, extractor, settings=None):
|
||||
self.extractor = extractor
|
||||
self.settings = settings
|
||||
|
||||
def format_catalog_info(self) -> str:
|
||||
"""Format catalog information for display"""
|
||||
@@ -55,39 +56,70 @@ class CatalogFormatter:
|
||||
if countries:
|
||||
lines.append(f"{TextFormatter.bold('Countries:')} {countries}")
|
||||
|
||||
# Poster - handle separately to avoid Rich markup processing
|
||||
# Poster - check settings for display mode
|
||||
poster_mode = self.settings.get("poster", "no") if self.settings else "no"
|
||||
poster_image_path = self.extractor.tmdb_extractor.extract_poster_image_path()
|
||||
if poster_image_path:
|
||||
|
||||
if poster_mode != "no" and poster_image_path:
|
||||
lines.append(f"{TextFormatter.bold('Poster:')}")
|
||||
poster_output = self._display_poster(poster_image_path)
|
||||
poster_output = self._display_poster(poster_image_path, poster_mode)
|
||||
elif poster_mode == "no":
|
||||
# Don't show poster at all
|
||||
poster_output = None
|
||||
else:
|
||||
# Poster path not cached yet
|
||||
poster_path = self.extractor.get("poster_path", "TMDB")
|
||||
if poster_path:
|
||||
lines.append(f"{TextFormatter.bold('Poster:')} {poster_path} (not cached yet)")
|
||||
poster_output = None
|
||||
|
||||
# Render text content with Rich markup
|
||||
text_content = "\n\n".join(lines) if lines else "No catalog information available"
|
||||
|
||||
from rich.console import Console
|
||||
from rich.markup import escape
|
||||
from io import StringIO
|
||||
|
||||
console = Console(file=StringIO(), width=120, legacy_windows=False)
|
||||
console.print(text_content, markup=True)
|
||||
rendered_text = console.file.getvalue()
|
||||
|
||||
# Append poster output directly (already contains ANSI codes from viu)
|
||||
# Append poster output if available
|
||||
# Escape ASCII art to prevent Rich from interpreting characters as markup
|
||||
if poster_output:
|
||||
return rendered_text + "\n" + poster_output
|
||||
# Escape special characters that Rich uses for markup
|
||||
escaped_poster = escape(poster_output)
|
||||
console2 = Console(file=StringIO(), width=120, legacy_windows=False)
|
||||
console2.print(escaped_poster, markup=False)
|
||||
return rendered_text + "\n" + console2.file.getvalue()
|
||||
else:
|
||||
return rendered_text
|
||||
|
||||
def _display_poster(self, image_path: str) -> str:
|
||||
"""Display poster image in terminal using viu"""
|
||||
import subprocess
|
||||
import shutil
|
||||
def _display_poster(self, image_path: str, mode: str) -> str:
|
||||
"""Display poster image based on mode setting.
|
||||
|
||||
Args:
|
||||
image_path: Path to the poster image
|
||||
mode: Display mode - "pseudo" for ASCII art, "viu" for viu rendering
|
||||
|
||||
Returns:
|
||||
Rendered poster as string
|
||||
"""
|
||||
if not os.path.exists(image_path):
|
||||
return f"Image file not found: {image_path}"
|
||||
|
||||
if mode == "viu":
|
||||
return self._display_poster_viu(image_path)
|
||||
elif mode == "pseudo":
|
||||
return self._display_poster_pseudo(image_path)
|
||||
else:
|
||||
return f"Unknown poster mode: {mode}"
|
||||
|
||||
def _display_poster_viu(self, image_path: str) -> str:
|
||||
"""Display poster using viu (not working in Textual, only in terminal)"""
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
# Check if viu is available
|
||||
if not shutil.which('viu'):
|
||||
return f"viu not installed. Install with: cargo install viu\nPoster at: {image_path}"
|
||||
@@ -95,17 +127,64 @@ class CatalogFormatter:
|
||||
try:
|
||||
# Run viu to render the image
|
||||
# -w 40: width in characters
|
||||
# -h 30: height in characters
|
||||
# -t: transparent background
|
||||
result = subprocess.run(
|
||||
['viu', '-w', '40', '-t', image_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout
|
||||
# Decode bytes output, preserving ANSI escape sequences
|
||||
return result.stdout.decode('utf-8', errors='replace')
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return f"Failed to render image with viu: {e.stderr}\nPoster at: {image_path}"
|
||||
stderr_msg = e.stderr.decode('utf-8', errors='replace') if e.stderr else 'Unknown error'
|
||||
return f"Failed to render image with viu: {stderr_msg}\nPoster at: {image_path}"
|
||||
except Exception as e:
|
||||
return f"Failed to display image: {e}\nPoster at: {image_path}"
|
||||
|
||||
def _display_poster_pseudo(self, image_path: str) -> str:
|
||||
"""Display poster image using ASCII art (pseudo graphics)"""
|
||||
try:
|
||||
from PIL import Image, ImageEnhance
|
||||
|
||||
# Open image
|
||||
img = Image.open(image_path)
|
||||
|
||||
# Enhance contrast for better detail
|
||||
enhancer = ImageEnhance.Contrast(img)
|
||||
img = enhancer.enhance(1.3)
|
||||
|
||||
# Convert to grayscale and resize
|
||||
# Compact size for ASCII art (35x35 -> 35x17 after row averaging)
|
||||
img = img.convert('L').resize((35, 35), Image.Resampling.LANCZOS)
|
||||
|
||||
# Extended ASCII characters from darkest to lightest (more gradient levels)
|
||||
# Using characters with different visual density for better detail
|
||||
ascii_chars = '$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,"^`\'. '
|
||||
|
||||
# Convert to ASCII
|
||||
pixels = img.getdata()
|
||||
width, height = img.size
|
||||
|
||||
ascii_art = []
|
||||
for y in range(0, height, 2): # Skip every other row for aspect ratio correction
|
||||
row = []
|
||||
for x in range(width):
|
||||
# Average of two rows for better aspect ratio
|
||||
pixel1 = pixels[y * width + x] if y < height else 255
|
||||
pixel2 = pixels[(y + 1) * width + x] if y + 1 < height else 255
|
||||
avg = (pixel1 + pixel2) // 2
|
||||
|
||||
# Map pixel brightness to character
|
||||
# Invert: 0 (black) -> dark char, 255 (white) -> light char
|
||||
char_index = (255 - avg) * (len(ascii_chars) - 1) // 255
|
||||
char = ascii_chars[char_index]
|
||||
row.append(char)
|
||||
ascii_art.append(''.join(row))
|
||||
|
||||
return '\n'.join(ascii_art)
|
||||
|
||||
except ImportError:
|
||||
return f"PIL not available for pseudo graphics\nPoster at: {image_path}"
|
||||
except Exception as e:
|
||||
return f"Failed to display image: {e}\nPoster at: {image_path}"
|
||||
@@ -301,6 +301,13 @@ Configure application settings.
|
||||
yield Button("Technical", id="mode_technical", variant="primary" if settings.get("mode") == "technical" else "default")
|
||||
yield Button("Catalog", id="mode_catalog", variant="primary" if settings.get("mode") == "catalog" else "default")
|
||||
|
||||
# Poster selection
|
||||
yield Static("Poster Display (Catalog Mode):", classes="label")
|
||||
with Horizontal():
|
||||
yield Button("No", id="poster_no", variant="primary" if settings.get("poster") == "no" else "default")
|
||||
yield Button("Pseudo", id="poster_pseudo", variant="primary" if settings.get("poster") == "pseudo" else "default")
|
||||
yield Button("Viu", id="poster_viu", variant="primary" if settings.get("poster") == "viu" else "default")
|
||||
|
||||
# TTL inputs
|
||||
yield Static("Cache TTL - Extractors (hours):", classes="label")
|
||||
yield Input(value=str(settings.get("cache_ttl_extractors") // 3600), id="ttl_extractors", classes="input_field")
|
||||
@@ -330,6 +337,17 @@ Configure application settings.
|
||||
cat_btn = self.query_one("#mode_catalog", Button)
|
||||
tech_btn.variant = "primary" if mode == "technical" else "default"
|
||||
cat_btn.variant = "primary" if mode == "catalog" else "default"
|
||||
elif event.button.id.startswith("poster_"):
|
||||
# Toggle poster buttons
|
||||
poster_mode = event.button.id.split("_")[1]
|
||||
self.app.settings.set("poster", poster_mode) # type: ignore
|
||||
# Update button variants
|
||||
no_btn = self.query_one("#poster_no", Button)
|
||||
pseudo_btn = self.query_one("#poster_pseudo", Button)
|
||||
viu_btn = self.query_one("#poster_viu", Button)
|
||||
no_btn.variant = "primary" if poster_mode == "no" else "default"
|
||||
pseudo_btn.variant = "primary" if poster_mode == "pseudo" else "default"
|
||||
viu_btn.variant = "primary" if poster_mode == "viu" else "default"
|
||||
|
||||
def save_settings(self):
|
||||
try:
|
||||
|
||||
@@ -173,7 +173,7 @@ class MetadataService:
|
||||
formatter = MediaPanelView(extractor)
|
||||
formatted_info = formatter.file_info_panel()
|
||||
else: # catalog
|
||||
formatter = CatalogFormatter(extractor)
|
||||
formatter = CatalogFormatter(extractor, self.settings)
|
||||
formatted_info = formatter.format_catalog_info()
|
||||
|
||||
# Generate proposed name
|
||||
|
||||
@@ -9,6 +9,7 @@ class Settings:
|
||||
|
||||
DEFAULTS = {
|
||||
"mode": "technical", # "technical" or "catalog"
|
||||
"poster": "no", # "no", "pseudo", "viu"
|
||||
"cache_ttl_extractors": 21600, # 6 hours in seconds
|
||||
"cache_ttl_tmdb": 21600, # 6 hours in seconds
|
||||
"cache_ttl_posters": 2592000, # 30 days in seconds
|
||||
|
||||
Reference in New Issue
Block a user