mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-08 05:40:38 -08:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13200e2d1f | ||
|
|
22f6e89400 | ||
|
|
8409fa7d43 | ||
|
|
c81da78190 | ||
|
|
e17ea4bb89 | ||
|
|
0087728aa8 | ||
|
|
9e48e02f7a | ||
|
|
1291d55ab0 | ||
|
|
b5c6a1e39e | ||
|
|
d6adb30802 | ||
|
|
1d08a69a85 | ||
|
|
1087ab3408 | ||
|
|
51afd504df | ||
|
|
75efc9d73a | ||
|
|
6b68086cff | ||
|
|
3686cdfdb3 | ||
|
|
83c98936d1 | ||
|
|
0891cb279a | ||
|
|
95ba96f537 | ||
|
|
586790173b | ||
|
|
1d19449ab7 | ||
|
|
e1f73334ef | ||
|
|
4faac017b5 | ||
|
|
bfbd2a57a0 | ||
|
|
9519472f83 | ||
|
|
5c0c119cbc | ||
|
|
87eb257a10 | ||
|
|
4a08076c3b | ||
|
|
0d239e6793 | ||
|
|
0a0d47ae88 | ||
|
|
2ba07d47b3 | ||
|
|
f1b520fe3c | ||
|
|
8cfcc26468 | ||
|
|
cd51edf0b8 | ||
|
|
6eb28cfa3d | ||
|
|
542d39fa6a | ||
|
|
e5e328148f |
70
README.md
70
README.md
@@ -196,20 +196,23 @@ Overview of main commands:
|
||||
|
||||
Configuration is directly passed into this command at run time to override your config.
|
||||
|
||||
Available options include:
|
||||
Available options for the fastanime command include:
|
||||
|
||||
- `--server;-s <server>` set the default server to auto select
|
||||
- `--continue;-c/--no-continue;-no-c` whether to continue from the last episode you were watching
|
||||
- `--quality;-q <0|1|2|3>` the link to choose from server
|
||||
- `--translation-type;- <dub|sub` what language for anime
|
||||
- `--auto-select;-a/--no-auto-select;-no-a` auto select title from provider results
|
||||
- `--auto-next;-A;/--no-auto-next;-no-A` auto select next episode
|
||||
- `-downloads-dir;-d <path>` set the folder to download anime into
|
||||
- `--server <server>` or `-s <server>` set the default server to auto select
|
||||
- `--continue/--no-continue` or `-c/-no-c` whether to continue from the last episode you were watching
|
||||
- `--local-history/--remote-history` whether to use remote or local history defaults to local
|
||||
- `--quality <1080/720/480/360>` or `-q <1080/720/480/360>` the link to choose from server
|
||||
- `--translation-type <dub/sub>` or `-t <dub/sub>` what language for anime
|
||||
- `--dub` dubbed anime
|
||||
- `--sub` subbed anime
|
||||
- `--auto-select/--no-auto-select` or `-a/-no-a` auto select title from provider results
|
||||
- `--auto-next/--no-auto-next` or `-A/-no-A` auto select next episode
|
||||
- `-downloads-dir <path>` or `-d <path>` set the folder to download anime into
|
||||
- `--fzf` use fzf for the ui
|
||||
- `--default` use the default ui
|
||||
- `--preview` show a preview when using fzf
|
||||
- `--no-preview` dont show a preview when using fzf
|
||||
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
|
||||
- `--format <yt-dlp format string>` or `-f <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
|
||||
- `--icons/--no-icons` toggle the visibility of the icons
|
||||
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
|
||||
- `--rofi` use rofi for the ui
|
||||
@@ -220,6 +223,20 @@ Available options include:
|
||||
- `--log-file` allow logging to a file
|
||||
- `--rich-traceback` allow rich traceback
|
||||
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
|
||||
- `--provider <allanime>` anime site of choice to scrape from
|
||||
|
||||
Example usage of the above options
|
||||
|
||||
```bash
|
||||
# downloading dubbed anime
|
||||
fastanime --dub download <anime>
|
||||
|
||||
# use icons and fzf for a more elegant ui with preview
|
||||
fastanime --icons --preview --fzf anilist
|
||||
|
||||
# use icons with default ui
|
||||
fastanime --icons --default anilist
|
||||
```
|
||||
|
||||
#### The anilist command :fire: :fire: :fire:
|
||||
|
||||
@@ -331,9 +348,21 @@ View and stream the anime you downloaded using MPV.
|
||||
```bash
|
||||
fastanime downloads
|
||||
|
||||
# view individual episodes
|
||||
fastanime downloads --view-episodes
|
||||
# --- or ---
|
||||
fastanime downloads -v
|
||||
|
||||
# to set seek time when using ffmpegthumbnailer for local previews
|
||||
# -1 means random and is the default
|
||||
fastanime downloads --time-to-seek <intRange(-1,100)>
|
||||
# --- or ---
|
||||
fastanime downloads --t <intRange(-1,100)>
|
||||
|
||||
# to get the path to the downloads folder set
|
||||
fastanime downloads --path
|
||||
# useful when you want to use the value for other programs
|
||||
|
||||
```
|
||||
|
||||
#### config subcommand
|
||||
@@ -434,29 +463,43 @@ Examples:
|
||||
```bash
|
||||
# to select episode from mpv without window closing
|
||||
script-message select-episode <episode-number>
|
||||
|
||||
# to select server from mpv without window closing
|
||||
script-message select-server <server-name>
|
||||
|
||||
# to select quality
|
||||
script-message select-quality <1080/720/480/360>
|
||||
```
|
||||
|
||||
## Configuration
|
||||
## configuration
|
||||
|
||||
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on Linux and mac or somewhere on windows; you can check by running `fastanime config --path`.
|
||||
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on arch linux; for the other operating systems you can check by running `fastanime config --path`.
|
||||
|
||||
```ini
|
||||
[stream]
|
||||
continue_from_history = True # Auto continue from watch history
|
||||
|
||||
# which history to use [local/remote]
|
||||
preferred_history = local
|
||||
|
||||
translation_type = sub # Preferred language for anime (options: dub, sub)
|
||||
|
||||
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
||||
|
||||
auto_next = False # Auto-select next episode
|
||||
|
||||
# Auto select the anime provider results with fuzzy find.
|
||||
# Note this wont always be correct.But 99% of the time will be.
|
||||
auto_select=True
|
||||
|
||||
# whether to skip the opening and ending theme songs
|
||||
# note requires ani-skip to be in path
|
||||
skip=false
|
||||
|
||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||
# used in the continue from time stamp
|
||||
error=3
|
||||
|
||||
use_mpv_mod=False
|
||||
|
||||
# the format of downloaded anime and trailer
|
||||
@@ -472,14 +515,19 @@ format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
|
||||
provider = allanime
|
||||
|
||||
preferred_language = romaji # Display language (options: english, romaji)
|
||||
|
||||
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
|
||||
|
||||
preview=false # whether to show a preview window when using fzf or rofi
|
||||
|
||||
use_fzf=False # whether to use fzf as the interface for the anilist command and others.
|
||||
|
||||
use_rofi=false # whether to use rofi for the ui
|
||||
|
||||
rofi_theme=<path-to-rofi-theme-file>
|
||||
|
||||
rofi_theme_input=<path-to-rofi-theme-file>
|
||||
|
||||
rofi_theme_confirm=<path-to-rofi-theme-file>
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v1.1.5"
|
||||
__version__ = "v1.7.1"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
|
||||
@@ -68,6 +68,11 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
type=bool,
|
||||
help="Continue from last episode?",
|
||||
)
|
||||
@click.option(
|
||||
"--local-history/--remote-history",
|
||||
type=bool,
|
||||
help="Whether to continue from local history or remote history",
|
||||
)
|
||||
@click.option(
|
||||
"--skip/--no-skip",
|
||||
type=bool,
|
||||
@@ -76,7 +81,14 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
@click.option(
|
||||
"-q",
|
||||
"--quality",
|
||||
type=click.Choice(["360", "720", "1080", "unknown"]),
|
||||
type=click.Choice(
|
||||
[
|
||||
"360",
|
||||
"480",
|
||||
"720",
|
||||
"1080",
|
||||
]
|
||||
),
|
||||
help="set the quality of the stream",
|
||||
)
|
||||
@click.option(
|
||||
@@ -139,6 +151,7 @@ def run_cli(
|
||||
server,
|
||||
format,
|
||||
continue_,
|
||||
local_history,
|
||||
skip,
|
||||
translation_type,
|
||||
quality,
|
||||
@@ -209,6 +222,11 @@ def run_cli(
|
||||
ctx.obj.auto_next = auto_next
|
||||
if ctx.get_parameter_source("icons") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.icons = icons
|
||||
if (
|
||||
ctx.get_parameter_source("local_history")
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
):
|
||||
ctx.obj.preferred_history = "local" if local_history else "remote"
|
||||
if (
|
||||
ctx.get_parameter_source("auto_select")
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import click
|
||||
|
||||
from ...completion_functions import anime_titles_shell_complete
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Search for anime using anilists api and get top ~50 results",
|
||||
short_help="Search for anime",
|
||||
)
|
||||
@click.argument(
|
||||
"title",
|
||||
)
|
||||
@click.argument("title", shell_complete=anime_titles_shell_complete)
|
||||
@click.pass_obj
|
||||
def search(config, title):
|
||||
from ....anilist import AniList
|
||||
|
||||
@@ -3,6 +3,8 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
from ..completion_functions import anime_titles_shell_complete
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..config import Config
|
||||
|
||||
@@ -12,22 +14,19 @@ if TYPE_CHECKING:
|
||||
short_help="Download anime",
|
||||
)
|
||||
@click.argument(
|
||||
"anime-title",
|
||||
required=True,
|
||||
"anime-title", required=True, shell_complete=anime_titles_shell_complete
|
||||
)
|
||||
@click.option(
|
||||
"--episode-range",
|
||||
"-r",
|
||||
help="A range of episodes to download",
|
||||
)
|
||||
@click.option(
|
||||
"--highest_priority",
|
||||
"-h",
|
||||
help="Choose stream indicated as highest priority",
|
||||
is_flag=True,
|
||||
help="A range of episodes to download (start-end)",
|
||||
)
|
||||
@click.pass_obj
|
||||
def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
def download(
|
||||
config: "Config",
|
||||
anime_title,
|
||||
episode_range,
|
||||
):
|
||||
from click import clear
|
||||
from rich import print
|
||||
from rich.progress import Progress
|
||||
@@ -54,7 +53,11 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
if not search_results:
|
||||
print("Search results failed")
|
||||
input("Enter to retry")
|
||||
download(config, anime_title, episode_range, highest_priority)
|
||||
download(
|
||||
config,
|
||||
anime_title,
|
||||
episode_range,
|
||||
)
|
||||
return
|
||||
search_results = search_results["results"]
|
||||
search_results_ = {
|
||||
@@ -85,16 +88,22 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
if not anime:
|
||||
print("Sth went wring anime no found")
|
||||
input("Enter to continue...")
|
||||
download(config, anime_title, episode_range, highest_priority)
|
||||
download(
|
||||
config,
|
||||
anime_title,
|
||||
episode_range,
|
||||
)
|
||||
return
|
||||
|
||||
episodes = anime["availableEpisodesDetail"][config.translation_type]
|
||||
if episode_range:
|
||||
episodes_start, episodes_end = episode_range.split("-")
|
||||
episodes_range = range(round(float(episodes_start)), round(float(episodes_end)))
|
||||
|
||||
else:
|
||||
episodes_start, episodes_end = 0, len(episodes)
|
||||
for episode in range(round(float(episodes_start)), round(float(episodes_end))):
|
||||
episodes_range = sorted(episodes, key=float)
|
||||
|
||||
for episode in episodes_range:
|
||||
try:
|
||||
episode = str(episode)
|
||||
if episode not in episodes:
|
||||
@@ -129,13 +138,16 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
# prompt for server selection
|
||||
servers = {server["server"]: server for server in streams}
|
||||
servers_names = list(servers.keys())
|
||||
if config.use_fzf:
|
||||
server = fzf.run(servers_names, "Select an link: ")
|
||||
if config.server in servers_names:
|
||||
server = config.server
|
||||
else:
|
||||
server = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
if config.use_fzf:
|
||||
server = fzf.run(servers_names, "Select an link: ")
|
||||
else:
|
||||
server = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, servers[server]["links"]
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
from ..config import Config
|
||||
|
||||
@@ -10,8 +12,16 @@ if TYPE_CHECKING:
|
||||
help="View and watch your downloads using mpv", short_help="Watch downloads"
|
||||
)
|
||||
@click.option("--path", "-p", help="print the downloads folder and exit", is_flag=True)
|
||||
@click.option("--view-episodes", "-v", help="View individual episodes", is_flag=True)
|
||||
@click.option(
|
||||
"--ffmpegthumbnailer-seek-time",
|
||||
"--time-to-seek",
|
||||
"-t",
|
||||
type=click.IntRange(-1, 100),
|
||||
help="ffmpegthumbnailer seek time [0-100]",
|
||||
)
|
||||
@click.pass_obj
|
||||
def downloads(config: "Config", path: bool):
|
||||
def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_seek_time):
|
||||
import os
|
||||
|
||||
from ...cli.utils.mpv import run_mpv
|
||||
@@ -20,6 +30,8 @@ def downloads(config: "Config", path: bool):
|
||||
from ..utils.tools import exit_app
|
||||
from ..utils.utils import fuzzy_inquirer
|
||||
|
||||
if not ffmpegthumbnailer_seek_time:
|
||||
ffmpegthumbnailer_seek_time = config.ffmpegthumbnailer_seek_time
|
||||
USER_VIDEOS_DIR = config.downloads_dir
|
||||
if path:
|
||||
print(USER_VIDEOS_DIR)
|
||||
@@ -27,24 +39,250 @@ def downloads(config: "Config", path: bool):
|
||||
if not os.path.exists(USER_VIDEOS_DIR):
|
||||
print("Downloads directory specified does not exist")
|
||||
return
|
||||
playlists = os.listdir(USER_VIDEOS_DIR)
|
||||
playlists.append("Exit")
|
||||
anime_downloads = os.listdir(USER_VIDEOS_DIR)
|
||||
anime_downloads.append("Exit")
|
||||
|
||||
def stream():
|
||||
def create_thumbnails(video_path, anime_title, downloads_thumbnail_cache_dir):
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
FFMPEG_THUMBNAILER = shutil.which("ffmpegthumbnailer")
|
||||
if not FFMPEG_THUMBNAILER:
|
||||
return
|
||||
|
||||
out = os.path.join(downloads_thumbnail_cache_dir, anime_title)
|
||||
if ffmpegthumbnailer_seek_time == -1:
|
||||
import random
|
||||
|
||||
seektime = str(random.randrange(0, 100))
|
||||
else:
|
||||
seektime = str(ffmpegthumbnailer_seek_time)
|
||||
_ = subprocess.run(
|
||||
[
|
||||
FFMPEG_THUMBNAILER,
|
||||
"-i",
|
||||
video_path,
|
||||
"-o",
|
||||
out,
|
||||
"-s",
|
||||
"0",
|
||||
"-t",
|
||||
seektime,
|
||||
],
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
|
||||
def get_previews_anime(workers=None, bg=True):
|
||||
import concurrent.futures
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
if not shutil.which("ffmpegthumbnailer"):
|
||||
print("ffmpegthumbnailer not found")
|
||||
logger.error("ffmpegthumbnailer not found")
|
||||
return
|
||||
|
||||
from ...constants import APP_CACHE_DIR
|
||||
from ..utils.scripts import fzf_preview
|
||||
|
||||
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _worker():
|
||||
# use concurrency to download the images as fast as possible
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
for anime_title in anime_downloads:
|
||||
anime_path = os.path.join(USER_VIDEOS_DIR, anime_title)
|
||||
if not os.path.isdir(anime_path):
|
||||
continue
|
||||
playlist = os.listdir(anime_path)
|
||||
if playlist:
|
||||
# actual link to download image from
|
||||
video_path = os.path.join(anime_path, playlist[0])
|
||||
future_to_url[
|
||||
executor.submit(
|
||||
create_thumbnails,
|
||||
video_path,
|
||||
anime_title,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
] = anime_title
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
logger.error("%r generated an exception: %s" % (url, e))
|
||||
|
||||
if bg:
|
||||
from threading import Thread
|
||||
|
||||
worker = Thread(target=_worker)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
else:
|
||||
_worker()
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then
|
||||
if ! fzf-preview %s/{} 2>/dev/null; then
|
||||
echo Loading...
|
||||
fi
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
downloads_thumbnail_cache_dir,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
return preview
|
||||
|
||||
def get_previews_episodes(anime_playlist_path, workers=None, bg=True):
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from ...constants import APP_CACHE_DIR
|
||||
from ..utils.scripts import fzf_preview
|
||||
|
||||
if not shutil.which("ffmpegthumbnailer"):
|
||||
print("ffmpegthumbnailer not found")
|
||||
logger.error("ffmpegthumbnailer not found")
|
||||
return
|
||||
|
||||
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _worker():
|
||||
import concurrent.futures
|
||||
|
||||
# use concurrency to download the images as fast as possible
|
||||
# anime_playlist_path = os.path.join(USER_VIDEOS_DIR, anime_playlist_path)
|
||||
if not os.path.isdir(anime_playlist_path):
|
||||
return
|
||||
anime_episodes = os.listdir(anime_playlist_path)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
for episode_title in anime_episodes:
|
||||
episode_path = os.path.join(anime_playlist_path, episode_title)
|
||||
|
||||
# actual link to download image from
|
||||
future_to_url[
|
||||
executor.submit(
|
||||
create_thumbnails,
|
||||
episode_path,
|
||||
episode_title,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
] = episode_title
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
logger.error("%r generated an exception: %s" % (url, e))
|
||||
|
||||
if bg:
|
||||
from threading import Thread
|
||||
|
||||
worker = Thread(target=_worker)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
else:
|
||||
_worker()
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then
|
||||
if ! fzf-preview %s/{} 2>/dev/null; then
|
||||
echo Loading...
|
||||
fi
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
downloads_thumbnail_cache_dir,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
return preview
|
||||
|
||||
def stream_episode(
|
||||
anime_playlist_path,
|
||||
):
|
||||
if view_episodes:
|
||||
if not os.path.isdir(anime_playlist_path):
|
||||
print(anime_playlist_path, "is not dir")
|
||||
exit_app(1)
|
||||
return
|
||||
episodes = os.listdir(anime_playlist_path)
|
||||
downloaded_episodes = [*episodes, "Back"]
|
||||
if config.use_fzf:
|
||||
if not config.preview:
|
||||
episode_title = fzf.run(
|
||||
downloaded_episodes,
|
||||
"Enter Episode ",
|
||||
)
|
||||
else:
|
||||
preview = get_previews_episodes(anime_playlist_path)
|
||||
episode_title = fzf.run(
|
||||
downloaded_episodes,
|
||||
"Enter Episode ",
|
||||
preview=preview,
|
||||
)
|
||||
elif config.use_rofi:
|
||||
episode_title = Rofi.run(downloaded_episodes, "Enter Episode")
|
||||
else:
|
||||
episode_title = fuzzy_inquirer(
|
||||
downloaded_episodes,
|
||||
"Enter Playlist Name: ",
|
||||
)
|
||||
if episode_title == "Back":
|
||||
stream_anime()
|
||||
return
|
||||
episode_path = os.path.join(anime_playlist_path, episode_title)
|
||||
run_mpv(episode_path)
|
||||
stream_episode(anime_playlist_path)
|
||||
|
||||
def stream_anime():
|
||||
if config.use_fzf:
|
||||
playlist_name = fzf.run(playlists, "Enter Playlist Name", "Downloads")
|
||||
if not config.preview:
|
||||
playlist_name = fzf.run(
|
||||
anime_downloads,
|
||||
"Enter Playlist Name",
|
||||
)
|
||||
else:
|
||||
preview = get_previews_anime()
|
||||
playlist_name = fzf.run(
|
||||
anime_downloads,
|
||||
"Enter Playlist Name",
|
||||
preview=preview,
|
||||
)
|
||||
elif config.use_rofi:
|
||||
playlist_name = Rofi.run(playlists, "Enter Playlist Name")
|
||||
playlist_name = Rofi.run(anime_downloads, "Enter Playlist Name")
|
||||
else:
|
||||
playlist_name = fuzzy_inquirer(
|
||||
playlists,
|
||||
anime_downloads,
|
||||
"Enter Playlist Name: ",
|
||||
)
|
||||
if playlist_name == "Exit":
|
||||
exit_app()
|
||||
return
|
||||
playlist = os.path.join(USER_VIDEOS_DIR, playlist_name)
|
||||
run_mpv(playlist)
|
||||
stream()
|
||||
if view_episodes:
|
||||
stream_episode(
|
||||
playlist,
|
||||
)
|
||||
else:
|
||||
run_mpv(playlist)
|
||||
stream_anime()
|
||||
|
||||
stream()
|
||||
stream_anime()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import click
|
||||
|
||||
from ...cli.config import Config
|
||||
from ..completion_functions import anime_titles_shell_complete
|
||||
|
||||
|
||||
@click.command(
|
||||
@@ -10,9 +11,11 @@ from ...cli.config import Config
|
||||
@click.option(
|
||||
"--episode-range",
|
||||
"-r",
|
||||
help="A range of episodes to binge",
|
||||
help="A range of episodes to binge (start-end)",
|
||||
)
|
||||
@click.argument(
|
||||
"anime_title", required=True, shell_complete=anime_titles_shell_complete
|
||||
)
|
||||
@click.argument("anime_title", required=True, type=str)
|
||||
@click.pass_obj
|
||||
def search(config: Config, anime_title: str, episode_range: str):
|
||||
from click import clear
|
||||
@@ -150,15 +153,18 @@ def search(config: Config, anime_title: str, episode_range: str):
|
||||
# prompt for server selection
|
||||
servers = {server["server"]: server for server in streams}
|
||||
servers_names = list(servers.keys())
|
||||
if config.use_fzf:
|
||||
server = fzf.run(servers_names, "Select an link: ")
|
||||
elif config.use_rofi:
|
||||
server = Rofi.run(servers_names, "Select an link")
|
||||
if config.server in servers_names:
|
||||
server = config.server
|
||||
else:
|
||||
server = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
if config.use_fzf:
|
||||
server = fzf.run(servers_names, "Select an link: ")
|
||||
elif config.use_rofi:
|
||||
server = Rofi.run(servers_names, "Select an link")
|
||||
else:
|
||||
server = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, servers[server]["links"]
|
||||
)
|
||||
|
||||
83
fastanime/cli/completion_functions.py
Normal file
83
fastanime/cli/completion_functions.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
||||
|
||||
|
||||
anime_title_query = """
|
||||
query($query:String){
|
||||
Page(perPage:50){
|
||||
pageInfo{
|
||||
total
|
||||
currentPage
|
||||
hasNextPage
|
||||
}
|
||||
media(search:$query,type:ANIME){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
romaji
|
||||
english
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def get_anime_titles(query: str, variables: dict = {}):
|
||||
"""the abstraction over all none authenticated requests and that returns data of a similar type
|
||||
|
||||
Args:
|
||||
query: the anilist query
|
||||
variables: the anilist api variables
|
||||
|
||||
Returns:
|
||||
a boolean indicating success and none or an anilist object depending on success
|
||||
"""
|
||||
from requests import post
|
||||
|
||||
try:
|
||||
response = post(
|
||||
ANILIST_ENDPOINT,
|
||||
json={"query": query, "variables": variables},
|
||||
timeout=10,
|
||||
)
|
||||
anilist_data = response.json()
|
||||
|
||||
# ensuring you dont get blocked
|
||||
if (
|
||||
int(response.headers.get("X-RateLimit-Remaining", 0)) < 30
|
||||
and not response.status_code == 500
|
||||
):
|
||||
print("Warning you are exceeding the allowed number of calls per minute")
|
||||
logger.warning(
|
||||
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
|
||||
)
|
||||
print("Forced timeout will now be initiated")
|
||||
import time
|
||||
|
||||
print("sleeping...")
|
||||
time.sleep(1 * 60)
|
||||
if response.status_code == 200:
|
||||
eng_titles = [
|
||||
anime["title"]["english"]
|
||||
for anime in anilist_data["data"]["Page"]["media"]
|
||||
if anime["title"]["english"]
|
||||
]
|
||||
romaji_titles = [
|
||||
anime["title"]["romaji"]
|
||||
for anime in anilist_data["data"]["Page"]["media"]
|
||||
if anime["title"]["romaji"]
|
||||
]
|
||||
return [*eng_titles, *romaji_titles]
|
||||
else:
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
return []
|
||||
|
||||
|
||||
def anime_titles_shell_complete(ctx, param, incomplete):
|
||||
return [name for name in get_anime_titles(anime_title_query, {"query": incomplete})]
|
||||
@@ -78,6 +78,7 @@ class Config(object):
|
||||
"translation_type": "sub",
|
||||
"server": "top",
|
||||
"continue_from_history": "True",
|
||||
"preferred_history": "local",
|
||||
"use_mpv_mod": "false",
|
||||
"force_window": "immediate",
|
||||
"preferred_language": "english",
|
||||
@@ -93,6 +94,7 @@ class Config(object):
|
||||
"rofi_theme": "",
|
||||
"rofi_theme_input": "",
|
||||
"rofi_theme_confirm": "",
|
||||
"ffmpegthumnailer_seek_time": "-1",
|
||||
}
|
||||
)
|
||||
self.configparser.add_section("stream")
|
||||
@@ -125,12 +127,14 @@ class Config(object):
|
||||
self.format = self.get_format()
|
||||
self.force_window = self.get_force_window()
|
||||
self.preferred_language = self.get_preferred_language()
|
||||
self.preferred_history = self.get_preferred_history()
|
||||
self.rofi_theme = self.get_rofi_theme()
|
||||
Rofi.rofi_theme = self.rofi_theme
|
||||
self.rofi_theme_input = self.get_rofi_theme_input()
|
||||
Rofi.rofi_theme_input = self.rofi_theme_input
|
||||
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
|
||||
Rofi.rofi_theme_confirm = self.rofi_theme_confirm
|
||||
self.ffmpegthumbnailer_seek_time = self.get_ffmpegthumnailer_seek_time()
|
||||
# ---- setup user data ------
|
||||
self.watch_history: dict = self.user_data.get("watch_history", {})
|
||||
self.anime_list: list = self.user_data.get("animelist", [])
|
||||
@@ -142,7 +146,7 @@ class Config(object):
|
||||
self._update_user_data()
|
||||
|
||||
def update_watch_history(
|
||||
self, anime_id: int, episode: str | None, start_time="0", total_time="0"
|
||||
self, anime_id: int, episode: str, start_time="0", total_time="0"
|
||||
):
|
||||
self.watch_history.update(
|
||||
{
|
||||
@@ -176,6 +180,9 @@ class Config(object):
|
||||
def get_provider(self):
|
||||
return self.configparser.get("general", "provider")
|
||||
|
||||
def get_ffmpegthumnailer_seek_time(self):
|
||||
return self.configparser.getint("general", "ffmpegthumnailer_seek_time")
|
||||
|
||||
def get_preferred_language(self):
|
||||
return self.configparser.get("general", "preferred_language")
|
||||
|
||||
@@ -232,6 +239,9 @@ class Config(object):
|
||||
def get_translation_type(self):
|
||||
return self.configparser.get("stream", "translation_type")
|
||||
|
||||
def get_preferred_history(self):
|
||||
return self.configparser.get("stream", "preferred_history")
|
||||
|
||||
def get_quality(self):
|
||||
return self.configparser.get("stream", "quality")
|
||||
|
||||
@@ -255,6 +265,10 @@ class Config(object):
|
||||
# Auto continue from watch history
|
||||
continue_from_history = {self.continue_from_history}
|
||||
|
||||
# which hostory to use [local/remote]
|
||||
preferred_history = {self.preferred_history}
|
||||
|
||||
|
||||
# Preferred language for anime (options: dub, sub)
|
||||
translation_type = {self.translation_type}
|
||||
|
||||
@@ -303,6 +317,10 @@ downloads_dir = {self.downloads_dir}
|
||||
# whether to show a preview window when using fzf or rofi
|
||||
preview = {self.preview}
|
||||
|
||||
# the time to seek when using ffmpegthumbnailer [-1 to 100]
|
||||
# -1 means random and is the default
|
||||
ffmpegthumbnailer_seek_time = {self.ffmpegthumbnailer_seek_time}
|
||||
|
||||
# whether to use fzf as the interface for the anilist command and others.
|
||||
use_fzf = {self.use_fzf}
|
||||
|
||||
@@ -311,9 +329,12 @@ use_rofi = {self.use_rofi}
|
||||
|
||||
# rofi theme to use
|
||||
rofi_theme = {self.rofi_theme}
|
||||
|
||||
rofi_theme_input = {self.rofi_theme_input}
|
||||
|
||||
rofi_theme_confirm = {self.rofi_theme_confirm}
|
||||
|
||||
|
||||
# whether to show the icons
|
||||
icons = {self.icons}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ if TYPE_CHECKING:
|
||||
from ..utils.tools import FastAnimeRuntimeState
|
||||
|
||||
|
||||
# TODO: make the error handling more sane
|
||||
def calculate_time_delta(start_time, end_time):
|
||||
"""helper function used to calculate the difference between two timestamps in seconds
|
||||
|
||||
@@ -97,8 +98,14 @@ def media_player_controls(
|
||||
current_episode_number,
|
||||
)
|
||||
|
||||
start_time = config.watch_history[str(anime_id_anilist)]["start_time"]
|
||||
print("[green]Continuing from:[/] ", start_time)
|
||||
if (
|
||||
config.watch_history[str(anime_id_anilist)]["episode"]
|
||||
== current_episode_number
|
||||
):
|
||||
start_time = config.watch_history[str(anime_id_anilist)]["start_time"]
|
||||
print("[green]Continuing from:[/] ", start_time)
|
||||
else:
|
||||
start_time = "0"
|
||||
custom_args = []
|
||||
if config.skip:
|
||||
if args := aniskip(
|
||||
@@ -365,35 +372,18 @@ def provider_anime_episode_servers_menu(
|
||||
# no need to get all servers if top just works
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
try:
|
||||
selected_server = next(episode_streams_generator, None)
|
||||
if not selected_server:
|
||||
if config.use_rofi:
|
||||
if Rofi.confirm("Sth went wrong enter to continue"):
|
||||
provider_anime_episode_servers_menu(
|
||||
config, fastanime_runtime_state
|
||||
)
|
||||
else:
|
||||
exit_app(1)
|
||||
else:
|
||||
print("Sth went wrong")
|
||||
input("Enter to continue...")
|
||||
provider_anime_episode_servers_menu(
|
||||
config, fastanime_runtime_state
|
||||
)
|
||||
return
|
||||
server_name = "top"
|
||||
except Exception as e:
|
||||
print("Failed to get streams. Reason:", e)
|
||||
if not config.use_rofi:
|
||||
input("Enter to coninue...")
|
||||
selected_server = next(episode_streams_generator, None)
|
||||
if not selected_server:
|
||||
if config.use_rofi:
|
||||
if Rofi.confirm("Sth went wrong enter to continue"):
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
if not Rofi.confirm(f"!!Sth went wrong!!: {e} Enter to continue"):
|
||||
exit_app(1)
|
||||
server_name = None
|
||||
selected_server = ""
|
||||
exit_app(1)
|
||||
else:
|
||||
print("Sth went wrong")
|
||||
input("Enter to continue...")
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
return
|
||||
else:
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching servers...", total=None)
|
||||
@@ -402,6 +392,17 @@ def provider_anime_episode_servers_menu(
|
||||
for episode_stream in episode_streams_generator
|
||||
}
|
||||
|
||||
if not episode_streams_dict:
|
||||
if config.use_rofi:
|
||||
if Rofi.confirm("Sth went wrong enter to continue"):
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
exit_app(1)
|
||||
else:
|
||||
print("Sth went wrong")
|
||||
input("Enter to continue...")
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
# check if user server exists and is actually a valid serrver then sets it
|
||||
if config.server and config.server in episode_streams_dict.keys():
|
||||
server_name = config.server
|
||||
@@ -478,7 +479,7 @@ def provider_anime_episode_servers_menu(
|
||||
AniList.update_anime_list(
|
||||
{
|
||||
"mediaId": anime_id_anilist,
|
||||
"progress": current_episode_number,
|
||||
"progress": int(float(current_episode_number)),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -486,7 +487,10 @@ def provider_anime_episode_servers_menu(
|
||||
start_time = config.watch_history.get(str(anime_id_anilist), {}).get(
|
||||
"start_time", "0"
|
||||
)
|
||||
if start_time != "0":
|
||||
episode_in_history = config.watch_history.get(str(anime_id_anilist), {}).get(
|
||||
"episode", ""
|
||||
)
|
||||
if start_time != "0" and episode_in_history == current_episode_number:
|
||||
print("[green]Continuing from:[/] ", start_time)
|
||||
custom_args = []
|
||||
if config.skip:
|
||||
@@ -512,7 +516,7 @@ def provider_anime_episode_servers_menu(
|
||||
script_opts = custom_args[1].split("=", 1)
|
||||
mpv._set_property("chapters-file", chapters_file[1])
|
||||
mpv._set_property("script-opts", script_opts[1])
|
||||
if not start_time == "0":
|
||||
if not start_time == "0" and episode_in_history == current_episode_number:
|
||||
mpv.start = start_time
|
||||
mpv.wait_for_shutdown()
|
||||
mpv.terminate()
|
||||
@@ -520,6 +524,8 @@ def provider_anime_episode_servers_menu(
|
||||
total_time = player.last_total_time
|
||||
current_episode_number = fastanime_runtime_state.provider_current_episode_number
|
||||
else:
|
||||
if not episode_in_history == current_episode_number:
|
||||
start_time = "0"
|
||||
stop_time, total_time = run_mpv(
|
||||
current_stream_link,
|
||||
selected_server["episode_title"],
|
||||
@@ -532,16 +538,26 @@ def provider_anime_episode_servers_menu(
|
||||
# this will try to update the episode to be the next episode if delta has reached a specific threshhold
|
||||
# this update will only apply locally
|
||||
# the remote(anilist) is only updated when its certain you are going to open the player
|
||||
available_episodes: list = sorted(
|
||||
fastanime_runtime_state.provider_available_episodes, key=float
|
||||
)
|
||||
if stop_time == "0" or total_time == "0":
|
||||
# increment the episode
|
||||
episode = str(int(current_episode_number) + 1)
|
||||
# increment the episodes
|
||||
next_episode = available_episodes.index(current_episode_number) + 1
|
||||
if next_episode >= len(available_episodes):
|
||||
next_episode = len(available_episodes) - 1
|
||||
episode = available_episodes[next_episode]
|
||||
else:
|
||||
error = config.error * 60
|
||||
delta = calculate_time_delta(stop_time, total_time)
|
||||
if delta.total_seconds() > error:
|
||||
episode = current_episode_number
|
||||
else:
|
||||
episode = str(int(current_episode_number) + 1)
|
||||
# increment the episodes
|
||||
next_episode = available_episodes.index(current_episode_number) + 1
|
||||
if next_episode >= len(available_episodes):
|
||||
next_episode = len(available_episodes) - 1
|
||||
episode = available_episodes[next_episode]
|
||||
stop_time = "0"
|
||||
total_time = "0"
|
||||
|
||||
@@ -592,9 +608,19 @@ def provider_anime_episodes_menu(
|
||||
user_watch_history.get(str(anime_id_anilist), {}).get("episode")
|
||||
in total_episodes
|
||||
):
|
||||
current_episode_number = user_watch_history[str(anime_id_anilist)][
|
||||
"episode"
|
||||
]
|
||||
if (
|
||||
config.preferred_history == "local"
|
||||
or not selected_anime_anilist["mediaListEntry"]
|
||||
):
|
||||
current_episode_number = user_watch_history[str(anime_id_anilist)][
|
||||
"episode"
|
||||
]
|
||||
else:
|
||||
current_episode_number = str(
|
||||
(selected_anime_anilist["mediaListEntry"] or {"progress": 0}).get(
|
||||
"progress"
|
||||
)
|
||||
)
|
||||
print(
|
||||
f"[bold cyan]Continuing from Episode:[/] [bold]{current_episode_number}[/]"
|
||||
)
|
||||
@@ -616,7 +642,7 @@ def provider_anime_episodes_menu(
|
||||
current_episode_number = ""
|
||||
|
||||
# prompt for episode number if not set
|
||||
if not current_episode_number:
|
||||
if not current_episode_number or current_episode_number not in total_episodes:
|
||||
choices = [*total_episodes, "Back"]
|
||||
if config.use_fzf:
|
||||
current_episode_number = fzf.run(
|
||||
@@ -652,7 +678,6 @@ def provider_anime_episodes_menu(
|
||||
provider_anime_episode_servers_menu(config, fastanime_runtime_state)
|
||||
|
||||
|
||||
# WARNING: Marked for deletion, the function is quite useless and function calls in python are expensive
|
||||
def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"):
|
||||
selected_anime: "SearchResult" = (
|
||||
fastanime_runtime_state.provider_anime_search_result
|
||||
@@ -672,8 +697,7 @@ def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
fetch_anime_episode(config, fastanime_runtime_state)
|
||||
return
|
||||
return fetch_anime_episode(config, fastanime_runtime_state)
|
||||
|
||||
fastanime_runtime_state.provider_anime = provider_anime
|
||||
provider_anime_episodes_menu(config, fastanime_runtime_state)
|
||||
@@ -719,8 +743,7 @@ def anime_provider_search_results_menu(
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
anime_provider_search_results_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
return anime_provider_search_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
provider_search_results = {
|
||||
anime["title"]: anime for anime in provider_search_results["results"]
|
||||
@@ -1125,7 +1148,7 @@ def media_actions_menu(
|
||||
f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next,
|
||||
f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history,
|
||||
f"{'🔙 ' if icons else ''}Back": anilist_results_menu,
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
f"{'❌ ' if icons else ''}Exit": lambda *_: exit_app(),
|
||||
}
|
||||
choices = list(options.keys())
|
||||
if config.use_fzf:
|
||||
@@ -1176,6 +1199,7 @@ def anilist_results_menu(
|
||||
anime["status"] == "RELEASING"
|
||||
and anime["nextAiringEpisode"]
|
||||
and progress > 0
|
||||
and anime["mediaListEntry"]
|
||||
):
|
||||
last_aired_episode = anime["nextAiringEpisode"]["episode"] - 1
|
||||
if last_aired_episode - progress > 0:
|
||||
|
||||
@@ -12,89 +12,11 @@ from yt_dlp.utils import clean_html
|
||||
from ...constants import APP_CACHE_DIR
|
||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from ...Utility import anilist_data_helper
|
||||
from ..utils.scripts import fzf_preview
|
||||
from ..utils.utils import get_true_fg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# this script was written by the fzf devs as an example on how to preview images
|
||||
# its only here for convinience
|
||||
fzf_preview = r"""
|
||||
#
|
||||
# The purpose of this script is to demonstrate how to preview a file or an
|
||||
# image in the preview window of fzf.
|
||||
#
|
||||
# Dependencies:
|
||||
# - https://github.com/sharkdp/bat
|
||||
# - https://github.com/hpjansson/chafa
|
||||
# - https://iterm2.com/utilities/imgcat
|
||||
fzf-preview(){
|
||||
if [[ $# -ne 1 ]]; then
|
||||
>&2 echo "usage: $0 FILENAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file=${1/#\~\//$HOME/}
|
||||
type=$(file --dereference --mime -- "$file")
|
||||
|
||||
if [[ ! $type =~ image/ ]]; then
|
||||
if [[ $type =~ =binary ]]; then
|
||||
file "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Sometimes bat is installed as batcat.
|
||||
if command -v batcat > /dev/null; then
|
||||
batname="batcat"
|
||||
elif command -v bat > /dev/null; then
|
||||
batname="bat"
|
||||
else
|
||||
cat "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
|
||||
exit
|
||||
fi
|
||||
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [[ $dim = x ]]; then
|
||||
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
|
||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||
# * https://github.com/junegunn/fzf/issues/2544
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 1. Use kitty icat on kitty terminal
|
||||
if [[ $KITTY_WINDOW_ID ]]; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
chafa -f sixel -s "$dim" "$file"
|
||||
# Add a new line character so that fzf can display multiple images in the preview window
|
||||
echo
|
||||
|
||||
# 3. If chafa is not found but imgcat is available, use it on iTerm2
|
||||
elif command -v imgcat > /dev/null; then
|
||||
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
|
||||
# user is running iTerm2. But for the sake of simplicity, we just assume
|
||||
# that's the case here.
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
|
||||
# 4. Cannot find any suitable method to preview the image
|
||||
else
|
||||
file "$file"
|
||||
fi
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# ---- aniskip intergration ----
|
||||
def aniskip(mal_id: int, episode: str):
|
||||
|
||||
@@ -101,7 +101,7 @@ class MpvPlayer(object):
|
||||
AniList.update_anime_list(
|
||||
{
|
||||
"mediaId": anime_id_anilist,
|
||||
"progress": current_episode_number,
|
||||
"progress": int(float(current_episode_number)),
|
||||
}
|
||||
)
|
||||
# get them juicy streams
|
||||
@@ -141,6 +141,7 @@ class MpvPlayer(object):
|
||||
return
|
||||
self.mpv_player._set_property("start", "0")
|
||||
stream_link = stream_link_["link"]
|
||||
fastanime_runtime_state.provider_current_episode_stream_link = stream_link
|
||||
return stream_link
|
||||
|
||||
def create_player(
|
||||
@@ -234,6 +235,7 @@ class MpvPlayer(object):
|
||||
@mpv_player.on_key_press("shift+t")
|
||||
def _toggle_translation_type():
|
||||
translation_type = "sub" if config.translation_type == "dub" else "dub"
|
||||
mpv_player.show_text("Changing translation type...")
|
||||
anime = anime_provider.get_anime(
|
||||
fastanime_runtime_state.provider_anime_search_result["id"],
|
||||
fastanime_runtime_state.selected_anime_anilist,
|
||||
|
||||
78
fastanime/cli/utils/scripts.py
Normal file
78
fastanime/cli/utils/scripts.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# this script was written by the fzf devs as an example on how to preview images
|
||||
# its only here for convinience
|
||||
fzf_preview = r"""
|
||||
#
|
||||
# The purpose of this script is to demonstrate how to preview a file or an
|
||||
# image in the preview window of fzf.
|
||||
#
|
||||
# Dependencies:
|
||||
# - https://github.com/sharkdp/bat
|
||||
# - https://github.com/hpjansson/chafa
|
||||
# - https://iterm2.com/utilities/imgcat
|
||||
fzf-preview(){
|
||||
if [[ $# -ne 1 ]]; then
|
||||
>&2 echo "usage: $0 FILENAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file=${1/#\~\//$HOME/}
|
||||
type=$(file --dereference --mime -- "$file")
|
||||
|
||||
if [[ ! $type =~ image/ ]]; then
|
||||
if [[ $type =~ =binary ]]; then
|
||||
file "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Sometimes bat is installed as batcat.
|
||||
if command -v batcat > /dev/null; then
|
||||
batname="batcat"
|
||||
elif command -v bat > /dev/null; then
|
||||
batname="bat"
|
||||
else
|
||||
cat "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
|
||||
exit
|
||||
fi
|
||||
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [[ $dim = x ]]; then
|
||||
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
|
||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||
# * https://github.com/junegunn/fzf/issues/2544
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 1. Use kitty icat on kitty terminal
|
||||
if [[ $KITTY_WINDOW_ID ]]; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
chafa -f sixel -s "$dim" "$file"
|
||||
# Add a new line character so that fzf can display multiple images in the preview window
|
||||
echo
|
||||
|
||||
# 3. If chafa is not found but imgcat is available, use it on iTerm2
|
||||
elif command -v imgcat > /dev/null; then
|
||||
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
|
||||
# user is running iTerm2. But for the sake of simplicity, we just assume
|
||||
# that's the case here.
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
|
||||
# 4. Cannot find any suitable method to preview the image
|
||||
else
|
||||
file "$file"
|
||||
fi
|
||||
}
|
||||
"""
|
||||
@@ -19,7 +19,7 @@ BG_GREEN = "\033[48;2;120;233;12;m"
|
||||
GREEN = "\033[38;2;45;24;45;m"
|
||||
|
||||
|
||||
def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]"):
|
||||
def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]", default=True):
|
||||
"""Helper function used to filter a list of EpisodeStream objects to one that has a corresponding quality
|
||||
|
||||
Args:
|
||||
@@ -30,8 +30,25 @@ def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]"):
|
||||
an EpisodeStream object or None incase the quality was not found
|
||||
"""
|
||||
for stream_link in stream_links:
|
||||
if stream_link["quality"] == quality:
|
||||
q = float(quality)
|
||||
Q = float(stream_link["quality"])
|
||||
# some providers have inaccurate eg qualities 718 instead of 720
|
||||
if Q < q + 80 and Q > q - 80:
|
||||
return stream_link
|
||||
else:
|
||||
if stream_links and default:
|
||||
from rich import print
|
||||
|
||||
try:
|
||||
print("[yellow bold]WARNING Qualities were:[/] ", stream_links)
|
||||
print(
|
||||
"[cyan bold]Using default of quality:[/] ",
|
||||
stream_links[0]["quality"],
|
||||
)
|
||||
return stream_links[0]
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
|
||||
|
||||
def format_bytes_to_human(num_of_bytes: float, suffix: str = "B"):
|
||||
|
||||
@@ -9,7 +9,6 @@ PLATFORM = system()
|
||||
|
||||
# ---- app deps ----
|
||||
APP_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
CONFIGS_DIR = os.path.join(APP_DIR, "configs")
|
||||
ASSETS_DIR = os.path.join(APP_DIR, "assets")
|
||||
|
||||
|
||||
@@ -24,7 +23,6 @@ PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
|
||||
# ----- user configs and data -----
|
||||
|
||||
S_PLATFORM = sys.platform
|
||||
|
||||
if S_PLATFORM == "win32":
|
||||
# app data
|
||||
app_data_dir_base = os.getenv("LOCALAPPDATA")
|
||||
@@ -36,7 +34,7 @@ if S_PLATFORM == "win32":
|
||||
APP_CACHE_DIR = os.path.join(APP_DATA_DIR, "cache")
|
||||
|
||||
# videos dir
|
||||
video_dir_base = os.path.expanduser("~/Videos")
|
||||
video_dir_base = os.path.join(Path().home(), "Videos")
|
||||
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
||||
|
||||
elif S_PLATFORM == "darwin":
|
||||
|
||||
@@ -231,7 +231,7 @@ $type:MediaType\
|
||||
search_query = (
|
||||
"""
|
||||
query($query:String,%s){
|
||||
Page(perPage:30,page:$page){
|
||||
Page(perPage:50,page:$page){
|
||||
pageInfo{
|
||||
total
|
||||
currentPage
|
||||
|
||||
@@ -89,27 +89,28 @@ class AnimePaheApi(AnimeProvider):
|
||||
if response.status_code == 200:
|
||||
if not data:
|
||||
data.update(response.json())
|
||||
if ep_data := response.json().get("data"):
|
||||
data["data"].extend(ep_data)
|
||||
if data["next_page_url"]:
|
||||
# TODO: Refine this
|
||||
time.sleep(
|
||||
random.choice(
|
||||
[
|
||||
0.25,
|
||||
0.1,
|
||||
0.5,
|
||||
0.75,
|
||||
1,
|
||||
]
|
||||
)
|
||||
)
|
||||
page += 1
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
|
||||
_pages_loader(
|
||||
url,
|
||||
page,
|
||||
else:
|
||||
if ep_data := response.json().get("data"):
|
||||
data["data"].extend(ep_data)
|
||||
if response.json()["next_page_url"]:
|
||||
# TODO: Refine this
|
||||
time.sleep(
|
||||
random.choice(
|
||||
[
|
||||
0.25,
|
||||
0.1,
|
||||
0.5,
|
||||
0.75,
|
||||
1,
|
||||
]
|
||||
)
|
||||
)
|
||||
page += 1
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
|
||||
_pages_loader(
|
||||
url,
|
||||
page,
|
||||
)
|
||||
|
||||
_pages_loader(
|
||||
url,
|
||||
@@ -119,7 +120,7 @@ class AnimePaheApi(AnimeProvider):
|
||||
if not data:
|
||||
return {}
|
||||
self.anime = data # pyright:ignore
|
||||
episodes = list(map(str, range(data["total"])))
|
||||
episodes = list(map(str, [episode["episode"] for episode in data["data"]]))
|
||||
title = ""
|
||||
return {
|
||||
"id": session_id,
|
||||
|
||||
@@ -36,7 +36,7 @@ hex_to_char = {
|
||||
|
||||
|
||||
def give_random_quality(links: list[dict]):
|
||||
qualities = cycle(["1080", "720", "360"])
|
||||
qualities = cycle(["1080", "720", "480", "360"])
|
||||
|
||||
return [
|
||||
{"link": link["link"], "quality": quality}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "1.1.5.dev1"
|
||||
version = "1.7.1"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
|
||||
Reference in New Issue
Block a user