mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-08 13:50:40 -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.
|
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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"]
|
||||||
)
|
)
|
||||||
|
|||||||
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",
|
"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}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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"
|
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"):
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user