Compare commits

...

37 Commits

Author SHA1 Message Date
Benex254
13200e2d1f chore: bump version 2024-08-16 14:24:59 +03:00
Benex254
22f6e89400 fix:preferred server not reflecting in command 2024-08-16 14:24:42 +03:00
Benex254
8409fa7d43 chore: bump version 2024-08-16 13:51:57 +03:00
Benex254
c81da78190 chore: bump version 2024-08-16 13:51:36 +03:00
Benex254
e17ea4bb89 fix(interface): incorrect loading of episode during replat 2024-08-16 13:50:58 +03:00
Benex254
0087728aa8 docs: update readme 2024-08-16 13:22:35 +03:00
Benex254
9e48e02f7a feat(downloads command): improve local downloads experience 2024-08-16 13:12:10 +03:00
Benex254
1291d55ab0 feat(downloads command): add previews 2024-08-16 11:38:18 +03:00
Benex254
b5c6a1e39e feat: improve path handling on windows 2024-08-16 10:54:13 +03:00
Benex254
d6adb30802 feat(download command): remove unused option and improve help message 2024-08-16 10:47:25 +03:00
Benex254
1d08a69a85 feat(search command): improve help message 2024-08-16 10:46:37 +03:00
Benex254
1087ab3408 chore: add error checking todo 2024-08-16 10:46:05 +03:00
Benex254
51afd504df chore: update config obj 2024-08-16 10:45:40 +03:00
Benex254
75efc9d73a docs: update readme 2024-08-16 10:45:18 +03:00
Benex254
6b68086cff feat(interface): improve watch history experience 2024-08-16 10:10:47 +03:00
Benex254
3686cdfdb3 feat(completions): enhance speed of loading completion functions 2024-08-15 12:21:29 +03:00
Benex254
83c98936d1 chore: bump version 2024-08-15 11:33:22 +03:00
Benex254
0891cb279a docs: update readme 2024-08-15 11:32:21 +03:00
Benex254
95ba96f537 chore: remove plyer 2024-08-15 11:25:51 +03:00
Benex254
586790173b docs: update readme 2024-08-15 11:25:22 +03:00
Benex254
1d19449ab7 chore: bump version 2024-08-15 10:52:59 +03:00
Benex254
e1f73334ef feat(cli): remove unknown as possible quality 2024-08-15 10:50:32 +03:00
Benex254
4faac017b5 feat(utils): add 480 as possible quality 2024-08-15 10:49:29 +03:00
Benex254
bfbd2a57a0 feat(cli): add 480 as a possible quality 2024-08-15 10:48:39 +03:00
Benex254
9519472f83 feat(utils): pretty colors when defaulting to quality 2024-08-15 10:48:15 +03:00
Benex254
5c0c119cbc feat(download command): use actual episodes if downloading all 2024-08-15 10:47:35 +03:00
Benex254
87eb257a10 chore: use plyer.sttoragepath if possible 2024-08-15 10:29:59 +03:00
Benex254
4a08076c3b feat: use actual episodes list than inference 2024-08-15 00:20:06 +03:00
Benex254
0d239e6793 chore: bump version 2024-08-14 22:43:05 +03:00
Benex254
0a0d47ae88 chore: raise search results for anilist 2024-08-14 22:42:35 +03:00
Benex254
2ba07d47b3 feat(cli): add anime title completions 2024-08-14 22:32:47 +03:00
Benex254
f1b520fe3c chore: bump version 2024-08-14 21:22:40 +03:00
Benex254
8cfcc26468 feat(animepahe): use true episodes 2024-08-14 21:20:49 +03:00
Benex254
cd51edf0b8 feat(interface): better post error response 2024-08-14 21:10:32 +03:00
Benex254
6eb28cfa3d feat(mpv): show feedback on toggle translation type 2024-08-14 20:43:26 +03:00
Benex254
542d39fa6a chore: bump version 2024-08-14 20:40:45 +03:00
Benex254
e5e328148f fix: failed quality selection 2024-08-14 20:39:58 +03:00
19 changed files with 679 additions and 211 deletions

View File

@@ -196,20 +196,23 @@ Overview of main commands:
Configuration is directly passed into this command at run time to override your config. 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 - `--server <server>` or `-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 - `--continue/--no-continue` or `-c/-no-c` whether to continue from the last episode you were watching
- `--quality;-q <0|1|2|3>` the link to choose from server - `--local-history/--remote-history` whether to use remote or local history defaults to local
- `--translation-type;- <dub|sub` what language for anime - `--quality <1080/720/480/360>` or `-q <1080/720/480/360>` the link to choose from server
- `--auto-select;-a/--no-auto-select;-no-a` auto select title from provider results - `--translation-type <dub/sub>` or `-t <dub/sub>` what language for anime
- `--auto-next;-A;/--no-auto-next;-no-A` auto select next episode - `--dub` dubbed anime
- `-downloads-dir;-d <path>` set the folder to download anime into - `--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 - `--fzf` use fzf for the ui
- `--default` use the default ui - `--default` use the default ui
- `--preview` show a preview when using fzf - `--preview` show a preview when using fzf
- `--no-preview` dont 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 - `--icons/--no-icons` toggle the visibility of the icons
- `--skip/--no-skip` whether to skip the opening and ending theme songs. - `--skip/--no-skip` whether to skip the opening and ending theme songs.
- `--rofi` use rofi for the ui - `--rofi` use rofi for the ui
@@ -220,6 +223,20 @@ Available options include:
- `--log-file` allow logging to a file - `--log-file` allow logging to a file
- `--rich-traceback` allow rich traceback - `--rich-traceback` allow rich traceback
- `--use-mpv-mod/--use-default-player` whether to use python-mpv - `--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: #### The anilist command :fire: :fire: :fire:
@@ -331,9 +348,21 @@ View and stream the anime you downloaded using MPV.
```bash ```bash
fastanime downloads 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 # to get the path to the downloads folder set
fastanime downloads --path fastanime downloads --path
# useful when you want to use the value for other programs # useful when you want to use the value for other programs
``` ```
#### config subcommand #### config subcommand
@@ -434,29 +463,43 @@ Examples:
```bash ```bash
# to select episode from mpv without window closing # to select episode from mpv without window closing
script-message select-episode <episode-number> script-message select-episode <episode-number>
# to select server from mpv without window closing # to select server from mpv without window closing
script-message select-server <server-name> 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 ```ini
[stream] [stream]
continue_from_history = True # Auto continue from watch history 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) translation_type = sub # Preferred language for anime (options: dub, sub)
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp) server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
auto_next = False # Auto-select next episode auto_next = False # Auto-select next episode
# Auto select the anime provider results with fuzzy find. # Auto select the anime provider results with fuzzy find.
# Note this wont always be correct.But 99% of the time will be. # Note this wont always be correct.But 99% of the time will be.
auto_select=True auto_select=True
# whether to skip the opening and ending theme songs # whether to skip the opening and ending theme songs
# note requires ani-skip to be in path # note requires ani-skip to be in path
skip=false skip=false
# the maximum delta time in minutes after which the episode should be considered as completed # the maximum delta time in minutes after which the episode should be considered as completed
# used in the continue from time stamp # used in the continue from time stamp
error=3 error=3
use_mpv_mod=False use_mpv_mod=False
# the format of downloaded anime and trailer # the format of downloaded anime and trailer
@@ -472,14 +515,19 @@ format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
provider = allanime provider = allanime
preferred_language = romaji # Display language (options: english, romaji) preferred_language = romaji # Display language (options: english, romaji)
downloads_dir = <Default-videos-dir>/FastAnime # Download directory downloads_dir = <Default-videos-dir>/FastAnime # Download directory
preview=false # whether to show a preview window when using fzf or rofi 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_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 use_rofi=false # whether to use rofi for the ui
rofi_theme=<path-to-rofi-theme-file> rofi_theme=<path-to-rofi-theme-file>
rofi_theme_input=<path-to-rofi-theme-file> rofi_theme_input=<path-to-rofi-theme-file>
rofi_theme_confirm=<path-to-rofi-theme-file> rofi_theme_confirm=<path-to-rofi-theme-file>

View File

@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
) # noqa: F541 ) # noqa: F541
__version__ = "v1.1.5" __version__ = "v1.7.1"
APP_NAME = "FastAnime" APP_NAME = "FastAnime"
AUTHOR = "Benex254" AUTHOR = "Benex254"

View File

@@ -68,6 +68,11 @@ signal.signal(signal.SIGINT, handle_exit)
type=bool, type=bool,
help="Continue from last episode?", 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( @click.option(
"--skip/--no-skip", "--skip/--no-skip",
type=bool, type=bool,
@@ -76,7 +81,14 @@ signal.signal(signal.SIGINT, handle_exit)
@click.option( @click.option(
"-q", "-q",
"--quality", "--quality",
type=click.Choice(["360", "720", "1080", "unknown"]), type=click.Choice(
[
"360",
"480",
"720",
"1080",
]
),
help="set the quality of the stream", help="set the quality of the stream",
) )
@click.option( @click.option(
@@ -139,6 +151,7 @@ def run_cli(
server, server,
format, format,
continue_, continue_,
local_history,
skip, skip,
translation_type, translation_type,
quality, quality,
@@ -209,6 +222,11 @@ def run_cli(
ctx.obj.auto_next = auto_next ctx.obj.auto_next = auto_next
if ctx.get_parameter_source("icons") == click.core.ParameterSource.COMMANDLINE: if ctx.get_parameter_source("icons") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.icons = icons 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 ( if (
ctx.get_parameter_source("auto_select") ctx.get_parameter_source("auto_select")
== click.core.ParameterSource.COMMANDLINE == click.core.ParameterSource.COMMANDLINE

View File

@@ -1,13 +1,13 @@
import click import click
from ...completion_functions import anime_titles_shell_complete
@click.command( @click.command(
help="Search for anime using anilists api and get top ~50 results", help="Search for anime using anilists api and get top ~50 results",
short_help="Search for anime", short_help="Search for anime",
) )
@click.argument( @click.argument("title", shell_complete=anime_titles_shell_complete)
"title",
)
@click.pass_obj @click.pass_obj
def search(config, title): def search(config, title):
from ....anilist import AniList from ....anilist import AniList

View File

@@ -3,6 +3,8 @@ from typing import TYPE_CHECKING
import click import click
from ..completion_functions import anime_titles_shell_complete
if TYPE_CHECKING: if TYPE_CHECKING:
from ..config import Config from ..config import Config
@@ -12,22 +14,19 @@ if TYPE_CHECKING:
short_help="Download anime", short_help="Download anime",
) )
@click.argument( @click.argument(
"anime-title", "anime-title", required=True, shell_complete=anime_titles_shell_complete
required=True,
) )
@click.option( @click.option(
"--episode-range", "--episode-range",
"-r", "-r",
help="A range of episodes to download", help="A range of episodes to download (start-end)",
)
@click.option(
"--highest_priority",
"-h",
help="Choose stream indicated as highest priority",
is_flag=True,
) )
@click.pass_obj @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 click import clear
from rich import print from rich import print
from rich.progress import Progress from rich.progress import Progress
@@ -54,7 +53,11 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
if not search_results: if not search_results:
print("Search results failed") print("Search results failed")
input("Enter to retry") input("Enter to retry")
download(config, anime_title, episode_range, highest_priority) download(
config,
anime_title,
episode_range,
)
return return
search_results = search_results["results"] search_results = search_results["results"]
search_results_ = { search_results_ = {
@@ -85,16 +88,22 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
if not anime: if not anime:
print("Sth went wring anime no found") print("Sth went wring anime no found")
input("Enter to continue...") input("Enter to continue...")
download(config, anime_title, episode_range, highest_priority) download(
config,
anime_title,
episode_range,
)
return return
episodes = anime["availableEpisodesDetail"][config.translation_type] episodes = anime["availableEpisodesDetail"][config.translation_type]
if episode_range: if episode_range:
episodes_start, episodes_end = episode_range.split("-") episodes_start, episodes_end = episode_range.split("-")
episodes_range = range(round(float(episodes_start)), round(float(episodes_end)))
else: else:
episodes_start, episodes_end = 0, len(episodes) episodes_range = sorted(episodes, key=float)
for episode in range(round(float(episodes_start)), round(float(episodes_end))):
for episode in episodes_range:
try: try:
episode = str(episode) episode = str(episode)
if episode not in episodes: if episode not in episodes:
@@ -129,13 +138,16 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
# prompt for server selection # prompt for server selection
servers = {server["server"]: server for server in streams} servers = {server["server"]: server for server in streams}
servers_names = list(servers.keys()) servers_names = list(servers.keys())
if config.use_fzf: if config.server in servers_names:
server = fzf.run(servers_names, "Select an link: ") server = config.server
else: else:
server = fuzzy_inquirer( if config.use_fzf:
servers_names, server = fzf.run(servers_names, "Select an link: ")
"Select link", else:
) server = fuzzy_inquirer(
servers_names,
"Select link",
)
stream_link = filter_by_quality( stream_link = filter_by_quality(
config.quality, servers[server]["links"] config.quality, servers[server]["links"]
) )

View File

@@ -1,7 +1,9 @@
import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import click import click
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from ..config import Config from ..config import Config
@@ -10,8 +12,16 @@ if TYPE_CHECKING:
help="View and watch your downloads using mpv", short_help="Watch downloads" 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("--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 @click.pass_obj
def downloads(config: "Config", path: bool): def downloads(config: "Config", path: bool, view_episodes, ffmpegthumbnailer_seek_time):
import os import os
from ...cli.utils.mpv import run_mpv 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.tools import exit_app
from ..utils.utils import fuzzy_inquirer from ..utils.utils import fuzzy_inquirer
if not ffmpegthumbnailer_seek_time:
ffmpegthumbnailer_seek_time = config.ffmpegthumbnailer_seek_time
USER_VIDEOS_DIR = config.downloads_dir USER_VIDEOS_DIR = config.downloads_dir
if path: if path:
print(USER_VIDEOS_DIR) print(USER_VIDEOS_DIR)
@@ -27,24 +39,250 @@ def downloads(config: "Config", path: bool):
if not os.path.exists(USER_VIDEOS_DIR): if not os.path.exists(USER_VIDEOS_DIR):
print("Downloads directory specified does not exist") print("Downloads directory specified does not exist")
return return
playlists = os.listdir(USER_VIDEOS_DIR) anime_downloads = os.listdir(USER_VIDEOS_DIR)
playlists.append("Exit") 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: 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: elif config.use_rofi:
playlist_name = Rofi.run(playlists, "Enter Playlist Name") playlist_name = Rofi.run(anime_downloads, "Enter Playlist Name")
else: else:
playlist_name = fuzzy_inquirer( playlist_name = fuzzy_inquirer(
playlists, anime_downloads,
"Enter Playlist Name: ", "Enter Playlist Name: ",
) )
if playlist_name == "Exit": if playlist_name == "Exit":
exit_app() exit_app()
return return
playlist = os.path.join(USER_VIDEOS_DIR, playlist_name) playlist = os.path.join(USER_VIDEOS_DIR, playlist_name)
run_mpv(playlist) if view_episodes:
stream() stream_episode(
playlist,
)
else:
run_mpv(playlist)
stream_anime()
stream() stream_anime()

View File

@@ -1,6 +1,7 @@
import click import click
from ...cli.config import Config from ...cli.config import Config
from ..completion_functions import anime_titles_shell_complete
@click.command( @click.command(
@@ -10,9 +11,11 @@ from ...cli.config import Config
@click.option( @click.option(
"--episode-range", "--episode-range",
"-r", "-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 @click.pass_obj
def search(config: Config, anime_title: str, episode_range: str): def search(config: Config, anime_title: str, episode_range: str):
from click import clear from click import clear
@@ -150,15 +153,18 @@ def search(config: Config, anime_title: str, episode_range: str):
# prompt for server selection # prompt for server selection
servers = {server["server"]: server for server in streams} servers = {server["server"]: server for server in streams}
servers_names = list(servers.keys()) servers_names = list(servers.keys())
if config.use_fzf: if config.server in servers_names:
server = fzf.run(servers_names, "Select an link: ") server = config.server
elif config.use_rofi:
server = Rofi.run(servers_names, "Select an link")
else: else:
server = fuzzy_inquirer( if config.use_fzf:
servers_names, server = fzf.run(servers_names, "Select an link: ")
"Select 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( stream_link = filter_by_quality(
config.quality, servers[server]["links"] config.quality, servers[server]["links"]
) )

View 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})]

View File

@@ -78,6 +78,7 @@ class Config(object):
"translation_type": "sub", "translation_type": "sub",
"server": "top", "server": "top",
"continue_from_history": "True", "continue_from_history": "True",
"preferred_history": "local",
"use_mpv_mod": "false", "use_mpv_mod": "false",
"force_window": "immediate", "force_window": "immediate",
"preferred_language": "english", "preferred_language": "english",
@@ -93,6 +94,7 @@ class Config(object):
"rofi_theme": "", "rofi_theme": "",
"rofi_theme_input": "", "rofi_theme_input": "",
"rofi_theme_confirm": "", "rofi_theme_confirm": "",
"ffmpegthumnailer_seek_time": "-1",
} }
) )
self.configparser.add_section("stream") self.configparser.add_section("stream")
@@ -125,12 +127,14 @@ class Config(object):
self.format = self.get_format() self.format = self.get_format()
self.force_window = self.get_force_window() self.force_window = self.get_force_window()
self.preferred_language = self.get_preferred_language() self.preferred_language = self.get_preferred_language()
self.preferred_history = self.get_preferred_history()
self.rofi_theme = self.get_rofi_theme() self.rofi_theme = self.get_rofi_theme()
Rofi.rofi_theme = self.rofi_theme Rofi.rofi_theme = self.rofi_theme
self.rofi_theme_input = self.get_rofi_theme_input() self.rofi_theme_input = self.get_rofi_theme_input()
Rofi.rofi_theme_input = self.rofi_theme_input Rofi.rofi_theme_input = self.rofi_theme_input
self.rofi_theme_confirm = self.get_rofi_theme_confirm() self.rofi_theme_confirm = self.get_rofi_theme_confirm()
Rofi.rofi_theme_confirm = self.rofi_theme_confirm Rofi.rofi_theme_confirm = self.rofi_theme_confirm
self.ffmpegthumbnailer_seek_time = self.get_ffmpegthumnailer_seek_time()
# ---- setup user data ------ # ---- setup user data ------
self.watch_history: dict = self.user_data.get("watch_history", {}) self.watch_history: dict = self.user_data.get("watch_history", {})
self.anime_list: list = self.user_data.get("animelist", []) self.anime_list: list = self.user_data.get("animelist", [])
@@ -142,7 +146,7 @@ class Config(object):
self._update_user_data() self._update_user_data()
def update_watch_history( 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( self.watch_history.update(
{ {
@@ -176,6 +180,9 @@ class Config(object):
def get_provider(self): def get_provider(self):
return self.configparser.get("general", "provider") 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): def get_preferred_language(self):
return self.configparser.get("general", "preferred_language") return self.configparser.get("general", "preferred_language")
@@ -232,6 +239,9 @@ class Config(object):
def get_translation_type(self): def get_translation_type(self):
return self.configparser.get("stream", "translation_type") return self.configparser.get("stream", "translation_type")
def get_preferred_history(self):
return self.configparser.get("stream", "preferred_history")
def get_quality(self): def get_quality(self):
return self.configparser.get("stream", "quality") return self.configparser.get("stream", "quality")
@@ -255,6 +265,10 @@ class Config(object):
# Auto continue from watch history # Auto continue from watch history
continue_from_history = {self.continue_from_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) # Preferred language for anime (options: dub, sub)
translation_type = {self.translation_type} 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 # whether to show a preview window when using fzf or rofi
preview = {self.preview} 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. # whether to use fzf as the interface for the anilist command and others.
use_fzf = {self.use_fzf} use_fzf = {self.use_fzf}
@@ -311,9 +329,12 @@ use_rofi = {self.use_rofi}
# rofi theme to use # rofi theme to use
rofi_theme = {self.rofi_theme} rofi_theme = {self.rofi_theme}
rofi_theme_input = {self.rofi_theme_input} rofi_theme_input = {self.rofi_theme_input}
rofi_theme_confirm = {self.rofi_theme_confirm} rofi_theme_confirm = {self.rofi_theme_confirm}
# whether to show the icons # whether to show the icons
icons = {self.icons} icons = {self.icons}

View File

@@ -31,6 +31,7 @@ if TYPE_CHECKING:
from ..utils.tools import FastAnimeRuntimeState from ..utils.tools import FastAnimeRuntimeState
# TODO: make the error handling more sane
def calculate_time_delta(start_time, end_time): def calculate_time_delta(start_time, end_time):
"""helper function used to calculate the difference between two timestamps in seconds """helper function used to calculate the difference between two timestamps in seconds
@@ -97,8 +98,14 @@ def media_player_controls(
current_episode_number, current_episode_number,
) )
start_time = config.watch_history[str(anime_id_anilist)]["start_time"] if (
print("[green]Continuing from:[/] ", start_time) 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 = [] custom_args = []
if config.skip: if config.skip:
if args := aniskip( if args := aniskip(
@@ -365,35 +372,18 @@ def provider_anime_episode_servers_menu(
# no need to get all servers if top just works # no need to get all servers if top just works
with Progress() as progress: with Progress() as progress:
progress.add_task("Fetching top server...", total=None) progress.add_task("Fetching top server...", total=None)
try: selected_server = next(episode_streams_generator, None)
selected_server = next(episode_streams_generator, None) if not selected_server:
if not selected_server: if config.use_rofi:
if config.use_rofi: if Rofi.confirm("Sth went wrong enter to continue"):
if Rofi.confirm("Sth went wrong enter to continue"): media_actions_menu(config, fastanime_runtime_state)
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...")
else: else:
if not Rofi.confirm(f"!!Sth went wrong!!: {e} Enter to continue"): exit_app(1)
exit_app(1) else:
server_name = None print("Sth went wrong")
selected_server = "" input("Enter to continue...")
media_actions_menu(config, fastanime_runtime_state) media_actions_menu(config, fastanime_runtime_state)
return return
else: else:
with Progress() as progress: with Progress() as progress:
progress.add_task("Fetching servers...", total=None) progress.add_task("Fetching servers...", total=None)
@@ -402,6 +392,17 @@ def provider_anime_episode_servers_menu(
for episode_stream in episode_streams_generator 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 # 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(): if config.server and config.server in episode_streams_dict.keys():
server_name = config.server server_name = config.server
@@ -478,7 +479,7 @@ def provider_anime_episode_servers_menu(
AniList.update_anime_list( AniList.update_anime_list(
{ {
"mediaId": anime_id_anilist, "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 = config.watch_history.get(str(anime_id_anilist), {}).get(
"start_time", "0" "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) print("[green]Continuing from:[/] ", start_time)
custom_args = [] custom_args = []
if config.skip: if config.skip:
@@ -512,7 +516,7 @@ def provider_anime_episode_servers_menu(
script_opts = custom_args[1].split("=", 1) script_opts = custom_args[1].split("=", 1)
mpv._set_property("chapters-file", chapters_file[1]) mpv._set_property("chapters-file", chapters_file[1])
mpv._set_property("script-opts", script_opts[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.start = start_time
mpv.wait_for_shutdown() mpv.wait_for_shutdown()
mpv.terminate() mpv.terminate()
@@ -520,6 +524,8 @@ def provider_anime_episode_servers_menu(
total_time = player.last_total_time total_time = player.last_total_time
current_episode_number = fastanime_runtime_state.provider_current_episode_number current_episode_number = fastanime_runtime_state.provider_current_episode_number
else: else:
if not episode_in_history == current_episode_number:
start_time = "0"
stop_time, total_time = run_mpv( stop_time, total_time = run_mpv(
current_stream_link, current_stream_link,
selected_server["episode_title"], 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 will try to update the episode to be the next episode if delta has reached a specific threshhold
# this update will only apply locally # this update will only apply locally
# the remote(anilist) is only updated when its certain you are going to open the player # 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": if stop_time == "0" or total_time == "0":
# increment the episode # increment the episodes
episode = str(int(current_episode_number) + 1) 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: else:
error = config.error * 60 error = config.error * 60
delta = calculate_time_delta(stop_time, total_time) delta = calculate_time_delta(stop_time, total_time)
if delta.total_seconds() > error: if delta.total_seconds() > error:
episode = current_episode_number episode = current_episode_number
else: 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" stop_time = "0"
total_time = "0" total_time = "0"
@@ -592,9 +608,19 @@ def provider_anime_episodes_menu(
user_watch_history.get(str(anime_id_anilist), {}).get("episode") user_watch_history.get(str(anime_id_anilist), {}).get("episode")
in total_episodes in total_episodes
): ):
current_episode_number = user_watch_history[str(anime_id_anilist)][ if (
"episode" 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( print(
f"[bold cyan]Continuing from Episode:[/] [bold]{current_episode_number}[/]" f"[bold cyan]Continuing from Episode:[/] [bold]{current_episode_number}[/]"
) )
@@ -616,7 +642,7 @@ def provider_anime_episodes_menu(
current_episode_number = "" current_episode_number = ""
# prompt for episode number if not set # 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"] choices = [*total_episodes, "Back"]
if config.use_fzf: if config.use_fzf:
current_episode_number = fzf.run( current_episode_number = fzf.run(
@@ -652,7 +678,6 @@ def provider_anime_episodes_menu(
provider_anime_episode_servers_menu(config, fastanime_runtime_state) 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"): def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"):
selected_anime: "SearchResult" = ( selected_anime: "SearchResult" = (
fastanime_runtime_state.provider_anime_search_result fastanime_runtime_state.provider_anime_search_result
@@ -672,8 +697,7 @@ def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"
else: else:
if not Rofi.confirm("Sth went wrong!!Enter to continue..."): if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
exit(1) exit(1)
fetch_anime_episode(config, fastanime_runtime_state) return fetch_anime_episode(config, fastanime_runtime_state)
return
fastanime_runtime_state.provider_anime = provider_anime fastanime_runtime_state.provider_anime = provider_anime
provider_anime_episodes_menu(config, fastanime_runtime_state) provider_anime_episodes_menu(config, fastanime_runtime_state)
@@ -719,8 +743,7 @@ def anime_provider_search_results_menu(
else: else:
if not Rofi.confirm("Sth went wrong!!Enter to continue..."): if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
exit(1) exit(1)
anime_provider_search_results_menu(config, fastanime_runtime_state) return anime_provider_search_results_menu(config, fastanime_runtime_state)
return
provider_search_results = { provider_search_results = {
anime["title"]: anime for anime in provider_search_results["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 auto next episode": _toggle_auto_next,
f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history, f"{'🔘 ' if icons else ''}Toggle continue from history": _toggle_continue_from_history,
f"{'🔙 ' if icons else ''}Back": anilist_results_menu, 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()) choices = list(options.keys())
if config.use_fzf: if config.use_fzf:
@@ -1176,6 +1199,7 @@ def anilist_results_menu(
anime["status"] == "RELEASING" anime["status"] == "RELEASING"
and anime["nextAiringEpisode"] and anime["nextAiringEpisode"]
and progress > 0 and progress > 0
and anime["mediaListEntry"]
): ):
last_aired_episode = anime["nextAiringEpisode"]["episode"] - 1 last_aired_episode = anime["nextAiringEpisode"]["episode"] - 1
if last_aired_episode - progress > 0: if last_aired_episode - progress > 0:

View File

@@ -12,89 +12,11 @@ from yt_dlp.utils import clean_html
from ...constants import APP_CACHE_DIR from ...constants import APP_CACHE_DIR
from ...libs.anilist.types import AnilistBaseMediaDataSchema from ...libs.anilist.types import AnilistBaseMediaDataSchema
from ...Utility import anilist_data_helper from ...Utility import anilist_data_helper
from ..utils.scripts import fzf_preview
from ..utils.utils import get_true_fg from ..utils.utils import get_true_fg
logger = logging.getLogger(__name__) 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 ---- # ---- aniskip intergration ----
def aniskip(mal_id: int, episode: str): def aniskip(mal_id: int, episode: str):

View File

@@ -101,7 +101,7 @@ class MpvPlayer(object):
AniList.update_anime_list( AniList.update_anime_list(
{ {
"mediaId": anime_id_anilist, "mediaId": anime_id_anilist,
"progress": current_episode_number, "progress": int(float(current_episode_number)),
} }
) )
# get them juicy streams # get them juicy streams
@@ -141,6 +141,7 @@ class MpvPlayer(object):
return return
self.mpv_player._set_property("start", "0") self.mpv_player._set_property("start", "0")
stream_link = stream_link_["link"] stream_link = stream_link_["link"]
fastanime_runtime_state.provider_current_episode_stream_link = stream_link
return stream_link return stream_link
def create_player( def create_player(
@@ -234,6 +235,7 @@ class MpvPlayer(object):
@mpv_player.on_key_press("shift+t") @mpv_player.on_key_press("shift+t")
def _toggle_translation_type(): def _toggle_translation_type():
translation_type = "sub" if config.translation_type == "dub" else "dub" translation_type = "sub" if config.translation_type == "dub" else "dub"
mpv_player.show_text("Changing translation type...")
anime = anime_provider.get_anime( anime = anime_provider.get_anime(
fastanime_runtime_state.provider_anime_search_result["id"], fastanime_runtime_state.provider_anime_search_result["id"],
fastanime_runtime_state.selected_anime_anilist, fastanime_runtime_state.selected_anime_anilist,

View 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
}
"""

View File

@@ -19,7 +19,7 @@ BG_GREEN = "\033[48;2;120;233;12;m"
GREEN = "\033[38;2;45;24;45;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 """Helper function used to filter a list of EpisodeStream objects to one that has a corresponding quality
Args: 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 an EpisodeStream object or None incase the quality was not found
""" """
for stream_link in stream_links: 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 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"): def format_bytes_to_human(num_of_bytes: float, suffix: str = "B"):

View File

@@ -9,7 +9,6 @@ PLATFORM = system()
# ---- app deps ---- # ---- app deps ----
APP_DIR = os.path.abspath(os.path.dirname(__file__)) 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") ASSETS_DIR = os.path.join(APP_DIR, "assets")
@@ -24,7 +23,6 @@ PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
# ----- user configs and data ----- # ----- user configs and data -----
S_PLATFORM = sys.platform S_PLATFORM = sys.platform
if S_PLATFORM == "win32": if S_PLATFORM == "win32":
# app data # app data
app_data_dir_base = os.getenv("LOCALAPPDATA") 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") APP_CACHE_DIR = os.path.join(APP_DATA_DIR, "cache")
# videos dir # 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) USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
elif S_PLATFORM == "darwin": elif S_PLATFORM == "darwin":

View File

@@ -231,7 +231,7 @@ $type:MediaType\
search_query = ( search_query = (
""" """
query($query:String,%s){ query($query:String,%s){
Page(perPage:30,page:$page){ Page(perPage:50,page:$page){
pageInfo{ pageInfo{
total total
currentPage currentPage

View File

@@ -89,27 +89,28 @@ class AnimePaheApi(AnimeProvider):
if response.status_code == 200: if response.status_code == 200:
if not data: if not data:
data.update(response.json()) data.update(response.json())
if ep_data := response.json().get("data"): else:
data["data"].extend(ep_data) if ep_data := response.json().get("data"):
if data["next_page_url"]: data["data"].extend(ep_data)
# TODO: Refine this if response.json()["next_page_url"]:
time.sleep( # TODO: Refine this
random.choice( time.sleep(
[ random.choice(
0.25, [
0.1, 0.25,
0.5, 0.1,
0.75, 0.5,
1, 0.75,
] 1,
) ]
)
page += 1
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
_pages_loader(
url,
page,
) )
)
page += 1
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
_pages_loader(
url,
page,
)
_pages_loader( _pages_loader(
url, url,
@@ -119,7 +120,7 @@ class AnimePaheApi(AnimeProvider):
if not data: if not data:
return {} return {}
self.anime = data # pyright:ignore self.anime = data # pyright:ignore
episodes = list(map(str, range(data["total"]))) episodes = list(map(str, [episode["episode"] for episode in data["data"]]))
title = "" title = ""
return { return {
"id": session_id, "id": session_id,

View File

@@ -36,7 +36,7 @@ hex_to_char = {
def give_random_quality(links: list[dict]): def give_random_quality(links: list[dict]):
qualities = cycle(["1080", "720", "360"]) qualities = cycle(["1080", "720", "480", "360"])
return [ return [
{"link": link["link"], "quality": quality} {"link": link["link"], "quality": quality}

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "fastanime" name = "fastanime"
version = "1.1.5.dev1" version = "1.7.1"
description = "A browser anime site experience from the terminal" description = "A browser anime site experience from the terminal"
authors = ["Benextempest <benextempest@gmail.com>"] authors = ["Benextempest <benextempest@gmail.com>"]
license = "UNLICENSE" license = "UNLICENSE"