mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-28 05:33:12 -08:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06506fb47f | ||
|
|
29480c64cd | ||
|
|
474da2f1fd | ||
|
|
abac604ccd | ||
|
|
27d71cbb23 | ||
|
|
615b420c74 | ||
|
|
f9c2b6e939 | ||
|
|
fd448ad701 | ||
|
|
b223a34879 | ||
|
|
76460b6c54 | ||
|
|
e58fd33fe0 | ||
|
|
931f9f10f8 | ||
|
|
0e9dbd2c6b | ||
|
|
3bdfa27e1c | ||
|
|
f46f09ffdf | ||
|
|
d309c04214 | ||
|
|
b19a323d15 | ||
|
|
ff94edfd05 | ||
|
|
59e1a82646 | ||
|
|
2e902fa4e7 | ||
|
|
d2e17af7a9 | ||
|
|
f1fa40c419 | ||
|
|
8bbde97403 | ||
|
|
a2b7d71eb2 | ||
|
|
67b4f0ea38 | ||
|
|
a6d5d5f37c | ||
|
|
67a066f16e | ||
|
|
e6297619d4 | ||
|
|
8f514858f2 | ||
|
|
eb9cffbd7a | ||
|
|
c5f9c37d3a | ||
|
|
44fd65ebab | ||
|
|
e919980ff7 | ||
|
|
6887c6ff10 | ||
|
|
b394de0b23 | ||
|
|
71003049d6 | ||
|
|
6f69b785d8 | ||
|
|
6756540fd1 | ||
|
|
e6b9df25dd | ||
|
|
f40dd2363a | ||
|
|
61a525ff94 | ||
|
|
27d671f89d | ||
|
|
58edb0427f | ||
|
|
7abcdc8f7c | ||
|
|
ca4ca0d476 | ||
|
|
5a337b1c97 | ||
|
|
6c94dd22fc | ||
|
|
8c7e1e201f | ||
|
|
98e41e1eb5 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -3,7 +3,7 @@
|
||||
github: benexl # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: benexl # Replace with a single Ko-fi username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -177,3 +177,4 @@ app/View/SearchScreen/search_screen.py~
|
||||
app/user_data.json
|
||||
.buildozer
|
||||
result
|
||||
repomix-output.xml
|
||||
|
||||
@@ -831,12 +831,10 @@ More pr's less issues 🙃
|
||||
|
||||
Show your support by starring the GitHub repository.
|
||||
|
||||
[](https://ko-fi.com/Y8Y8ZAA7N)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> This project currently scrapes allanime, hianime, nyaa, yugen and animepahe.
|
||||
> This project currently scrapes allanime, hianime, nyaa and animepahe.
|
||||
> The developer(s) of this application does not have any affiliation with the content providers available, and this application hosts zero content.
|
||||
> [DISCLAIMER](https://github.com/Benexl/FastAnime/blob/master/DISCLAIMER.md)
|
||||
|
||||
@@ -46,6 +46,7 @@ class YtDLPDownloader:
|
||||
force_ffmpeg=False,
|
||||
hls_use_mpegts=False,
|
||||
hls_use_h264=False,
|
||||
nocheckcertificate=False,
|
||||
):
|
||||
"""Helper function that downloads anime given url and path details
|
||||
|
||||
@@ -87,6 +88,7 @@ class YtDLPDownloader:
|
||||
"format": vid_format,
|
||||
"compat_opts": ("allow-unsafe-ext",) if force_unknown_ext else tuple(),
|
||||
"progress_hooks": progress_hooks,
|
||||
"nocheckcertificate": nocheckcertificate,
|
||||
}
|
||||
urls = [url]
|
||||
if sub:
|
||||
|
||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v2.8.8"
|
||||
__version__ = "v2.9.9"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benexl"
|
||||
|
||||
@@ -133,10 +133,14 @@ def update_app(force=False):
|
||||
if sys.prefix == sys.base_prefix:
|
||||
# ensure NOT in a venv, where --user flag can cause an error.
|
||||
# TODO: Get value of 'include-system-site-packages' in pyenv.cfg.
|
||||
args.append('--user')
|
||||
args.append("--user")
|
||||
|
||||
process = subprocess.run(args)
|
||||
if process.returncode == 0:
|
||||
print(
|
||||
"[green]Its recommended to run the following after updating:\n\tfastanime config --update (to get the latest config docs)\n\tfastanime cache --clean (to get rid of any potential issues)[/]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return True, release_json
|
||||
else:
|
||||
return False, release_json
|
||||
|
||||
@@ -34,7 +34,7 @@ class LazyGroup(click.Group):
|
||||
# get the Command object from that module
|
||||
cmd_object = getattr(mod, cmd_object_name)
|
||||
# check the result to make debugging easier
|
||||
if not isinstance(cmd_object, click.BaseCommand):
|
||||
if not isinstance(cmd_object, click.Command):
|
||||
raise ValueError(
|
||||
f"Lazy loading of {import_path} failed by returning "
|
||||
"a non-command object"
|
||||
|
||||
@@ -123,6 +123,11 @@ from .data import (
|
||||
is_flag=True,
|
||||
help="Use H.264 (MP4) for hls streams, resulted in .mp4 file (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.option(
|
||||
"--no-check-certificates",
|
||||
is_flag=True,
|
||||
help="Suppress HTTPS certificate validation",
|
||||
)
|
||||
@click.option(
|
||||
"--max-results", "-M", type=int, help="The maximum number of results to show"
|
||||
)
|
||||
@@ -149,6 +154,7 @@ def download(
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
hls_use_h264,
|
||||
no_check_certificates,
|
||||
max_results,
|
||||
):
|
||||
from rich import print
|
||||
@@ -386,6 +392,7 @@ def download(
|
||||
force_ffmpeg=force_ffmpeg,
|
||||
hls_use_mpegts=hls_use_mpegts,
|
||||
hls_use_h264=hls_use_h264,
|
||||
nocheckcertificate=no_check_certificates,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
@@ -129,6 +129,11 @@ if TYPE_CHECKING:
|
||||
is_flag=True,
|
||||
help="Use H.264 (MP4) for hls streams, resulted in .mp4 file (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.option(
|
||||
"--no-check-certificates",
|
||||
is_flag=True,
|
||||
help="Suppress HTTPS certificate validation",
|
||||
)
|
||||
@click.pass_obj
|
||||
def download(
|
||||
config: "Config",
|
||||
@@ -145,6 +150,7 @@ def download(
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
hls_use_h264,
|
||||
no_check_certificates,
|
||||
):
|
||||
import time
|
||||
|
||||
@@ -164,7 +170,7 @@ def download(
|
||||
move_preferred_subtitle_lang_to_top,
|
||||
)
|
||||
|
||||
force_ffmpeg |= (hls_use_mpegts or hls_use_h264)
|
||||
force_ffmpeg |= hls_use_mpegts or hls_use_h264
|
||||
|
||||
anime_provider = AnimeProvider(config.provider)
|
||||
anilist_anime_info = None
|
||||
@@ -208,6 +214,7 @@ def download(
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
hls_use_h264,
|
||||
no_check_certificates,
|
||||
)
|
||||
return
|
||||
search_results = search_results["results"]
|
||||
@@ -262,6 +269,7 @@ def download(
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
hls_use_h264,
|
||||
no_check_certificates,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -294,6 +302,7 @@ def download(
|
||||
|
||||
else:
|
||||
episodes_range = sorted(episodes, key=float)
|
||||
print(f"[green bold]Downloading: [/] {episodes_range}")
|
||||
|
||||
if config.normalize_titles:
|
||||
from ...libs.common.mini_anilist import get_basic_anime_info_by_title
|
||||
@@ -398,6 +407,7 @@ def download(
|
||||
force_ffmpeg=force_ffmpeg,
|
||||
hls_use_mpegts=hls_use_mpegts,
|
||||
hls_use_h264=hls_use_h264,
|
||||
nocheckcertificate=no_check_certificates,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
@@ -66,7 +66,9 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ...MangaProvider import MangaProvider
|
||||
|
||||
from ..utils.feh import feh_manga_viewer
|
||||
from ..utils.icat import icat_manga_viewer
|
||||
|
||||
manga_title = anime_titles[0]
|
||||
|
||||
@@ -136,7 +138,12 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
||||
print(
|
||||
f"[purple bold]Now Reading: [/] {search_result_manga_title} [cyan bold]Chapter:[/] {chapter_info['title']}"
|
||||
)
|
||||
feh_manga_viewer(chapter_info["thumbnails"], str(chapter_info["title"]))
|
||||
if config.manga_viewer == "feh":
|
||||
feh_manga_viewer(chapter_info["thumbnails"], str(chapter_info["title"]))
|
||||
elif config.manga_viewer == "icat":
|
||||
icat_manga_viewer(
|
||||
chapter_info["thumbnails"], str(chapter_info["title"])
|
||||
)
|
||||
if anilist_helper:
|
||||
anilist_helper.update_anime_list(
|
||||
{"mediaId": anilist_id, "progress": chapter_number}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from configparser import ConfigParser
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from rich import print
|
||||
|
||||
from ..constants import (
|
||||
ASSETS_DIR,
|
||||
S_PLATFORM,
|
||||
@@ -38,6 +42,7 @@ class Config(object):
|
||||
default_config = {
|
||||
"auto_next": "False",
|
||||
"menu_order": "",
|
||||
"manga_viewer": "feh",
|
||||
"auto_select": "True",
|
||||
"cache_requests": "true",
|
||||
"check_for_updates": "True",
|
||||
@@ -47,6 +52,7 @@ class Config(object):
|
||||
"disable_mpv_popen": "True",
|
||||
"discord": "False",
|
||||
"episode_complete_at": "80",
|
||||
"use_experimental_fzf_anilist_search": "True",
|
||||
"ffmpegthumbnailer_seek_time": "-1",
|
||||
"force_forward_tracking": "true",
|
||||
"force_window": "immediate",
|
||||
@@ -60,6 +66,8 @@ class Config(object):
|
||||
"normalize_titles": "True",
|
||||
"notification_duration": "120",
|
||||
"max_cache_lifetime": "03:00:00",
|
||||
"mpv_args": "",
|
||||
"mpv_pre_args": "",
|
||||
"per_page": "15",
|
||||
"player": "mpv",
|
||||
"preferred_history": "local",
|
||||
@@ -96,8 +104,16 @@ class Config(object):
|
||||
self.configparser.add_section("anilist")
|
||||
|
||||
# --- set config values from file or using defaults ---
|
||||
if os.path.exists(USER_CONFIG_PATH) and not no_config:
|
||||
self.configparser.read(USER_CONFIG_PATH, encoding="utf-8")
|
||||
try:
|
||||
if os.path.exists(USER_CONFIG_PATH) and not no_config:
|
||||
self.configparser.read(USER_CONFIG_PATH, encoding="utf-8")
|
||||
except Exception as e:
|
||||
print(
|
||||
"[yellow]Warning[/]: Failed to read config file using default configuration",
|
||||
file=sys.stderr,
|
||||
)
|
||||
logger.error(f"Failed to read config file: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
# get the configuration
|
||||
self.auto_next = self.configparser.getboolean("stream", "auto_next")
|
||||
@@ -120,6 +136,9 @@ class Config(object):
|
||||
self.episode_complete_at = self.configparser.getint(
|
||||
"stream", "episode_complete_at"
|
||||
)
|
||||
self.use_experimental_fzf_anilist_search = self.configparser.getboolean(
|
||||
"general", "use_experimental_fzf_anilist_search"
|
||||
)
|
||||
self.ffmpegthumbnailer_seek_time = self.configparser.getint(
|
||||
"general", "ffmpegthumbnailer_seek_time"
|
||||
)
|
||||
@@ -149,6 +168,8 @@ class Config(object):
|
||||
+ max_cache_lifetime[1] * 3600
|
||||
+ max_cache_lifetime[2] * 60
|
||||
)
|
||||
self.mpv_args = self.configparser.get("general", "mpv_args")
|
||||
self.mpv_pre_args = self.configparser.get("general", "mpv_pre_args")
|
||||
self.per_page = self.configparser.get("anilist", "per_page")
|
||||
self.player = self.configparser.get("stream", "player")
|
||||
self.preferred_history = self.configparser.get("stream", "preferred_history")
|
||||
@@ -171,6 +192,7 @@ class Config(object):
|
||||
self.skip = self.configparser.getboolean("stream", "skip")
|
||||
self.sort_by = self.configparser.get("anilist", "sort_by")
|
||||
self.menu_order = self.configparser.get("general", "menu_order")
|
||||
self.manga_viewer = self.configparser.get("general", "manga_viewer")
|
||||
self.sub_lang = self.configparser.get("general", "sub_lang")
|
||||
self.translation_type = self.configparser.get("stream", "translation_type")
|
||||
self.use_fzf = self.configparser.getboolean("general", "use_fzf")
|
||||
@@ -198,7 +220,10 @@ class Config(object):
|
||||
def set_fastanime_config_environs(self):
|
||||
current_config = []
|
||||
for key in self.default_config:
|
||||
current_config.append((f"FASTANIME_{key.upper()}", str(getattr(self, key))))
|
||||
if not os.environ.get(f"FASTANIME_{key.upper()}"):
|
||||
current_config.append(
|
||||
(f"FASTANIME_{key.upper()}", str(getattr(self, key)))
|
||||
)
|
||||
os.environ.update(current_config)
|
||||
|
||||
def update_user(self, user):
|
||||
@@ -448,6 +473,27 @@ recent = {self.recent}
|
||||
# https://discord.com/oauth2/authorize?client_id=1292070065583165512
|
||||
discord = {self.discord}
|
||||
|
||||
# comma separated list of args that will be passed to mpv
|
||||
# example: --vo=kitty,--fullscreen,--volume=50
|
||||
mpv_args = {self.mpv_args}
|
||||
|
||||
# command line options passed before the mpv command
|
||||
# example: kitty
|
||||
# useful incase of wanting to run sth like: kitty mpv --vo=kitty <url>
|
||||
mpv_pre_args = {self.mpv_pre_args}
|
||||
|
||||
# choose manga viewer [feh/icat]
|
||||
# feh is the default and requires feh to be installed
|
||||
# icat is for kitty terminal users only
|
||||
manga_viewer = {self.manga_viewer}
|
||||
|
||||
# a little little something i introduced
|
||||
# remember how in a browser site when you search for an anime it dynamically reloads
|
||||
# after every type
|
||||
# well who says it cant be done in the terminal lol
|
||||
# though its still experimental lol
|
||||
# use this to disable it
|
||||
use_experimental_fzf_anilist_search = {self.use_experimental_fzf_anilist_search}
|
||||
|
||||
[stream]
|
||||
# the quality of the stream [1080,720,480,360]
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
from hashlib import sha256
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from click import clear
|
||||
@@ -56,8 +57,10 @@ def calculate_percentage_completion(start_time, end_time):
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def discord_updater(show,episode,switch):
|
||||
discord.discord_connect(show,episode,switch)
|
||||
|
||||
def discord_updater(show, episode, switch):
|
||||
discord.discord_connect(show, episode, switch)
|
||||
|
||||
|
||||
def media_player_controls(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
@@ -326,30 +329,56 @@ def media_player_controls(
|
||||
media_player_controls(config, fastanime_runtime_state)
|
||||
|
||||
icons = config.icons
|
||||
options = {
|
||||
f"{'🔂 ' if icons else ''}Replay": _replay,
|
||||
f"{'⏭ ' if icons else ''}Next Episode": _next_episode,
|
||||
f"{'⏮ ' if icons else ''}Previous Episode": _previous_episode,
|
||||
f"{'🗃️ ' if icons else ''}Episodes": _episodes,
|
||||
f"{'📀 ' if icons else ''}Change Quality": _change_quality,
|
||||
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
||||
f"{'💽 ' if icons else ''}Servers": _servers,
|
||||
f"{'📱 ' if icons else ''}Main Menu": lambda: fastanime_main_menu(
|
||||
config, fastanime_runtime_state
|
||||
),
|
||||
f"{'📜 ' if icons else ''}Media Actions Menu": lambda: media_actions_menu(
|
||||
config, fastanime_runtime_state
|
||||
),
|
||||
f"{'🔎 ' if icons else ''}Anilist Results Menu": lambda: anilist_results_menu(
|
||||
config, fastanime_runtime_state
|
||||
),
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
options = {}
|
||||
|
||||
# Only show Next Episode option if the current episode is not the last one
|
||||
current_index = available_episodes.index(current_episode_number)
|
||||
if current_index < len(available_episodes) - 1:
|
||||
options[f"{'⏭ ' if icons else ''}Next Episode"] = _next_episode
|
||||
|
||||
def _toggle_auto_next(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
):
|
||||
"""helper function to toggle auto next
|
||||
|
||||
Args:
|
||||
config: [TODO:description]
|
||||
fastanime_runtime_state: [TODO:description]
|
||||
"""
|
||||
config.auto_next = not config.auto_next
|
||||
media_player_controls(config, fastanime_runtime_state)
|
||||
|
||||
options.update(
|
||||
{
|
||||
f"{'🔂 ' if icons else ''}Replay": _replay,
|
||||
f"{'⏮ ' if icons else ''}Previous Episode": _previous_episode,
|
||||
f"{'🗃️ ' if icons else ''}Episodes": _episodes,
|
||||
f"{'📀 ' if icons else ''}Change Quality": _change_quality,
|
||||
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
||||
f"{'💠 ' if icons else ''}Toggle auto next episode": lambda: _toggle_auto_next(
|
||||
config, fastanime_runtime_state
|
||||
),
|
||||
f"{'💽 ' if icons else ''}Servers": _servers,
|
||||
f"{'📱 ' if icons else ''}Main Menu": lambda: fastanime_main_menu(
|
||||
config, fastanime_runtime_state
|
||||
),
|
||||
f"{'📜 ' if icons else ''}Media Actions Menu": lambda: media_actions_menu(
|
||||
config, fastanime_runtime_state
|
||||
),
|
||||
f"{'🔎 ' if icons else ''}Anilist Results Menu": lambda: anilist_results_menu(
|
||||
config, fastanime_runtime_state
|
||||
),
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
)
|
||||
|
||||
if config.auto_next:
|
||||
print("Auto selecting next episode")
|
||||
_next_episode()
|
||||
return
|
||||
if current_index < len(available_episodes) - 1:
|
||||
print("Auto selecting next episode")
|
||||
_next_episode()
|
||||
return
|
||||
else:
|
||||
print("Last episode reached")
|
||||
|
||||
choices = list(options.keys())
|
||||
if config.use_fzf:
|
||||
@@ -517,7 +546,10 @@ def provider_anime_episode_servers_menu(
|
||||
# update discord activity for user
|
||||
switch = threading.Event()
|
||||
if config.discord:
|
||||
discord_proc = threading.Thread(target=discord_updater, args=(provider_anime_title,current_episode_number,switch))
|
||||
discord_proc = threading.Thread(
|
||||
target=discord_updater,
|
||||
args=(provider_anime_title, current_episode_number, switch),
|
||||
)
|
||||
discord_proc.start()
|
||||
|
||||
# try to get the timestamp you left off from if available
|
||||
@@ -701,12 +733,12 @@ def provider_anime_episodes_menu(
|
||||
# the user watch history thats locally available
|
||||
# will be preferred over remote
|
||||
if (
|
||||
user_watch_history.get(str(anime_id_anilist), {}).get("episode_no")
|
||||
in available_episodes
|
||||
config.preferred_history == "local"
|
||||
or not selected_anime_anilist["mediaListEntry"]
|
||||
):
|
||||
if (
|
||||
config.preferred_history == "local"
|
||||
or not selected_anime_anilist["mediaListEntry"]
|
||||
user_watch_history.get(str(anime_id_anilist), {}).get("episode_no")
|
||||
in available_episodes
|
||||
):
|
||||
current_episode_number = user_watch_history[str(anime_id_anilist)][
|
||||
"episode_no"
|
||||
@@ -751,6 +783,7 @@ def provider_anime_episodes_menu(
|
||||
"progress"
|
||||
)
|
||||
)
|
||||
current_episode_number = str(int(current_episode_number) + 1)
|
||||
if current_episode_number not in available_episodes:
|
||||
current_episode_number = ""
|
||||
print(
|
||||
@@ -778,7 +811,9 @@ def provider_anime_episodes_menu(
|
||||
)
|
||||
|
||||
if not preview:
|
||||
print("Failed to find bash executable which is necessary for preview with fzf.\nIf you are on Windows, please make sure Git is installed and available in PATH.")
|
||||
print(
|
||||
"Failed to find bash executable which is necessary for preview with fzf.\nIf you are on Windows, please make sure Git is installed and available in PATH."
|
||||
)
|
||||
|
||||
current_episode_number = fzf.run(
|
||||
choices, prompt="Select Episode", header=anime_title, preview=preview
|
||||
@@ -969,6 +1004,229 @@ def anime_provider_search_results_menu(
|
||||
fetch_anime_episode(config, fastanime_runtime_state)
|
||||
|
||||
|
||||
def download_anime(config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"):
|
||||
import time
|
||||
|
||||
from rich.prompt import Confirm, Prompt
|
||||
from thefuzz import fuzz
|
||||
|
||||
from ...Utility.downloader.downloader import downloader
|
||||
|
||||
download_dir = config.downloads_dir
|
||||
force_unknown_ext = True
|
||||
verbose = False
|
||||
silent = True
|
||||
merge = Confirm.ask("Merge audio and video", default=True)
|
||||
clean = Confirm.ask("Clean up files", default=True)
|
||||
prompt = Confirm.ask("Prompt incase for actions while downloading", default=True)
|
||||
force_ffmpeg = Confirm.ask("Force ffmpeg", default=False)
|
||||
hls_use_mpegts = Confirm.ask("Use mpegts", default=False)
|
||||
hls_use_h264 = Confirm.ask("Use h264", default=False)
|
||||
nocheckcertificate = True
|
||||
|
||||
force_ffmpeg |= hls_use_mpegts or hls_use_h264
|
||||
anime_title = Prompt.ask(
|
||||
"Anime title", default=fastanime_runtime_state.selected_anime_title_anilist
|
||||
)
|
||||
translation_type = Prompt.ask("Translation type", default=config.translation_type)
|
||||
anime_provider = config.anime_provider
|
||||
anilist_anime_info = fastanime_runtime_state.selected_anime_anilist
|
||||
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
||||
# ---- search for anime ----
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Search Results...", total=None)
|
||||
search_results = anime_provider.search_for_anime(
|
||||
anime_title, translation_type=translation_type
|
||||
)
|
||||
if not search_results:
|
||||
print("Search results failed")
|
||||
input("Enter to retry")
|
||||
return
|
||||
search_results = search_results["results"]
|
||||
if not search_results:
|
||||
print("Nothing muches your search term")
|
||||
return
|
||||
search_results_ = {
|
||||
search_result["title"]: search_result for search_result in search_results
|
||||
}
|
||||
|
||||
if config.auto_select:
|
||||
selected_anime_title = max(
|
||||
search_results_.keys(),
|
||||
key=lambda title: fuzz.ratio(
|
||||
anime_normalizer.get(title, title), anime_title
|
||||
),
|
||||
)
|
||||
print("[cyan]Auto selecting:[/] ", selected_anime_title)
|
||||
else:
|
||||
choices = list(search_results_.keys())
|
||||
if config.use_fzf:
|
||||
selected_anime_title = fzf.run(choices, "Please Select title", "FastAnime")
|
||||
else:
|
||||
selected_anime_title = fuzzy_inquirer(
|
||||
choices,
|
||||
"Please Select title",
|
||||
)
|
||||
|
||||
# ---- fetch anime ----
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Anime...", total=None)
|
||||
anime: Anime | None = anime_provider.get_anime(
|
||||
search_results_[selected_anime_title]["id"]
|
||||
)
|
||||
if not anime:
|
||||
print("Sth went wring anime no found")
|
||||
input("Enter to continue...")
|
||||
return
|
||||
|
||||
episodes = sorted(
|
||||
anime["availableEpisodesDetail"][config.translation_type], key=float
|
||||
)
|
||||
episode_ranges = Prompt.ask(
|
||||
"Enter episode ranges (e.g 1:12 or 1:12:2 or 1: or :12 or 5)"
|
||||
).split(" ")
|
||||
episodes_range = []
|
||||
if episode_ranges != [""]:
|
||||
for episode_range in episode_ranges:
|
||||
if ":" in episode_range:
|
||||
ep_range_tuple = episode_range.split(":")
|
||||
if len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||
episodes_start, episodes_end = ep_range_tuple
|
||||
episodes_range += episodes[
|
||||
int(episodes_start) - 1 : int(episodes_end)
|
||||
]
|
||||
elif len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||
episodes_start, episodes_end, step = ep_range_tuple
|
||||
episodes_range += episodes[
|
||||
int(episodes_start) - 1 : int(episodes_end) : int(step)
|
||||
]
|
||||
else:
|
||||
episodes_start, episodes_end = ep_range_tuple
|
||||
if episodes_start.strip():
|
||||
episodes_range += episodes[int(episodes_start) - 1 :]
|
||||
elif episodes_end.strip():
|
||||
episodes_range += episodes[: int(episodes_end)]
|
||||
else:
|
||||
episodes_range += episodes
|
||||
else:
|
||||
episodes_range += [episode_range] if episode_range in episodes else []
|
||||
|
||||
else:
|
||||
episodes_range = sorted(episodes, key=float)
|
||||
|
||||
episodes_range = list(
|
||||
dict.fromkeys(episodes_range)
|
||||
) # To preserve order while removing duplicates
|
||||
print(f"[green bold]Downloading: [/] {episodes_range}")
|
||||
|
||||
if config.normalize_titles:
|
||||
from ...libs.common.mini_anilist import get_basic_anime_info_by_title
|
||||
|
||||
anilist_anime_info = get_basic_anime_info_by_title(anime["title"])
|
||||
|
||||
# lets download em
|
||||
for episode in episodes_range:
|
||||
try:
|
||||
episode = str(episode)
|
||||
if episode not in episodes:
|
||||
print(f"[cyan]Warning[/]: Episode {episode} not found, skipping")
|
||||
continue
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Episode Streams...", total=None)
|
||||
streams = anime_provider.get_episode_streams(
|
||||
anime["id"], episode, config.translation_type
|
||||
)
|
||||
if not streams:
|
||||
print("No streams skipping")
|
||||
continue
|
||||
# ---- fetch servers ----
|
||||
if config.server == "top":
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
server_name = next(streams, None)
|
||||
if not server_name:
|
||||
print("Sth went wrong when fetching the server")
|
||||
continue
|
||||
stream_link = filter_by_quality(config.quality, server_name["links"])
|
||||
if not stream_link:
|
||||
print("[yellow bold]WARNING:[/] No streams found")
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
continue
|
||||
link = stream_link["link"]
|
||||
provider_headers = server_name["headers"]
|
||||
episode_title = server_name["episode_title"]
|
||||
subtitles = server_name["subtitles"]
|
||||
else:
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching servers", total=None)
|
||||
# prompt for server selection
|
||||
servers = {server["server"]: server for server in streams}
|
||||
servers_names = list(servers.keys())
|
||||
if config.server in servers_names:
|
||||
server_name = config.server
|
||||
else:
|
||||
if config.use_fzf:
|
||||
server_name = fzf.run(servers_names, "Select an link")
|
||||
else:
|
||||
server_name = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, servers[server_name]["links"]
|
||||
)
|
||||
if not stream_link:
|
||||
print("[yellow bold]WARNING:[/] No streams found")
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
continue
|
||||
link = stream_link["link"]
|
||||
provider_headers = servers[server_name]["headers"]
|
||||
|
||||
subtitles = servers[server_name]["subtitles"]
|
||||
episode_title = servers[server_name]["episode_title"]
|
||||
|
||||
if anilist_anime_info:
|
||||
selected_anime_title = (
|
||||
anilist_anime_info["title"][config.preferred_language]
|
||||
or anilist_anime_info["title"]["romaji"]
|
||||
or anilist_anime_info["title"]["english"]
|
||||
)
|
||||
import re
|
||||
|
||||
if anilist_anime_info.get("streamingEpisodes"):
|
||||
for episode_detail in anilist_anime_info["streamingEpisodes"]:
|
||||
if re.match(f"Episode {episode} ", episode_detail["title"]):
|
||||
episode_title = episode_detail["title"]
|
||||
break
|
||||
print(f"[purple]Now Downloading:[/] {episode_title}")
|
||||
subtitles = move_preferred_subtitle_lang_to_top(subtitles, config.sub_lang)
|
||||
downloader._download_file(
|
||||
link,
|
||||
selected_anime_title,
|
||||
episode_title,
|
||||
download_dir,
|
||||
silent,
|
||||
vid_format=config.format,
|
||||
force_unknown_ext=force_unknown_ext,
|
||||
verbose=verbose,
|
||||
headers=provider_headers,
|
||||
sub=subtitles[0]["url"] if subtitles else "",
|
||||
merge=merge,
|
||||
clean=clean,
|
||||
prompt=prompt,
|
||||
force_ffmpeg=force_ffmpeg,
|
||||
hls_use_mpegts=hls_use_mpegts,
|
||||
hls_use_h264=hls_use_h264,
|
||||
nocheckcertificate=nocheckcertificate,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
|
||||
|
||||
#
|
||||
# ---- ANILIST MEDIA ACTIONS MENU ----
|
||||
#
|
||||
@@ -1376,7 +1634,10 @@ def media_actions_menu(
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
|
||||
relations = relations[1]["data"]["Page"]["relations"] # pyright:ignore
|
||||
relations = relations[1]["data"]["Media"]["relations"] # pyright:ignore
|
||||
relations["nodes"] = [
|
||||
node for node in relations["nodes"] if node.get("type") == "ANIME"
|
||||
]
|
||||
fastanime_runtime_state.anilist_results_data = {
|
||||
"data": {"Page": {"media": relations["nodes"]}} # pyright:ignore
|
||||
}
|
||||
@@ -1413,6 +1674,12 @@ def media_actions_menu(
|
||||
}
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
def _download_anime(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
):
|
||||
download_anime(config, fastanime_runtime_state)
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
|
||||
icons = config.icons
|
||||
options = {
|
||||
f"{'📽️ ' if icons else ''}Stream ({progress}/{episodes_total})": _stream_anime,
|
||||
@@ -1428,6 +1695,7 @@ def media_actions_menu(
|
||||
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
||||
f"{'💽 ' if icons else ''}Change Provider": _change_provider,
|
||||
f"{'💽 ' if icons else ''}Change Player": _change_player,
|
||||
f"{'📥 ' if icons else ''}Download Anime": _download_anime,
|
||||
f"{'🔘 ' if icons else ''}Toggle auto select anime": _toggle_auto_select, # WARN: problematic if you choose an anime that doesnt match id
|
||||
f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next,
|
||||
f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history,
|
||||
@@ -1503,7 +1771,9 @@ def anilist_results_menu(
|
||||
|
||||
preview = get_fzf_anime_preview(search_results, anime_data.keys())
|
||||
if not preview:
|
||||
print("Failed to find bash executable which is necessary for preview with fzf.\nIf you are on Windows, please make sure Git is installed and available in PATH.")
|
||||
print(
|
||||
"Failed to find bash executable which is necessary for preview with fzf.\nIf you are on Windows, please make sure Git is installed and available in PATH."
|
||||
)
|
||||
|
||||
selected_anime_title = fzf.run(
|
||||
choices,
|
||||
@@ -1524,7 +1794,9 @@ def anilist_results_menu(
|
||||
get_rofi_icons(search_results, anime_data.keys())
|
||||
choices = []
|
||||
for title in anime_data.keys():
|
||||
icon_path = os.path.join(IMAGES_CACHE_DIR, title)
|
||||
icon_path = os.path.join(
|
||||
IMAGES_CACHE_DIR, sha256(title.encode("utf-8")).hexdigest()
|
||||
)
|
||||
choices.append(f"{title}\0icon\x1f{icon_path}.png")
|
||||
choices.append("Back")
|
||||
selected_anime_title = Rofi.run_with_icons(choices, "Select Anime")
|
||||
@@ -1672,9 +1944,15 @@ def _anilist_search(config: "Config", page=1):
|
||||
# TODO: Add filters and other search features
|
||||
if config.use_rofi:
|
||||
search_term = str(Rofi.ask("Search for"))
|
||||
elif config.use_fzf and config.use_experimental_fzf_anilist_search:
|
||||
search_term = fzf.search_for_anime()
|
||||
else:
|
||||
search_term = Prompt.ask("[cyan]Search for[/]")
|
||||
|
||||
# Return to main menu if search term is empty
|
||||
if not search_term.strip():
|
||||
return False, "Search canceled - return to main menu"
|
||||
|
||||
return AniList.search(query=search_term, page=page)
|
||||
|
||||
|
||||
|
||||
102
fastanime/cli/utils/icat.py
Normal file
102
fastanime/cli/utils/icat.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
from sys import exit
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.align import Align
|
||||
from rich.text import Text
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def get_key():
|
||||
"""Read a single keypress (including arrows)."""
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
ch1 = sys.stdin.read(1)
|
||||
if ch1 == "\x1b":
|
||||
ch2 = sys.stdin.read(2)
|
||||
return ch1 + ch2
|
||||
return ch1
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
|
||||
|
||||
def draw_banner_at(msg: str, row: int):
|
||||
"""Move cursor to `row`, then render a centered, cyan-bordered panel."""
|
||||
sys.stdout.write(f"\x1b[{row};1H")
|
||||
text = Text(msg, justify="center")
|
||||
panel = Panel(Align(text, align="center"), border_style="cyan", padding=(1, 2))
|
||||
console.print(panel)
|
||||
|
||||
|
||||
def icat_manga_viewer(image_links: list[str], window_title: str):
|
||||
ICAT = shutil.which("kitty")
|
||||
if not ICAT:
|
||||
console.print("[bold red]kitty (for icat) not found[/]")
|
||||
exit(1)
|
||||
|
||||
idx, total = 0, len(image_links)
|
||||
title = f"{window_title} ({total} images)"
|
||||
show_banner = True
|
||||
|
||||
try:
|
||||
while True:
|
||||
console.clear()
|
||||
term_width, term_height = shutil.get_terminal_size((80, 24))
|
||||
panel_height = 0
|
||||
|
||||
# Calculate space for image based on banner visibility
|
||||
if show_banner:
|
||||
msg_lines = 3 # Title + blank + controls
|
||||
panel_height = msg_lines + 4 # Padding and borders
|
||||
image_height = term_height - panel_height - 1
|
||||
else:
|
||||
image_height = term_height
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
ICAT,
|
||||
"+kitten",
|
||||
"icat",
|
||||
"--clear",
|
||||
"--scale-up",
|
||||
"--place",
|
||||
f"{term_width}x{image_height}@0x0",
|
||||
"--z-index",
|
||||
"-1",
|
||||
image_links[idx],
|
||||
]
|
||||
)
|
||||
|
||||
if show_banner:
|
||||
controls = (
|
||||
f"[{idx + 1}/{total}] Prev: [h/←] Next: [l/→] "
|
||||
f"Toggle Banner: [b] Quit: [q/Ctrl-C]"
|
||||
)
|
||||
msg = f"{title}\n\n{controls}"
|
||||
start_row = term_height - panel_height
|
||||
draw_banner_at(msg, start_row)
|
||||
|
||||
# key handling
|
||||
key = get_key()
|
||||
if key in ("l", "\x1b[C"):
|
||||
idx = (idx + 1) % total
|
||||
elif key in ("h", "\x1b[D"):
|
||||
idx = (idx - 1) % total
|
||||
elif key == "b":
|
||||
show_banner = not show_banner
|
||||
elif key in ("q", "\x03"):
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
console.clear()
|
||||
console.print("Exited viewer.", style="bold")
|
||||
@@ -12,12 +12,13 @@ logger = logging.getLogger(__name__)
|
||||
mpv_av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)")
|
||||
|
||||
|
||||
def stream_video(MPV, url, mpv_args, custom_args):
|
||||
def stream_video(MPV, url, mpv_args, custom_args, pre_args=[]):
|
||||
last_time = "0"
|
||||
total_time = "0"
|
||||
if os.environ.get("FASTANIME_DISABLE_MPV_POPEN", "False") == "False":
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
pre_args
|
||||
+ [
|
||||
MPV,
|
||||
url,
|
||||
*mpv_args,
|
||||
@@ -59,7 +60,7 @@ def stream_video(MPV, url, mpv_args, custom_args):
|
||||
process.wait()
|
||||
else:
|
||||
proc = subprocess.run(
|
||||
[MPV, url, *mpv_args, *custom_args],
|
||||
pre_args + [MPV, url, *mpv_args, *custom_args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
@@ -208,5 +209,14 @@ def run_mpv(
|
||||
mpv_args.append(f"--title={title}")
|
||||
if ytdl_format:
|
||||
mpv_args.append(f"--ytdl-format={ytdl_format}")
|
||||
stop_time, total_time = stream_video(MPV, link, mpv_args, custom_args)
|
||||
|
||||
if user_args := os.environ.get("FASTANIME_MPV_ARGS"):
|
||||
mpv_args.extend(user_args.split(","))
|
||||
|
||||
pre_args = []
|
||||
if user_args := os.environ.get("FASTANIME_MPV_PRE_ARGS"):
|
||||
pre_args = user_args.split(",")
|
||||
stop_time, total_time = stream_video(
|
||||
MPV, link, mpv_args, custom_args, pre_args
|
||||
)
|
||||
return stop_time, total_time
|
||||
|
||||
@@ -846,6 +846,7 @@ query ($id: Int) {
|
||||
nodes {
|
||||
id
|
||||
idMal
|
||||
type
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
|
||||
@@ -75,6 +75,7 @@ class AnimePahe(AnimeProvider):
|
||||
session_id,
|
||||
params,
|
||||
page,
|
||||
standardized_episode_number,
|
||||
):
|
||||
response = self.session.get(ANIMEPAHE_ENDPOINT, params=params)
|
||||
response.raise_for_status()
|
||||
@@ -107,12 +108,23 @@ class AnimePahe(AnimeProvider):
|
||||
"sort": "episode_asc",
|
||||
},
|
||||
page=page,
|
||||
standardized_episode_number=standardized_episode_number,
|
||||
)
|
||||
else:
|
||||
for episode in data.get("data", []):
|
||||
if episode["episode"] % 1 == 0:
|
||||
standardized_episode_number += 1
|
||||
episode.update({"episode": standardized_episode_number})
|
||||
else:
|
||||
standardized_episode_number += episode["episode"] % 1
|
||||
episode.update({"episode": standardized_episode_number})
|
||||
standardized_episode_number = int(standardized_episode_number)
|
||||
return data
|
||||
|
||||
@debug_provider
|
||||
def get_anime(self, session_id: str, **kwargs):
|
||||
page = 1
|
||||
standardized_episode_number = 0
|
||||
if d := self.store.get(str(session_id), "search_result"):
|
||||
anime_result: "AnimePaheSearchResult" = d
|
||||
data: "AnimePaheAnimePage" = {} # pyright:ignore
|
||||
@@ -127,6 +139,7 @@ class AnimePahe(AnimeProvider):
|
||||
"page": page,
|
||||
},
|
||||
page=page,
|
||||
standardized_episode_number=standardized_episode_number,
|
||||
)
|
||||
|
||||
if not data:
|
||||
|
||||
@@ -9,6 +9,7 @@ from click import clear
|
||||
from rich import print
|
||||
|
||||
from ...cli.utils.tools import exit_app
|
||||
from .scripts import FETCH_ANIME_SCRIPT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -129,7 +130,7 @@ class FZF:
|
||||
encoding="utf-8",
|
||||
)
|
||||
if not result or result.returncode != 0 or not result.stdout:
|
||||
if result.returncode == 130: # fzf terminated by ctrl-c
|
||||
if result.returncode == 130: # fzf terminated by ctrl-c
|
||||
exit_app()
|
||||
|
||||
print("sth went wrong :confused:")
|
||||
@@ -198,9 +199,46 @@ class FZF:
|
||||
# os.environ["FZF_DEFAULT_OPTS"] = ""
|
||||
return result
|
||||
|
||||
def search_for_anime(self):
|
||||
|
||||
commands = [
|
||||
"--preview",
|
||||
f"{FETCH_ANIME_SCRIPT}fetch_anime_details {{}}",
|
||||
"--prompt",
|
||||
"Search For Anime: ",
|
||||
"--header",
|
||||
"Type to search, results are dynamically loaded, enter to select",
|
||||
"--bind",
|
||||
f"change:reload({FETCH_ANIME_SCRIPT}fetch_anime_for_fzf {{q}})",
|
||||
"--preview-window",
|
||||
"wrap",
|
||||
# "--bind",
|
||||
# f"enter:become(echo {{}})",
|
||||
"--reverse",
|
||||
]
|
||||
|
||||
if not self.FZF_EXECUTABLE:
|
||||
raise Exception("fzf executable not found")
|
||||
os.environ["SHELL"] = "bash"
|
||||
|
||||
result = subprocess.run(
|
||||
[self.FZF_EXECUTABLE, *commands],
|
||||
input="",
|
||||
stdout=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
if not result or result.returncode != 0 or not result.stdout:
|
||||
if result.returncode == 130: # fzf terminated by ctrl-c
|
||||
exit_app()
|
||||
return ""
|
||||
|
||||
return result.stdout.strip().split("|")[0].strip()
|
||||
|
||||
|
||||
fzf = FZF()
|
||||
|
||||
if __name__ == "__main__":
|
||||
action = fzf.run([*os.listdir(), "exit"], "Prompt: ", "Header", preview="bat {}")
|
||||
print(action)
|
||||
print(fzf.search_for_anime())
|
||||
exit()
|
||||
|
||||
76
fastanime/libs/fzf/scripts.py
Normal file
76
fastanime/libs/fzf/scripts.py
Normal file
@@ -0,0 +1,76 @@
|
||||
FETCH_ANIME_SCRIPT = r"""
|
||||
fetch_anime_for_fzf() {
|
||||
local search_term="$1"
|
||||
if [ -z "$search_term" ]; then exit 0; fi
|
||||
|
||||
local query='
|
||||
query ($search: String) {
|
||||
Page(page: 1, perPage: 25) {
|
||||
media(search: $search, type: ANIME, sort: [SEARCH_MATCH]) {
|
||||
id
|
||||
title { romaji english }
|
||||
meanScore
|
||||
format
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
|
||||
local json_payload
|
||||
json_payload=$(jq -n --arg query "$query" --arg search "$search_term" \
|
||||
'{query: $query, variables: {search: $search}}')
|
||||
|
||||
curl --silent \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "Accept: application/json" \
|
||||
--request POST \
|
||||
--data "$json_payload" \
|
||||
https://graphql.anilist.co | \
|
||||
jq -r '.data.Page.media[]? | select(.title.romaji) |
|
||||
"\(.title.english // .title.romaji) | Score: \(.meanScore // "N/A") | ID: \(.id)"'
|
||||
}
|
||||
fetch_anime_details() {
|
||||
local anime_id
|
||||
anime_id=$(echo "$1" | sed -n 's/.*ID: \([0-9]*\).*/\1/p')
|
||||
if [ -z "$anime_id" ]; then echo "Select an item to see details..."; return; fi
|
||||
|
||||
local query='
|
||||
query ($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
title { romaji english }
|
||||
description(asHtml: false)
|
||||
genres
|
||||
meanScore
|
||||
episodes
|
||||
status
|
||||
format
|
||||
season
|
||||
seasonYear
|
||||
studios(isMain: true) { nodes { name } }
|
||||
}
|
||||
}
|
||||
'
|
||||
local json_payload
|
||||
json_payload=$(jq -n --arg query "$query" --argjson id "$anime_id" \
|
||||
'{query: $query, variables: {id: $id}}')
|
||||
|
||||
# Fetch and format details for the preview window
|
||||
curl --silent \
|
||||
--header "Content-Type: application/json" \
|
||||
--header "Accept: application/json" \
|
||||
--request POST \
|
||||
--data "$json_payload" \
|
||||
https://graphql.anilist.co | \
|
||||
jq -r '
|
||||
.data.Media |
|
||||
"Title: \(.title.english // .title.romaji)\n" +
|
||||
"Score: \(.meanScore // "N/A") | Episodes: \(.episodes // "N/A")\n" +
|
||||
"Status: \(.status // "N/A") | Format: \(.format // "N/A")\n" +
|
||||
"Season: \(.season // "N/A") \(.seasonYear // "")\n" +
|
||||
"Genres: \(.genres | join(", "))\n" +
|
||||
"Studio: \(.studios.nodes[0].name // "N/A")\n\n" +
|
||||
"\(.description | gsub("<br><br>"; "\n\n") | gsub("<[^>]*>"; "") | gsub("""; "\""))"
|
||||
'
|
||||
}
|
||||
"""
|
||||
12
flake.nix
12
flake.nix
@@ -14,9 +14,9 @@
|
||||
pythonPackages = python.pkgs;
|
||||
fastanimeEnv = pythonPackages.buildPythonApplication {
|
||||
pname = "fastanime";
|
||||
version = "2.8.8";
|
||||
version = "2.9.9";
|
||||
|
||||
src = ./.;
|
||||
src = self;
|
||||
|
||||
preBuild = ''
|
||||
sed -i 's/rich>=13.9.2/rich>=13.8.1/' pyproject.toml
|
||||
@@ -50,14 +50,20 @@
|
||||
|
||||
# DevShell for development
|
||||
devShells.default = pkgs.mkShell {
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.libxcrypt-legacy ];
|
||||
buildInputs = [
|
||||
fastanimeEnv
|
||||
pythonPackages.hatchling
|
||||
pkgs.mpv
|
||||
pkgs.libmpv
|
||||
pkgs.fzf
|
||||
pkgs.rofi
|
||||
pkgs.uv
|
||||
pkgs.pyright
|
||||
];
|
||||
shellHook = ''
|
||||
uv venv -q
|
||||
source ./.venv/bin/activate
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "fastanime"
|
||||
version = "2.8.8"
|
||||
version = "2.9.9"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
license = "UNLICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
Reference in New Issue
Block a user