diff --git a/fastanime/assets/defaults/ascii-art b/fastanime/assets/defaults/ascii-art new file mode 100644 index 0000000..d9c53d9 --- /dev/null +++ b/fastanime/assets/defaults/ascii-art @@ -0,0 +1,6 @@ +███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗ +██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝ +█████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░ +██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░ +██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗ +╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝ diff --git a/fastanime/libs/providers/anime/allanime/queries/anime.gql b/fastanime/assets/graphql/allanime/queries/anime.gql similarity index 100% rename from fastanime/libs/providers/anime/allanime/queries/anime.gql rename to fastanime/assets/graphql/allanime/queries/anime.gql diff --git a/fastanime/libs/providers/anime/allanime/queries/episodes.gql b/fastanime/assets/graphql/allanime/queries/episodes.gql similarity index 100% rename from fastanime/libs/providers/anime/allanime/queries/episodes.gql rename to fastanime/assets/graphql/allanime/queries/episodes.gql diff --git a/fastanime/libs/providers/anime/allanime/queries/search.gql b/fastanime/assets/graphql/allanime/queries/search.gql similarity index 100% rename from fastanime/libs/providers/anime/allanime/queries/search.gql rename to fastanime/assets/graphql/allanime/queries/search.gql diff --git a/fastanime/libs/api/anilist/mutations/delete-list-entry.gql b/fastanime/assets/graphql/anilist/mutations/delete-list-entry.gql similarity index 100% rename from fastanime/libs/api/anilist/mutations/delete-list-entry.gql rename to fastanime/assets/graphql/anilist/mutations/delete-list-entry.gql diff --git a/fastanime/libs/api/anilist/mutations/mark-read.gql b/fastanime/assets/graphql/anilist/mutations/mark-read.gql similarity index 100% rename from fastanime/libs/api/anilist/mutations/mark-read.gql rename to fastanime/assets/graphql/anilist/mutations/mark-read.gql diff --git a/fastanime/libs/api/anilist/mutations/media-list.gql b/fastanime/assets/graphql/anilist/mutations/media-list.gql similarity index 100% rename from fastanime/libs/api/anilist/mutations/media-list.gql rename to fastanime/assets/graphql/anilist/mutations/media-list.gql diff --git a/fastanime/libs/api/anilist/queries/airing.gql b/fastanime/assets/graphql/anilist/queries/airing.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/airing.gql rename to fastanime/assets/graphql/anilist/queries/airing.gql diff --git a/fastanime/libs/api/anilist/queries/anime.gql b/fastanime/assets/graphql/anilist/queries/anime.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/anime.gql rename to fastanime/assets/graphql/anilist/queries/anime.gql diff --git a/fastanime/libs/api/anilist/queries/character.gql b/fastanime/assets/graphql/anilist/queries/character.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/character.gql rename to fastanime/assets/graphql/anilist/queries/character.gql diff --git a/fastanime/libs/api/anilist/queries/favourite.gql b/fastanime/assets/graphql/anilist/queries/favourite.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/favourite.gql rename to fastanime/assets/graphql/anilist/queries/favourite.gql diff --git a/fastanime/libs/api/anilist/queries/get-medialist-item.gql b/fastanime/assets/graphql/anilist/queries/get-medialist-item.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/get-medialist-item.gql rename to fastanime/assets/graphql/anilist/queries/get-medialist-item.gql diff --git a/fastanime/libs/api/anilist/queries/logged-in-user.gql b/fastanime/assets/graphql/anilist/queries/logged-in-user.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/logged-in-user.gql rename to fastanime/assets/graphql/anilist/queries/logged-in-user.gql diff --git a/fastanime/libs/api/anilist/queries/media-list.gql b/fastanime/assets/graphql/anilist/queries/media-list.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/media-list.gql rename to fastanime/assets/graphql/anilist/queries/media-list.gql diff --git a/fastanime/libs/api/anilist/queries/media-relations.gql b/fastanime/assets/graphql/anilist/queries/media-relations.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/media-relations.gql rename to fastanime/assets/graphql/anilist/queries/media-relations.gql diff --git a/fastanime/libs/api/anilist/queries/notifications.gql b/fastanime/assets/graphql/anilist/queries/notifications.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/notifications.gql rename to fastanime/assets/graphql/anilist/queries/notifications.gql diff --git a/fastanime/libs/api/anilist/queries/popular.gql b/fastanime/assets/graphql/anilist/queries/popular.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/popular.gql rename to fastanime/assets/graphql/anilist/queries/popular.gql diff --git a/fastanime/libs/api/anilist/queries/recently-updated.gql b/fastanime/assets/graphql/anilist/queries/recently-updated.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/recently-updated.gql rename to fastanime/assets/graphql/anilist/queries/recently-updated.gql diff --git a/fastanime/libs/api/anilist/queries/recommended.gql b/fastanime/assets/graphql/anilist/queries/recommended.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/recommended.gql rename to fastanime/assets/graphql/anilist/queries/recommended.gql diff --git a/fastanime/libs/api/anilist/queries/reviews.gql b/fastanime/assets/graphql/anilist/queries/reviews.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/reviews.gql rename to fastanime/assets/graphql/anilist/queries/reviews.gql diff --git a/fastanime/libs/api/anilist/queries/score.gql b/fastanime/assets/graphql/anilist/queries/score.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/score.gql rename to fastanime/assets/graphql/anilist/queries/score.gql diff --git a/fastanime/libs/api/anilist/queries/search.gql b/fastanime/assets/graphql/anilist/queries/search.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/search.gql rename to fastanime/assets/graphql/anilist/queries/search.gql diff --git a/fastanime/libs/api/anilist/queries/trending.gql b/fastanime/assets/graphql/anilist/queries/trending.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/trending.gql rename to fastanime/assets/graphql/anilist/queries/trending.gql diff --git a/fastanime/libs/api/anilist/queries/upcoming.gql b/fastanime/assets/graphql/anilist/queries/upcoming.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/upcoming.gql rename to fastanime/assets/graphql/anilist/queries/upcoming.gql diff --git a/fastanime/libs/api/anilist/queries/user-info.gql b/fastanime/assets/graphql/anilist/queries/user-info.gql similarity index 100% rename from fastanime/libs/api/anilist/queries/user-info.gql rename to fastanime/assets/graphql/anilist/queries/user-info.gql diff --git a/fastanime/assets/scripts/fzf/episode-info.template.sh b/fastanime/assets/scripts/fzf/episode-info.template.sh new file mode 100755 index 0000000..919b5ca --- /dev/null +++ b/fastanime/assets/scripts/fzf/episode-info.template.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# +# Episode Preview Info Script Template +# This script formats and displays episode information in the FZF preview pane. +# Some values are injected by python those with '{name}' syntax using .replace() + + +draw_rule + +echo "{TITLE}"| fold -s -w "$WIDTH" + +draw_rule + +print_kv "Duration" "{DURATION}" +print_kv "Status" "{STATUS}" + +draw_rule + +print_kv "Total Episodes" "{EPISODES}" +print_kv "Next Episode" "{NEXT_EPISODE}" + +draw_rule + +print_kv "Progress" "{USER_PROGRESS}" +print_kv "List Status" "{USER_STATUS}" + +draw_rule + +print_kv "Start Date" "{START_DATE}" +print_kv "End Date" "{END_DATE}" + +draw_rule + diff --git a/fastanime/assets/scripts/fzf/info.template.sh b/fastanime/assets/scripts/fzf/info.template.sh new file mode 100755 index 0000000..550955e --- /dev/null +++ b/fastanime/assets/scripts/fzf/info.template.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# +# FastAnime Preview Info Script Template +# This script formats and displays the textual information in the FZF preview pane. +# Some values are injected by python those with '{name}' syntax using .replace() + + +draw_rule + +print_kv "Title" "{TITLE}" + +draw_rule + +# Emojis take up double the space +score_multiplier=1 +if ! [ "{SCORE}" = "N/A" ];then + score_multiplier=2 +fi +print_kv "Score" "{SCORE}" $score_multiplier + +print_kv "Favourites" "{FAVOURITES}" +print_kv "Popularity" "{POPULARITY}" +print_kv "Status" "{STATUS}" + +draw_rule + +print_kv "Episodes" "{EPISODES}" +print_kv "Next Episode" "{NEXT_EPISODE}" +print_kv "Duration" "{DURATION}" + +draw_rule + +print_kv "Genres" "{GENRES}" +print_kv "Format" "{FORMAT}" + +draw_rule + +print_kv "List Status" "{USER_STATUS}" +print_kv "Progress" "{USER_PROGRESS}" + +draw_rule + +print_kv "Start Date" "{START_DATE}" +print_kv "End Date" "{END_DATE}" + +draw_rule + +print_kv "Studios" "{STUDIOS}" +print_kv "Synonymns" "{SYNONYMNS}" +print_kv "Tags" "{TAGS}" + +draw_rule + +# Synopsis +echo "{SYNOPSIS}" | fold -s -w "$WIDTH" diff --git a/fastanime/libs/selectors/fzf/scripts/preview.sh b/fastanime/assets/scripts/fzf/preview.template.sh old mode 100644 new mode 100755 similarity index 62% rename from fastanime/libs/selectors/fzf/scripts/preview.sh rename to fastanime/assets/scripts/fzf/preview.template.sh index 33b1aba..b2917fe --- a/fastanime/libs/selectors/fzf/scripts/preview.sh +++ b/fastanime/assets/scripts/fzf/preview.template.sh @@ -1,13 +1,13 @@ #!/bin/sh # -# FastAnime FZF Preview Script Template +# FZF Preview Script Template # -# This script is a template. The placeholders in curly braces, like -# placeholder, are filled in by the Python application at runtime. -# It is executed by `sh -c "..."` for each item fzf previews. -# The first argument ($1) is the item string from fzf (the sanitized title). +# This script is a template. The placeholders in curly braces, like {NAME} +# are dynamically filled by python using .replace() + +WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80 +IMAGE_RENDERER="{IMAGE_RENDERER}" -IMAGE_RENDERER="{image_renderer}" generate_sha256() { local input @@ -30,6 +30,7 @@ generate_sha256() { echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n' fi } + fzf_preview() { file=$1 @@ -74,13 +75,59 @@ fzf_preview() { echo either icat for kitty terminal and wezterm or imgcat or chafa fi } + + +# --- Helper function for printing a key-value pair, aligning the value to the right --- +print_kv() { + local key="$1" + local value="$2" + local key_len=${#key} + local value_len=${#value} + local multiplier="${3:-1}" + + # Correctly calculate padding by accounting for the key, the ": ", and the value. + local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier)) + + # If the text is too long to fit, just add a single space for separation. + if [ "$padding_len" -lt 1 ]; then + padding_len=1 + value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))") + printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value" + else + printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value" + fi +} + +# --- Draw a rule across the screen --- +# TODO: figure out why this method does not work in fzf +draw_rule() { + local rule + # Generate the line of '─' characters, removing the trailing newline `tr` adds. + rule=$(printf '%*s' "$WIDTH" | tr ' ' '─' | tr -d '\n') + # Print the rule with colors and a single, clean newline. + printf "{C_RULE}%s{RESET}\\n" "$rule" +} + + +draw_rule(){ + ll=2 + while [ $ll -le $FZF_PREVIEW_COLUMNS ];do + echo -n -e "{C_RULE}─{RESET}" + ((ll++)) + done + echo +} + # Generate the same cache key that the Python worker uses +# {PREFIX} is used only on episode previews to make sure they are unique title={} hash=$(generate_sha256 "{PREFIX}$title") -# Display image if configured and the cached file exists -if [ "{preview_mode}" = "full" ] || [ "{preview_mode}" = "image" ]; then - image_file="{image_cache_path}{path_sep}$hash.png" +# +# --- Display image if configured and the cached file exists --- +# +if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "image" ]; then + image_file="{IMAGE_CACHE_PATH}{PATH_SEP}$hash.png" if [ -f "$image_file" ]; then fzf_preview "$image_file" else @@ -89,8 +136,8 @@ if [ "{preview_mode}" = "full" ] || [ "{preview_mode}" = "image" ]; then echo # Add a newline for spacing fi # Display text info if configured and the cached file exists -if [ "{preview_mode}" = "full" ] || [ "{preview_mode}" = "text" ]; then - info_file="{info_cache_path}{path_sep}$hash" +if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then + info_file="{INFO_CACHE_PATH}{PATH_SEP}$hash" if [ -f "$info_file" ]; then source "$info_file" else diff --git a/fastanime/libs/selectors/fzf/scripts/search.sh b/fastanime/assets/scripts/fzf/search.template.sh old mode 100644 new mode 100755 similarity index 100% rename from fastanime/libs/selectors/fzf/scripts/search.sh rename to fastanime/assets/scripts/fzf/search.template.sh diff --git a/fastanime/cli/config/generate.py b/fastanime/cli/config/generate.py index 5489184..8c3692e 100644 --- a/fastanime/cli/config/generate.py +++ b/fastanime/cli/config/generate.py @@ -5,7 +5,9 @@ from ...core.config import AppConfig from ...core.constants import APP_ASCII_ART, DISCORD_INVITE, PROJECT_NAME, REPO_HOME # The header for the config file. -config_asci = "\n".join([f"# {line}" for line in APP_ASCII_ART.split()]) +config_asci = "\n".join( + [f"# {line}" for line in APP_ASCII_ART.read_text(encoding="utf-8").split()] +) CONFIG_HEADER = f""" # ============================================================================== # diff --git a/fastanime/cli/services/feedback/service.py b/fastanime/cli/services/feedback/service.py index bfe2b3f..b130717 100644 --- a/fastanime/cli/services/feedback/service.py +++ b/fastanime/cli/services/feedback/service.py @@ -24,7 +24,7 @@ class FeedbackService: console.print(f"{main_msg}\n[dim]{details}[/dim]") else: console.print(main_msg) - time.sleep(5) + # time.sleep(5) def error(self, message: str, details: Optional[str] = None) -> None: """Show an error message with optional details.""" @@ -57,7 +57,7 @@ class FeedbackService: console.print(f"{main_msg}\n[dim]{details}[/dim]") else: console.print(main_msg) - time.sleep(5) + # time.sleep(5) @contextmanager def progress( diff --git a/fastanime/cli/utils/previews.py b/fastanime/cli/utils/previews.py index a358563..2c9060e 100644 --- a/fastanime/cli/utils/previews.py +++ b/fastanime/cli/utils/previews.py @@ -2,27 +2,280 @@ import concurrent.futures import logging import os from hashlib import sha256 +from pathlib import Path from threading import Thread from typing import List import httpx from ...core.config import AppConfig -from ...core.constants import APP_CACHE_DIR, APP_DIR, PLATFORM +from ...core.constants import APP_CACHE_DIR, PLATFORM, SCRIPTS_DIR from ...core.utils.file import AtomicWriter from ...libs.api.types import MediaItem from . import ansi, formatters logger = logging.getLogger(__name__) -# --- Constants for Paths --- +os.environ["SHELL"] = "bash" + PREVIEWS_CACHE_DIR = APP_CACHE_DIR / "previews" IMAGES_CACHE_DIR = PREVIEWS_CACHE_DIR / "images" INFO_CACHE_DIR = PREVIEWS_CACHE_DIR / "info" -FZF_SCRIPTS_DIR = APP_DIR / "libs" / "selectors" / "fzf" / "scripts" -PREVIEW_SCRIPT_TEMPLATE_PATH = FZF_SCRIPTS_DIR / "preview.sh" -INFO_SCRIPT_TEMPLATE_PATH = FZF_SCRIPTS_DIR / "info.sh" -EPISODE_INFO_SCRIPT_TEMPLATE_PATH = FZF_SCRIPTS_DIR / "episode_info.sh" + +FZF_SCRIPTS_DIR = SCRIPTS_DIR / "fzf" +TEMPLATE_PREVIEW_SCRIPT = Path(str(FZF_SCRIPTS_DIR / "preview.template.sh")).read_text( + encoding="utf-8" +) +TEMPLATE_INFO_SCRIPT = Path(str(FZF_SCRIPTS_DIR / "info.template.sh")).read_text( + encoding="utf-8" +) +TEMPLATE_EPISODE_INFO_SCRIPT = Path( + str(FZF_SCRIPTS_DIR / "episode-info.template.sh") +).read_text(encoding="utf-8") + + +def get_anime_preview( + items: List[MediaItem], titles: List[str], config: AppConfig +) -> str: + # Ensure cache directories exist on startup + IMAGES_CACHE_DIR.mkdir(parents=True, exist_ok=True) + INFO_CACHE_DIR.mkdir(parents=True, exist_ok=True) + + HEADER_COLOR = config.fzf.preview_header_color.split(",") + SEPARATOR_COLOR = config.fzf.preview_separator_color.split(",") + + preview_script = TEMPLATE_PREVIEW_SCRIPT + + # Start the non-blocking background Caching + Thread(target=_cache_worker, args=(items, titles, config), daemon=True).start() + + # 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_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": "", + } + + for key, value in replacements.items(): + preview_script = preview_script.replace(f"{{{key}}}", value) + + return preview_script + + +def _cache_worker(media_items: List[MediaItem], titles: List[str], config: AppConfig): + """The background task that fetches and saves all necessary preview data.""" + with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: + for media_item, title_str in zip(media_items, titles): + hash_id = _get_cache_hash(title_str) + if config.general.preview in ("full", "image") and media_item.cover_image: + if not (IMAGES_CACHE_DIR / f"{hash_id}.png").exists(): + executor.submit( + _save_image_from_url, media_item.cover_image.large, hash_id + ) + if config.general.preview in ("full", "text"): + # TODO: Come up with a better caching pattern for now just let it be remade + if not (INFO_CACHE_DIR / hash_id).exists() or True: + info_text = _populate_info_template(media_item, config) + executor.submit(_save_info_text, info_text, hash_id) + + +def _populate_info_template(media_item: MediaItem, config: AppConfig) -> str: + """ + Takes the info.sh template and injects formatted, shell-safe data. + """ + info_script = TEMPLATE_INFO_SCRIPT + description = formatters.clean_html( + media_item.description or "No description available." + ) + + # Escape all variables before injecting them into the script + replacements = { + "TITLE": formatters.shell_safe( + media_item.title.english or media_item.title.romaji + ), + "STATUS": formatters.shell_safe(media_item.status.value), + "FORMAT": formatters.shell_safe(media_item.format.value), + "NEXT_EPISODE": formatters.shell_safe( + f"Episode {media_item.next_airing.episode} on {formatters.format_date(media_item.next_airing.airing_at, '%A, %d %B %Y at %X)')}" + if media_item.next_airing + else "N/A" + ), + "EPISODES": formatters.shell_safe(str(media_item.episodes)), + "DURATION": formatters.shell_safe( + formatters.format_media_duration(media_item.duration) + ), + "SCORE": formatters.shell_safe( + formatters.format_score_stars_full(media_item.average_score) + ), + "FAVOURITES": formatters.shell_safe( + formatters.format_number_with_commas(media_item.favourites) + ), + "POPULARITY": formatters.shell_safe( + formatters.format_number_with_commas(media_item.popularity) + ), + "GENRES": formatters.shell_safe( + formatters.format_list_with_commas([v.value for v in media_item.genres]) + ), + "TAGS": formatters.shell_safe( + formatters.format_list_with_commas([t.name.value for t in media_item.tags]) + ), + "STUDIOS": formatters.shell_safe( + formatters.format_list_with_commas( + [t.name for t in media_item.studios if t.name] + ) + ), + "SYNONYMNS": formatters.shell_safe( + formatters.format_list_with_commas(media_item.synonymns) + ), + "USER_STATUS": formatters.shell_safe( + media_item.user_status.status.value + if media_item.user_status and media_item.user_status.status + else "NOT_ON_LIST" + ), + "USER_PROGRESS": formatters.shell_safe( + f"Episode {media_item.user_status.progress}" + if media_item.user_status + else "0" + ), + "START_DATE": formatters.shell_safe( + formatters.format_date(media_item.start_date) + ), + "END_DATE": formatters.shell_safe(formatters.format_date(media_item.end_date)), + "SYNOPSIS": formatters.shell_safe(description), + } + + for key, value in replacements.items(): + info_script = info_script.replace(f"{{{key}}}", value) + + return info_script + + +def get_episode_preview( + episodes: List[str], media_item: MediaItem, config: AppConfig +) -> str: + IMAGES_CACHE_DIR.mkdir(parents=True, exist_ok=True) + INFO_CACHE_DIR.mkdir(parents=True, exist_ok=True) + + HEADER_COLOR = config.fzf.preview_header_color.split(",") + SEPARATOR_COLOR = config.fzf.preview_separator_color.split(",") + + preview_script = TEMPLATE_PREVIEW_SCRIPT + # Start background caching for episodes + Thread( + target=_episode_cache_worker, args=(episodes, media_item, config), daemon=True + ).start() + + # 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_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": f"{media_item.title.english}_Episode_", + } + + for key, value in replacements.items(): + preview_script = preview_script.replace(f"{{{key}}}", value) + + return preview_script + + +def _episode_cache_worker( + episodes: List[str], media_item: MediaItem, config: AppConfig +): + """Background task that fetches and saves episode preview data.""" + streaming_episodes = {ep.title: ep for ep in media_item.streaming_episodes} + + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + for episode_str in episodes: + hash_id = _get_cache_hash( + f"{media_item.title.english}_Episode_{episode_str}" + ) + + # Find matching streaming episode + title = None + thumbnail = None + for title, ep in streaming_episodes.items(): + if f"Episode {episode_str} -" in title or title.endswith( + f" {episode_str}" + ): + title = title + thumbnail = ep.thumbnail + break + + # Fallback if no streaming episode found + if not title: + title = f"Episode {episode_str}" + + # Download thumbnail if available + if thumbnail: + executor.submit(_save_image_from_url, thumbnail, hash_id) + + # Generate and save episode info + episode_info = _populate_episode_info_template(config, title, media_item) + executor.submit(_save_info_text, episode_info, hash_id) + + +def _populate_episode_info_template( + config: AppConfig, title: str, media_item: MediaItem +) -> str: + """ + Takes the episode_info.sh template and injects episode-specific formatted data. + """ + episode_info_script = TEMPLATE_EPISODE_INFO_SCRIPT + + replacements = { + "TITLE": formatters.shell_safe(title), + "NEXT_EPISODE": formatters.shell_safe( + f"Episode {media_item.next_airing.episode} on {formatters.format_date(media_item.next_airing.airing_at, '%A, %d %B %Y at %X)')}" + if media_item.next_airing + else "N/A" + ), + "DURATION": formatters.format_media_duration(media_item.duration), + "STATUS": formatters.shell_safe(media_item.status.value), + "EPISODES": formatters.shell_safe(str(media_item.episodes)), + "USER_STATUS": formatters.shell_safe( + media_item.user_status.status.value + if media_item.user_status and media_item.user_status.status + else "NOT_ON_LIST" + ), + "USER_PROGRESS": formatters.shell_safe( + f"Episode {media_item.user_status.progress}" + if media_item.user_status + else "0" + ), + "START_DATE": formatters.shell_safe( + formatters.format_date(media_item.start_date) + ), + "END_DATE": formatters.shell_safe(formatters.format_date(media_item.end_date)), + } + + for key, value in replacements.items(): + episode_info_script = episode_info_script.replace(f"{{{key}}}", value) + + return episode_info_script def _get_cache_hash(text: str) -> str: @@ -53,290 +306,3 @@ def _save_info_text(info_text: str, hash_id: str): f.write(info_text) except IOError as e: logger.error(f"Failed to write info cache for {hash_id}: {e}") - - -def _populate_info_template(item: MediaItem, config: AppConfig) -> str: - """ - Takes the info.sh template and injects formatted, shell-safe data. - """ - template = INFO_SCRIPT_TEMPLATE_PATH.read_text(encoding="utf-8") - description = formatters.clean_html(item.description or "No description available.") - - HEADER_COLOR = config.fzf.preview_header_color.split(",") - SEPARATOR_COLOR = config.fzf.preview_separator_color.split(",") - - # Escape all variables before injecting them into the script - replacements = { - # - # plain text - # - "TITLE": formatters.shell_safe(item.title.english or item.title.romaji), - "STATUS": formatters.shell_safe(item.status.value), - "FORMAT": formatters.shell_safe(item.format.value), - # - # numerical - # - "NEXT_EPISODE": formatters.shell_safe( - f"Episode {item.next_airing.episode} on {formatters.format_date(item.next_airing.airing_at, '%A, %d %B %Y at %X)')}" - if item.next_airing - else "N/A" - ), - "EPISODES": formatters.shell_safe(str(item.episodes)), - "DURATION": formatters.shell_safe( - formatters.format_media_duration(item.duration) - ), - "SCORE": formatters.shell_safe( - formatters.format_score_stars_full(item.average_score) - ), - "FAVOURITES": formatters.shell_safe( - formatters.format_number_with_commas(item.favourites) - ), - "POPULARITY": formatters.shell_safe( - formatters.format_number_with_commas(item.popularity) - ), - # - # list - # - "GENRES": formatters.shell_safe( - formatters.format_list_with_commas([v.value for v in item.genres]) - ), - "TAGS": formatters.shell_safe( - formatters.format_list_with_commas([t.name.value for t in item.tags]) - ), - "STUDIOS": formatters.shell_safe( - formatters.format_list_with_commas([t.name for t in item.studios if t.name]) - ), - "SYNONYMNS": formatters.shell_safe( - formatters.format_list_with_commas(item.synonymns) - ), - # - # user - # - "USER_STATUS": formatters.shell_safe( - item.user_status.status.value - if item.user_status and item.user_status.status - else "NOT_ON_LIST" - ), - "USER_PROGRESS": formatters.shell_safe( - f"Episode {item.user_status.progress}" if item.user_status else "0" - ), - # - # dates - # - "START_DATE": formatters.shell_safe(formatters.format_date(item.start_date)), - "END_DATE": formatters.shell_safe(formatters.format_date(item.end_date)), - # - # big guy - # - "SYNOPSIS": formatters.shell_safe(description), - # - # 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, - } - - for key, value in replacements.items(): - template = template.replace(f"{{{key}}}", value) - - return template - - -def _cache_worker(items: List[MediaItem], titles: List[str], config: AppConfig): - """The background task that fetches and saves all necessary preview data.""" - with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: - for item, title_str in zip(items, titles): - hash_id = _get_cache_hash(title_str) - if config.general.preview in ("full", "image") and item.cover_image: - if not (IMAGES_CACHE_DIR / f"{hash_id}.png").exists(): - executor.submit( - _save_image_from_url, item.cover_image.large, hash_id - ) - if config.general.preview in ("full", "text"): - # TODO: Come up with a better caching pattern for now just let it be remade - if not (INFO_CACHE_DIR / hash_id).exists() or True: - info_text = _populate_info_template(item, config) - executor.submit(_save_info_text, info_text, hash_id) - - -def get_anime_preview( - items: List[MediaItem], titles: List[str], config: AppConfig -) -> str: - """ - Starts a background task to cache preview data and returns the fzf preview command - by formatting a shell script template. - """ - # Ensure cache directories exist on startup - IMAGES_CACHE_DIR.mkdir(parents=True, exist_ok=True) - INFO_CACHE_DIR.mkdir(parents=True, exist_ok=True) - - # Start the non-blocking background Caching - Thread(target=_cache_worker, args=(items, titles, config), daemon=True).start() - - # Read the shell script template from the file system. - try: - template = PREVIEW_SCRIPT_TEMPLATE_PATH.read_text(encoding="utf-8") - except FileNotFoundError: - logger.error( - f"Preview script template not found at {PREVIEW_SCRIPT_TEMPLATE_PATH}" - ) - return "echo 'Error: Preview script template not found.'" - - # Prepare values to inject into the template - path_sep = "\\" if PLATFORM == "win32" else "/" - - # Format the template with the dynamic values - final_script = ( - template.replace("{preview_mode}", config.general.preview) - .replace("{image_cache_path}", str(IMAGES_CACHE_DIR)) - .replace("{info_cache_path}", str(INFO_CACHE_DIR)) - .replace("{path_sep}", path_sep) - .replace("{image_renderer}", config.general.image_renderer) - .replace("{PREFIX}", "") - ) - # ) - - # Return the command for fzf to execute. `sh -c` is used to run the script string. - # The -- "{}" ensures that the selected item is passed as the first argument ($1) - # to the script, even if it contains spaces or special characters. - os.environ["SHELL"] = "bash" - return final_script - - -# --- Episode Preview Functionality --- - - -def _populate_episode_info_template(episode_data: dict, config: AppConfig) -> str: - """ - Takes the episode_info.sh template and injects episode-specific formatted data. - """ - template = EPISODE_INFO_SCRIPT_TEMPLATE_PATH.read_text(encoding="utf-8") - - HEADER_COLOR = config.fzf.preview_header_color.split(",") - SEPARATOR_COLOR = config.fzf.preview_separator_color.split(",") - - # Escape all variables before injecting them into the script - replacements = { - "TITLE": formatters.shell_safe(episode_data.get("title", "Episode")), - "SCORE": formatters.shell_safe("N/A"), # Episodes don't have scores - "STATUS": formatters.shell_safe(episode_data.get("status", "Available")), - "FAVOURITES": formatters.shell_safe("N/A"), # Episodes don't have favorites - "GENRES": formatters.shell_safe( - episode_data.get("duration", "Unknown duration") - ), - "SYNOPSIS": formatters.shell_safe( - episode_data.get("description", "No episode description available.") - ), - # 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, - } - - for key, value in replacements.items(): - template = template.replace(f"{{{key}}}", value) - - return template - - -def _episode_cache_worker(episodes: List[str], anime: MediaItem, config: AppConfig): - """Background task that fetches and saves episode preview data.""" - streaming_episodes = {ep.title: ep for ep in anime.streaming_episodes} - - with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: - for episode_str in episodes: - hash_id = _get_cache_hash(f"{anime.title.english}_Episode_{episode_str}") - - # Find matching streaming episode - episode_data = None - for title, ep in streaming_episodes.items(): - if f"Episode {episode_str} -" in title or title.endswith( - f" {episode_str}" - ): - episode_data = { - "title": title, - "thumbnail": ep.thumbnail, - "description": f"Episode {episode_str} of {anime.title.english or anime.title.romaji}", - "duration": f"{anime.duration} min" - if anime.duration - else "Unknown duration", - "status": "Available", - } - break - - # Fallback if no streaming episode found - if not episode_data: - episode_data = { - "title": f"Episode {episode_str}", - "thumbnail": None, - "description": f"Episode {episode_str} of {anime.title.english or anime.title.romaji}", - "duration": f"{anime.duration} min" - if anime.duration - else "Unknown duration", - "status": "Available", - } - - # Download thumbnail if available - if episode_data["thumbnail"]: - executor.submit( - _save_image_from_url, episode_data["thumbnail"], hash_id - ) - - # Generate and save episode info - episode_info = _populate_episode_info_template(episode_data, config) - executor.submit(_save_info_text, episode_info, hash_id) - - -def get_episode_preview( - episodes: List[str], anime: MediaItem, config: AppConfig -) -> str: - """ - Starts a background task to cache episode preview data and returns the fzf preview command. - - Args: - episodes: List of episode numbers as strings - anime: MediaItem containing the anime data with streaming episodes - config: Application configuration - - Returns: - FZF preview command string - """ - # TODO: finish implementation of episode preview - # Ensure cache directories exist - IMAGES_CACHE_DIR.mkdir(parents=True, exist_ok=True) - INFO_CACHE_DIR.mkdir(parents=True, exist_ok=True) - - # Start background caching for episodes - Thread( - target=_episode_cache_worker, args=(episodes, anime, config), daemon=True - ).start() - - # Read the shell script template - try: - template = PREVIEW_SCRIPT_TEMPLATE_PATH.read_text(encoding="utf-8") - except FileNotFoundError: - logger.error( - f"Preview script template not found at {PREVIEW_SCRIPT_TEMPLATE_PATH}" - ) - return "echo 'Error: Preview script template not found.'" - - # Prepare values to inject into the template - path_sep = "\\" if PLATFORM == "win32" else "/" - - # Format the template with the dynamic values - final_script = ( - template.replace("{preview_mode}", config.general.preview) - .replace("{image_cache_path}", str(IMAGES_CACHE_DIR)) - .replace("{info_cache_path}", str(INFO_CACHE_DIR)) - .replace("{path_sep}", path_sep) - .replace("{image_renderer}", config.general.image_renderer) - .replace("{PREFIX}", f"{anime.title.english}_Episode_") - ) - - os.environ["SHELL"] = "bash" - return final_script diff --git a/fastanime/core/config/defaults.py b/fastanime/core/config/defaults.py index a75527a..6efaa39 100644 --- a/fastanime/core/config/defaults.py +++ b/fastanime/core/config/defaults.py @@ -1,4 +1,4 @@ -from ..constants import APP_DATA_DIR, APP_NAME, USER_VIDEOS_DIR +from ..constants import APP_DATA_DIR, DEFAULTS_DIR, USER_VIDEOS_DIR # GeneralConfig GENERAL_PYGMENT_STYLE = "github-dark" @@ -42,10 +42,18 @@ SERVICE_CLEANUP_COMPLETED_DAYS = 7 SERVICE_NOTIFICATION_ENABLED = True # FzfConfig +FZF_OPTS = DEFAULTS_DIR / "fzf-opts" FZF_HEADER_COLOR = "95,135,175" FZF_PREVIEW_HEADER_COLOR = "215,0,95" FZF_PREVIEW_SEPARATOR_COLOR = "208,208,208" +# RofiConfig +_ROFI_THEMES_DIR = DEFAULTS_DIR / "rofi-themes" +ROFI_THEME_MAIN = _ROFI_THEMES_DIR / "main.rasi" +ROFI_THEME_INPUT = _ROFI_THEMES_DIR / "input.rasi" +ROFI_THEME_CONFIRM = _ROFI_THEMES_DIR / "confirm.rasi" +ROFI_THEME_PREVIEW = _ROFI_THEMES_DIR / "preview.rasi" + # MpvConfig MPV_ARGS = "" MPV_PRE_ARGS = "" diff --git a/fastanime/core/config/model.py b/fastanime/core/config/model.py index 38e899a..6f50040 100644 --- a/fastanime/core/config/model.py +++ b/fastanime/core/config/model.py @@ -4,13 +4,6 @@ from typing import Literal from pydantic import BaseModel, Field, PrivateAttr, computed_field -from ...core.constants import ( - FZF_DEFAULT_OPTS, - ROFI_THEME_CONFIRM, - ROFI_THEME_INPUT, - ROFI_THEME_MAIN, - ROFI_THEME_PREVIEW, -) from ...libs.api.types import MediaSort, UserMediaListSort from ...libs.providers.anime.types import ProviderName, ProviderServer from ..constants import APP_ASCII_ART @@ -198,11 +191,15 @@ class SessionsConfig(OtherConfig): class FzfConfig(OtherConfig): """Configuration specific to the FZF selector.""" - _opts: str = PrivateAttr(default=FZF_DEFAULT_OPTS.read_text(encoding="utf-8")) + _opts: str = PrivateAttr( + default_factory=lambda: defaults.FZF_OPTS.read_text(encoding="utf-8") + ) header_color: str = Field( default=defaults.FZF_HEADER_COLOR, description=desc.FZF_HEADER_COLOR ) - _header_ascii_art: str = PrivateAttr(default=APP_ASCII_ART) + _header_ascii_art: str = PrivateAttr( + default_factory=lambda: APP_ASCII_ART.read_text(encoding="utf-8") + ) preview_header_color: str = Field( default=defaults.FZF_PREVIEW_HEADER_COLOR, description=desc.FZF_PREVIEW_HEADER_COLOR, @@ -239,19 +236,19 @@ class RofiConfig(OtherConfig): """Configuration specific to the Rofi selector.""" theme_main: Path = Field( - default=Path(str(ROFI_THEME_MAIN)), + default_factory=lambda: Path(str(defaults.ROFI_THEME_MAIN)), description=desc.ROFI_THEME_MAIN, ) theme_preview: Path = Field( - default=Path(str(ROFI_THEME_PREVIEW)), + default_factory=lambda: Path(str(defaults.ROFI_THEME_PREVIEW)), description=desc.ROFI_THEME_PREVIEW, ) theme_confirm: Path = Field( - default=Path(str(ROFI_THEME_CONFIRM)), + default_factory=lambda: Path(str(defaults.ROFI_THEME_CONFIRM)), description=desc.ROFI_THEME_CONFIRM, ) theme_input: Path = Field( - default=Path(str(ROFI_THEME_INPUT)), + default_factory=lambda: Path(str(defaults.ROFI_THEME_INPUT)), description=desc.ROFI_THEME_INPUT, ) diff --git a/fastanime/core/constants.py b/fastanime/core/constants.py index e286342..4d60d4a 100644 --- a/fastanime/core/constants.py +++ b/fastanime/core/constants.py @@ -4,8 +4,11 @@ from importlib import metadata, resources from pathlib import Path PLATFORM = sys.platform -APP_NAME = os.environ.get("FASTANIME_APP_NAME", "fastanime") + PROJECT_NAME = "FASTANIME" +APP_NAME = os.environ.get(f"{PROJECT_NAME}_APP_NAME", PROJECT_NAME.lower()) + +USER_NAME = os.environ.get("USERNAME", "User") __version__ = metadata.version(PROJECT_NAME) @@ -13,7 +16,9 @@ AUTHOR = "Benexl" GIT_REPO = "github.com" GIT_PROTOCOL = "https://" REPO_HOME = f"https://{GIT_REPO}/{AUTHOR}/FastAnime" + DISCORD_INVITE = "https://discord.gg/C4rhMA4mmK" + ANILIST_AUTH = ( "https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token" ) @@ -27,20 +32,13 @@ except ModuleNotFoundError: APP_DIR = Path(__file__).resolve().parent.parent ASSETS_DIR = APP_DIR / "assets" -DEFAULTS = ASSETS_DIR / "defaults" +DEFAULTS_DIR = ASSETS_DIR / "defaults" +SCRIPTS_DIR = ASSETS_DIR / "scripts" +GRAPHQL_DIR = ASSETS_DIR / "graphql" ICONS_DIR = ASSETS_DIR / "icons" -# rofi files -_ROFI_THEMES_DIR = DEFAULTS / "rofi-themes" -ROFI_THEME_MAIN = _ROFI_THEMES_DIR / "main.rasi" -ROFI_THEME_INPUT = _ROFI_THEMES_DIR / "input.rasi" -ROFI_THEME_CONFIRM = _ROFI_THEMES_DIR / "confirm.rasi" -ROFI_THEME_PREVIEW = _ROFI_THEMES_DIR / "preview.rasi" - -# fzf -FZF_DEFAULT_OPTS = DEFAULTS / "fzf-opts" - -USER_NAME = os.environ.get("USERNAME", "Anime Fan") +ICON_PATH = ICONS_DIR / ("logo.ico" if PLATFORM == "Win32" else "logo.png") +APP_ASCII_ART = DEFAULTS_DIR / "ascii-art" try: import click @@ -83,15 +81,3 @@ USER_VIDEOS_DIR.mkdir(parents=True, exist_ok=True) USER_CONFIG_PATH = APP_DATA_DIR / "config.ini" LOG_FILE_PATH = APP_CACHE_DIR / "fastanime.log" - -ICON_PATH = ICONS_DIR / ("logo.ico" if PLATFORM == "Win32" else "logo.png") - - -APP_ASCII_ART = """\ -███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗ -██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝ -█████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░ -██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░ -██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗ -╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝ -""" diff --git a/fastanime/libs/api/anilist/api.py b/fastanime/libs/api/anilist/api.py index e1d09f1..53692bb 100644 --- a/fastanime/libs/api/anilist/api.py +++ b/fastanime/libs/api/anilist/api.py @@ -155,7 +155,7 @@ class AniListApi(BaseApiClient): "type": params.type.value if params.type else "ANIME", } response = execute_graphql( - ANILIST_ENDPOINT, self.http_client, gql.GET_USER_MEDIA_LIST, variables + ANILIST_ENDPOINT, self.http_client, gql.SEARCH_USER_MEDIA_LIST, variables ) return mapper.to_generic_user_list_result(response.json()) if response else None diff --git a/fastanime/libs/api/anilist/gql.py b/fastanime/libs/api/anilist/gql.py index 580b907..220c3f3 100644 --- a/fastanime/libs/api/anilist/gql.py +++ b/fastanime/libs/api/anilist/gql.py @@ -1,27 +1,21 @@ -from ....core.constants import APP_DIR +from ....core.constants import GRAPHQL_DIR -_ANILIST_PATH = APP_DIR / "libs" / "api" / "anilist" +_ANILIST_PATH = GRAPHQL_DIR / "anilist" _QUERIES_PATH = _ANILIST_PATH / "queries" _MUTATIONS_PATH = _ANILIST_PATH / "mutations" +SEARCH_MEDIA = _QUERIES_PATH / "search.gql" +SEARCH_USER_MEDIA_LIST = _QUERIES_PATH / "media-list.gql" + GET_AIRING_SCHEDULE = _QUERIES_PATH / "airing.gql" -GET_ANIME_DETAILS = _QUERIES_PATH / "anime.gql" GET_CHARACTERS = _QUERIES_PATH / "character.gql" -GET_FAVOURITES = _QUERIES_PATH / "favourite.gql" GET_MEDIA_LIST_ITEM = _QUERIES_PATH / "get-medialist-item.gql" GET_LOGGED_IN_USER = _QUERIES_PATH / "logged-in-user.gql" -GET_USER_MEDIA_LIST = _QUERIES_PATH / "media-list.gql" GET_MEDIA_RELATIONS = _QUERIES_PATH / "media-relations.gql" GET_NOTIFICATIONS = _QUERIES_PATH / "notifications.gql" -GET_POPULAR = _QUERIES_PATH / "popular.gql" -GET_RECENTLY_UPDATED = _QUERIES_PATH / "recently-updated.gql" GET_RECOMMENDATIONS = _QUERIES_PATH / "recommended.gql" GET_REVIEWS = _QUERIES_PATH / "reviews.gql" -GET_SCORES = _QUERIES_PATH / "score.gql" -SEARCH_MEDIA = _QUERIES_PATH / "search.gql" -GET_TRENDING = _QUERIES_PATH / "trending.gql" -GET_UPCOMING = _QUERIES_PATH / "upcoming.gql" GET_USER_INFO = _QUERIES_PATH / "user-info.gql" diff --git a/fastanime/libs/providers/anime/allanime/constants.py b/fastanime/libs/providers/anime/allanime/constants.py index 4c3dc6c..15e3634 100644 --- a/fastanime/libs/providers/anime/allanime/constants.py +++ b/fastanime/libs/providers/anime/allanime/constants.py @@ -1,6 +1,6 @@ import re -from .....core.constants import APP_DIR +from .....core.constants import GRAPHQL_DIR SERVERS_AVAILABLE = [ "sharepoint", @@ -28,7 +28,7 @@ MP4_SERVER_JUICY_STREAM_REGEX = re.compile( ) # graphql files -GQLS = APP_DIR / "libs" / "providers" / "anime" / "allanime" / "queries" -SEARCH_GQL = GQLS / "search.gql" -ANIME_GQL = GQLS / "anime.gql" -EPISODE_GQL = GQLS / "episodes.gql" +_GQL_QUERIES = GRAPHQL_DIR / "allanime" / "queries" +SEARCH_GQL = _GQL_QUERIES / "search.gql" +ANIME_GQL = _GQL_QUERIES / "anime.gql" +EPISODE_GQL = _GQL_QUERIES / "episodes.gql" diff --git a/fastanime/libs/selectors/fzf/scripts/episode_info.sh b/fastanime/libs/selectors/fzf/scripts/episode_info.sh deleted file mode 100644 index d5de81c..0000000 --- a/fastanime/libs/selectors/fzf/scripts/episode_info.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/sh -# -# FastAnime Episode Preview Info Script Template -# This script formats and displays episode information in the FZF preview pane. -# Values are injected by python using .replace() - -# --- Terminal Dimensions --- -WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80 - -# --- Helper function for printing a key-value pair, aligning the value to the right --- -print_kv() { - local key="$1" - local value="$2" - local key_len=${#key} - local value_len=${#value} - local multiplier="${3:-1}" - - # Correctly calculate padding by accounting for the key, the ": ", and the value. - local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier)) - - # If the text is too long to fit, just add a single space for separation. - if [ "$padding_len" -lt 1 ]; then - padding_len=1 - value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))") - printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value" - else - printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value" - fi - -} - -# --- Draw a rule across the screen --- -draw_rule() { - local rule - # Generate the line of '─' characters, removing the trailing newline `tr` adds. - rule=$(printf '%*s' "$WIDTH" | tr ' ' '─' | tr -d '\n') - # Print the rule with colors and a single, clean newline. - printf "{C_RULE}%s{RESET}\\n" "$rule" -} - - -draw_rule(){ - ll=2 - while [ $ll -le $FZF_PREVIEW_COLUMNS ];do - echo -n -e "{C_RULE}─{RESET}" - ((ll++)) - done - echo -} - -# --- Display Episode Content --- -draw_rule -echo "{TITLE}"| fold -s -w "$WIDTH" -draw_rule - -# Episode-specific information -# print_kv "Duration" "{GENRES}" -# print_kv "Status" "{STATUS}" -# draw_rule - -# Episode description/summary -# echo "{SYNOPSIS}" | fold -s -w "$WIDTH" diff --git a/fastanime/libs/selectors/fzf/scripts/info.sh b/fastanime/libs/selectors/fzf/scripts/info.sh deleted file mode 100644 index 4b7b070..0000000 --- a/fastanime/libs/selectors/fzf/scripts/info.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/sh -# -# FastAnime Preview Info Script Template -# This script formats and displays the textual information in the FZF preview pane. -# Some values are injected by python those with '{name}' syntax using .replace() - - -# --- Terminal Dimensions --- -WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80 - -# --- Helper function for printing a key-value pair, aligning the value to the right --- -print_kv() { - local key="$1" - local value="$2" - local key_len=${#key} - local value_len=${#value} - local multiplier="${3:-1}" - - # Correctly calculate padding by accounting for the key, the ": ", and the value. - local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier)) - - # If the text is too long to fit, just add a single space for separation. - if [ "$padding_len" -lt 1 ]; then - padding_len=1 - value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))") - printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value" - else - printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value" - fi - -} - -# --- Draw a rule across the screen --- -draw_rule() { - local rule - # Generate the line of '─' characters, removing the trailing newline `tr` adds. - rule=$(printf '%*s' "$WIDTH" | tr ' ' '─' | tr -d '\n') - # Print the rule with colors and a single, clean newline. - printf "{C_RULE}%s{RESET}\\n" "$rule" -} - - -draw_rule(){ - ll=2 - while [ $ll -le $FZF_PREVIEW_COLUMNS ];do - echo -n -e "{C_RULE}─{RESET}" - ((ll++)) - done - echo -} - -# --- Display Content --- -draw_rule -print_kv "Title" "{TITLE}" - -draw_rule - -# Key-Value Stats Section -score_multiplier=1 -if ! [ "{SCORE}" = "N/A" ];then - score_multiplier=2 -fi -print_kv "Score" "{SCORE}" $score_multiplier -print_kv "Favourites" "{FAVOURITES}" -print_kv "Popularity" "{POPULARITY}" -print_kv "Status" "{STATUS}" - -draw_rule - -print_kv "Episodes" "{EPISODES}" -print_kv "Next Episode" "{NEXT_EPISODE}" -print_kv "Duration" "{DURATION}" - -draw_rule - -print_kv "Genres" "{GENRES}" -print_kv "Format" "{FORMAT}" - -draw_rule - -print_kv "List Status" "{USER_STATUS}" -print_kv "Progress" "{USER_PROGRESS}" - -draw_rule - -print_kv "Start Date" "{START_DATE}" -print_kv "End Date" "{END_DATE}" - -draw_rule - -print_kv "Studios" "{STUDIOS}" -print_kv "Synonymns" "{SYNONYMNS}" -print_kv "Tags" "{TAGS}" - -draw_rule - -# Synopsis -echo "{SYNOPSIS}" | fold -s -w "$WIDTH"