feat: implement the main preview text logic in python

This commit is contained in:
Benexl
2025-10-31 22:32:51 +03:00
parent 9f5c895bf5
commit 515660b0f6
8 changed files with 170 additions and 51 deletions

View File

@@ -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}"""))

View File

@@ -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...")

View File

@@ -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(

View File

@@ -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."""