mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-08 13:50:40 -08:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09ce90f8b0 | ||
|
|
ec40406437 | ||
|
|
a1d1fca538 | ||
|
|
88776ce134 | ||
|
|
35caa93a56 | ||
|
|
4e19c2c108 | ||
|
|
b7ba85fb96 | ||
|
|
eb8b7ea534 | ||
|
|
d2aae33654 | ||
|
|
a2872db216 | ||
|
|
030f77bbf3 | ||
|
|
8f5e01295f | ||
|
|
7b2096e0eb | ||
|
|
45ccaec458 | ||
|
|
b4cbb57f29 | ||
|
|
c4255fc748 | ||
|
|
fa42d0e403 | ||
|
|
0e5cb56970 | ||
|
|
33d69cb95a | ||
|
|
1fbca22be8 | ||
|
|
1a88b6e998 | ||
|
|
3c09268da9 | ||
|
|
c0e5f5dd49 | ||
|
|
1bbc9506c2 | ||
|
|
c66cc52d53 | ||
|
|
e5f4a61a4e | ||
|
|
739d041c58 | ||
|
|
f12d5ab06c | ||
|
|
c3a3041cfb | ||
|
|
594c687c8b | ||
|
|
91b5d3ea40 | ||
|
|
8c30a7667c | ||
|
|
179fbe59ac | ||
|
|
5bfc210f59 | ||
|
|
eb9c200fca | ||
|
|
603efd56e8 | ||
|
|
4d74dfa339 | ||
|
|
4681e38153 | ||
|
|
242003500d | ||
|
|
66ab365657 | ||
|
|
bc8d7b2e28 | ||
|
|
db3a1f7175 | ||
|
|
3a51e0225e | ||
|
|
7b3388939c | ||
|
|
fcf875bdb2 | ||
|
|
0e4624297c | ||
|
|
91b54dfcb9 |
113
README.md
113
README.md
@@ -2,18 +2,17 @@
|
||||
|
||||
Welcome to **FastAnime**, anime site experience from the terminal.
|
||||
|
||||
|
||||
**fzf mode**
|
||||
|
||||
[fa_fzf_demo.webm](https://github.com/user-attachments/assets/b1fecf25-e358-4e8b-a144-bcb7947210cf)
|
||||
|
||||
|
||||
**other modes:**
|
||||
|
||||
<details>
|
||||
<summary><b>rofi mode</b></summary>
|
||||
|
||||
[fa_rofi_mode.webm](https://github.com/user-attachments/assets/2ce669bf-b62f-4c44-bd79-cf0dcaddf37a)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -23,11 +22,8 @@ Welcome to **FastAnime**, anime site experience from the terminal.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magic-tape](https://gitlab.com/christosangel/magic-tape/-/tree/main?ref_type=heads) and [ani-cli](https://github.com/pystardust/ani-cli).
|
||||
|
||||
|
||||
|
||||
<!--toc:start-->
|
||||
|
||||
- [FastAnime](#fastanime)
|
||||
@@ -40,13 +36,18 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
||||
- [External Dependencies](#external-dependencies)
|
||||
- [Usage](#usage)
|
||||
- [The Commandline interface :fire:](#the-commandline-interface-fire)
|
||||
- [The anilist command](#the-anilist-command)
|
||||
- [The anilist command :fire: :fire: :fire:](#the-anilist-command-fire-fire-fire)
|
||||
- [Running without any subcommand](#running-without-any-subcommand)
|
||||
- [Subcommands](#subcommands)
|
||||
- [download subcommand](#download-subcommand)
|
||||
- [search subcommand](#search-subcommand)
|
||||
- [downloads subcommand](#downloads-subcommand)
|
||||
- [config subcommand](#config-subcommand)
|
||||
- [cache subcommand](#cache-subcommand)
|
||||
- [completions subcommand](#completions-subcommand)
|
||||
- [MPV specific commands](#mpv-specific-commands)
|
||||
- [Added keybindings](#added-keybindings)
|
||||
- [Added script messages](#added-script-messages)
|
||||
- [Configuration](#configuration)
|
||||
- [Contributing](#contributing)
|
||||
- [Receiving Support](#receiving-support)
|
||||
@@ -55,8 +56,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> This project currently scrapes allanime and is in no way related to them. The site is in the public domain and can be access by any one with a browser.
|
||||
|
||||
> This project currently scrapes allanime and animepahe and is in no way related to them nor does the project own any content servers. The site is in the public domain and can be access by any one with a browser.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -75,12 +75,21 @@ Preferred method of installation since [Pipx](https://github.com/pypa/pipx) crea
|
||||
```bash
|
||||
|
||||
pipx install fastanime
|
||||
# -- or for the development version --
|
||||
pipx install 'fastanime==<latest-pre-release-tag>.dev1'
|
||||
# example
|
||||
# pipx install 'fastanime==0.60.1.dev1'
|
||||
|
||||
```
|
||||
|
||||
#### Using pip
|
||||
|
||||
```bash
|
||||
pip install fastanime
|
||||
# -- or for the development version --
|
||||
pip install 'fastanime==<latest-pre-release-tag>.dev1'
|
||||
# example
|
||||
# pip install 'fastanime==0.60.1.dev1'
|
||||
```
|
||||
|
||||
### Installing the bleeding edge version
|
||||
@@ -143,7 +152,7 @@ fastanime --version
|
||||
|
||||
### External Dependencies
|
||||
|
||||
The only required external dependency, unless you won't be streaming, is [MPV](https://mpv.io/installation/), which i recommend installing with [uosc](https://github.com/tomasklaen/uosc) and [thumbfast](https://github.com/po5/thumbfast) for the best experience since they add a better interface to it.
|
||||
The only required external dependency, unless you won't be streaming, is [MPV](https://mpv.io/installation/), which i recommend installing with [uosc](https://github.com/tomasklaen/uosc) :fire: and [thumbfast](https://github.com/po5/thumbfast) for the best experience since they add a better interface to it.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
@@ -152,13 +161,13 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
|
||||
> everything you could ever need with a small footprint.
|
||||
> But if you have a reason feel free to encourage as to do so.
|
||||
|
||||
**Other dependencies that will just make your experience better:**
|
||||
**Other external dependencies that will just make your experience better:**
|
||||
|
||||
- [fzf](https://github.com/junegunn/fzf) :fire: which is used as a better alternative to the ui.
|
||||
- [chafa](https://github.com/hpjansson/chafa) currently the best cross platform and cross terminal image viewer for the terminal.
|
||||
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it](https://github.com/chase/awrit?tab=readme-ov-file)!!
|
||||
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
|
||||
- [ani-skip](https://github.com/synacktraa/ani-skip) :fire: used for skipping the opening and ending theme songs
|
||||
- [ani-skip](https://github.com/synacktraa/ani-skip) used for skipping the opening and ending theme songs
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -181,6 +190,7 @@ Overview of main commands:
|
||||
- `fastanime search`: Powerful command meant for binging since it doesn't require the interfaces
|
||||
- `fastanime downloads`: View downloaded anime and watch with MPV.
|
||||
- `fastanime config`: Quickly edit configuration settings.
|
||||
- `fastanime cache`: Quickly manage the cache fastanime uses
|
||||
|
||||
Configuration is directly passed into this command at run time to override your config.
|
||||
|
||||
@@ -204,6 +214,10 @@ Available options include:
|
||||
- `--rofi-theme <path>` theme to use with rofi
|
||||
- `--rofi-theme-input <path>` theme to use with rofi input
|
||||
- `--rofi-theme-confirm <path>` theme to use with rofi confirm
|
||||
- `--log` allow logging to stdout
|
||||
- `--log-file` allow logging to a file
|
||||
- `--rich-traceback` allow rich traceback
|
||||
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
|
||||
|
||||
#### The anilist command :fire: :fire: :fire:
|
||||
|
||||
@@ -331,11 +345,79 @@ fastanime config
|
||||
|
||||
# to get config path which is useful if you want to use it for another program.
|
||||
fastanime config --path
|
||||
|
||||
# add a desktop entry
|
||||
fastanime config --desktop-entry
|
||||
```
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` in case you don't know.
|
||||
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` .
|
||||
|
||||
#### cache subcommand
|
||||
|
||||
Easily manage the data fastanime has cached; for the previews.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# delete everything in the cache dir
|
||||
fastanime cache --clean
|
||||
|
||||
# print the path to the cache dir and exit
|
||||
fastanime cache --path
|
||||
|
||||
# print the current size of the cache dir and exit
|
||||
fastanime cache --size
|
||||
|
||||
# open the cache dir and exit
|
||||
fastanime cache
|
||||
```
|
||||
|
||||
#### completions subcommand
|
||||
|
||||
Helper command to setup shell completions
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# try to detect your shell and print completions
|
||||
fastanime completions
|
||||
# print fish completions
|
||||
fastanime completions --fish
|
||||
# print bash completions
|
||||
fastanime completions --bash
|
||||
# print zsh completions
|
||||
fastanime completions --zsh
|
||||
```
|
||||
|
||||
## MPV specific commands
|
||||
|
||||
The project now allows on the fly media controls directly from mpv. This means you can go to the next or previous episode without the window ever closing thus offering a seamless experience.
|
||||
This is all powered with [python-mpv]() which enables writing mpv scripts with python just like how it would be done in lua.
|
||||
|
||||
### Added keybindings
|
||||
|
||||
`<shift>+n` fetch the next episode
|
||||
|
||||
`<shift>+p` fetch the previous episode
|
||||
|
||||
`<shift>+t` toggle the translation type from dub to sub
|
||||
|
||||
`<shift>+a` toggle auto next episode
|
||||
|
||||
`<shit>+r` reload episode
|
||||
|
||||
### Added script messages
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# to select episode from mpv without window closing
|
||||
script-message select-episode <episode-number>
|
||||
# to select server from mpv without window closing
|
||||
script-message select-server <server-name>
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -356,6 +438,7 @@ skip=false
|
||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||
# used in the continue from time stamp
|
||||
error=3
|
||||
use_mpv_mod=False
|
||||
|
||||
# the format of downloaded anime and trailer
|
||||
# based on yt-dlp format and passed directly to it
|
||||
@@ -366,6 +449,9 @@ error=3
|
||||
format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
|
||||
|
||||
[general]
|
||||
# can be [allanime,animepahe]
|
||||
provider = allanime
|
||||
|
||||
preferred_language = romaji # Display language (options: english, romaji)
|
||||
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
|
||||
preview=false # whether to show a preview window when using fzf or rofi
|
||||
@@ -393,7 +479,7 @@ notification_duration=2
|
||||
|
||||
We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider.
|
||||
|
||||
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed.
|
||||
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed or if you are in a rush for the feature to be merged just open a pr.
|
||||
|
||||
## Receiving Support
|
||||
|
||||
@@ -405,7 +491,6 @@ For inquiries, join our [Discord Server](https://discord.gg/4NUTj5Pt).
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
## Supporting the Project
|
||||
|
||||
Show your support by starring our GitHub repository or [buying us a coffee](https://ko-fi.com/benex254).
|
||||
|
||||
@@ -12,7 +12,7 @@ from .libs.anime_provider import anime_sources
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterator
|
||||
|
||||
from .libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from .libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from .libs.anime_provider.types import Anime, SearchResults, Server
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from ..libs.anilist.anilist_data_schema import (
|
||||
AnilistDateObject,
|
||||
AnilistMediaNextAiringEpisode,
|
||||
)
|
||||
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
|
||||
|
||||
|
||||
# TODO: Add formating options for the final date
|
||||
|
||||
@@ -3,8 +3,7 @@ from queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
import yt_dlp
|
||||
|
||||
from ..utils import sanitize_filename
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from functools import lru_cache
|
||||
|
||||
from thefuzz import fuzz
|
||||
|
||||
from fastanime.libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from fastanime.libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
|
||||
from .data import anime_normalizer
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v0.60.1"
|
||||
__version__ = "v0.62.0"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
|
||||
@@ -3,8 +3,7 @@ import signal
|
||||
import click
|
||||
|
||||
from .. import __version__
|
||||
from ..libs.anime_provider import anime_sources
|
||||
from ..libs.anime_provider.allanime.constants import SERVERS_AVAILABLE
|
||||
from ..libs.anime_provider import SERVERS_AVAILABLE, anime_sources
|
||||
from ..Utility.data import anilist_sort_normalizer
|
||||
from .commands import LazyGroup
|
||||
|
||||
@@ -15,6 +14,7 @@ commands = {
|
||||
"config": "config.config",
|
||||
"downloads": "downloads.downloads",
|
||||
"cache": "cache.cache",
|
||||
"completions": "completions.completions",
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
@click.option(
|
||||
"-s",
|
||||
"--server",
|
||||
type=click.Choice([*SERVERS_AVAILABLE, "top"], case_sensitive=False),
|
||||
type=click.Choice([*SERVERS_AVAILABLE, "top"]),
|
||||
help="Server of choice",
|
||||
)
|
||||
@click.option(
|
||||
@@ -76,7 +76,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
@click.option(
|
||||
"-q",
|
||||
"--quality",
|
||||
type=click.IntRange(0, 3),
|
||||
type=click.Choice(["360", "720", "1080", "unknown"]),
|
||||
help="set the quality of the stream",
|
||||
)
|
||||
@click.option(
|
||||
@@ -126,6 +126,9 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
help="Rofi theme to use for the user input prompt",
|
||||
type=click.Path(),
|
||||
)
|
||||
@click.option(
|
||||
"--use-mpv-mod/--use-default-player", help="Whether to use python-mpv", type=bool
|
||||
)
|
||||
@click.pass_context
|
||||
def run_cli(
|
||||
ctx: click.Context,
|
||||
@@ -155,6 +158,7 @@ def run_cli(
|
||||
rofi_theme,
|
||||
rofi_theme_confirm,
|
||||
rofi_theme_input,
|
||||
use_mpv_mod,
|
||||
):
|
||||
from .config import Config
|
||||
|
||||
@@ -196,7 +200,6 @@ def run_cli(
|
||||
|
||||
if provider:
|
||||
ctx.obj.provider = provider
|
||||
ctx.obj.load_config()
|
||||
if server:
|
||||
ctx.obj.server = server
|
||||
if format:
|
||||
@@ -217,6 +220,11 @@ def run_cli(
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
):
|
||||
ctx.obj.auto_select = auto_select
|
||||
if (
|
||||
ctx.get_parameter_source("use_mpv_mod")
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
):
|
||||
ctx.obj.use_mpv_mod = use_mpv_mod
|
||||
if sort_by:
|
||||
ctx.obj.sort_by = sort_by
|
||||
if downloads_dir:
|
||||
|
||||
@@ -33,3 +33,7 @@ def cache(clean, path, size):
|
||||
fp = os.path.join(dirpath, f)
|
||||
total_size += os.path.getsize(fp)
|
||||
print("Total Size: ", sizeof_fmt(total_size))
|
||||
else:
|
||||
import click
|
||||
|
||||
click.launch(APP_CACHE_DIR)
|
||||
|
||||
124
fastanime/cli/commands/completions.py
Normal file
124
fastanime/cli/commands/completions.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(help="Helper command to get shell completions")
|
||||
@click.option("--fish", is_flag=True)
|
||||
@click.option("--zsh", is_flag=True)
|
||||
@click.option("--bash", is_flag=True)
|
||||
def completions(fish, zsh, bash):
|
||||
if not fish or not zsh or not bash:
|
||||
import os
|
||||
|
||||
shell_env = os.environ.get("SHELL", "")
|
||||
if "fish" in shell_env:
|
||||
current_shell = "fish"
|
||||
elif "zsh" in shell_env:
|
||||
current_shell = "zsh"
|
||||
elif "bash" in shell_env:
|
||||
current_shell = "bash"
|
||||
else:
|
||||
current_shell = None
|
||||
else:
|
||||
current_shell = None
|
||||
if fish or current_shell == "fish" and not zsh and not bash:
|
||||
print(
|
||||
"""
|
||||
function _fastanime_completion;
|
||||
set -l response (env _FASTANIME_COMPLETE=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) fastanime);
|
||||
|
||||
for completion in $response;
|
||||
set -l metadata (string split "," $completion);
|
||||
|
||||
if test $metadata[1] = "dir";
|
||||
__fish_complete_directories $metadata[2];
|
||||
else if test $metadata[1] = "file";
|
||||
__fish_complete_path $metadata[2];
|
||||
else if test $metadata[1] = "plain";
|
||||
echo $metadata[2];
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
complete --no-files --command fastanime --arguments "(_fastanime_completion)";
|
||||
"""
|
||||
)
|
||||
elif zsh or current_shell == "zsh" and not bash:
|
||||
print(
|
||||
"""
|
||||
#compdef fastanime
|
||||
|
||||
_fastanime_completion() {
|
||||
local -a completions
|
||||
local -a completions_with_descriptions
|
||||
local -a response
|
||||
(( ! $+commands[fastanime] )) && return 1
|
||||
|
||||
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _FASTANIME_COMPLETE=zsh_complete fastanime)}")
|
||||
|
||||
for type key descr in ${response}; do
|
||||
if [[ "$type" == "plain" ]]; then
|
||||
if [[ "$descr" == "_" ]]; then
|
||||
completions+=("$key")
|
||||
else
|
||||
completions_with_descriptions+=("$key":"$descr")
|
||||
fi
|
||||
elif [[ "$type" == "dir" ]]; then
|
||||
_path_files -/
|
||||
elif [[ "$type" == "file" ]]; then
|
||||
_path_files -f
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$completions_with_descriptions" ]; then
|
||||
_describe -V unsorted completions_with_descriptions -U
|
||||
fi
|
||||
|
||||
if [ -n "$completions" ]; then
|
||||
compadd -U -V unsorted -a completions
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
|
||||
# autoload from fpath, call function directly
|
||||
_fastanime_completion "$@"
|
||||
else
|
||||
# eval/source/. command, register function for later
|
||||
compdef _fastanime_completion fastanime
|
||||
fi
|
||||
"""
|
||||
)
|
||||
elif bash or current_shell == "bash":
|
||||
print(
|
||||
"""
|
||||
_fastanime_completion() {
|
||||
local IFS=$'\n'
|
||||
local response
|
||||
|
||||
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _FASTANIME_COMPLETE=bash_complete $1)
|
||||
|
||||
for completion in $response; do
|
||||
IFS=',' read type value <<< "$completion"
|
||||
|
||||
if [[ $type == 'dir' ]]; then
|
||||
COMPREPLY=()
|
||||
compopt -o dirnames
|
||||
elif [[ $type == 'file' ]]; then
|
||||
COMPREPLY=()
|
||||
compopt -o default
|
||||
elif [[ $type == 'plain' ]]; then
|
||||
COMPREPLY+=($value)
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_fastanime_completion_setup() {
|
||||
complete -o nosort -F _fastanime_completion fastanime
|
||||
}
|
||||
|
||||
_fastanime_completion_setup;
|
||||
"""
|
||||
)
|
||||
else:
|
||||
print("Could not detect shell")
|
||||
@@ -37,7 +37,7 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
from ...libs.fzf import fzf
|
||||
from ...Utility.downloader.downloader import downloader
|
||||
from ..utils.tools import exit_app
|
||||
from ..utils.utils import fuzzy_inquirer
|
||||
from ..utils.utils import filter_by_quality, fuzzy_inquirer
|
||||
|
||||
anime_provider = AnimeProvider(config.provider)
|
||||
|
||||
@@ -104,36 +104,37 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
if not streams:
|
||||
print("No streams skipping")
|
||||
continue
|
||||
|
||||
with Progress() as progress:
|
||||
if highest_priority:
|
||||
progress.add_task("Fetching highest priority stream", total=None)
|
||||
streams = list(streams)
|
||||
links = [
|
||||
(link.get("priority", 0), link["link"])
|
||||
for server in streams
|
||||
for link in server["links"]
|
||||
]
|
||||
link = max(links, key=lambda x: x[0])[1]
|
||||
episode_title = streams[0]["episode_title"]
|
||||
elif config.server == "top":
|
||||
progress.add_task("Fetching Top Server", total=None)
|
||||
# ---- fetch servers ----
|
||||
if config.server == "top":
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
server = next(streams)
|
||||
link = server["links"][config.quality]["link"]
|
||||
episode_title = server["episode_title"]
|
||||
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.use_fzf:
|
||||
server = fzf.run(servers_names, "Select an link: ")
|
||||
else:
|
||||
# TODO: Make this better but no rush whats the point of manual selection
|
||||
progress.add_task("Fetching links", total=None)
|
||||
streams = list(streams)
|
||||
links = [
|
||||
link["link"] for server in streams for link in server["links"]
|
||||
]
|
||||
episode_title = streams[0]["episode_title"]
|
||||
if config.use_fzf:
|
||||
link = fzf.run(links, "Select link", "Links")
|
||||
else:
|
||||
link = fuzzy_inquirer("Select link", links)
|
||||
server = fuzzy_inquirer("Select link", servers_names)
|
||||
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(
|
||||
|
||||
@@ -26,7 +26,7 @@ def search(config: Config, anime_title: str, episode_range: str):
|
||||
from ...libs.rofi import Rofi
|
||||
from ..utils.mpv import run_mpv
|
||||
from ..utils.tools import exit_app
|
||||
from ..utils.utils import fuzzy_inquirer
|
||||
from ..utils.utils import filter_by_quality, fuzzy_inquirer
|
||||
|
||||
anime_provider = AnimeProvider(config.provider)
|
||||
|
||||
@@ -102,6 +102,7 @@ def search(config: Config, anime_title: str, episode_range: str):
|
||||
)
|
||||
except StopIteration:
|
||||
print("[green]Completed binge sequence[/]:smile:")
|
||||
input("Enter to continue...")
|
||||
|
||||
if not episode or episode not in episodes:
|
||||
if config.use_fzf:
|
||||
@@ -121,25 +122,48 @@ def search(config: Config, anime_title: str, episode_range: str):
|
||||
print("Failed to get streams")
|
||||
return
|
||||
|
||||
# ---- fetch servers ----
|
||||
with Progress() as progress:
|
||||
try:
|
||||
# ---- fetch servers ----
|
||||
if config.server == "top":
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
server = next(streams)
|
||||
link = server["links"][config.quality]["link"]
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
server = next(streams)
|
||||
stream_link = filter_by_quality(config.quality, server["links"])
|
||||
if not stream_link:
|
||||
print("Quality not found")
|
||||
input("Enter to continue")
|
||||
stream_anime()
|
||||
return
|
||||
link = stream_link["link"]
|
||||
episode_title = server["episode_title"]
|
||||
else:
|
||||
progress.add_task("Fetching servers", total=None)
|
||||
links = [link["link"] for server in streams for link in server["links"]]
|
||||
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:
|
||||
link = fzf.run(links, "Select an link: ", header=search_result)
|
||||
server = fzf.run(servers_names, "Select an link: ")
|
||||
elif config.use_rofi:
|
||||
link = Rofi.run(links, "Select an link")
|
||||
server = Rofi.run(servers_names, "Select an link")
|
||||
else:
|
||||
link = fuzzy_inquirer("Select link", links)
|
||||
server = fuzzy_inquirer("Select link", servers_names)
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, servers[server]["links"]
|
||||
)
|
||||
if not stream_link:
|
||||
print("Quality not found")
|
||||
input("Enter to continue")
|
||||
stream_anime()
|
||||
return
|
||||
link = stream_link["link"]
|
||||
episode_title = servers[server]["episode_title"]
|
||||
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
|
||||
|
||||
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
|
||||
|
||||
run_mpv(link, search_result)
|
||||
run_mpv(link, episode_title)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
input("Enter to continue")
|
||||
stream_anime()
|
||||
|
||||
stream_anime()
|
||||
|
||||
@@ -28,7 +28,7 @@ class Config(object):
|
||||
{
|
||||
"server": "top",
|
||||
"continue_from_history": "True",
|
||||
"quality": "0",
|
||||
"quality": "1080",
|
||||
"auto_next": "False",
|
||||
"auto_select": "True",
|
||||
"sort_by": "search match",
|
||||
@@ -47,7 +47,8 @@ class Config(object):
|
||||
"rofi_theme": "",
|
||||
"rofi_theme_input": "",
|
||||
"rofi_theme_confirm": "",
|
||||
"use_mpv_mod": "true",
|
||||
"use_mpv_mod": "false",
|
||||
"force_window": "immediate",
|
||||
}
|
||||
)
|
||||
self.configparser.add_section("stream")
|
||||
@@ -78,6 +79,7 @@ class Config(object):
|
||||
self.error = self.get_error()
|
||||
self.server = self.get_server()
|
||||
self.format = self.get_format()
|
||||
self.force_window = self.get_force_window()
|
||||
self.preferred_language = self.get_preferred_language()
|
||||
self.rofi_theme = self.get_rofi_theme()
|
||||
Rofi.rofi_theme = self.rofi_theme
|
||||
@@ -145,6 +147,9 @@ class Config(object):
|
||||
def get_skip(self):
|
||||
return self.configparser.getboolean("stream", "skip")
|
||||
|
||||
def get_force_window(self):
|
||||
return self.configparser.get("stream", "force_window")
|
||||
|
||||
def get_icons(self):
|
||||
return self.configparser.getboolean("general", "icons")
|
||||
|
||||
@@ -170,7 +175,7 @@ class Config(object):
|
||||
return self.configparser.getboolean("stream", "auto_select")
|
||||
|
||||
def get_quality(self):
|
||||
return self.configparser.getint("stream", "quality")
|
||||
return self.configparser.get("stream", "quality")
|
||||
|
||||
def get_use_mpv_mod(self):
|
||||
return self.configparser.getboolean("stream", "use_mpv_mod")
|
||||
|
||||
@@ -11,20 +11,21 @@ from InquirerPy.validator import EmptyInputValidator
|
||||
from rich import print
|
||||
from rich.progress import Progress
|
||||
from rich.prompt import Confirm, Prompt
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ...anilist import AniList
|
||||
from ...constants import USER_CONFIG_PATH
|
||||
from ...libs.fzf import fzf
|
||||
from ...libs.rofi import Rofi
|
||||
from ...Utility.data import anime_normalizer
|
||||
from ...Utility.utils import anime_title_percentage_match, sanitize_filename
|
||||
from ...Utility.utils import anime_title_percentage_match
|
||||
from ..utils.mpv import run_mpv
|
||||
from ..utils.tools import QueryDict, exit_app
|
||||
from ..utils.utils import fuzzy_inquirer
|
||||
from ..utils.utils import filter_by_quality, fuzzy_inquirer
|
||||
from .utils import aniskip
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from ...libs.anime_provider.types import Anime, SearchResult, Server
|
||||
from ..config import Config
|
||||
|
||||
@@ -80,6 +81,7 @@ def player_controls(config: "Config", anilist_config: QueryDict):
|
||||
from ..utils.player import player
|
||||
|
||||
mpv = player.create_player(
|
||||
current_link,
|
||||
config.anime_provider,
|
||||
anilist_config,
|
||||
config,
|
||||
@@ -92,7 +94,6 @@ def player_controls(config: "Config", anilist_config: QueryDict):
|
||||
mpv._set_property("chapters-file", chapters_file[1])
|
||||
mpv._set_property("script-opts", script_opts[1])
|
||||
mpv.start = start_time
|
||||
mpv.play(current_link)
|
||||
mpv.wait_for_shutdown()
|
||||
mpv.terminate()
|
||||
stop_time = player.last_stop_time
|
||||
@@ -191,7 +192,7 @@ def player_controls(config: "Config", anilist_config: QueryDict):
|
||||
|
||||
def _change_quality():
|
||||
# extract the actual link urls
|
||||
options = [link["link"] for link in links]
|
||||
options = [link["quality"] for link in links]
|
||||
|
||||
# prompt for new quality
|
||||
if config.use_fzf:
|
||||
@@ -202,7 +203,7 @@ def player_controls(config: "Config", anilist_config: QueryDict):
|
||||
quality = Rofi.run(options, "Select Quality")
|
||||
else:
|
||||
quality = fuzzy_inquirer("Select Quality", options)
|
||||
config.quality = options.index(quality) # set quality
|
||||
config.quality = quality # set quality
|
||||
player_controls(config, anilist_config)
|
||||
|
||||
def _change_translation_type():
|
||||
@@ -261,7 +262,7 @@ def player_controls(config: "Config", anilist_config: QueryDict):
|
||||
|
||||
def fetch_streams(config: "Config", anilist_config: QueryDict):
|
||||
# user config
|
||||
quality: int = config.quality
|
||||
quality: str = config.quality
|
||||
|
||||
# internal config
|
||||
episode_number: str = anilist_config.episode_number
|
||||
@@ -331,12 +332,14 @@ def fetch_streams(config: "Config", anilist_config: QueryDict):
|
||||
selected_server = episode_streams_dict[server]
|
||||
|
||||
links = selected_server["links"]
|
||||
if quality > len(links) - 1:
|
||||
quality = config.quality = len(links) - 1
|
||||
elif quality < 0:
|
||||
quality = config.quality = 0
|
||||
stream_link = links[quality]["link"]
|
||||
|
||||
stream_link_ = filter_by_quality(quality, links)
|
||||
if not stream_link_:
|
||||
print("Quality not found")
|
||||
input("Enter to continue...")
|
||||
anilist_options(config, anilist_config)
|
||||
return
|
||||
stream_link = stream_link_["link"]
|
||||
# update internal config
|
||||
anilist_config.current_stream_links = links
|
||||
anilist_config.current_stream_link = stream_link
|
||||
@@ -373,7 +376,11 @@ def fetch_streams(config: "Config", anilist_config: QueryDict):
|
||||
from ..utils.player import player
|
||||
|
||||
mpv = player.create_player(
|
||||
anime_provider, anilist_config, config, selected_server["episode_title"]
|
||||
stream_link,
|
||||
anime_provider,
|
||||
anilist_config,
|
||||
config,
|
||||
selected_server["episode_title"],
|
||||
)
|
||||
|
||||
if custom_args and None:
|
||||
@@ -382,7 +389,6 @@ def fetch_streams(config: "Config", anilist_config: QueryDict):
|
||||
mpv._set_property("chapters-file", chapters_file[1])
|
||||
mpv._set_property("script-opts", script_opts[1])
|
||||
mpv.start = start_time
|
||||
mpv.play(stream_link)
|
||||
mpv.wait_for_shutdown()
|
||||
mpv.terminate()
|
||||
stop_time = player.last_stop_time
|
||||
@@ -750,7 +756,24 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _toggle_auto_next(config, anilist_config):
|
||||
config.auto_select = not config.auto_select
|
||||
config.auto_next = not config.auto_next
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _change_provider(config: "Config", anilist_config):
|
||||
options = ["allanime", "animepahe"]
|
||||
if config.use_fzf:
|
||||
provider = fzf.run(
|
||||
options, prompt="Select Translation Type:", header="Language Options"
|
||||
)
|
||||
elif config.use_rofi:
|
||||
provider = Rofi.run(options, "Select Translation Type")
|
||||
else:
|
||||
provider = fuzzy_inquirer("Select translation type", options)
|
||||
|
||||
config.provider = provider
|
||||
config.anime_provider.provider = provider
|
||||
config.anime_provider.lazyload_provider()
|
||||
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
icons = config.icons
|
||||
@@ -762,6 +785,7 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
f"{'📤 ' if icons else ''}Remove from List": _remove_from_list,
|
||||
f"{'📖 ' if icons else ''}View Info": _view_info,
|
||||
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
||||
f"{'💽 ' if icons else ''}Change Provider": _change_provider,
|
||||
f"{'🔘 ' if icons else ''}Toggle auto select anime": _toggle_auto_select, # problematic if you choose an anime that doesnt match id
|
||||
f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next,
|
||||
f"{'🔙 ' if icons else ''}Back": select_anime,
|
||||
@@ -928,9 +952,9 @@ def anilist(config: "Config", anilist_config: QueryDict):
|
||||
return AniList.search(id_in=anime_list)
|
||||
|
||||
def edit_config():
|
||||
import subprocess
|
||||
from click import edit
|
||||
|
||||
subprocess.run([os.environ.get("EDITOR", "open"), USER_CONFIG_PATH])
|
||||
edit(filename=USER_CONFIG_PATH)
|
||||
if config.use_rofi:
|
||||
config.load_config()
|
||||
config.use_rofi = True
|
||||
|
||||
@@ -9,7 +9,7 @@ from threading import Thread
|
||||
import requests
|
||||
|
||||
from ...constants import APP_CACHE_DIR
|
||||
from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from ...Utility import anilist_data_helper
|
||||
from ...Utility.utils import remove_html_tags
|
||||
from ..utils.utils import get_true_fg
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
|
||||
import mpv
|
||||
|
||||
from ...anilist import AniList
|
||||
from .utils import filter_by_quality
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
@@ -27,9 +28,13 @@ class MpvPlayer(object):
|
||||
last_stop_time_secs = 0
|
||||
last_total_time_secs = 0
|
||||
current_media_title = ""
|
||||
player_fetching = False
|
||||
|
||||
def get_episode(
|
||||
self, type: "Literal['next','previous','reload','custom']", ep_no=None
|
||||
self,
|
||||
type: "Literal['next','previous','reload','custom']",
|
||||
ep_no=None,
|
||||
server="top",
|
||||
):
|
||||
anilist_config = self.anilist_config
|
||||
config = self.config
|
||||
@@ -62,7 +67,9 @@ class MpvPlayer(object):
|
||||
elif type == "custom":
|
||||
if not ep_no or ep_no not in episodes:
|
||||
self.mpv_player.show_text("Episode number not specified or invalid")
|
||||
self.mpv_player.show_text(f"Acceptable episodes are: {episodes}")
|
||||
self.mpv_player.show_text(
|
||||
f"Acceptable episodes are: {episodes}",
|
||||
)
|
||||
return
|
||||
|
||||
self.mpv_player.show_text(f"Fetching episode {ep_no}")
|
||||
@@ -97,18 +104,36 @@ class MpvPlayer(object):
|
||||
return None
|
||||
|
||||
# always select the first
|
||||
selected_server = next(episode_streams)
|
||||
if server == "top":
|
||||
selected_server = next(episode_streams)
|
||||
else:
|
||||
episode_streams_dict = {
|
||||
episode_stream["server"]: episode_stream
|
||||
for episode_stream in episode_streams
|
||||
}
|
||||
selected_server = episode_streams_dict.get(server)
|
||||
if selected_server is None:
|
||||
self.mpv_player.show_text(
|
||||
f"Invalid server!!; servers available are: {episode_streams_dict.keys()}",
|
||||
)
|
||||
return None
|
||||
self.current_media_title = selected_server["episode_title"]
|
||||
links = selected_server["links"]
|
||||
if quality > len(links) - 1:
|
||||
quality = config.quality = len(links) - 1
|
||||
elif quality < 0:
|
||||
quality = config.quality = 0
|
||||
stream_link = links[quality]["link"]
|
||||
|
||||
stream_link_ = filter_by_quality(quality, links)
|
||||
if not stream_link_:
|
||||
self.mpv_player.show_text("Quality not found")
|
||||
return
|
||||
stream_link = stream_link_["link"]
|
||||
return stream_link
|
||||
|
||||
def create_player(
|
||||
self, anime_provider: "AnimeProvider", anilist_config, config: "Config", title
|
||||
self,
|
||||
stream_link,
|
||||
anime_provider: "AnimeProvider",
|
||||
anilist_config,
|
||||
config: "Config",
|
||||
title,
|
||||
):
|
||||
self.anime_provider = anime_provider
|
||||
self.anilist_config = anilist_config
|
||||
@@ -120,13 +145,52 @@ class MpvPlayer(object):
|
||||
self.current_media_title = ""
|
||||
|
||||
mpv_player = mpv.MPV(
|
||||
log_handler=print,
|
||||
loglevel="error",
|
||||
config=True,
|
||||
input_default_bindings=True,
|
||||
input_vo_keyboard=True,
|
||||
osc=True,
|
||||
ytdl=True,
|
||||
)
|
||||
mpv_player.force_window = config.force_window
|
||||
# mpv_player.cache = "yes"
|
||||
# mpv_player.cache_pause = "no"
|
||||
mpv_player.title = title
|
||||
|
||||
mpv_player.play(stream_link)
|
||||
|
||||
# -- events --
|
||||
@mpv_player.event_callback("file-loaded")
|
||||
def set_total_time(event, *args):
|
||||
d = mpv_player._get_property("duration")
|
||||
self.player_fetching = False
|
||||
if isinstance(d, float):
|
||||
self.last_total_time = format_time(d)
|
||||
|
||||
@mpv_player.property_observer("time-pos")
|
||||
def handle_time_start_update(*args):
|
||||
if len(args) > 1:
|
||||
value = args[1]
|
||||
if value is not None:
|
||||
self.last_stop_time = format_time(value)
|
||||
|
||||
@mpv_player.property_observer("time-remaining")
|
||||
def handle_time_remaining_update(
|
||||
property, time_remaining: float | None = None, *args
|
||||
):
|
||||
if time_remaining is not None:
|
||||
if time_remaining < 1 and config.auto_next and not self.player_fetching:
|
||||
print("Auto Fetching Next Episode")
|
||||
self.player_fetching = True
|
||||
url = self.get_episode("next")
|
||||
if url:
|
||||
mpv_player.loadfile(
|
||||
url,
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
# -- keybindings --
|
||||
@mpv_player.on_key_press("shift+n")
|
||||
def _next_episode():
|
||||
url = self.get_episode("next")
|
||||
@@ -134,18 +198,6 @@ class MpvPlayer(object):
|
||||
mpv_player.loadfile(url, options=f"title={self.current_media_title}")
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
@mpv_player.event_callback("file-loaded")
|
||||
def set_total_time(event, *args):
|
||||
d = mpv_player._get_property("duration")
|
||||
if isinstance(d, float):
|
||||
self.last_total_time = format_time(d)
|
||||
|
||||
@mpv_player.event_callback("shutdown")
|
||||
def set_total_time_on_shutdown(event, *args):
|
||||
d = mpv_player._get_property("duration")
|
||||
if isinstance(d, float):
|
||||
self.last_total_time = format_time(d)
|
||||
|
||||
@mpv_player.on_key_press("shift+p")
|
||||
def _previous_episode():
|
||||
url = self.get_episode("previous")
|
||||
@@ -190,31 +242,11 @@ class MpvPlayer(object):
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
@mpv_player.property_observer("time-pos")
|
||||
def handle_time_start_update(*args):
|
||||
if len(args) > 1:
|
||||
value = args[1]
|
||||
if value is not None:
|
||||
self.last_stop_time_secs = value
|
||||
self.last_stop_time = format_time(value)
|
||||
|
||||
@mpv_player.property_observer("time-remaining")
|
||||
def handle_time_remaining_update(*args):
|
||||
if len(args) > 1:
|
||||
value = args[1]
|
||||
if value is not None:
|
||||
rem_time = value
|
||||
if rem_time < 10 and config.auto_next:
|
||||
url = self.get_episode("next")
|
||||
if url:
|
||||
mpv_player.loadfile(
|
||||
url,
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
# -- script messages --
|
||||
@mpv_player.message_handler("select-episode")
|
||||
def select_episode(episode: bytes | None = None, *args):
|
||||
if not episode:
|
||||
mpv_player.show_text("No episode was selected")
|
||||
return
|
||||
url = self.get_episode("custom", episode.decode())
|
||||
if url:
|
||||
@@ -223,11 +255,51 @@ class MpvPlayer(object):
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
mpv_player.register_message_handler("select-episode", select_episode)
|
||||
@mpv_player.message_handler("select-server")
|
||||
def select_server(server: bytes | None = None, *args):
|
||||
if not server:
|
||||
mpv_player.show_text("No server was selected")
|
||||
return
|
||||
url = self.get_episode("reload", server=server.decode())
|
||||
if url:
|
||||
mpv_player.loadfile(
|
||||
url,
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
else:
|
||||
pass
|
||||
|
||||
@mpv_player.message_handler("select-quality")
|
||||
def select_quality(quality_raw: bytes | None = None, *args):
|
||||
if not quality_raw:
|
||||
mpv_player.show_text("No quality was selected")
|
||||
return
|
||||
q = ["360", "720", "1080"]
|
||||
quality = quality_raw.decode()
|
||||
links: list = anilist_config.current_stream_links
|
||||
q = [link["quality"] for link in links]
|
||||
if quality in q:
|
||||
config.quality = quality
|
||||
stream_link_ = filter_by_quality(quality, links)
|
||||
if not stream_link_:
|
||||
mpv_player.show_text("Quality not found")
|
||||
return
|
||||
mpv_player.show_text(f"Changing to stream of quality {quality}")
|
||||
stream_link = stream_link_["link"]
|
||||
mpv_player.loadfile(stream_link)
|
||||
else:
|
||||
mpv_player.show_text(f"invalid quality!! Valid quality includes: {q}")
|
||||
|
||||
# -- events --
|
||||
mpv_player.observe_property("time-pos", handle_time_start_update)
|
||||
mpv_player.register_event_callback(set_total_time)
|
||||
mpv_player.register_event_callback(set_total_time_on_shutdown)
|
||||
mpv_player.observe_property("time-remaining", handle_time_remaining_update)
|
||||
mpv_player.register_event_callback(set_total_time)
|
||||
|
||||
# --script-messages --
|
||||
mpv_player.register_message_handler("select-episode", select_episode)
|
||||
mpv_player.register_message_handler("select-server", select_server)
|
||||
mpv_player.register_message_handler("select-quality", select_quality)
|
||||
|
||||
self.mpv_player = mpv_player
|
||||
return mpv_player
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from InquirerPy import inquirer
|
||||
from thefuzz import fuzz
|
||||
@@ -6,7 +7,8 @@ from thefuzz import fuzz
|
||||
from ...Utility.data import anime_normalizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...libs.anime_provider.types import EpisodeStream
|
||||
|
||||
# Define ANSI escape codes as constants
|
||||
RESET = "\033[0m"
|
||||
@@ -20,6 +22,12 @@ BG_GREEN = "\033[48;2;120;233;12;m"
|
||||
GREEN = "\033[38;2;45;24;45;m"
|
||||
|
||||
|
||||
def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]"):
|
||||
for stream_link in stream_links:
|
||||
if stream_link["quality"] == quality:
|
||||
return stream_link
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix="B"):
|
||||
for unit in ("", "K", "M", "G", "T", "P", "E", "Z"):
|
||||
if abs(num) < 1024.0:
|
||||
|
||||
@@ -20,6 +20,8 @@ if PLATFORM == "Windows":
|
||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
|
||||
else:
|
||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.png")
|
||||
PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
|
||||
|
||||
|
||||
# ----- user configs and data -----
|
||||
APP_DATA_DIR = dirs.user_config_dir
|
||||
|
||||
@@ -29,7 +29,7 @@ from .queries_graphql import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .anilist_data_schema import (
|
||||
from .types import (
|
||||
AnilistDataSchema,
|
||||
AnilistMediaLists,
|
||||
AnilistMediaListStatus,
|
||||
@@ -126,7 +126,9 @@ class AniListApi:
|
||||
return self._make_authenticated_request(media_list_mutation, variables)
|
||||
|
||||
def get_anime_list(
|
||||
self, status: "AnilistMediaListStatus"
|
||||
self,
|
||||
status: "AnilistMediaListStatus",
|
||||
type="ANIME",
|
||||
) -> tuple[bool, "AnilistMediaLists"] | tuple[bool, None]:
|
||||
"""gets an anime list from your media list given the list status
|
||||
|
||||
@@ -136,7 +138,7 @@ class AniListApi:
|
||||
Returns:
|
||||
a media list
|
||||
"""
|
||||
variables = {"status": status, "userId": self.user_id}
|
||||
variables = {"status": status, "userId": self.user_id, "type": type}
|
||||
return self._make_authenticated_request(media_list_query, variables)
|
||||
|
||||
def get_medialist_entry(
|
||||
@@ -310,6 +312,7 @@ class AniListApi:
|
||||
start_greater: int | None = None,
|
||||
start_lesser: int | None = None,
|
||||
page: int | None = None,
|
||||
type="ANIME",
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -329,65 +332,74 @@ class AniListApi:
|
||||
variables = {"id": id}
|
||||
return self.get_data(anime_query, variables)
|
||||
|
||||
def get_trending(self, *_, **kwargs):
|
||||
def get_trending(self, type="ANIME", *_, **kwargs):
|
||||
"""
|
||||
Gets the currently trending anime
|
||||
"""
|
||||
trending = self.get_data(trending_query)
|
||||
variables = {"type": type}
|
||||
trending = self.get_data(trending_query, variables)
|
||||
return trending
|
||||
|
||||
def get_most_favourite(self, *_, **kwargs):
|
||||
def get_most_favourite(self, type="ANIME", *_, **kwargs):
|
||||
"""
|
||||
Gets the most favoured anime on anilist
|
||||
"""
|
||||
most_favourite = self.get_data(most_favourite_query)
|
||||
variables = {"type": type}
|
||||
most_favourite = self.get_data(most_favourite_query, variables)
|
||||
return most_favourite
|
||||
|
||||
def get_most_scored(self, *_, **kwargs):
|
||||
def get_most_scored(self, type="ANIME", *_, **kwargs):
|
||||
"""
|
||||
Gets most scored anime on anilist
|
||||
"""
|
||||
most_scored = self.get_data(most_scored_query)
|
||||
variables = {"type": type}
|
||||
most_scored = self.get_data(most_scored_query, variables)
|
||||
return most_scored
|
||||
|
||||
def get_most_recently_updated(self, *_, **kwargs):
|
||||
def get_most_recently_updated(self, type="ANIME", *_, **kwargs):
|
||||
"""
|
||||
Gets most recently updated anime from anilist
|
||||
"""
|
||||
most_recently_updated = self.get_data(most_recently_updated_query)
|
||||
variables = {"type": type}
|
||||
most_recently_updated = self.get_data(most_recently_updated_query, variables)
|
||||
return most_recently_updated
|
||||
|
||||
def get_most_popular(self):
|
||||
def get_most_popular(
|
||||
self,
|
||||
type="ANIME",
|
||||
):
|
||||
"""
|
||||
Gets most popular anime on anilist
|
||||
"""
|
||||
most_popular = self.get_data(most_popular_query)
|
||||
variables = {"type": type}
|
||||
most_popular = self.get_data(most_popular_query, variables)
|
||||
return most_popular
|
||||
|
||||
def get_upcoming_anime(self, page: int = 1, *_, **kwargs):
|
||||
def get_upcoming_anime(self, type="ANIME", page: int = 1, *_, **kwargs):
|
||||
"""
|
||||
Gets upcoming anime from anilist
|
||||
"""
|
||||
variables = {"page": page}
|
||||
variables = {"page": page, "type": type}
|
||||
upcoming_anime = self.get_data(upcoming_anime_query, variables)
|
||||
return upcoming_anime
|
||||
|
||||
# NOTE: THe following methods will probably be scraped soon
|
||||
def get_recommended_anime_for(self, id: int, *_, **kwargs):
|
||||
recommended_anime = self.get_data(recommended_query)
|
||||
def get_recommended_anime_for(self, id: int, type="ANIME", *_, **kwargs):
|
||||
variables = {"type": type}
|
||||
recommended_anime = self.get_data(recommended_query, variables)
|
||||
return recommended_anime
|
||||
|
||||
def get_charcters_of(self, id: int, *_, **kwargs):
|
||||
def get_charcters_of(self, id: int, type="ANIME", *_, **kwargs):
|
||||
variables = {"id": id}
|
||||
characters = self.get_data(anime_characters_query, variables)
|
||||
return characters
|
||||
|
||||
def get_related_anime_for(self, id: int, *_, **kwargs):
|
||||
def get_related_anime_for(self, id: int, type="ANIME", *_, **kwargs):
|
||||
variables = {"id": id}
|
||||
related_anime = self.get_data(anime_relations_query, variables)
|
||||
return related_anime
|
||||
|
||||
def get_airing_schedule_for(self, id: int, *_, **kwargs):
|
||||
def get_airing_schedule_for(self, id: int, type="ANIME", *_, **kwargs):
|
||||
variables = {"id": id}
|
||||
airing_schedule = self.get_data(airing_schedule_query, variables)
|
||||
return airing_schedule
|
||||
|
||||
@@ -122,13 +122,13 @@ mutation($mediaId:Int,$scoreRaw:Int,$repeat:Int,$progress:Int,$status:MediaListS
|
||||
"""
|
||||
|
||||
media_list_query = """
|
||||
query ($userId: Int, $status: MediaListStatus) {
|
||||
query ($userId: Int, $status: MediaListStatus,$type:MediaType) {
|
||||
Page {
|
||||
pageInfo {
|
||||
currentPage
|
||||
total
|
||||
}
|
||||
mediaList(userId: $userId, status: $status, type: ANIME) {
|
||||
mediaList(userId: $userId, status: $status, type: $type) {
|
||||
mediaId
|
||||
|
||||
media {
|
||||
@@ -223,7 +223,8 @@ $averageScore_lesser:Int,\
|
||||
$startDate_greater:FuzzyDateInt,\
|
||||
$startDate_lesser:FuzzyDateInt,\
|
||||
$endDate_greater:FuzzyDateInt,\
|
||||
$endDate_lesser:FuzzyDateInt\
|
||||
$endDate_lesser:FuzzyDateInt,\
|
||||
$type:MediaType\
|
||||
"
|
||||
# FuzzyDateInt = (yyyymmdd)
|
||||
# MediaStatus = (FINISHED,RELEASING,NOT_YET_RELEASED,CANCELLED,HIATUS)
|
||||
@@ -255,7 +256,7 @@ query($query:String,%s){
|
||||
endDate_greater:$endDate_greater,
|
||||
endDate_lesser:$endDate_lesser,
|
||||
sort:$sort,
|
||||
type:ANIME
|
||||
type:$type
|
||||
)
|
||||
{
|
||||
id
|
||||
@@ -316,10 +317,10 @@ query($query:String,%s){
|
||||
)
|
||||
|
||||
trending_query = """
|
||||
query{
|
||||
query($type:MediaType){
|
||||
Page(perPage:15){
|
||||
|
||||
media(sort:TRENDING_DESC,type:ANIME,genre_not_in:["hentai"]){
|
||||
media(sort:TRENDING_DESC,type:$type,genre_not_in:["hentai"]){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
@@ -376,9 +377,9 @@ query{
|
||||
|
||||
# mosts
|
||||
most_favourite_query = """
|
||||
query{
|
||||
query($type:MediaType){
|
||||
Page(perPage:15){
|
||||
media(sort:FAVOURITES_DESC,type:ANIME,genre_not_in:["hentai"]){
|
||||
media(sort:FAVOURITES_DESC,type:$type,genre_not_in:["hentai"]){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
@@ -435,9 +436,9 @@ query{
|
||||
"""
|
||||
|
||||
most_scored_query = """
|
||||
query{
|
||||
query($type:MediaType){
|
||||
Page(perPage:15){
|
||||
media(sort:SCORE_DESC,type:ANIME,genre_not_in:["hentai"]){
|
||||
media(sort:SCORE_DESC,type:$type,genre_not_in:["hentai"]){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
@@ -494,9 +495,9 @@ query{
|
||||
"""
|
||||
|
||||
most_popular_query = """
|
||||
query{
|
||||
query($type:MediaType){
|
||||
Page(perPage:15){
|
||||
media(sort:POPULARITY_DESC,type:ANIME,genre_not_in:["hentai"]){
|
||||
media(sort:POPULARITY_DESC,type:$type,genre_not_in:["hentai"]){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
@@ -553,9 +554,9 @@ query{
|
||||
"""
|
||||
|
||||
most_recently_updated_query = """
|
||||
query{
|
||||
query($type:MediaType){
|
||||
Page(perPage:15){
|
||||
media(sort:UPDATED_AT_DESC,type:ANIME,averageScore_greater:50,genre_not_in:["hentai"],status:RELEASING){
|
||||
media(sort:UPDATED_AT_DESC,type:$type,averageScore_greater:50,genre_not_in:["hentai"],status:RELEASING){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
@@ -611,9 +612,9 @@ query{
|
||||
"""
|
||||
|
||||
recommended_query = """
|
||||
query {
|
||||
query($type:MediaType){
|
||||
Page(perPage:15) {
|
||||
media( type: ANIME,genre_not_in:["hentai"]) {
|
||||
media( type: $type,genre_not_in:["hentai"]) {
|
||||
recommendations(sort:RATING_DESC){
|
||||
nodes{
|
||||
media{
|
||||
@@ -671,9 +672,9 @@ query {
|
||||
"""
|
||||
|
||||
anime_characters_query = """
|
||||
query($id:Int){
|
||||
query($id:Int,$type:MediaType){
|
||||
Page {
|
||||
media(id:$id, type: ANIME) {
|
||||
media(id:$id, type: $type) {
|
||||
characters {
|
||||
nodes {
|
||||
name {
|
||||
@@ -706,9 +707,9 @@ query($id:Int){
|
||||
|
||||
|
||||
anime_relations_query = """
|
||||
query ($id: Int) {
|
||||
query ($id: Int,$type:MediaType) {
|
||||
Page(perPage: 20) {
|
||||
media(id: $id, sort: POPULARITY_DESC, type: ANIME,genre_not_in:["hentai"]) {
|
||||
media(id: $id, sort: POPULARITY_DESC, type: $type,genre_not_in:["hentai"]) {
|
||||
relations {
|
||||
nodes {
|
||||
id
|
||||
@@ -763,9 +764,9 @@ query ($id: Int) {
|
||||
"""
|
||||
|
||||
airing_schedule_query = """
|
||||
query ($id: Int) {
|
||||
query ($id: Int,$type:MediaType) {
|
||||
Page {
|
||||
media(id: $id, sort: POPULARITY_DESC, type: ANIME) {
|
||||
media(id: $id, sort: POPULARITY_DESC, type: $type) {
|
||||
airingSchedule(notYetAired:true){
|
||||
nodes{
|
||||
airingAt
|
||||
@@ -780,7 +781,7 @@ query ($id: Int) {
|
||||
"""
|
||||
|
||||
upcoming_anime_query = """
|
||||
query ($page: Int) {
|
||||
query ($page: Int,$type:MediaType) {
|
||||
Page(page: $page) {
|
||||
pageInfo {
|
||||
total
|
||||
@@ -788,7 +789,7 @@ query ($page: Int) {
|
||||
currentPage
|
||||
hasNextPage
|
||||
}
|
||||
media(type: ANIME, status: NOT_YET_RELEASED,sort:POPULARITY_DESC,genre_not_in:["hentai"]) {
|
||||
media(type: $type, status: NOT_YET_RELEASED,sort:POPULARITY_DESC,genre_not_in:["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
anime_sources = {
|
||||
"allanime": "api.AllAnimeAPI",
|
||||
"animepahe": "api.AnimePaheApi",
|
||||
"aniwatch": "api.AniWatchApi",
|
||||
}
|
||||
SERVERS_AVAILABLE = [
|
||||
"sharepoint",
|
||||
"dropbox",
|
||||
"gogoanime",
|
||||
"weTransfer",
|
||||
"wixmp",
|
||||
"kwik",
|
||||
]
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING
|
||||
from requests.exceptions import Timeout
|
||||
|
||||
from ...anime_provider.base_provider import AnimeProvider
|
||||
from ..utils import decode_hex_string
|
||||
from ..utils import decode_hex_string, give_random_quality
|
||||
from .constants import (
|
||||
ALLANIME_API_ENDPOINT,
|
||||
ALLANIME_BASE,
|
||||
@@ -215,7 +215,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
} # pyright:ignore
|
||||
case "Kir":
|
||||
logger.debug("allanime:Found streams from wetransfer")
|
||||
@@ -225,7 +225,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
} # pyright:ignore
|
||||
case "S-mp4":
|
||||
logger.debug("allanime:Found streams from sharepoint")
|
||||
@@ -235,7 +235,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
} # pyright:ignore
|
||||
case "Sak":
|
||||
logger.debug("allanime:Found streams from dropbox")
|
||||
@@ -245,7 +245,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
} # pyright:ignore
|
||||
case "Default":
|
||||
logger.debug("allanime:Found streams from wixmp")
|
||||
@@ -255,7 +255,7 @@ class AllAnimeAPI(AnimeProvider):
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
} # pyright:ignore
|
||||
except Timeout:
|
||||
logger.error(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from ..types import Anime, EpisodesDetail, SearchResults
|
||||
from ..types import Anime, AnimeEpisodeDetails, SearchResults
|
||||
from .types import AllAnimeEpisode, AllAnimeSearchResults, AllAnimeShow
|
||||
|
||||
# TODO: scrap this module and do the transformations directly from the provider class
|
||||
|
||||
|
||||
def normalize_search_results(search_results: AllAnimeSearchResults) -> SearchResults:
|
||||
page_info = search_results["shows"]["pageInfo"]
|
||||
@@ -15,7 +17,7 @@ def normalize_search_results(search_results: AllAnimeSearchResults) -> SearchRes
|
||||
results.append(normalized_result)
|
||||
|
||||
normalized_search_results: SearchResults = {
|
||||
"pageInfo": page_info,
|
||||
"pageInfo": page_info, # pyright:ignore
|
||||
"results": results,
|
||||
}
|
||||
|
||||
@@ -25,9 +27,11 @@ def normalize_search_results(search_results: AllAnimeSearchResults) -> SearchRes
|
||||
def normalize_anime(anime: AllAnimeShow) -> Anime:
|
||||
id: str = anime["_id"]
|
||||
title: str = anime["name"]
|
||||
availableEpisodesDetail: EpisodesDetail = anime["availableEpisodesDetail"]
|
||||
availableEpisodesDetail: AnimeEpisodeDetails = anime[
|
||||
"availableEpisodesDetail"
|
||||
] # pyright:ignore
|
||||
type = anime.get("__typename")
|
||||
normalized_anime: Anime = {
|
||||
normalized_anime: Anime = { # pyright:ignore
|
||||
"id": id,
|
||||
"title": title,
|
||||
"availableEpisodesDetail": availableEpisodesDetail,
|
||||
|
||||
@@ -1,63 +1,214 @@
|
||||
import requests
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .constants import ANIMEPAHE_BASE, ANIMEPAHE_ENDPOINT, REQUEST_HEADERS
|
||||
from yt_dlp.utils import (
|
||||
extract_attributes,
|
||||
get_element_by_id,
|
||||
get_element_text_and_html_by_tag,
|
||||
get_elements_html_by_class,
|
||||
)
|
||||
|
||||
from ..base_provider import AnimeProvider
|
||||
from .constants import (
|
||||
ANIMEPAHE_BASE,
|
||||
ANIMEPAHE_ENDPOINT,
|
||||
REQUEST_HEADERS,
|
||||
SERVER_HEADERS,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..types import Anime
|
||||
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimeSearchResult
|
||||
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: hack this to completion
|
||||
class AnimePaheApi:
|
||||
def search_for_anime(self, user_query, *args):
|
||||
class AnimePaheApi(AnimeProvider):
|
||||
search_page: "AnimePaheSearchPage"
|
||||
anime: "AnimePaheAnimePage"
|
||||
|
||||
def search_for_anime(self, user_query: str, *args):
|
||||
try:
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
||||
headers = {**REQUEST_HEADERS}
|
||||
response = requests.get(url, headers=headers)
|
||||
response = self.session.get(url, headers=headers)
|
||||
if not response.status_code == 200:
|
||||
return
|
||||
data = response.json()
|
||||
data: "AnimePaheSearchPage" = response.json()
|
||||
self.search_page = data
|
||||
|
||||
return {
|
||||
"pageInfo": {"total": data["total"]},
|
||||
"pageInfo": {
|
||||
"total": data["total"],
|
||||
"perPage": data["per_page"],
|
||||
"currentPage": data["current_page"],
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"availableEpisodes": list(range(result["episodes"])),
|
||||
"id": result["session"],
|
||||
"title": result["title"],
|
||||
"availableEpisodes": result["episodes"],
|
||||
"type": result["type"],
|
||||
"year": result["year"],
|
||||
"score": result["score"],
|
||||
"status": result["status"],
|
||||
"season": result["season"],
|
||||
"poster": result["poster"],
|
||||
}
|
||||
for result in data["data"]
|
||||
],
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
input()
|
||||
logger.error(f"AnimePahe(search): {e}")
|
||||
return {}
|
||||
|
||||
def get_anime(self, session_id: str, *args):
|
||||
url = "https://animepahe.ru/api?m=release&id=&sort=episode_asc&page=1"
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page=1"
|
||||
response = requests.get(url, headers=REQUEST_HEADERS)
|
||||
if not response.status_code == 200:
|
||||
return
|
||||
data = response.json()
|
||||
self.current = data
|
||||
episodes = list(map(str, range(data["total"])))
|
||||
return {
|
||||
"id": session_id,
|
||||
"title": "none",
|
||||
"availableEpisodesDetail": {
|
||||
"sub": episodes,
|
||||
"dub": episodes,
|
||||
"raw": episodes,
|
||||
},
|
||||
}
|
||||
try:
|
||||
anime_result: "AnimeSearchResult" = [
|
||||
anime
|
||||
for anime in self.search_page["data"]
|
||||
if anime["session"] == session_id
|
||||
][0]
|
||||
url = (
|
||||
f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page=1"
|
||||
)
|
||||
response = self.session.get(url, headers=REQUEST_HEADERS)
|
||||
if not response.status_code == 200:
|
||||
return {}
|
||||
data: "AnimePaheAnimePage" = response.json()
|
||||
self.anime = data
|
||||
episodes = list(map(str, range(data["total"])))
|
||||
title = ""
|
||||
return {
|
||||
"id": session_id,
|
||||
"title": anime_result["title"],
|
||||
"year": anime_result["year"],
|
||||
"season": anime_result["season"],
|
||||
"poster": anime_result["poster"],
|
||||
"score": anime_result["score"],
|
||||
"availableEpisodesDetail": {
|
||||
"sub": episodes,
|
||||
"dub": episodes,
|
||||
"raw": episodes,
|
||||
},
|
||||
"episodesInfo": [
|
||||
{
|
||||
"title": episode["title"] or f"{title};{episode['episode']}",
|
||||
"episode": episode["episode"],
|
||||
"id": episode["session"],
|
||||
"translation_type": episode["audio"],
|
||||
"duration": episode["duration"],
|
||||
"poster": episode["snapshot"],
|
||||
}
|
||||
for episode in data["data"]
|
||||
],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"AnimePahe(anime): {e}")
|
||||
return {}
|
||||
|
||||
def get_episode_streams(
|
||||
self, anime: "Anime", episode_number: str, translation_type, *args
|
||||
):
|
||||
# extract episode details from memory
|
||||
episode = [
|
||||
episode
|
||||
for episode in self.anime["data"]
|
||||
if float(episode["episode"]) == float(episode_number)
|
||||
]
|
||||
if not episode:
|
||||
logger.error(f"AnimePahe(streams): episode {episode_number} doesn't exist")
|
||||
raise Exception("Episode not found")
|
||||
episode = episode[0]
|
||||
|
||||
def get_episode_streams(self, anime, episode, *args):
|
||||
episode_id = self.current["data"][int(episode)]["session"]
|
||||
anime_id = anime["id"]
|
||||
url = f"{ANIMEPAHE_BASE}play/{anime_id}{episode_id}"
|
||||
response = requests.get(url, headers=REQUEST_HEADERS)
|
||||
print(response.status_code)
|
||||
input()
|
||||
if not response.status_code == 200:
|
||||
print(response.text)
|
||||
return
|
||||
print(response.text)
|
||||
input()
|
||||
# fetch the episode page
|
||||
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
|
||||
response = self.session.get(url, headers=REQUEST_HEADERS)
|
||||
# get the element containing links to juicy streams
|
||||
c = get_element_by_id("resolutionMenu", response.text)
|
||||
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
|
||||
# convert the elements containing embed links to a neat dict containing:
|
||||
# data-src
|
||||
# data-audio
|
||||
# data-resolution
|
||||
res_dicts = [extract_attributes(item) for item in resolutionMenuItems]
|
||||
|
||||
# get the episode title
|
||||
episode_title = (
|
||||
episode["title"] or f"{anime['title']}; Episode {episode['episode']}"
|
||||
)
|
||||
# get all links
|
||||
streams = {"server": "kwik", "links": [], "episode_title": episode_title}
|
||||
for res_dict in res_dicts:
|
||||
# get embed url
|
||||
embed_url = res_dict["data-src"]
|
||||
data_audio = "dub" if res_dict["data-audio"] == "eng" else "sub"
|
||||
# filter streams by translation_type
|
||||
if data_audio != translation_type:
|
||||
continue
|
||||
|
||||
if not embed_url:
|
||||
logger.warn(
|
||||
"AnimePahe: embed url not found please report to the developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
# get embed page
|
||||
embed_response = self.session.get(embed_url, headers=SERVER_HEADERS)
|
||||
embed = embed_response.text
|
||||
# search for the encoded js
|
||||
encoded_js = None
|
||||
for _ in range(7):
|
||||
content, html = get_element_text_and_html_by_tag("script", embed)
|
||||
if not content:
|
||||
embed = embed.replace(html, "")
|
||||
continue
|
||||
encoded_js = content
|
||||
break
|
||||
if not encoded_js:
|
||||
logger.warn(
|
||||
"AnimePahe: Encoded js not found please report to the developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
# execute the encoded js with node for now or maybe forever in odrder to get a more workable info
|
||||
NODE = shutil.which("node")
|
||||
if not NODE:
|
||||
logger.warn(
|
||||
"AnimePahe: animepahe currently requires node js to extract them juicy streams"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
result = subprocess.run(
|
||||
[NODE, "-e", encoded_js],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
# decoded js
|
||||
evaluted_js = result.stderr
|
||||
if not evaluted_js:
|
||||
logger.warn(
|
||||
"AnimePahe: could not decode encoded js using node please report to developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
# get that juicy stream
|
||||
match = JUICY_STREAM_REGEX.search(evaluted_js)
|
||||
if not match:
|
||||
logger.warn(
|
||||
"AnimePahe: could not find the juicy stream please report to developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
# get the actual hls stream link
|
||||
juicy_stream = match.group(1)
|
||||
# add the link
|
||||
streams["links"].append(
|
||||
{
|
||||
"quality": res_dict["data-resolution"],
|
||||
"translation_type": data_audio,
|
||||
"link": juicy_stream,
|
||||
}
|
||||
)
|
||||
yield streams
|
||||
|
||||
@@ -2,7 +2,7 @@ from yt_dlp.utils.networking import random_user_agent
|
||||
|
||||
USER_AGENT = random_user_agent()
|
||||
ANIMEPAHE = "animepahe.ru"
|
||||
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}/"
|
||||
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
|
||||
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
|
||||
|
||||
REQUEST_HEADERS = {
|
||||
@@ -20,3 +20,20 @@ REQUEST_HEADERS = {
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"TE": "trailers",
|
||||
}
|
||||
SERVER_HEADERS = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
"DNT": "1",
|
||||
"Alt-Used": "kwik.si",
|
||||
"Connection": "keep-alive",
|
||||
"Referer": ANIMEPAHE_BASE,
|
||||
"Cookie": "kwik_session=eyJpdiI6IlZ5UDd0c0lKTDB1NXlhTHZPeWxFc2c9PSIsInZhbHVlIjoieDJZbGhZUG1QZDNaeWtqR3lwWFNnREdhaHBxNVZRMWNDOHVucGpiMHRJOVdhVmpBc3lpTko1VExRMTFWcE1yUVJtVitoTWdOOU5ObTQ0Q0dHU0MzZU0yRUVvNmtWcUdmY3R4UWx4YklJTmpUL0ZodjhtVEpjWU96cEZoUUhUbVYiLCJtYWMiOiI2OGY2YThkOGU0MTgwOThmYzcyZThmNzFlZjlhMzQzMDgwNjlmMTc4NTIzMzc2YjE3YjNmMWQyNTk4NzczMmZiIiwidGFnIjoiIn0%3D; srv=s0; cf_clearance=QMoZtUpZrX0Mh4XJiFmFSSmoWndISPne5FcsGmKKvTQ-1723297585-1.0.1.1-6tVUnP.aef9XeNj0CnN.19D1el_r53t.lhqddX.J88gohH9UnsPWKeJ4yT0pTbcaGRbPuXTLOS.U72.wdy.gMg",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"Sec-Fetch-Dest": "iframe",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "cross-site",
|
||||
"Sec-Fetch-User": "?1",
|
||||
"Priority": "u=4",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
|
||||
class AnimeSearchResult(TypedDict):
|
||||
id: int
|
||||
title: str
|
||||
type: str
|
||||
episodes: int
|
||||
status: str
|
||||
season: str
|
||||
year: int
|
||||
score: int
|
||||
poster: str
|
||||
session: str
|
||||
|
||||
|
||||
class AnimePaheSearchPage(TypedDict):
|
||||
total: int
|
||||
per_page: int
|
||||
current_page: int
|
||||
last_page: int
|
||||
_from: int
|
||||
to: int
|
||||
data: list[AnimeSearchResult]
|
||||
|
||||
|
||||
class Episode(TypedDict):
|
||||
id: int
|
||||
anime_id: int
|
||||
episode: int
|
||||
episode2: int
|
||||
edition: str
|
||||
title: str
|
||||
snapshot: str # episode image
|
||||
disc: str
|
||||
audio: Literal["eng", "jpn"]
|
||||
duration: str # time 00:00:00
|
||||
session: str
|
||||
filler: int
|
||||
created_at: str
|
||||
|
||||
|
||||
class AnimePaheAnimePage(TypedDict):
|
||||
total: int
|
||||
per_page: int
|
||||
current_page: int
|
||||
last_page: int
|
||||
next_page_url: str | None
|
||||
prev_page_url: str | None
|
||||
_from: int
|
||||
to: int
|
||||
data: list[Episode]
|
||||
|
||||
|
||||
class Server:
|
||||
type: str
|
||||
data_src = "https://kwik.si/e/PImJ0u7Y3M0G"
|
||||
data_fansub: str
|
||||
data_resolution: Literal["360", "720", "1080"]
|
||||
data_audio: Literal["eng", "jpn"]
|
||||
data_av1: str
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
from ...anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from ..base_provider import AnimeProvider
|
||||
|
||||
"""
|
||||
"Zoro": {
|
||||
"27": {
|
||||
"identifier": "27",
|
||||
"image": "https://cdn.noitatnemucod.net/thumbnail/300x400/100/ce5e539af63e42431621fc66a47fbec1.jpg",
|
||||
"malId": 1,
|
||||
"aniId": 1,
|
||||
"page": "Zoro",
|
||||
"title": "Cowboy Bebop",
|
||||
"type": "anime",
|
||||
"url": "https://hianime.to/cowboy-bebop-27"
|
||||
}
|
||||
},
|
||||
|
||||
episode info = https://hianime.to/ajax/v2/episode/list/27
|
||||
"""
|
||||
|
||||
|
||||
# TODO: complete this
|
||||
class AniWatchApi(AnimeProvider):
|
||||
def search_for_anime(
|
||||
self, anilist_selected_anime: AnilistBaseMediaDataSchema, *args
|
||||
):
|
||||
return {
|
||||
"pageInfo": 1,
|
||||
"results": [
|
||||
{
|
||||
"id": anilist_selected_anime["id"],
|
||||
"title": anilist_selected_anime["title"],
|
||||
"availableEpisodes": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def get_anime(self, id: int):
|
||||
url = f"https://raw.githubusercontent.com/bal-mackup/mal-backup/master/anilist/anime/{id}.json"
|
||||
response = self.session.get(url)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
data["Sites"]["Zoro"]
|
||||
return {"id": ""}
|
||||
else:
|
||||
return {}
|
||||
|
||||
def get_episode_streams(self, id: int, episode: str, translation_type: str):
|
||||
pass
|
||||
@@ -1,8 +1,18 @@
|
||||
from typing import TypedDict
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
|
||||
class PageInfo(TypedDict):
|
||||
total: int
|
||||
perPage: int
|
||||
currentPage: int
|
||||
|
||||
|
||||
#
|
||||
# class EpisodesDetail(TypedDict):
|
||||
# dub: int
|
||||
# sub: int
|
||||
# raw: int
|
||||
#
|
||||
|
||||
|
||||
# search data
|
||||
@@ -11,6 +21,10 @@ class SearchResult(TypedDict):
|
||||
title: str
|
||||
availableEpisodes: list[str]
|
||||
type: str
|
||||
score: int
|
||||
status: str
|
||||
season: str
|
||||
poster: str
|
||||
|
||||
|
||||
class SearchResults(TypedDict):
|
||||
@@ -19,30 +33,39 @@ class SearchResults(TypedDict):
|
||||
|
||||
|
||||
# anime data
|
||||
class EpisodesDetail(TypedDict):
|
||||
dub: int
|
||||
sub: int
|
||||
raw: int
|
||||
class AnimeEpisodeDetails(TypedDict):
|
||||
dub: list[str]
|
||||
sub: list[str]
|
||||
raw: list[str]
|
||||
|
||||
|
||||
class AnimeEpisode(TypedDict):
|
||||
id: str
|
||||
title: str
|
||||
|
||||
|
||||
class Anime(TypedDict):
|
||||
id: str
|
||||
title: str
|
||||
availableEpisodesDetail: EpisodesDetail
|
||||
availableEpisodesDetail: AnimeEpisodeDetails
|
||||
type: str | None
|
||||
episodesInfo: list[AnimeEpisode] | None
|
||||
poster: str
|
||||
year: str
|
||||
|
||||
|
||||
class EpisodeStream(TypedDict):
|
||||
resolution: str
|
||||
resolution: str | None
|
||||
link: str
|
||||
hls: bool | None
|
||||
mp4: bool
|
||||
priority: int
|
||||
headers: dict
|
||||
fromCache: str
|
||||
mp4: bool | None
|
||||
priority: int | None
|
||||
headers: dict | None
|
||||
quality: Literal["360", "720", "1080", "unknown"]
|
||||
translation_type: Literal["dub", "sub"]
|
||||
|
||||
|
||||
class Server(TypedDict):
|
||||
server: str
|
||||
episode_title: str | None
|
||||
links: list
|
||||
links: list[EpisodeStream]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
from itertools import cycle
|
||||
|
||||
# Dictionary to map hex values to characters
|
||||
hex_to_char = {
|
||||
@@ -34,6 +35,15 @@ hex_to_char = {
|
||||
}
|
||||
|
||||
|
||||
def give_random_quality(links: list[dict]):
|
||||
qualities = cycle(["1080", "720", "360"])
|
||||
|
||||
return [
|
||||
{"link": link["link"], "quality": quality}
|
||||
for link, quality in zip(links, qualities)
|
||||
]
|
||||
|
||||
|
||||
def decode_hex_string(hex_string):
|
||||
"""some of the sources encrypt the urls into hex codes this function decrypts the urls
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import sys
|
||||
from typing import Callable, List
|
||||
|
||||
# TODO: will probably scrap art not to useful
|
||||
from art import text2art
|
||||
from click import clear
|
||||
from rich import print
|
||||
|
||||
@@ -22,6 +21,17 @@ FZF_DEFAULT_OPTS = """
|
||||
--marker=">" --pointer="◆" --separator="─" --scrollbar="│"
|
||||
"""
|
||||
|
||||
HEADER = """
|
||||
|
||||
███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗
|
||||
██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝
|
||||
█████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░
|
||||
██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░
|
||||
██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗
|
||||
╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class FZF:
|
||||
"""an abstraction over the fzf commandline utility
|
||||
@@ -128,7 +138,7 @@ class FZF:
|
||||
self,
|
||||
fzf_input: list[str],
|
||||
prompt: str,
|
||||
header: str,
|
||||
header: str = HEADER,
|
||||
preview: str | None = None,
|
||||
expect: str | None = None,
|
||||
validator: Callable | None = None,
|
||||
@@ -149,7 +159,7 @@ class FZF:
|
||||
_commands = [
|
||||
*self.default_options,
|
||||
"--header",
|
||||
text2art(header),
|
||||
HEADER,
|
||||
"--header-first",
|
||||
"--prompt",
|
||||
prompt.title(),
|
||||
|
||||
16
poetry.lock
generated
16
poetry.lock
generated
@@ -1,19 +1,5 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "art"
|
||||
version = "6.2"
|
||||
description = "ASCII Art Library For Python"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "art-6.2-py3-none-any.whl", hash = "sha256:d632d1d3f5fabcaf8673abe934b51df0017bc914d106e89d45ae4ebef0e3149a"},
|
||||
{file = "art-6.2.tar.gz", hash = "sha256:506a0c4f261289a0e0d088de7beffcb1835078c4e44b0c5353bdaf47b490e76f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["bandit (>=1.5.1)", "coverage (>=4.1)", "pydocstyle (>=3.0.0)", "vulture (>=1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "autoflake"
|
||||
version = "2.3.1"
|
||||
@@ -1421,4 +1407,4 @@ test = ["pytest (>=8.1,<9.0)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "5305621bc02d824065519913f0d754e269f1e4525ba8796be08504120614259d"
|
||||
content-hash = "83ec7de7d9466dcd1fadef4b21eec2a879cc9a7d526992ed280b6af53b49d9f1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "0.60.1.dev1"
|
||||
version = "0.62.0.dev1"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
@@ -13,7 +13,6 @@ rich = "^13.7.1"
|
||||
click = "^8.1.7"
|
||||
inquirerpy = "^0.3.4"
|
||||
platformdirs = "^4.2.2"
|
||||
art = "^6.2"
|
||||
python-dotenv = "^1.0.1"
|
||||
thefuzz = "^0.22.1"
|
||||
requests = "^2.32.3"
|
||||
|
||||
@@ -45,6 +45,16 @@ def test_search_help(runner: CliRunner):
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_cache_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["cache", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_completions_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["completions", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_anilist_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["anilist", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
Reference in New Issue
Block a user