diff --git a/viu_media/assets/scripts/fzf/airing_schedule_info.py b/viu_media/assets/scripts/fzf/airing_schedule_info.py new file mode 100644 index 0000000..e69de29 diff --git a/viu_media/assets/scripts/fzf/character_info.py b/viu_media/assets/scripts/fzf/character_info.py new file mode 100644 index 0000000..e69de29 diff --git a/viu_media/assets/scripts/fzf/episode_info.py b/viu_media/assets/scripts/fzf/episode_info.py new file mode 100644 index 0000000..e69de29 diff --git a/viu_media/assets/scripts/fzf/info.py b/viu_media/assets/scripts/fzf/info.py new file mode 100644 index 0000000..5cb08ca --- /dev/null +++ b/viu_media/assets/scripts/fzf/info.py @@ -0,0 +1,87 @@ +import sys +from rich.console import Console +from rich.table import Table +from rich.rule import Rule +from rich.markdown import Markdown + +console = Console(force_terminal=True, color_system="truecolor") + +HEADER_COLOR = sys.argv[1] +SEPARATOR_COLOR = sys.argv[2] + +console.print("{TITLE}", justify="center") + +left = [ + ( + "Score", + "Favorites", + "Popularity", + "Status", + ), + ( + "Episodes", + "Next Episode", + "Duration", + ), + ( + "Genres", + "Format", + ), + ( + "List Status", + "Progress", + ), + ( + "Start Date", + "End Date", + ), + ( + "Studios", + "Synonymns", + "Tags", + ), +] +right = [ + ( + "{SCORE}", + "{FAVOURITES}", + "{POPULARITY}", + "{STATUS}", + ), + ( + "{EPISODES}", + "{NEXT_EPISODE}", + "{DURATION}", + ), + ( + "{GENRES}", + "{FORMAT}", + ), + ( + "{USER_STATUS}", + "{USER_PROGRESS}", + ), + ( + "{START_DATE}", + "{END_DATE}", + ), + ( + "{STUDIOS}", + "{SYNONYMNS}", + "{TAGS}", + ), +] + + +for L_grp, R_grp in zip(left, right): + table = Table.grid(expand=True) + table.add_column(justify="left", no_wrap=True) + table.add_column(justify="right", overflow="fold") + for L, R in zip(L_grp, R_grp): + table.add_row(f"[bold rgb({HEADER_COLOR})]{L}: [/]", f"{R}") + + console.print(Rule(style=f"rgb({SEPARATOR_COLOR})")) + console.print(table) + +console.print(Rule(title="Description", style=f"rgb({SEPARATOR_COLOR})")) +console.print(Markdown("""{SYNOPSIS}""")) diff --git a/viu_media/assets/scripts/fzf/preview.py b/viu_media/assets/scripts/fzf/preview.py new file mode 100644 index 0000000..6a2665e --- /dev/null +++ b/viu_media/assets/scripts/fzf/preview.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# +# FZF Preview Script Template +# +# This script is a template. The placeholders in curly braces, like {NAME} +# are dynamically filled by python using .replace() + +from pathlib import Path +from hashlib import sha256 +import subprocess +import sys +from rich.console import Console +from rich.rule import Rule + +# dynamically filled variables +PREVIEW_MODE = "{PREVIEW_MODE}" +IMAGE_CACHE_DIR = Path("{IMAGE_CACHE_DIR}") +INFO_CACHE_DIR = Path("{INFO_CACHE_DIR}") +IMAGE_RENDERER = "{IMAGE_RENDERER}" +HEADER_COLOR = "{HEADER_COLOR}" +SEPARATOR_COLOR = "{SEPARATOR_COLOR}" +PREFIX = "{PREFIX}" +SCALE_UP = "{SCALE_UP}" == "True" + +# fzf passes the title with quotes, so we need to trim them +TITLE = sys.argv[1] + +hash = f"{PREFIX}-{sha256(TITLE.encode('utf-8')).hexdigest()}" + +console = Console(force_terminal=True, color_system="truecolor") +if PREVIEW_MODE == "image" or PREVIEW_MODE == "full": + preview_image_path = IMAGE_CACHE_DIR / f"{hash}.png" + if preview_image_path.exists(): + print("rendering image") + else: + print("🖼️ Loading image...") + +console.print(Rule(style=f"rgb({SEPARATOR_COLOR})")) +if PREVIEW_MODE == "info" or PREVIEW_MODE == "full": + preview_info_path = INFO_CACHE_DIR / f"{hash}.py" + if preview_info_path.exists(): + subprocess.run( + [sys.executable, str(preview_info_path), HEADER_COLOR, SEPARATOR_COLOR] + ) + else: + console.print("📝 Loading details...") diff --git a/viu_media/assets/scripts/fzf/review_info.py b/viu_media/assets/scripts/fzf/review_info.py new file mode 100644 index 0000000..e69de29 diff --git a/viu_media/cli/utils/preview.py b/viu_media/cli/utils/preview.py index 57bb044..226ef00 100644 --- a/viu_media/cli/utils/preview.py +++ b/viu_media/cli/utils/preview.py @@ -1,7 +1,7 @@ import logging -import os import re from hashlib import sha256 +import sys from typing import Dict, List, Optional import httpx @@ -117,7 +117,7 @@ def _get_episode_image(episode: str, media_item: MediaItem) -> str: logger = logging.getLogger(__name__) -os.environ["SHELL"] = "bash" +# os.environ["SHELL"] = sys.executable PREVIEWS_CACHE_DIR = APP_CACHE_DIR / "previews" IMAGES_CACHE_DIR = PREVIEWS_CACHE_DIR / "images" @@ -127,21 +127,11 @@ CHARACTERS_CACHE_DIR = PREVIEWS_CACHE_DIR / "characters" AIRING_SCHEDULE_CACHE_DIR = PREVIEWS_CACHE_DIR / "airing_schedule" FZF_SCRIPTS_DIR = SCRIPTS_DIR / "fzf" -TEMPLATE_PREVIEW_SCRIPT = (FZF_SCRIPTS_DIR / "preview.template.sh").read_text( - encoding="utf-8" -) -TEMPLATE_REVIEW_PREVIEW_SCRIPT = ( - FZF_SCRIPTS_DIR / "review-preview.template.sh" -).read_text(encoding="utf-8") -TEMPLATE_CHARACTER_PREVIEW_SCRIPT = ( - FZF_SCRIPTS_DIR / "character-preview.template.sh" -).read_text(encoding="utf-8") -TEMPLATE_AIRING_SCHEDULE_PREVIEW_SCRIPT = ( - FZF_SCRIPTS_DIR / "airing-schedule-preview.template.sh" -).read_text(encoding="utf-8") -DYNAMIC_PREVIEW_SCRIPT = (FZF_SCRIPTS_DIR / "dynamic-preview.template.sh").read_text( - encoding="utf-8" -) +TEMPLATE_PREVIEW_SCRIPT = (FZF_SCRIPTS_DIR / "preview.py").read_text(encoding="utf-8") +TEMPLATE_REVIEW_PREVIEW_SCRIPT = "" +TEMPLATE_CHARACTER_PREVIEW_SCRIPT = "" +TEMPLATE_AIRING_SCHEDULE_PREVIEW_SCRIPT = "" +DYNAMIC_PREVIEW_SCRIPT = "" EPISODE_PATTERN = re.compile(r"^Episode\s+(\d+)\s-\s.*") @@ -300,30 +290,28 @@ def get_anime_preview( logger.error(f"Failed to start background caching: {e}") # Continue with script generation even if caching fails - # Prepare values to inject into the template - path_sep = "\\" if PLATFORM == "win32" else "/" - # Format the template with the dynamic values replacements = { "PREVIEW_MODE": config.general.preview, - "IMAGE_CACHE_PATH": str(IMAGES_CACHE_DIR), - "INFO_CACHE_PATH": str(INFO_CACHE_DIR), - "PATH_SEP": path_sep, + "IMAGE_CACHE_DIR": str(IMAGES_CACHE_DIR), + "INFO_CACHE_DIR": str(INFO_CACHE_DIR), "IMAGE_RENDERER": config.general.image_renderer, # Color codes - "C_TITLE": ansi.get_true_fg(HEADER_COLOR, bold=True), - "C_KEY": ansi.get_true_fg(HEADER_COLOR, bold=True), - "C_VALUE": ansi.get_true_fg(HEADER_COLOR, bold=True), - "C_RULE": ansi.get_true_fg(SEPARATOR_COLOR, bold=True), - "RESET": ansi.RESET, - "PREFIX": "", - "SCALE_UP": " --scale-up" if config.general.preview_scale_up else "", + "HEADER_COLOR": ",".join(HEADER_COLOR), + "SEPARATOR_COLOR": ",".join(SEPARATOR_COLOR), + "PREFIX": "search-results", + "SCALE_UP": str(config.general.preview_scale_up), } for key, value in replacements.items(): preview_script = preview_script.replace(f"{{{key}}}", value) - return preview_script + (APP_CACHE_DIR / "preview_script.py").write_text(preview_script, encoding="utf-8") + + preview_script_final = ( + f"{sys.executable} {APP_CACHE_DIR / 'preview_script.py'} {{}}" + ) + return preview_script_final def get_episode_preview( diff --git a/viu_media/cli/utils/preview_workers.py b/viu_media/cli/utils/preview_workers.py index 7967ab5..46a6935 100644 --- a/viu_media/cli/utils/preview_workers.py +++ b/viu_media/cli/utils/preview_workers.py @@ -31,20 +31,18 @@ logger = logging.getLogger(__name__) FZF_SCRIPTS_DIR = SCRIPTS_DIR / "fzf" -TEMPLATE_INFO_SCRIPT = (FZF_SCRIPTS_DIR / "info.template.sh").read_text( +TEMPLATE_INFO_SCRIPT = (FZF_SCRIPTS_DIR / "info.py").read_text(encoding="utf-8") +TEMPLATE_EPISODE_INFO_SCRIPT = (FZF_SCRIPTS_DIR / "episode_info.py").read_text( encoding="utf-8" ) -TEMPLATE_EPISODE_INFO_SCRIPT = (FZF_SCRIPTS_DIR / "episode-info.template.sh").read_text( +TEMPLATE_REVIEW_INFO_SCRIPT = (FZF_SCRIPTS_DIR / "review_info.py").read_text( encoding="utf-8" ) -TEMPLATE_REVIEW_INFO_SCRIPT = (FZF_SCRIPTS_DIR / "review-info.template.sh").read_text( +TEMPLATE_CHARACTER_INFO_SCRIPT = (FZF_SCRIPTS_DIR / "character_info.py").read_text( encoding="utf-8" ) -TEMPLATE_CHARACTER_INFO_SCRIPT = ( - FZF_SCRIPTS_DIR / "character-info.template.sh" -).read_text(encoding="utf-8") TEMPLATE_AIRING_SCHEDULE_INFO_SCRIPT = ( - FZF_SCRIPTS_DIR / "airing-schedule-info.template.sh" + FZF_SCRIPTS_DIR / "airing_schedule_info.py" ).read_text(encoding="utf-8") @@ -103,29 +101,29 @@ class PreviewCacheWorker(ManagedBackgroundWorker): raise RuntimeError("PreviewCacheWorker is not running") for media_item, title_str in zip(media_items, titles): - hash_id = self._get_cache_hash(title_str) + selection_title = self._get_selection_title(title_str) # Submit image download task if needed if config.general.preview in ("full", "image") and media_item.cover_image: - image_path = self.images_cache_dir / f"{hash_id}.png" + image_path = self.images_cache_dir / f"{selection_title}.png" if not image_path.exists(): self.submit_function( self._download_and_save_image, media_item.cover_image.large, - hash_id, + selection_title, ) # Submit info generation task if needed if config.general.preview in ("full", "text"): info_text = self._generate_info_text(media_item, config) - self.submit_function(self._save_info_text, info_text, hash_id) + self.submit_function(self._save_info_text, info_text, selection_title) - def _download_and_save_image(self, url: str, hash_id: str) -> None: + def _download_and_save_image(self, url: str, selection_title: str) -> None: """Download an image and save it to cache.""" if not self._http_client: raise RuntimeError("HTTP client not initialized") - image_path = self.images_cache_dir / f"{hash_id}.png" + image_path = self.images_cache_dir / f"{selection_title}.png" try: with self._http_client.stream("GET", url) as response: @@ -135,7 +133,7 @@ class PreviewCacheWorker(ManagedBackgroundWorker): for chunk in response.iter_bytes(): f.write(chunk) - logger.debug(f"Successfully cached image: {hash_id}") + logger.debug(f"Successfully cached image: {selection_title}") except Exception as e: logger.error(f"Failed to download image {url}: {e}") @@ -216,22 +214,22 @@ class PreviewCacheWorker(ManagedBackgroundWorker): return info_script - def _save_info_text(self, info_text: str, hash_id: str) -> None: + def _save_info_text(self, info_text: str, selection_title: str) -> None: """Save info text to cache.""" try: - info_path = self.info_cache_dir / hash_id + info_path = self.info_cache_dir / f"{selection_title}.py" with AtomicWriter(info_path) as f: f.write(info_text) - logger.debug(f"Successfully cached info: {hash_id}") + logger.debug(f"Successfully cached info: {selection_title}") except IOError as e: - logger.error(f"Failed to write info cache for {hash_id}: {e}") + logger.error(f"Failed to write info cache for {selection_title}: {e}") raise - def _get_cache_hash(self, text: str) -> str: + def _get_selection_title(self, text: str) -> str: """Generate a cache hash for the given text.""" from hashlib import sha256 - return sha256(text.encode("utf-8")).hexdigest() + return f"search-results-{sha256(text.encode('utf-8')).hexdigest()}" def _on_task_completed(self, task: WorkerTask, future) -> None: """Handle task completion with enhanced logging."""