mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-12 15:50:01 -08:00
refactor: reorganize assets
This commit is contained in:
6
fastanime/assets/defaults/ascii-art
Normal file
6
fastanime/assets/defaults/ascii-art
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗
|
||||||
|
██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝
|
||||||
|
█████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░
|
||||||
|
██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░
|
||||||
|
██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗
|
||||||
|
╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝
|
||||||
33
fastanime/assets/scripts/fzf/episode-info.template.sh
Executable file
33
fastanime/assets/scripts/fzf/episode-info.template.sh
Executable file
@@ -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
|
||||||
|
|
||||||
55
fastanime/assets/scripts/fzf/info.template.sh
Executable file
55
fastanime/assets/scripts/fzf/info.template.sh
Executable file
@@ -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"
|
||||||
69
fastanime/libs/selectors/fzf/scripts/preview.sh → fastanime/assets/scripts/fzf/preview.template.sh
Normal file → Executable file
69
fastanime/libs/selectors/fzf/scripts/preview.sh → fastanime/assets/scripts/fzf/preview.template.sh
Normal file → Executable file
@@ -1,13 +1,13 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#
|
#
|
||||||
# FastAnime FZF Preview Script Template
|
# FZF Preview Script Template
|
||||||
#
|
#
|
||||||
# This script is a template. The placeholders in curly braces, like
|
# This script is a template. The placeholders in curly braces, like {NAME}
|
||||||
# placeholder, are filled in by the Python application at runtime.
|
# are dynamically filled by python using .replace()
|
||||||
# It is executed by `sh -c "..."` for each item fzf previews.
|
|
||||||
# The first argument ($1) is the item string from fzf (the sanitized title).
|
WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80
|
||||||
|
IMAGE_RENDERER="{IMAGE_RENDERER}"
|
||||||
|
|
||||||
IMAGE_RENDERER="{image_renderer}"
|
|
||||||
generate_sha256() {
|
generate_sha256() {
|
||||||
local input
|
local input
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@ generate_sha256() {
|
|||||||
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
|
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
fzf_preview() {
|
fzf_preview() {
|
||||||
file=$1
|
file=$1
|
||||||
|
|
||||||
@@ -74,13 +75,59 @@ fzf_preview() {
|
|||||||
echo either icat for kitty terminal and wezterm or imgcat or chafa
|
echo either icat for kitty terminal and wezterm or imgcat or chafa
|
||||||
fi
|
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
|
# Generate the same cache key that the Python worker uses
|
||||||
|
# {PREFIX} is used only on episode previews to make sure they are unique
|
||||||
title={}
|
title={}
|
||||||
hash=$(generate_sha256 "{PREFIX}$title")
|
hash=$(generate_sha256 "{PREFIX}$title")
|
||||||
|
|
||||||
# Display image if configured and the cached file exists
|
#
|
||||||
if [ "{preview_mode}" = "full" ] || [ "{preview_mode}" = "image" ]; then
|
# --- Display image if configured and the cached file exists ---
|
||||||
image_file="{image_cache_path}{path_sep}$hash.png"
|
#
|
||||||
|
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "image" ]; then
|
||||||
|
image_file="{IMAGE_CACHE_PATH}{PATH_SEP}$hash.png"
|
||||||
if [ -f "$image_file" ]; then
|
if [ -f "$image_file" ]; then
|
||||||
fzf_preview "$image_file"
|
fzf_preview "$image_file"
|
||||||
else
|
else
|
||||||
@@ -89,8 +136,8 @@ if [ "{preview_mode}" = "full" ] || [ "{preview_mode}" = "image" ]; then
|
|||||||
echo # Add a newline for spacing
|
echo # Add a newline for spacing
|
||||||
fi
|
fi
|
||||||
# Display text info if configured and the cached file exists
|
# Display text info if configured and the cached file exists
|
||||||
if [ "{preview_mode}" = "full" ] || [ "{preview_mode}" = "text" ]; then
|
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
|
||||||
info_file="{info_cache_path}{path_sep}$hash"
|
info_file="{INFO_CACHE_PATH}{PATH_SEP}$hash"
|
||||||
if [ -f "$info_file" ]; then
|
if [ -f "$info_file" ]; then
|
||||||
source "$info_file"
|
source "$info_file"
|
||||||
else
|
else
|
||||||
0
fastanime/libs/selectors/fzf/scripts/search.sh → fastanime/assets/scripts/fzf/search.template.sh
Normal file → Executable file
0
fastanime/libs/selectors/fzf/scripts/search.sh → fastanime/assets/scripts/fzf/search.template.sh
Normal file → Executable file
@@ -5,7 +5,9 @@ from ...core.config import AppConfig
|
|||||||
from ...core.constants import APP_ASCII_ART, DISCORD_INVITE, PROJECT_NAME, REPO_HOME
|
from ...core.constants import APP_ASCII_ART, DISCORD_INVITE, PROJECT_NAME, REPO_HOME
|
||||||
|
|
||||||
# The header for the config file.
|
# 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"""
|
CONFIG_HEADER = f"""
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class FeedbackService:
|
|||||||
console.print(f"{main_msg}\n[dim]{details}[/dim]")
|
console.print(f"{main_msg}\n[dim]{details}[/dim]")
|
||||||
else:
|
else:
|
||||||
console.print(main_msg)
|
console.print(main_msg)
|
||||||
time.sleep(5)
|
# time.sleep(5)
|
||||||
|
|
||||||
def error(self, message: str, details: Optional[str] = None) -> None:
|
def error(self, message: str, details: Optional[str] = None) -> None:
|
||||||
"""Show an error message with optional details."""
|
"""Show an error message with optional details."""
|
||||||
@@ -57,7 +57,7 @@ class FeedbackService:
|
|||||||
console.print(f"{main_msg}\n[dim]{details}[/dim]")
|
console.print(f"{main_msg}\n[dim]{details}[/dim]")
|
||||||
else:
|
else:
|
||||||
console.print(main_msg)
|
console.print(main_msg)
|
||||||
time.sleep(5)
|
# time.sleep(5)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def progress(
|
def progress(
|
||||||
|
|||||||
@@ -2,27 +2,280 @@ import concurrent.futures
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
from pathlib import Path
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from ...core.config import AppConfig
|
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 ...core.utils.file import AtomicWriter
|
||||||
from ...libs.api.types import MediaItem
|
from ...libs.api.types import MediaItem
|
||||||
from . import ansi, formatters
|
from . import ansi, formatters
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# --- Constants for Paths ---
|
os.environ["SHELL"] = "bash"
|
||||||
|
|
||||||
PREVIEWS_CACHE_DIR = APP_CACHE_DIR / "previews"
|
PREVIEWS_CACHE_DIR = APP_CACHE_DIR / "previews"
|
||||||
IMAGES_CACHE_DIR = PREVIEWS_CACHE_DIR / "images"
|
IMAGES_CACHE_DIR = PREVIEWS_CACHE_DIR / "images"
|
||||||
INFO_CACHE_DIR = PREVIEWS_CACHE_DIR / "info"
|
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"
|
FZF_SCRIPTS_DIR = SCRIPTS_DIR / "fzf"
|
||||||
INFO_SCRIPT_TEMPLATE_PATH = FZF_SCRIPTS_DIR / "info.sh"
|
TEMPLATE_PREVIEW_SCRIPT = Path(str(FZF_SCRIPTS_DIR / "preview.template.sh")).read_text(
|
||||||
EPISODE_INFO_SCRIPT_TEMPLATE_PATH = FZF_SCRIPTS_DIR / "episode_info.sh"
|
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:
|
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)
|
f.write(info_text)
|
||||||
except IOError as e:
|
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 {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
|
|
||||||
|
|||||||
@@ -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
|
# GeneralConfig
|
||||||
GENERAL_PYGMENT_STYLE = "github-dark"
|
GENERAL_PYGMENT_STYLE = "github-dark"
|
||||||
@@ -42,10 +42,18 @@ SERVICE_CLEANUP_COMPLETED_DAYS = 7
|
|||||||
SERVICE_NOTIFICATION_ENABLED = True
|
SERVICE_NOTIFICATION_ENABLED = True
|
||||||
|
|
||||||
# FzfConfig
|
# FzfConfig
|
||||||
|
FZF_OPTS = DEFAULTS_DIR / "fzf-opts"
|
||||||
FZF_HEADER_COLOR = "95,135,175"
|
FZF_HEADER_COLOR = "95,135,175"
|
||||||
FZF_PREVIEW_HEADER_COLOR = "215,0,95"
|
FZF_PREVIEW_HEADER_COLOR = "215,0,95"
|
||||||
FZF_PREVIEW_SEPARATOR_COLOR = "208,208,208"
|
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
|
# MpvConfig
|
||||||
MPV_ARGS = ""
|
MPV_ARGS = ""
|
||||||
MPV_PRE_ARGS = ""
|
MPV_PRE_ARGS = ""
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ from typing import Literal
|
|||||||
|
|
||||||
from pydantic import BaseModel, Field, PrivateAttr, computed_field
|
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.api.types import MediaSort, UserMediaListSort
|
||||||
from ...libs.providers.anime.types import ProviderName, ProviderServer
|
from ...libs.providers.anime.types import ProviderName, ProviderServer
|
||||||
from ..constants import APP_ASCII_ART
|
from ..constants import APP_ASCII_ART
|
||||||
@@ -198,11 +191,15 @@ class SessionsConfig(OtherConfig):
|
|||||||
class FzfConfig(OtherConfig):
|
class FzfConfig(OtherConfig):
|
||||||
"""Configuration specific to the FZF selector."""
|
"""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(
|
header_color: str = Field(
|
||||||
default=defaults.FZF_HEADER_COLOR, description=desc.FZF_HEADER_COLOR
|
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(
|
preview_header_color: str = Field(
|
||||||
default=defaults.FZF_PREVIEW_HEADER_COLOR,
|
default=defaults.FZF_PREVIEW_HEADER_COLOR,
|
||||||
description=desc.FZF_PREVIEW_HEADER_COLOR,
|
description=desc.FZF_PREVIEW_HEADER_COLOR,
|
||||||
@@ -239,19 +236,19 @@ class RofiConfig(OtherConfig):
|
|||||||
"""Configuration specific to the Rofi selector."""
|
"""Configuration specific to the Rofi selector."""
|
||||||
|
|
||||||
theme_main: Path = Field(
|
theme_main: Path = Field(
|
||||||
default=Path(str(ROFI_THEME_MAIN)),
|
default_factory=lambda: Path(str(defaults.ROFI_THEME_MAIN)),
|
||||||
description=desc.ROFI_THEME_MAIN,
|
description=desc.ROFI_THEME_MAIN,
|
||||||
)
|
)
|
||||||
theme_preview: Path = Field(
|
theme_preview: Path = Field(
|
||||||
default=Path(str(ROFI_THEME_PREVIEW)),
|
default_factory=lambda: Path(str(defaults.ROFI_THEME_PREVIEW)),
|
||||||
description=desc.ROFI_THEME_PREVIEW,
|
description=desc.ROFI_THEME_PREVIEW,
|
||||||
)
|
)
|
||||||
theme_confirm: Path = Field(
|
theme_confirm: Path = Field(
|
||||||
default=Path(str(ROFI_THEME_CONFIRM)),
|
default_factory=lambda: Path(str(defaults.ROFI_THEME_CONFIRM)),
|
||||||
description=desc.ROFI_THEME_CONFIRM,
|
description=desc.ROFI_THEME_CONFIRM,
|
||||||
)
|
)
|
||||||
theme_input: Path = Field(
|
theme_input: Path = Field(
|
||||||
default=Path(str(ROFI_THEME_INPUT)),
|
default_factory=lambda: Path(str(defaults.ROFI_THEME_INPUT)),
|
||||||
description=desc.ROFI_THEME_INPUT,
|
description=desc.ROFI_THEME_INPUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ from importlib import metadata, resources
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
PLATFORM = sys.platform
|
PLATFORM = sys.platform
|
||||||
APP_NAME = os.environ.get("FASTANIME_APP_NAME", "fastanime")
|
|
||||||
PROJECT_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)
|
__version__ = metadata.version(PROJECT_NAME)
|
||||||
|
|
||||||
@@ -13,7 +16,9 @@ AUTHOR = "Benexl"
|
|||||||
GIT_REPO = "github.com"
|
GIT_REPO = "github.com"
|
||||||
GIT_PROTOCOL = "https://"
|
GIT_PROTOCOL = "https://"
|
||||||
REPO_HOME = f"https://{GIT_REPO}/{AUTHOR}/FastAnime"
|
REPO_HOME = f"https://{GIT_REPO}/{AUTHOR}/FastAnime"
|
||||||
|
|
||||||
DISCORD_INVITE = "https://discord.gg/C4rhMA4mmK"
|
DISCORD_INVITE = "https://discord.gg/C4rhMA4mmK"
|
||||||
|
|
||||||
ANILIST_AUTH = (
|
ANILIST_AUTH = (
|
||||||
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
|
"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
|
APP_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
ASSETS_DIR = APP_DIR / "assets"
|
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"
|
ICONS_DIR = ASSETS_DIR / "icons"
|
||||||
|
|
||||||
# rofi files
|
ICON_PATH = ICONS_DIR / ("logo.ico" if PLATFORM == "Win32" else "logo.png")
|
||||||
_ROFI_THEMES_DIR = DEFAULTS / "rofi-themes"
|
APP_ASCII_ART = DEFAULTS_DIR / "ascii-art"
|
||||||
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")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import click
|
import click
|
||||||
@@ -83,15 +81,3 @@ USER_VIDEOS_DIR.mkdir(parents=True, exist_ok=True)
|
|||||||
|
|
||||||
USER_CONFIG_PATH = APP_DATA_DIR / "config.ini"
|
USER_CONFIG_PATH = APP_DATA_DIR / "config.ini"
|
||||||
LOG_FILE_PATH = APP_CACHE_DIR / "fastanime.log"
|
LOG_FILE_PATH = APP_CACHE_DIR / "fastanime.log"
|
||||||
|
|
||||||
ICON_PATH = ICONS_DIR / ("logo.ico" if PLATFORM == "Win32" else "logo.png")
|
|
||||||
|
|
||||||
|
|
||||||
APP_ASCII_ART = """\
|
|
||||||
███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗
|
|
||||||
██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝
|
|
||||||
█████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░
|
|
||||||
██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░
|
|
||||||
██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗
|
|
||||||
╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝
|
|
||||||
"""
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class AniListApi(BaseApiClient):
|
|||||||
"type": params.type.value if params.type else "ANIME",
|
"type": params.type.value if params.type else "ANIME",
|
||||||
}
|
}
|
||||||
response = execute_graphql(
|
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
|
return mapper.to_generic_user_list_result(response.json()) if response else None
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
_QUERIES_PATH = _ANILIST_PATH / "queries"
|
||||||
_MUTATIONS_PATH = _ANILIST_PATH / "mutations"
|
_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_AIRING_SCHEDULE = _QUERIES_PATH / "airing.gql"
|
||||||
GET_ANIME_DETAILS = _QUERIES_PATH / "anime.gql"
|
|
||||||
GET_CHARACTERS = _QUERIES_PATH / "character.gql"
|
GET_CHARACTERS = _QUERIES_PATH / "character.gql"
|
||||||
GET_FAVOURITES = _QUERIES_PATH / "favourite.gql"
|
|
||||||
GET_MEDIA_LIST_ITEM = _QUERIES_PATH / "get-medialist-item.gql"
|
GET_MEDIA_LIST_ITEM = _QUERIES_PATH / "get-medialist-item.gql"
|
||||||
GET_LOGGED_IN_USER = _QUERIES_PATH / "logged-in-user.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_MEDIA_RELATIONS = _QUERIES_PATH / "media-relations.gql"
|
||||||
GET_NOTIFICATIONS = _QUERIES_PATH / "notifications.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_RECOMMENDATIONS = _QUERIES_PATH / "recommended.gql"
|
||||||
GET_REVIEWS = _QUERIES_PATH / "reviews.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"
|
GET_USER_INFO = _QUERIES_PATH / "user-info.gql"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .....core.constants import APP_DIR
|
from .....core.constants import GRAPHQL_DIR
|
||||||
|
|
||||||
SERVERS_AVAILABLE = [
|
SERVERS_AVAILABLE = [
|
||||||
"sharepoint",
|
"sharepoint",
|
||||||
@@ -28,7 +28,7 @@ MP4_SERVER_JUICY_STREAM_REGEX = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# graphql files
|
# graphql files
|
||||||
GQLS = APP_DIR / "libs" / "providers" / "anime" / "allanime" / "queries"
|
_GQL_QUERIES = GRAPHQL_DIR / "allanime" / "queries"
|
||||||
SEARCH_GQL = GQLS / "search.gql"
|
SEARCH_GQL = _GQL_QUERIES / "search.gql"
|
||||||
ANIME_GQL = GQLS / "anime.gql"
|
ANIME_GQL = _GQL_QUERIES / "anime.gql"
|
||||||
EPISODE_GQL = GQLS / "episodes.gql"
|
EPISODE_GQL = _GQL_QUERIES / "episodes.gql"
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
Reference in New Issue
Block a user