mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-13 00:00:01 -08:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ec3136734 | ||
|
|
943fca43cf | ||
|
|
b2e00feb94 | ||
|
|
f726c8d55c | ||
|
|
57db2e0626 | ||
|
|
40f66b5fde | ||
|
|
c87417e5e7 | ||
|
|
a841dd6f66 | ||
|
|
d6e85bad5c | ||
|
|
b590ac1e91 | ||
|
|
9cfa3aeea5 | ||
|
|
18c60691ca | ||
|
|
2e9fadf3b2 | ||
|
|
510b47b187 | ||
|
|
49c4d0eec0 | ||
|
|
8367f7bbed | ||
|
|
0182f674e0 | ||
|
|
2b50fb4c97 | ||
|
|
2602a20aa7 | ||
|
|
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 |
125
README.md
125
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,29 @@ 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
|
||||||
|
- `--sync-play` or `-sp` use syncplay for streaming anime so you can watch with your friends
|
||||||
|
|
||||||
|
Example usage of the above options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# example of syncplay intergration
|
||||||
|
fastanime --sync-play --server sharepoint search -t <anime-title>
|
||||||
|
|
||||||
|
# --- or ---
|
||||||
|
|
||||||
|
# to watch with anilist intergration
|
||||||
|
fastanime --sync-play --server sharepoint anilist
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
@@ -281,12 +307,14 @@ end
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> To sign in just run `fastanime anilist login` and follow the instructions.
|
> To sign in just run `fastanime anilist login` and follow the instructions.
|
||||||
> To view your login status `fastanime anilist login --status`
|
> To view your login status `fastanime anilist login --status`
|
||||||
|
> To erase login data `fastanime anilist login --erase`
|
||||||
|
|
||||||
#### download subcommand
|
#### download subcommand
|
||||||
|
|
||||||
Download anime to watch later dub or sub with this one command.
|
Download anime to watch later dub or sub with this one command.
|
||||||
Its optimized for scripting due to fuzzy matching.
|
Its optimized for scripting due to fuzzy matching; basically you don't have to manually select search results.
|
||||||
So every step of the way has been and can be automated.
|
So every step of the way has been and can be automated.
|
||||||
|
Uses a list slicing syntax similar to that of python as the value for the `-r` option.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
>
|
>
|
||||||
@@ -297,29 +325,57 @@ So every step of the way has been and can be automated.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Download all available episodes
|
# Download all available episodes
|
||||||
fastanime download <anime-title>
|
# multiple titles can be specified with -t option
|
||||||
|
fastanime download -t <anime-title> -t <anime-title>
|
||||||
|
# -- or --
|
||||||
|
fastanime download -t <anime-title> -t <anime-title> -r ':'
|
||||||
|
|
||||||
|
# download latest episode for the two anime titles
|
||||||
|
# the number can be any no of latest episodes but a minus sign
|
||||||
|
# must be present
|
||||||
|
fastanime download -t <anime-title> -t <anime-title> -r '-1'
|
||||||
|
|
||||||
|
# latest 5
|
||||||
|
fastanime download -t <anime-title> -t <anime-title> -r '-5'
|
||||||
|
|
||||||
# Download specific episode range
|
# Download specific episode range
|
||||||
# be sure to observe the range Syntax
|
# be sure to observe the range Syntax
|
||||||
fastanime download <anime-title> -r <episodes-start>-<episodes-end>
|
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>:<step>'
|
||||||
|
|
||||||
|
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>'
|
||||||
|
|
||||||
|
fastanime download <anime-title> -r '<episodes-start>:'
|
||||||
|
|
||||||
|
fastanime download <anime-title> -r ':<episodes-end>'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### search subcommand
|
#### search subcommand
|
||||||
|
|
||||||
Powerful command mainly aimed at binging anime. Since it doesn't require interaction with the interfaces.
|
Powerful command mainly aimed at binging anime. Since it doesn't require interaction with the interfaces.
|
||||||
|
Uses a list slicing syntax similar to that of python as the value of the `-r` option.
|
||||||
|
|
||||||
**Syntax:**
|
**Syntax:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# basic form where you will still be prompted for the episode number
|
# basic form where you will still be prompted for the episode number
|
||||||
fastanime search <anime-title>
|
# multiple titles can be specified with the -t option
|
||||||
|
fastanime search -t <anime-title> -t <anime-title>
|
||||||
|
|
||||||
# binge all episodes with this command
|
# binge all episodes with this command
|
||||||
fastanime search <anime-title> -r -
|
fastanime search -t <anime-title> -r ':'
|
||||||
|
|
||||||
|
# watch latest episode
|
||||||
|
fastanime search -t <anime-title> -r '-1'
|
||||||
|
|
||||||
# binge a specific episode range with this command
|
# binge a specific episode range with this command
|
||||||
# be sure to observe the range Syntax
|
# be sure to observe the range Syntax
|
||||||
fastanime search <anime-title> -r <episodes-start>-<episodes-end>
|
fastanime search -t <anime-title> -r '<start>:<stop>'
|
||||||
|
|
||||||
|
fastanime search -t <anime-title> -r '<start>:<stop>:<step>'
|
||||||
|
|
||||||
|
fastanime search -t <anime-title> -r '<start>:'
|
||||||
|
|
||||||
|
fastanime search -t <anime-title> -r ':<end>'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### downloads subcommand
|
#### downloads subcommand
|
||||||
@@ -331,9 +387,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 +502,47 @@ 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
|
||||||
|
|
||||||
|
# force mpv window
|
||||||
|
# passed directly to mpv so values are same
|
||||||
|
force_window = immediate
|
||||||
|
|
||||||
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 +558,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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def sort_by_episode_number(filename: str):
|
||||||
|
import re
|
||||||
|
|
||||||
|
match = re.search(r"\d+", filename)
|
||||||
|
return int(match.group()) if match else 0
|
||||||
|
|
||||||
|
|
||||||
def anime_title_percentage_match(
|
def anime_title_percentage_match(
|
||||||
possible_user_requested_anime_title: str, anime: "AnilistBaseMediaDataSchema"
|
possible_user_requested_anime_title: str, anime: "AnilistBaseMediaDataSchema"
|
||||||
) -> float:
|
) -> float:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
|||||||
) # noqa: F541
|
) # noqa: F541
|
||||||
|
|
||||||
|
|
||||||
__version__ = "v1.1.7"
|
__version__ = "v2.0.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(
|
||||||
@@ -129,6 +141,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--use-mpv-mod/--use-default-player", help="Whether to use python-mpv", type=bool
|
"--use-mpv-mod/--use-default-player", help="Whether to use python-mpv", type=bool
|
||||||
)
|
)
|
||||||
|
@click.option("--sync-play", "-sp", help="Use sync play", is_flag=True)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def run_cli(
|
def run_cli(
|
||||||
ctx: click.Context,
|
ctx: click.Context,
|
||||||
@@ -139,6 +152,7 @@ def run_cli(
|
|||||||
server,
|
server,
|
||||||
format,
|
format,
|
||||||
continue_,
|
continue_,
|
||||||
|
local_history,
|
||||||
skip,
|
skip,
|
||||||
translation_type,
|
translation_type,
|
||||||
quality,
|
quality,
|
||||||
@@ -158,6 +172,7 @@ def run_cli(
|
|||||||
rofi_theme_confirm,
|
rofi_theme_confirm,
|
||||||
rofi_theme_input,
|
rofi_theme_input,
|
||||||
use_mpv_mod,
|
use_mpv_mod,
|
||||||
|
sync_play,
|
||||||
):
|
):
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
@@ -192,6 +207,8 @@ def run_cli(
|
|||||||
|
|
||||||
install()
|
install()
|
||||||
|
|
||||||
|
if sync_play:
|
||||||
|
ctx.obj.sync_play = sync_play
|
||||||
if provider:
|
if provider:
|
||||||
ctx.obj.provider = provider
|
ctx.obj.provider = provider
|
||||||
if server:
|
if server:
|
||||||
@@ -209,6 +226,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
|
||||||
|
|||||||
@@ -8,41 +8,54 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
@click.command(help="Login to your anilist account")
|
@click.command(help="Login to your anilist account")
|
||||||
@click.option("--status", "-s", help="Whether you are logged in or not", is_flag=True)
|
@click.option("--status", "-s", help="Whether you are logged in or not", is_flag=True)
|
||||||
|
@click.option("--erase", "-e", help="Erase your login details", is_flag=True)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def login(config: "Config", status):
|
def login(config: "Config", status, erase):
|
||||||
from click import launch
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.prompt import Confirm, Prompt
|
from rich.prompt import Confirm, Prompt
|
||||||
|
|
||||||
from ....anilist import AniList
|
|
||||||
from ...utils.tools import exit_app
|
from ...utils.tools import exit_app
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
is_logged_in = True if config.user else False
|
is_logged_in = True if config.user else False
|
||||||
message = (
|
message = (
|
||||||
"You are logged in :happy:" if is_logged_in else "You arent logged in :cry"
|
"You are logged in :smile:" if is_logged_in else "You arent logged in :cry:"
|
||||||
)
|
)
|
||||||
print(message)
|
print(message)
|
||||||
print(config.user)
|
print(config.user)
|
||||||
exit_app()
|
exit_app()
|
||||||
if config.user:
|
elif erase:
|
||||||
print("Already logged in :confused:")
|
if Confirm.ask(
|
||||||
if not Confirm.ask("or would you like to reloggin", default=True):
|
"Are you sure you want to erase your login status", default=False
|
||||||
|
):
|
||||||
|
config.update_user({})
|
||||||
|
print("Success")
|
||||||
|
exit_app(0)
|
||||||
|
else:
|
||||||
|
exit_app(1)
|
||||||
|
else:
|
||||||
|
from click import launch
|
||||||
|
|
||||||
|
from ....anilist import AniList
|
||||||
|
|
||||||
|
if config.user:
|
||||||
|
print("Already logged in :confused:")
|
||||||
|
if not Confirm.ask("or would you like to reloggin", default=True):
|
||||||
|
exit_app()
|
||||||
|
# ---- new loggin -----
|
||||||
|
print(
|
||||||
|
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
||||||
|
)
|
||||||
|
launch(config.fastanime_anilist_app_login_url, wait=True)
|
||||||
|
print("Please paste the token provided here")
|
||||||
|
token = Prompt.ask("Enter token")
|
||||||
|
user = AniList.login_user(token)
|
||||||
|
if not user:
|
||||||
|
print("Sth went wrong", user)
|
||||||
exit_app()
|
exit_app()
|
||||||
# ---- new loggin -----
|
return
|
||||||
print(
|
user["token"] = token
|
||||||
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
config.update_user(user)
|
||||||
)
|
print("Successfully saved credentials")
|
||||||
launch(config.fastanime_anilist_app_login_url, wait=True)
|
print(user)
|
||||||
print("Please paste the token provided here")
|
|
||||||
token = Prompt.ask("Enter token")
|
|
||||||
user = AniList.login_user(token)
|
|
||||||
if not user:
|
|
||||||
print("Sth went wrong", user)
|
|
||||||
exit_app()
|
exit_app()
|
||||||
return
|
|
||||||
user["token"] = token
|
|
||||||
config.update_user(user)
|
|
||||||
print("Successfully saved credentials")
|
|
||||||
print(user)
|
|
||||||
exit_app()
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -11,24 +13,25 @@ if TYPE_CHECKING:
|
|||||||
help="Download anime using the anime provider for a specified range",
|
help="Download anime using the anime provider for a specified range",
|
||||||
short_help="Download anime",
|
short_help="Download anime",
|
||||||
)
|
)
|
||||||
@click.argument(
|
@click.option(
|
||||||
"anime-title",
|
"--anime-titles",
|
||||||
|
"--anime_title",
|
||||||
|
"-t",
|
||||||
required=True,
|
required=True,
|
||||||
|
shell_complete=anime_titles_shell_complete,
|
||||||
|
multiple=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(
|
||||||
from click import clear
|
config: "Config",
|
||||||
|
anime_titles: list,
|
||||||
|
episode_range,
|
||||||
|
):
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.progress import Progress
|
from rich.progress import Progress
|
||||||
from thefuzz import fuzz
|
from thefuzz import fuzz
|
||||||
@@ -45,120 +48,156 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
|||||||
translation_type = config.translation_type
|
translation_type = config.translation_type
|
||||||
download_dir = config.downloads_dir
|
download_dir = config.downloads_dir
|
||||||
|
|
||||||
# ---- search for anime ----
|
print(f"[green bold]Queued:[/] {anime_titles}")
|
||||||
with Progress() as progress:
|
for anime_title in anime_titles:
|
||||||
progress.add_task("Fetching Search Results...", total=None)
|
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
||||||
search_results = anime_provider.search_for_anime(
|
# ---- search for anime ----
|
||||||
anime_title, translation_type=translation_type
|
with Progress() as progress:
|
||||||
)
|
progress.add_task("Fetching Search Results...", total=None)
|
||||||
if not search_results:
|
search_results = anime_provider.search_for_anime(
|
||||||
print("Search results failed")
|
anime_title, translation_type=translation_type
|
||||||
input("Enter to retry")
|
)
|
||||||
download(config, anime_title, episode_range, highest_priority)
|
if not search_results:
|
||||||
return
|
print("Search results failed")
|
||||||
search_results = search_results["results"]
|
input("Enter to retry")
|
||||||
search_results_ = {
|
download(
|
||||||
search_result["title"]: search_result for search_result in search_results
|
config,
|
||||||
}
|
anime_title,
|
||||||
|
episode_range,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
search_results = search_results["results"]
|
||||||
|
search_results_ = {
|
||||||
|
search_result["title"]: search_result for search_result in search_results
|
||||||
|
}
|
||||||
|
|
||||||
if config.auto_select:
|
if config.auto_select:
|
||||||
search_result = max(
|
search_result = max(
|
||||||
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
|
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
|
||||||
)
|
)
|
||||||
print("[cyan]Auto selecting:[/] ", search_result)
|
print("[cyan]Auto selecting:[/] ", search_result)
|
||||||
else:
|
|
||||||
choices = list(search_results_.keys())
|
|
||||||
if config.use_fzf:
|
|
||||||
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
|
|
||||||
else:
|
else:
|
||||||
search_result = fuzzy_inquirer(
|
choices = list(search_results_.keys())
|
||||||
choices,
|
if config.use_fzf:
|
||||||
"Please Select title",
|
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
|
||||||
)
|
|
||||||
|
|
||||||
# ---- fetch anime ----
|
|
||||||
with Progress() as progress:
|
|
||||||
progress.add_task("Fetching Anime...", total=None)
|
|
||||||
anime: Anime | None = anime_provider.get_anime(
|
|
||||||
search_results_[search_result]["id"]
|
|
||||||
)
|
|
||||||
if not anime:
|
|
||||||
print("Sth went wring anime no found")
|
|
||||||
input("Enter to continue...")
|
|
||||||
download(config, anime_title, episode_range, highest_priority)
|
|
||||||
return
|
|
||||||
|
|
||||||
episodes = anime["availableEpisodesDetail"][config.translation_type]
|
|
||||||
if episode_range:
|
|
||||||
episodes_start, episodes_end = episode_range.split("-")
|
|
||||||
|
|
||||||
else:
|
|
||||||
episodes_start, episodes_end = 0, len(episodes)
|
|
||||||
for episode in range(round(float(episodes_start)), round(float(episodes_end))):
|
|
||||||
try:
|
|
||||||
episode = str(episode)
|
|
||||||
if episode not in episodes:
|
|
||||||
print(f"[cyan]Warning[/]: Episode {episode} not found, skipping")
|
|
||||||
continue
|
|
||||||
with Progress() as progress:
|
|
||||||
progress.add_task("Fetching Episode Streams...", total=None)
|
|
||||||
streams = anime_provider.get_episode_streams(
|
|
||||||
anime, episode, config.translation_type
|
|
||||||
)
|
|
||||||
if not streams:
|
|
||||||
print("No streams skipping")
|
|
||||||
continue
|
|
||||||
# ---- fetch servers ----
|
|
||||||
if config.server == "top":
|
|
||||||
with Progress() as progress:
|
|
||||||
progress.add_task("Fetching top server...", total=None)
|
|
||||||
server = next(streams, None)
|
|
||||||
if not server:
|
|
||||||
print("Sth went wrong when fetching the server")
|
|
||||||
continue
|
|
||||||
stream_link = filter_by_quality(config.quality, server["links"])
|
|
||||||
if not stream_link:
|
|
||||||
print("Quality not found")
|
|
||||||
input("Enter to continue")
|
|
||||||
continue
|
|
||||||
link = stream_link["link"]
|
|
||||||
episode_title = server["episode_title"]
|
|
||||||
else:
|
else:
|
||||||
with Progress() as progress:
|
search_result = fuzzy_inquirer(
|
||||||
progress.add_task("Fetching servers", total=None)
|
choices,
|
||||||
# prompt for server selection
|
"Please Select title",
|
||||||
servers = {server["server"]: server for server in streams}
|
|
||||||
servers_names = list(servers.keys())
|
|
||||||
if config.use_fzf:
|
|
||||||
server = fzf.run(servers_names, "Select an link: ")
|
|
||||||
else:
|
|
||||||
server = fuzzy_inquirer(
|
|
||||||
servers_names,
|
|
||||||
"Select link",
|
|
||||||
)
|
|
||||||
stream_link = filter_by_quality(
|
|
||||||
config.quality, servers[server]["links"]
|
|
||||||
)
|
)
|
||||||
if not stream_link:
|
|
||||||
print("Quality not found")
|
|
||||||
continue
|
|
||||||
link = stream_link["link"]
|
|
||||||
|
|
||||||
episode_title = servers[server]["episode_title"]
|
# ---- fetch anime ----
|
||||||
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
|
with Progress() as progress:
|
||||||
|
progress.add_task("Fetching Anime...", total=None)
|
||||||
downloader._download_file(
|
anime: Anime | None = anime_provider.get_anime(
|
||||||
link,
|
search_results_[search_result]["id"]
|
||||||
anime["title"],
|
|
||||||
episode_title,
|
|
||||||
download_dir,
|
|
||||||
True,
|
|
||||||
config.format,
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
if not anime:
|
||||||
print(e)
|
print("Sth went wring anime no found")
|
||||||
time.sleep(1)
|
input("Enter to continue...")
|
||||||
print("Continuing")
|
download(
|
||||||
clear()
|
config,
|
||||||
|
anime_title,
|
||||||
|
episode_range,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
episodes = sorted(
|
||||||
|
anime["availableEpisodesDetail"][config.translation_type], key=float
|
||||||
|
)
|
||||||
|
if episode_range:
|
||||||
|
if ":" in episode_range:
|
||||||
|
ep_range_tuple = episode_range.split(":")
|
||||||
|
if len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
episodes_range = episodes[int(episodes_start) : int(episodes_end)]
|
||||||
|
elif len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end, step = ep_range_tuple
|
||||||
|
episodes_range = episodes[
|
||||||
|
int(episodes_start) : int(episodes_end) : int(step)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
if episodes_start.strip():
|
||||||
|
episodes_range = episodes[int(episodes_start) :]
|
||||||
|
elif episodes_end.strip():
|
||||||
|
episodes_range = episodes[: int(episodes_end)]
|
||||||
|
else:
|
||||||
|
episodes_range = episodes
|
||||||
|
else:
|
||||||
|
episodes_range = episodes[int(episode_range) :]
|
||||||
|
print(f"[green bold]Downloading: [/] {episodes_range}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
episodes_range = sorted(episodes, key=float)
|
||||||
|
|
||||||
|
for episode in episodes_range:
|
||||||
|
try:
|
||||||
|
episode = str(episode)
|
||||||
|
if episode not in episodes:
|
||||||
|
print(f"[cyan]Warning[/]: Episode {episode} not found, skipping")
|
||||||
|
continue
|
||||||
|
with Progress() as progress:
|
||||||
|
progress.add_task("Fetching Episode Streams...", total=None)
|
||||||
|
streams = anime_provider.get_episode_streams(
|
||||||
|
anime, episode, config.translation_type
|
||||||
|
)
|
||||||
|
if not streams:
|
||||||
|
print("No streams skipping")
|
||||||
|
continue
|
||||||
|
# ---- fetch servers ----
|
||||||
|
if config.server == "top":
|
||||||
|
with Progress() as progress:
|
||||||
|
progress.add_task("Fetching top server...", total=None)
|
||||||
|
server = next(streams, None)
|
||||||
|
if not server:
|
||||||
|
print("Sth went wrong when fetching the server")
|
||||||
|
continue
|
||||||
|
stream_link = filter_by_quality(config.quality, server["links"])
|
||||||
|
if not stream_link:
|
||||||
|
print("Quality not found")
|
||||||
|
input("Enter to continue")
|
||||||
|
continue
|
||||||
|
link = stream_link["link"]
|
||||||
|
episode_title = server["episode_title"]
|
||||||
|
else:
|
||||||
|
with Progress() as progress:
|
||||||
|
progress.add_task("Fetching servers", total=None)
|
||||||
|
# prompt for server selection
|
||||||
|
servers = {server["server"]: server for server in streams}
|
||||||
|
servers_names = list(servers.keys())
|
||||||
|
if config.server in servers_names:
|
||||||
|
server = config.server
|
||||||
|
else:
|
||||||
|
if config.use_fzf:
|
||||||
|
server = fzf.run(servers_names, "Select an link: ")
|
||||||
|
else:
|
||||||
|
server = fuzzy_inquirer(
|
||||||
|
servers_names,
|
||||||
|
"Select link",
|
||||||
|
)
|
||||||
|
stream_link = filter_by_quality(
|
||||||
|
config.quality, servers[server]["links"]
|
||||||
|
)
|
||||||
|
if not stream_link:
|
||||||
|
print("Quality not found")
|
||||||
|
continue
|
||||||
|
link = stream_link["link"]
|
||||||
|
|
||||||
|
episode_title = servers[server]["episode_title"]
|
||||||
|
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
|
||||||
|
|
||||||
|
downloader._download_file(
|
||||||
|
link,
|
||||||
|
anime["title"],
|
||||||
|
episode_title,
|
||||||
|
download_dir,
|
||||||
|
True,
|
||||||
|
config.format,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
time.sleep(1)
|
||||||
|
print("Continuing")
|
||||||
print("Done Downloading")
|
print("Done Downloading")
|
||||||
exit_app()
|
exit_app()
|
||||||
|
|||||||
@@ -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,16 +12,27 @@ 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
|
||||||
from ...libs.fzf import fzf
|
from ...libs.fzf import fzf
|
||||||
from ...libs.rofi import Rofi
|
from ...libs.rofi import Rofi
|
||||||
|
from ...Utility.utils import sort_by_episode_number
|
||||||
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 +40,258 @@ 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 = sorted(
|
||||||
playlists.append("Exit")
|
os.listdir(USER_VIDEOS_DIR),
|
||||||
|
)
|
||||||
|
anime_downloads.append("Exit")
|
||||||
|
|
||||||
def stream():
|
def create_thumbnails(video_path, anime_title, downloads_thumbnail_cache_dir):
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
FFMPEG_THUMBNAILER = shutil.which("ffmpegthumbnailer")
|
||||||
|
if not FFMPEG_THUMBNAILER:
|
||||||
|
return
|
||||||
|
|
||||||
|
out = os.path.join(downloads_thumbnail_cache_dir, anime_title)
|
||||||
|
if ffmpegthumbnailer_seek_time == -1:
|
||||||
|
import random
|
||||||
|
|
||||||
|
seektime = str(random.randrange(0, 100))
|
||||||
|
else:
|
||||||
|
seektime = str(ffmpegthumbnailer_seek_time)
|
||||||
|
_ = subprocess.run(
|
||||||
|
[
|
||||||
|
FFMPEG_THUMBNAILER,
|
||||||
|
"-i",
|
||||||
|
video_path,
|
||||||
|
"-o",
|
||||||
|
out,
|
||||||
|
"-s",
|
||||||
|
"0",
|
||||||
|
"-t",
|
||||||
|
seektime,
|
||||||
|
],
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_previews_anime(workers=None, bg=True):
|
||||||
|
import concurrent.futures
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
if not shutil.which("ffmpegthumbnailer"):
|
||||||
|
print("ffmpegthumbnailer not found")
|
||||||
|
logger.error("ffmpegthumbnailer not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
from ...constants import APP_CACHE_DIR
|
||||||
|
from ..utils.scripts import fzf_preview
|
||||||
|
|
||||||
|
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||||
|
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _worker():
|
||||||
|
# use concurrency to download the images as fast as possible
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
|
# load the jobs
|
||||||
|
future_to_url = {}
|
||||||
|
for anime_title in anime_downloads:
|
||||||
|
anime_path = os.path.join(USER_VIDEOS_DIR, anime_title)
|
||||||
|
if not os.path.isdir(anime_path):
|
||||||
|
continue
|
||||||
|
playlist = sorted(
|
||||||
|
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 = sorted(
|
||||||
|
os.listdir(anime_playlist_path), key=sort_by_episode_number
|
||||||
|
)
|
||||||
|
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 = sorted(
|
||||||
|
os.listdir(anime_playlist_path), key=sort_by_episode_number
|
||||||
|
)
|
||||||
|
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,20 +1,28 @@
|
|||||||
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(
|
||||||
help="This subcommand directly interacts with the provider to enable basic streaming. Useful for binging anime.",
|
help="This subcommand directly interacts with the provider to enable basic streaming. Useful for binging anime.",
|
||||||
short_help="Binge anime",
|
short_help="Binge anime",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--anime-titles",
|
||||||
|
"--anime_title",
|
||||||
|
"-t",
|
||||||
|
required=True,
|
||||||
|
shell_complete=anime_titles_shell_complete,
|
||||||
|
multiple=True,
|
||||||
|
)
|
||||||
@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, type=str)
|
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def search(config: Config, anime_title: str, episode_range: str):
|
def search(config: Config, anime_titles: str, episode_range: str):
|
||||||
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
|
||||||
@@ -30,151 +38,184 @@ def search(config: Config, anime_title: str, episode_range: str):
|
|||||||
|
|
||||||
anime_provider = AnimeProvider(config.provider)
|
anime_provider = AnimeProvider(config.provider)
|
||||||
|
|
||||||
# ---- search for anime ----
|
print(f"[green bold]Streaming:[/] {anime_titles}")
|
||||||
with Progress() as progress:
|
for anime_title in anime_titles:
|
||||||
progress.add_task("Fetching Search Results...", total=None)
|
# ---- search for anime ----
|
||||||
search_results = anime_provider.search_for_anime(
|
|
||||||
anime_title, config.translation_type
|
|
||||||
)
|
|
||||||
if not search_results:
|
|
||||||
print("Search results not found")
|
|
||||||
input("Enter to retry")
|
|
||||||
search(config, anime_title, episode_range)
|
|
||||||
return
|
|
||||||
search_results = search_results["results"]
|
|
||||||
if not search_results:
|
|
||||||
print("Anime not found :cry:")
|
|
||||||
exit_app()
|
|
||||||
search_results_ = {
|
|
||||||
search_result["title"]: search_result for search_result in search_results
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.auto_select:
|
|
||||||
search_result = max(
|
|
||||||
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
|
|
||||||
)
|
|
||||||
print("[cyan]Auto Selecting:[/] ", search_result)
|
|
||||||
|
|
||||||
else:
|
|
||||||
choices = list(search_results_.keys())
|
|
||||||
if config.use_fzf:
|
|
||||||
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
|
|
||||||
elif config.use_rofi:
|
|
||||||
search_result = Rofi.run(choices, "Please Select Title")
|
|
||||||
else:
|
|
||||||
search_result = fuzzy_inquirer(
|
|
||||||
choices,
|
|
||||||
"Please Select Title",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---- fetch selected anime ----
|
|
||||||
with Progress() as progress:
|
|
||||||
progress.add_task("Fetching Anime...", total=None)
|
|
||||||
anime: Anime | None = anime_provider.get_anime(
|
|
||||||
search_results_[search_result]["id"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not anime:
|
|
||||||
print("Sth went wring anime no found")
|
|
||||||
input("Enter to continue...")
|
|
||||||
search(config, anime_title, episode_range)
|
|
||||||
return
|
|
||||||
episode_range_ = None
|
|
||||||
episodes = anime["availableEpisodesDetail"][config.translation_type]
|
|
||||||
if episode_range:
|
|
||||||
episodes_start, episodes_end = episode_range.split("-")
|
|
||||||
if episodes_start and episodes_end:
|
|
||||||
episode_range_ = iter(
|
|
||||||
range(round(float(episodes_start)), round(float(episodes_end)) + 1)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
episode_range_ = iter(sorted(episodes, key=float))
|
|
||||||
|
|
||||||
def stream_anime():
|
|
||||||
clear()
|
|
||||||
episode = None
|
|
||||||
|
|
||||||
if episode_range_:
|
|
||||||
try:
|
|
||||||
episode = str(next(episode_range_))
|
|
||||||
print(
|
|
||||||
f"[cyan]Auto selecting:[/] {search_result} [cyan]Episode:[/] {episode}"
|
|
||||||
)
|
|
||||||
except StopIteration:
|
|
||||||
print("[green]Completed binge sequence[/]:smile:")
|
|
||||||
input("Enter to continue...")
|
|
||||||
|
|
||||||
if not episode or episode not in episodes:
|
|
||||||
if config.use_fzf:
|
|
||||||
episode = fzf.run(episodes, "Select an episode: ", header=search_result)
|
|
||||||
elif config.use_rofi:
|
|
||||||
episode = Rofi.run(episodes, "Select an episode")
|
|
||||||
else:
|
|
||||||
episode = fuzzy_inquirer(
|
|
||||||
episodes,
|
|
||||||
"Select episode",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---- fetch streams ----
|
|
||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Episode Streams...", total=None)
|
progress.add_task("Fetching Search Results...", total=None)
|
||||||
streams = anime_provider.get_episode_streams(
|
search_results = anime_provider.search_for_anime(
|
||||||
anime, episode, config.translation_type
|
anime_title, config.translation_type
|
||||||
)
|
)
|
||||||
if not streams:
|
if not search_results:
|
||||||
print("Failed to get streams")
|
print("Search results not found")
|
||||||
|
input("Enter to retry")
|
||||||
|
search(config, anime_title, episode_range)
|
||||||
|
return
|
||||||
|
search_results = search_results["results"]
|
||||||
|
if not search_results:
|
||||||
|
print("Anime not found :cry:")
|
||||||
|
exit_app()
|
||||||
|
search_results_ = {
|
||||||
|
search_result["title"]: search_result for search_result in search_results
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.auto_select:
|
||||||
|
search_result = max(
|
||||||
|
search_results_.keys(), key=lambda title: fuzz.ratio(title, anime_title)
|
||||||
|
)
|
||||||
|
print("[cyan]Auto Selecting:[/] ", search_result)
|
||||||
|
|
||||||
|
else:
|
||||||
|
choices = list(search_results_.keys())
|
||||||
|
if config.use_fzf:
|
||||||
|
search_result = fzf.run(choices, "Please Select title: ", "FastAnime")
|
||||||
|
elif config.use_rofi:
|
||||||
|
search_result = Rofi.run(choices, "Please Select Title")
|
||||||
|
else:
|
||||||
|
search_result = fuzzy_inquirer(
|
||||||
|
choices,
|
||||||
|
"Please Select Title",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- fetch selected anime ----
|
||||||
|
with Progress() as progress:
|
||||||
|
progress.add_task("Fetching Anime...", total=None)
|
||||||
|
anime: Anime | None = anime_provider.get_anime(
|
||||||
|
search_results_[search_result]["id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not anime:
|
||||||
|
print("Sth went wring anime no found")
|
||||||
|
input("Enter to continue...")
|
||||||
|
search(config, anime_title, episode_range)
|
||||||
|
return
|
||||||
|
episodes_range = []
|
||||||
|
episodes: list[str] = sorted(
|
||||||
|
anime["availableEpisodesDetail"][config.translation_type], key=float
|
||||||
|
)
|
||||||
|
if episode_range:
|
||||||
|
if ":" in episode_range:
|
||||||
|
ep_range_tuple = episode_range.split(":")
|
||||||
|
if len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end, step = ep_range_tuple
|
||||||
|
episodes_range = episodes[
|
||||||
|
int(episodes_start) : int(episodes_end) : int(step)
|
||||||
|
]
|
||||||
|
|
||||||
|
elif len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
episodes_range = episodes[int(episodes_start) : int(episodes_end)]
|
||||||
|
else:
|
||||||
|
episodes_start, episodes_end = ep_range_tuple
|
||||||
|
if episodes_start.strip():
|
||||||
|
episodes_range = episodes[int(episodes_start) :]
|
||||||
|
elif episodes_end.strip():
|
||||||
|
episodes_range = episodes[: int(episodes_end)]
|
||||||
|
else:
|
||||||
|
episodes_range = episodes
|
||||||
|
else:
|
||||||
|
episodes_range = episodes[int(episode_range) :]
|
||||||
|
|
||||||
|
episodes_range = iter(episodes_range)
|
||||||
|
|
||||||
|
def stream_anime():
|
||||||
|
clear()
|
||||||
|
episode = None
|
||||||
|
|
||||||
|
if episodes_range:
|
||||||
|
try:
|
||||||
|
episode = next(episodes_range) # pyright:ignore
|
||||||
|
print(
|
||||||
|
f"[cyan]Auto selecting:[/] {search_result} [cyan]Episode:[/] {episode}"
|
||||||
|
)
|
||||||
|
except StopIteration:
|
||||||
|
print("[green]Completed binge sequence[/]:smile:")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not episode or episode not in episodes:
|
||||||
|
choices = [*episodes, "end"]
|
||||||
|
if config.use_fzf:
|
||||||
|
episode = fzf.run(
|
||||||
|
choices, "Select an episode: ", header=search_result
|
||||||
|
)
|
||||||
|
elif config.use_rofi:
|
||||||
|
episode = Rofi.run(choices, "Select an episode")
|
||||||
|
else:
|
||||||
|
episode = fuzzy_inquirer(
|
||||||
|
choices,
|
||||||
|
"Select episode",
|
||||||
|
)
|
||||||
|
if episode == "end":
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
# ---- fetch streams ----
|
||||||
# ---- fetch servers ----
|
with Progress() as progress:
|
||||||
if config.server == "top":
|
progress.add_task("Fetching Episode Streams...", total=None)
|
||||||
with Progress() as progress:
|
streams = anime_provider.get_episode_streams(
|
||||||
progress.add_task("Fetching top server...", total=None)
|
anime, episode, config.translation_type
|
||||||
server = next(streams, None)
|
)
|
||||||
if not server:
|
if not streams:
|
||||||
print("Sth went wrong when fetching the episode")
|
print("Failed to get streams")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# ---- fetch servers ----
|
||||||
|
if config.server == "top":
|
||||||
|
with Progress() as progress:
|
||||||
|
progress.add_task("Fetching top server...", total=None)
|
||||||
|
server = next(streams, None)
|
||||||
|
if not server:
|
||||||
|
print("Sth went wrong when fetching the episode")
|
||||||
|
input("Enter to continue")
|
||||||
|
stream_anime()
|
||||||
|
return
|
||||||
|
stream_link = filter_by_quality(config.quality, server["links"])
|
||||||
|
if not stream_link:
|
||||||
|
print("Quality not found")
|
||||||
input("Enter to continue")
|
input("Enter to continue")
|
||||||
stream_anime()
|
stream_anime()
|
||||||
return
|
return
|
||||||
stream_link = filter_by_quality(config.quality, server["links"])
|
link = stream_link["link"]
|
||||||
if not stream_link:
|
episode_title = server["episode_title"]
|
||||||
print("Quality not found")
|
|
||||||
input("Enter to continue")
|
|
||||||
stream_anime()
|
|
||||||
return
|
|
||||||
link = stream_link["link"]
|
|
||||||
episode_title = server["episode_title"]
|
|
||||||
else:
|
|
||||||
with Progress() as progress:
|
|
||||||
progress.add_task("Fetching servers", total=None)
|
|
||||||
# prompt for server selection
|
|
||||||
servers = {server["server"]: server for server in streams}
|
|
||||||
servers_names = list(servers.keys())
|
|
||||||
if config.use_fzf:
|
|
||||||
server = fzf.run(servers_names, "Select an link: ")
|
|
||||||
elif config.use_rofi:
|
|
||||||
server = Rofi.run(servers_names, "Select an link")
|
|
||||||
else:
|
else:
|
||||||
server = fuzzy_inquirer(
|
with Progress() as progress:
|
||||||
servers_names,
|
progress.add_task("Fetching servers", total=None)
|
||||||
"Select link",
|
# prompt for server selection
|
||||||
|
servers = {server["server"]: server for server in streams}
|
||||||
|
servers_names = list(servers.keys())
|
||||||
|
if config.server in servers_names:
|
||||||
|
server = config.server
|
||||||
|
else:
|
||||||
|
if config.use_fzf:
|
||||||
|
server = fzf.run(servers_names, "Select an link: ")
|
||||||
|
elif config.use_rofi:
|
||||||
|
server = Rofi.run(servers_names, "Select an link")
|
||||||
|
else:
|
||||||
|
server = fuzzy_inquirer(
|
||||||
|
servers_names,
|
||||||
|
"Select link",
|
||||||
|
)
|
||||||
|
stream_link = filter_by_quality(
|
||||||
|
config.quality, servers[server]["links"]
|
||||||
)
|
)
|
||||||
stream_link = filter_by_quality(
|
if not stream_link:
|
||||||
config.quality, servers[server]["links"]
|
print("Quality not found")
|
||||||
)
|
input("Enter to continue")
|
||||||
if not stream_link:
|
stream_anime()
|
||||||
print("Quality not found")
|
return
|
||||||
input("Enter to continue")
|
link = stream_link["link"]
|
||||||
stream_anime()
|
episode_title = servers[server]["episode_title"]
|
||||||
return
|
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
|
||||||
link = stream_link["link"]
|
|
||||||
episode_title = servers[server]["episode_title"]
|
if config.sync_play:
|
||||||
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
|
SyncPlayer(link, episode_title)
|
||||||
|
else:
|
||||||
|
run_mpv(link, episode_title)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
input("Enter to continue")
|
||||||
|
stream_anime()
|
||||||
|
|
||||||
run_mpv(link, episode_title)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
input("Enter to continue")
|
|
||||||
stream_anime()
|
stream_anime()
|
||||||
|
|
||||||
stream_anime()
|
|
||||||
|
|||||||
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})]
|
||||||
@@ -55,6 +55,7 @@ class Config(object):
|
|||||||
user: [TODO:attribute]
|
user: [TODO:attribute]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
sync_play = False
|
||||||
anime_list: list
|
anime_list: list
|
||||||
watch_history: dict
|
watch_history: dict
|
||||||
fastanime_anilist_app_login_url = (
|
fastanime_anilist_app_login_url = (
|
||||||
@@ -78,6 +79,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 +95,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 +128,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 +147,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 +181,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 +240,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 +266,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}
|
||||||
|
|
||||||
@@ -281,6 +296,10 @@ error = {self.error}
|
|||||||
# adding more options to it
|
# adding more options to it
|
||||||
use_mpv_mod = {self.use_mpv_mod}
|
use_mpv_mod = {self.use_mpv_mod}
|
||||||
|
|
||||||
|
# force mpv window
|
||||||
|
# passed directly to mpv so values are same
|
||||||
|
force_window = immediate
|
||||||
|
|
||||||
# the format of downloaded anime and trailer
|
# the format of downloaded anime and trailer
|
||||||
# based on yt-dlp format and passed directly to it
|
# based on yt-dlp format and passed directly to it
|
||||||
# learn more by looking it up on their site
|
# learn more by looking it up on their site
|
||||||
@@ -303,6 +322,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 +334,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(
|
||||||
@@ -106,7 +113,13 @@ def media_player_controls(
|
|||||||
current_episode_number,
|
current_episode_number,
|
||||||
):
|
):
|
||||||
custom_args.extend(args)
|
custom_args.extend(args)
|
||||||
if config.use_mpv_mod:
|
if config.sync_play:
|
||||||
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
|
stop_time, total_time = SyncPlayer(
|
||||||
|
current_episode_stream_link, selected_server["episode_title"]
|
||||||
|
)
|
||||||
|
elif config.use_mpv_mod:
|
||||||
from ..utils.player import player
|
from ..utils.player import player
|
||||||
|
|
||||||
mpv = player.create_player(
|
mpv = player.create_player(
|
||||||
@@ -365,35 +378,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 +398,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 +485,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 +493,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:
|
||||||
@@ -495,7 +505,13 @@ def provider_anime_episode_servers_menu(
|
|||||||
current_episode_number,
|
current_episode_number,
|
||||||
):
|
):
|
||||||
custom_args.extend(args)
|
custom_args.extend(args)
|
||||||
if config.use_mpv_mod:
|
if config.sync_play:
|
||||||
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
|
stop_time, total_time = SyncPlayer(
|
||||||
|
current_stream_link, selected_server["episode_title"]
|
||||||
|
)
|
||||||
|
elif config.use_mpv_mod:
|
||||||
from ..utils.player import player
|
from ..utils.player import player
|
||||||
|
|
||||||
mpv = player.create_player(
|
mpv = player.create_player(
|
||||||
@@ -512,7 +528,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 +536,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 +550,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 +620,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 +654,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 +690,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
|
||||||
@@ -832,6 +869,12 @@ def media_actions_menu(
|
|||||||
config: [TODO:description]
|
config: [TODO:description]
|
||||||
fastanime_runtime_state: [TODO:description]
|
fastanime_runtime_state: [TODO:description]
|
||||||
"""
|
"""
|
||||||
|
if not config.user:
|
||||||
|
print("You aint logged in")
|
||||||
|
input("Enter to continue")
|
||||||
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
return
|
||||||
|
|
||||||
anime_lists = {
|
anime_lists = {
|
||||||
"Watching": "CURRENT",
|
"Watching": "CURRENT",
|
||||||
"Paused": "PAUSED",
|
"Paused": "PAUSED",
|
||||||
@@ -876,6 +919,11 @@ def media_actions_menu(
|
|||||||
config: [TODO:description]
|
config: [TODO:description]
|
||||||
fastanime_runtime_state: [TODO:description]
|
fastanime_runtime_state: [TODO:description]
|
||||||
"""
|
"""
|
||||||
|
if not config.user:
|
||||||
|
print("You aint logged in")
|
||||||
|
input("Enter to continue")
|
||||||
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
return
|
||||||
if config.use_rofi:
|
if config.use_rofi:
|
||||||
score = Rofi.ask("Enter Score", is_int=True)
|
score = Rofi.ask("Enter Score", is_int=True)
|
||||||
score = max(100, min(0, score))
|
score = max(100, min(0, score))
|
||||||
@@ -1123,7 +1171,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:
|
||||||
@@ -1174,6 +1222,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"] or {}).get("status", "") == "CURRENT"
|
||||||
):
|
):
|
||||||
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(
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
"""
|
||||||
21
fastanime/cli/utils/syncplay.py
Normal file
21
fastanime/cli/utils/syncplay.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from .tools import exit_app
|
||||||
|
|
||||||
|
|
||||||
|
def SyncPlayer(url: str, anime_title, *args):
|
||||||
|
# TODO: handle m3u8 multi quality streams
|
||||||
|
#
|
||||||
|
# check for SyncPlay
|
||||||
|
SYNCPLAY_EXECUTABLE = shutil.which("syncplay")
|
||||||
|
if not SYNCPLAY_EXECUTABLE:
|
||||||
|
print("Syncplay not found")
|
||||||
|
exit_app(1)
|
||||||
|
return "0", "0"
|
||||||
|
# start SyncPlayer
|
||||||
|
subprocess.run(
|
||||||
|
[SYNCPLAY_EXECUTABLE, url, "--", f"--force-media-title={anime_title}"]
|
||||||
|
)
|
||||||
|
# for compatability
|
||||||
|
return "0", "0"
|
||||||
@@ -37,9 +37,14 @@ def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]", default
|
|||||||
return stream_link
|
return stream_link
|
||||||
else:
|
else:
|
||||||
if stream_links and default:
|
if stream_links and default:
|
||||||
|
from rich import print
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("Qualities were: ", stream_links)
|
print("[yellow bold]WARNING Qualities were:[/] ", stream_links)
|
||||||
print("Using default of quality: ", stream_links[0]["quality"])
|
print(
|
||||||
|
"[cyan bold]Using default of quality:[/] ",
|
||||||
|
stream_links[0]["quality"],
|
||||||
|
)
|
||||||
return stream_links[0]
|
return stream_links[0]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
|
|||||||
status
|
status
|
||||||
description
|
description
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -231,7 +232,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
|
||||||
@@ -275,6 +276,7 @@ query($query:String,%s){
|
|||||||
|
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -356,6 +358,7 @@ query($type:MediaType){
|
|||||||
day
|
day
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -396,6 +399,7 @@ query($type:MediaType){
|
|||||||
|
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -455,6 +459,7 @@ query($type:MediaType){
|
|||||||
|
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -520,6 +525,7 @@ query($type:MediaType){
|
|||||||
episodes
|
episodes
|
||||||
genres
|
genres
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -572,6 +578,7 @@ query($type:MediaType){
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -630,6 +637,7 @@ query($type:MediaType){
|
|||||||
large
|
large
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -724,6 +732,7 @@ query ($id: Int,$type:MediaType) {
|
|||||||
large
|
large
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -805,6 +814,7 @@ query ($page: Int,$type:MediaType) {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
@@ -855,6 +865,7 @@ query($id:Int){
|
|||||||
english
|
english
|
||||||
}
|
}
|
||||||
mediaListEntry{
|
mediaListEntry{
|
||||||
|
status
|
||||||
id
|
id
|
||||||
progress
|
progress
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.7.dev1"
|
version = "2.0.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