diff --git a/dist/renamer-0.7.3-py3-none-any.whl b/dist/renamer-0.7.3-py3-none-any.whl new file mode 100644 index 0000000..2305363 Binary files /dev/null and b/dist/renamer-0.7.3-py3-none-any.whl differ diff --git a/pyproject.toml b/pyproject.toml index 4df51d9..a386fa7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/renamer/app.py b/renamer/app.py index b1fd19f..c97cf0b 100644 --- a/renamer/app.py +++ b/renamer/app.py @@ -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 diff --git a/renamer/formatters/catalog_formatter.py b/renamer/formatters/catalog_formatter.py index 80f964c..6d4bcdc 100644 --- a/renamer/formatters/catalog_formatter.py +++ b/renamer/formatters/catalog_formatter.py @@ -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}" \ No newline at end of file diff --git a/renamer/screens.py b/renamer/screens.py index d1dc27b..eb66b2a 100644 --- a/renamer/screens.py +++ b/renamer/screens.py @@ -300,7 +300,14 @@ Configure application settings. with Horizontal(): 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: diff --git a/renamer/services/metadata_service.py b/renamer/services/metadata_service.py index e4587a7..4e17416 100644 --- a/renamer/services/metadata_service.py +++ b/renamer/services/metadata_service.py @@ -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 diff --git a/renamer/settings.py b/renamer/settings.py index f0b9e7c..3c0391f 100644 --- a/renamer/settings.py +++ b/renamer/settings.py @@ -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 diff --git a/uv.lock b/uv.lock index a6f28ae..0a4af1b 100644 --- a/uv.lock +++ b/uv.lock @@ -462,7 +462,7 @@ wheels = [ [[package]] name = "renamer" -version = "0.7.2" +version = "0.7.3" source = { editable = "." } dependencies = [ { name = "langcodes" },