mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-13 00:00:01 -08:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9fc210264 | ||
|
|
68fdb7835f | ||
|
|
531929aab5 | ||
|
|
2ca5985b9a | ||
|
|
684f8c57a8 | ||
|
|
324fef36ac | ||
|
|
9d62915f2b | ||
|
|
a4e9e5f29e | ||
|
|
d00c958ff2 | ||
|
|
bc2ac69b9a | ||
|
|
01fa96c27a | ||
|
|
6c1bbfe50a | ||
|
|
ecc4e85079 | ||
|
|
1cd743acdf | ||
|
|
23dd969d37 | ||
|
|
d21f6b5ab0 | ||
|
|
640bb12c44 | ||
|
|
453e4c1b74 | ||
|
|
4dc3d1b0bb | ||
|
|
4df57f9410 | ||
|
|
baa94efc24 | ||
|
|
f5d18512f8 | ||
|
|
72037eea07 | ||
|
|
f5c120ebb8 | ||
|
|
5f2b88bd9b | ||
|
|
b346801dba | ||
|
|
1b1a05e2b3 | ||
|
|
8716fb2e1d | ||
|
|
12a38d6d48 | ||
|
|
e6aa508644 | ||
|
|
584a2ee3f1 | ||
|
|
385dd4337d | ||
|
|
1c70a2122d | ||
|
|
46b9b844d4 | ||
|
|
272042ec35 | ||
|
|
56632cf77c | ||
|
|
e8dacf0722 | ||
|
|
b95d49429c | ||
|
|
ca087b2e94 | ||
|
|
3f33ae3738 | ||
|
|
94a282a320 | ||
|
|
0b379ec813 | ||
|
|
6b0a013705 | ||
|
|
6c1f8d09e6 | ||
|
|
6bb2c89a8c | ||
|
|
9f56b74ff0 | ||
|
|
4d03b86498 | ||
|
|
fab86090a3 | ||
|
|
71d258385c | ||
|
|
bc55ed6e81 | ||
|
|
197bfa9f8a | ||
|
|
f84c60e6bc |
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Build release distributions
|
||||
run: |
|
||||
|
||||
133
README.md
133
README.md
@@ -1,11 +1,36 @@
|
||||
# Fast Anime
|
||||
# FastAnime
|
||||
|
||||
Welcome to **FastAnime**, an anime scrapper that brings a browser experience to the terminal.
|
||||
Welcome to **FastAnime**, anime site experience from the terminal.
|
||||
|
||||
[intro.webm](https://github.com/user-attachments/assets/036af7fc-83ff-4f9b-bda6-0c913f7d0f38)
|
||||
[fa_demo.webm](https://github.com/user-attachments/assets/bb46642c-176e-42b3-a533-ff55d4dac111)
|
||||
|
||||
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)
|
||||
- [Installation](#installation)
|
||||
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
|
||||
- [Using pipx](#using-pipx)
|
||||
- [Using pip](#using-pip)
|
||||
- [Installing the bleeding edge version](#installing-the-bleeding-edge-version)
|
||||
- [Building from the source](#building-from-the-source)
|
||||
- [External Dependencies](#external-dependencies)
|
||||
- [Usage](#usage)
|
||||
- [The Commandline interface :fire:](#the-commandline-interface-fire)
|
||||
- [The anilist command](#the-anilist-command)
|
||||
- [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)
|
||||
- [Configuration](#configuration)
|
||||
- [Contributing](#contributing)
|
||||
- [Receiving Support](#receiving-support)
|
||||
- [Supporting the Project](#supporting-the-project)
|
||||
<!--toc:end-->
|
||||
|
||||
> [!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.
|
||||
@@ -14,27 +39,6 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
||||
>
|
||||
> The docs are still being worked on and are far from completion.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
|
||||
- [Using pipx](#using-pipx)
|
||||
- [Using pip](#using-pip)
|
||||
- [Installing the building edge version](#installing-the-bleeding-edge-version)
|
||||
- [Building from the source](#building-from-the-source)
|
||||
- [External Dependencies](#external-dependencies)
|
||||
- [Usage](#usage)
|
||||
- [The Commandline interface](#the-commandline-interface-fire)
|
||||
- [The anilist command](#the-anilist-command)
|
||||
- [download subcommand](#download-subcommand)
|
||||
- [search subcommand](#search-subcommand)
|
||||
- [downloads subcommand](#downloads-subcommand)
|
||||
- [config subcommand](#config-subcommand)
|
||||
- [Configuration](#configuration)
|
||||
- [Contributing](#contributing)
|
||||
- [Receiving Support](#receiving-support)
|
||||
- [Supporting the Project](#supporting-the-project)
|
||||
|
||||
## Installation
|
||||
|
||||
The app can run wherever python can run. So all you need to have is python installed on your device.
|
||||
@@ -63,7 +67,7 @@ pip install fastanime
|
||||
|
||||
### Installing the bleeding edge version
|
||||
|
||||
To install the latest build which are created on every push by Github actions, download the [fastanime_debug_build](https://github.com/Benex254/FastAnime/actions) of your choosing from the Github actions page.
|
||||
To install the latest build which are created on every push by GitHub actions, download the [fastanime_debug_build](https://github.com/Benex254/FastAnime/actions) of your choosing from the GitHub actions page.
|
||||
Then:
|
||||
|
||||
```bash
|
||||
@@ -130,12 +134,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 dependecies that will just make your experience better:**
|
||||
**Other 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]()!!
|
||||
- [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
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -143,7 +148,7 @@ The app offers both a graphical interface (under development) and a robust comma
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The GUI is in development; use the CLI for now.
|
||||
> The GUI is mostly in hiatus; use the CLI for now.
|
||||
> However, you can try it out before i decided to change my objective by checking out this [release](https://github.com/Benex254/FastAnime/tree/v0.20.0).
|
||||
> But be reassured for those who aren't terminal chads, i will still complete the GUI for the fun of it
|
||||
|
||||
@@ -153,13 +158,13 @@ Designed for power users who prefer efficiency over browser-based streaming and
|
||||
|
||||
Overview of main commands:
|
||||
|
||||
- `fastanime anilist`: Powerful command for browsing and exploring anime due to Anilist intergration.
|
||||
- `fastanime anilist`: Powerful command for browsing and exploring anime due to AniList integration.
|
||||
- `fastanime download`: Download anime.
|
||||
- `fastanime search`: Powerful command meant for binging since it doesn't require the interfaces
|
||||
- `fastanime downloads`: View downloaded anime and watch with mpv.
|
||||
- `fastanime downloads`: View downloaded anime and watch with MPV.
|
||||
- `fastanime config`: Quickly edit configuration settings.
|
||||
|
||||
Configuration is directly passed into this command at run time to overide your config.
|
||||
Configuration is directly passed into this command at run time to override your config.
|
||||
|
||||
Available options include:
|
||||
|
||||
@@ -174,9 +179,11 @@ Available options include:
|
||||
- `--default` use the default ui
|
||||
- `--preview` show a preview when using fzf
|
||||
- `--no-preview` dont show a preview when using fzf
|
||||
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. works when `--server gogoanime`
|
||||
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
|
||||
- `--icons/--no-icons` toggle the visibility of the icons
|
||||
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
|
||||
|
||||
#### The anilist command
|
||||
#### The anilist command :fire: :fire: :fire:
|
||||
|
||||
Stream, browse, and discover anime efficiently from the terminal using the [AniList API](https://github.com/AniList/ApiV2-GraphQL-Docs).
|
||||
|
||||
@@ -196,6 +203,47 @@ The subcommands are mainly their as convenience. Since all the features already
|
||||
- `fastanime anilist favourites`: Top 15 favorite anime.
|
||||
- `fastanime anilist random`: get random anime
|
||||
|
||||
The following are commands you can only run if you are signed in to your AniList account:
|
||||
|
||||
- `fastanime anilist watching`
|
||||
- `fastanime anilist planning`
|
||||
- `fastanime anilist rewatching`
|
||||
- `fastanime anilist dropped`
|
||||
- `fastanime anilist paused`
|
||||
- `fastanime anilist completed`
|
||||
|
||||
Plus: `fastanime anilist notifier` :fire:
|
||||
|
||||
```bash
|
||||
# basic form
|
||||
fastanime anilist notifier
|
||||
|
||||
# with logging to stdout
|
||||
fastanime --log anilist notifier
|
||||
|
||||
# with logging to a file. stored in the same place as your config
|
||||
fastanime --log-file anilist notifier
|
||||
```
|
||||
|
||||
The above commands will start a loop that checks every 2 minutes if any of the anime in your watch list that are aireing has just released a new episode.
|
||||
|
||||
The notification will consist of a cover image of the anime in none windows systems.
|
||||
|
||||
You can place the command among your machines startup scripts.
|
||||
|
||||
For fish users for example you can decide to put this in your `~/.config/fish/config.fish`:
|
||||
|
||||
```fish
|
||||
if ! ps aux | grep -q '[f]astanime .* notifier'
|
||||
echo initializing fastanime anilist notifier
|
||||
fastanime --log-file anilist notifier>/dev/null &
|
||||
end
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To sign in just run `fastanime anilist login` and follow the instructions.
|
||||
> To view your login status `fastanime anilist login --status`
|
||||
|
||||
#### download subcommand
|
||||
|
||||
Download anime to watch later dub or sub with this one command.
|
||||
@@ -269,17 +317,23 @@ fastanime config --path
|
||||
|
||||
## 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.
|
||||
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`.
|
||||
|
||||
```ini
|
||||
[stream]
|
||||
continue_from_history = True # Auto continue from watch history
|
||||
translation_type = sub # Preferred language for anime (options: dub, sub)
|
||||
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top)
|
||||
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
||||
auto_next = False # Auto-select next episode
|
||||
# Auto select the anime provider results with fuzzyfind.
|
||||
# Auto select the anime provider results with fuzzy find.
|
||||
# Note this wont always be correct.But 99% of the time will be.
|
||||
auto_select=True
|
||||
# whether to skip the opening and ending theme songs
|
||||
# note requires ani-skip to be in path
|
||||
skip=false
|
||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||
# used in the continue from time stamp
|
||||
error=3
|
||||
|
||||
# the format of downloaded anime and trailer
|
||||
# based on yt-dlp format and passed directly to it
|
||||
@@ -295,6 +349,13 @@ downloads_dir = <Default-videos-dir>/FastAnime # Download directory
|
||||
use_fzf=False # whether to use fzf as the interface for the anilist command and others.
|
||||
preview=false # whether to show a preview window when using fzf
|
||||
|
||||
# whether to show the icons
|
||||
icons=false
|
||||
|
||||
# the duration in minutes a notification will stay in the screen
|
||||
# used by notifier command
|
||||
notification_duration=2
|
||||
|
||||
[anilist]
|
||||
# Not implemented yet
|
||||
```
|
||||
|
||||
4
fa
4
fa
@@ -1,2 +1,2 @@
|
||||
#! /usr/bin/bash
|
||||
poetry run fastanime $*
|
||||
#!/usr/bin/env sh
|
||||
exec "${PYTHON:-python3}" -Werror -Xdev "$(dirname "$(realpath "$0")")/fastanime/__main__.py" "$@"
|
||||
|
||||
@@ -8,7 +8,7 @@ from subprocess import PIPE, Popen
|
||||
import requests
|
||||
from rich import print
|
||||
|
||||
from .. import APP_NAME, AUTHOR, GIT_REPO, REPO, __version__
|
||||
from .. import APP_NAME, AUTHOR, GIT_REPO, __version__
|
||||
|
||||
API_URL = f"https://api.{GIT_REPO}/repos/{AUTHOR}/{APP_NAME}/releases/latest"
|
||||
|
||||
@@ -91,13 +91,12 @@ def update_app():
|
||||
else:
|
||||
executable = sys.executable
|
||||
|
||||
app_package_url = f"https://{REPO}/releases/download/{tag_name}/fastanime-{tag_name.replace("v","")}.tar.gz"
|
||||
args = [
|
||||
executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
app_package_url,
|
||||
APP_NAME,
|
||||
"--user",
|
||||
"--no-warn-script-location",
|
||||
]
|
||||
|
||||
@@ -53,7 +53,8 @@ class YtDLPDownloader:
|
||||
anime_title = sanitize_filename(title[0])
|
||||
episode_title = sanitize_filename(title[1])
|
||||
ydl_opts = {
|
||||
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s", # Specify the output path and template
|
||||
# Specify the output path and template
|
||||
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
|
||||
"progress_hooks": [
|
||||
main_progress_hook,
|
||||
], # Progress hook
|
||||
|
||||
@@ -2,13 +2,13 @@ import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .. import USER_DATA_PATH
|
||||
from ..constants import USER_DATA_PATH
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserData:
|
||||
user_data = {"watch_history": {}, "animelist": []}
|
||||
user_data = {"watch_history": {}, "animelist": [], "user": {}}
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
@@ -23,6 +23,10 @@ class UserData:
|
||||
self.user_data["watch_history"] = watch_history
|
||||
self._update_user_data()
|
||||
|
||||
def update_user_info(self, user: dict):
|
||||
self.user_data["user"] = user
|
||||
self._update_user_data()
|
||||
|
||||
def update_animelist(self, anime_list: list):
|
||||
self.user_data["animelist"] = list(set(anime_list))
|
||||
self._update_user_data()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -10,6 +11,7 @@ from fastanime.libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchem
|
||||
|
||||
from .data import anime_normalizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# TODO: make it use color_text instead of fixed vals
|
||||
# from .kivy_markup_helper import color_text
|
||||
|
||||
@@ -127,6 +129,7 @@ def anime_title_percentage_match(
|
||||
fuzz.ratio(title_a.lower(), possible_user_requested_anime_title.lower()),
|
||||
fuzz.ratio(title_b.lower(), possible_user_requested_anime_title.lower()),
|
||||
)
|
||||
logger.info(f"{locals()}")
|
||||
return percentage_ratio
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
raise ImportError(
|
||||
"You are using an unsupported version of Python. Only Python versions 3.8 and above are supported by yt-dlp"
|
||||
) # noqa: F541
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from platform import platform
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from platformdirs import PlatformDirs
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -15,43 +19,12 @@ if os.environ.get("FA_RICH_TRACEBACK", False):
|
||||
|
||||
|
||||
# initiate constants
|
||||
__version__ = "v0.30.0"
|
||||
__version__ = "v0.40.1"
|
||||
|
||||
PLATFORM = platform()
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
GIT_REPO = "github.com"
|
||||
REPO = f"{GIT_REPO}/{AUTHOR}/{APP_NAME}"
|
||||
USER_NAME = os.environ.get("USERNAME", f"{APP_NAME} user")
|
||||
|
||||
|
||||
dirs = PlatformDirs(appname=APP_NAME, appauthor=AUTHOR, ensure_exists=True)
|
||||
|
||||
|
||||
# ---- app deps ----
|
||||
APP_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
CONFIGS_DIR = os.path.join(APP_DIR, "configs")
|
||||
ASSETS_DIR = os.path.join(APP_DIR, "assets")
|
||||
|
||||
# ----- user configs and data -----
|
||||
APP_DATA_DIR = dirs.user_config_dir
|
||||
if not APP_DATA_DIR:
|
||||
APP_DATA_DIR = dirs.user_data_dir
|
||||
|
||||
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
|
||||
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
|
||||
|
||||
# cache dir
|
||||
APP_CACHE_DIR = dirs.user_cache_dir
|
||||
|
||||
# video dir
|
||||
USER_VIDEOS_DIR = os.path.join(dirs.user_videos_dir, APP_NAME)
|
||||
|
||||
# web dirs
|
||||
|
||||
WEB_DIR = os.path.join(APP_DIR, "web")
|
||||
FRONTEND_DIR = os.path.join(WEB_DIR, "frontend")
|
||||
BACKEND_DIR = os.path.join(WEB_DIR, "backend")
|
||||
|
||||
|
||||
def FastAnime():
|
||||
@@ -72,6 +45,22 @@ def FastAnime():
|
||||
handlers=[RichHandler()], # Use RichHandler to format the logs
|
||||
)
|
||||
sys.argv.remove("--log")
|
||||
if "--log-file" in sys.argv:
|
||||
# Configure logging
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from .constants import NOTIFIER_LOG_FILE_PATH
|
||||
|
||||
logging.getLogger(__name__)
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG, # Set the logging level to DEBUG
|
||||
# Use a simple message format
|
||||
format="%(asctime)s%(levelname)s: %(message)s",
|
||||
datefmt="[%d/%m/%Y@%H:%M:%S]", # Use a custom date format
|
||||
filename=NOTIFIER_LOG_FILE_PATH,
|
||||
filemode="a", # Use RichHandler to format the logs
|
||||
)
|
||||
sys.argv.remove("--log-file")
|
||||
|
||||
from .cli import run_cli
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __package__ is None and not getattr(sys, "frozen", False):
|
||||
@@ -10,15 +9,6 @@ if __package__ is None and not getattr(sys, "frozen", False):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
in_development = bool(os.environ.get("FA_DEVELOPMENT", False))
|
||||
from . import FastAnime
|
||||
|
||||
if in_development:
|
||||
FastAnime()
|
||||
else:
|
||||
try:
|
||||
FastAnime()
|
||||
except Exception as e:
|
||||
from .Utility.utils import write_crash
|
||||
|
||||
write_crash(e)
|
||||
FastAnime()
|
||||
|
||||
BIN
fastanime/assets/logo.ico
Normal file
BIN
fastanime/assets/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
BIN
fastanime/assets/logo.png
Normal file
BIN
fastanime/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
@@ -50,7 +50,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
@click.option(
|
||||
"-s",
|
||||
"--server",
|
||||
type=click.Choice(SERVERS_AVAILABLE, case_sensitive=False),
|
||||
type=click.Choice([*SERVERS_AVAILABLE, "top"], case_sensitive=False),
|
||||
help="Server of choice",
|
||||
)
|
||||
@click.option(
|
||||
@@ -66,6 +66,11 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
type=bool,
|
||||
help="Continue from last episode?",
|
||||
)
|
||||
@click.option(
|
||||
"--skip/--no-skip",
|
||||
type=bool,
|
||||
help="Skip opening and ending theme songs?",
|
||||
)
|
||||
@click.option(
|
||||
"-q",
|
||||
"--quality",
|
||||
@@ -100,6 +105,11 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
@click.option("--default", is_flag=True, help="Use the default interface")
|
||||
@click.option("--preview", is_flag=True, help="Show preview when using fzf")
|
||||
@click.option("--no-preview", is_flag=True, help="Dont show preview when using fzf")
|
||||
@click.option(
|
||||
"--icons/--no-icons",
|
||||
type=bool,
|
||||
help="Use icons in the interfaces",
|
||||
)
|
||||
@click.pass_context
|
||||
def run_cli(
|
||||
ctx: click.Context,
|
||||
@@ -107,6 +117,7 @@ def run_cli(
|
||||
server,
|
||||
format,
|
||||
continue_,
|
||||
skip,
|
||||
translation_type,
|
||||
quality,
|
||||
auto_next,
|
||||
@@ -117,6 +128,7 @@ def run_cli(
|
||||
default,
|
||||
preview,
|
||||
no_preview,
|
||||
icons,
|
||||
):
|
||||
ctx.obj = Config()
|
||||
if provider:
|
||||
@@ -128,10 +140,15 @@ def run_cli(
|
||||
ctx.obj.format = format
|
||||
if ctx.get_parameter_source("continue_") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.continue_from_history = continue_
|
||||
if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.skip = skip
|
||||
|
||||
if quality:
|
||||
ctx.obj.quality = quality
|
||||
if ctx.get_parameter_source("auto-next") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.auto_next = auto_next
|
||||
if ctx.get_parameter_source("icons") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.icons = icons
|
||||
if (
|
||||
ctx.get_parameter_source("auto_select")
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import click
|
||||
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import anilist as anilist_interface
|
||||
from ...utils.tools import QueryDict
|
||||
from .completed import completed
|
||||
from .dropped import dropped
|
||||
from .favourites import favourites
|
||||
from .login import login
|
||||
from .notifier import notifier
|
||||
from .paused import paused
|
||||
from .planning import planning
|
||||
from .popular import popular
|
||||
from .random_anime import random_anime
|
||||
from .recent import recent
|
||||
from .rewatching import rewatching
|
||||
from .scores import scores
|
||||
from .search import search
|
||||
from .trending import trending
|
||||
from .upcoming import upcoming
|
||||
from .watching import watching
|
||||
|
||||
commands = {
|
||||
"trending": trending,
|
||||
@@ -20,6 +29,14 @@ commands = {
|
||||
"popular": popular,
|
||||
"favourites": favourites,
|
||||
"random": random_anime,
|
||||
"login": login,
|
||||
"watching": watching,
|
||||
"paused": paused,
|
||||
"rewatching": rewatching,
|
||||
"dropped": dropped,
|
||||
"completed": completed,
|
||||
"planning": planning,
|
||||
"notifier": notifier,
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +48,8 @@ commands = {
|
||||
)
|
||||
@click.pass_context
|
||||
def anilist(ctx: click.Context):
|
||||
if user := ctx.obj.user:
|
||||
AniList.update_login_info(user, user["token"])
|
||||
if ctx.invoked_subcommand is None:
|
||||
anilist_config = QueryDict()
|
||||
anilist_interface(ctx.obj, anilist_config)
|
||||
|
||||
29
fastanime/cli/commands/anilist/completed.py
Normal file
29
fastanime/cli/commands/anilist/completed.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import click
|
||||
|
||||
from fastanime.cli.config import Config
|
||||
from fastanime.cli.interfaces import anilist_interfaces
|
||||
from fastanime.cli.utils.tools import QueryDict, exit_app
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
|
||||
@click.command(help="View anime you completed")
|
||||
@click.pass_obj
|
||||
def completed(config: Config):
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("COMPLETED")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
29
fastanime/cli/commands/anilist/dropped.py
Normal file
29
fastanime/cli/commands/anilist/dropped.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import click
|
||||
|
||||
from fastanime.cli.config import Config
|
||||
from fastanime.cli.interfaces import anilist_interfaces
|
||||
from fastanime.cli.utils.tools import QueryDict, exit_app
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
|
||||
@click.command(help="View anime you dropped")
|
||||
@click.pass_obj
|
||||
def dropped(config: Config):
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("DROPPED")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
44
fastanime/cli/commands/anilist/login.py
Normal file
44
fastanime/cli/commands/anilist/login.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import webbrowser
|
||||
|
||||
import click
|
||||
from rich import print
|
||||
from rich.prompt import Confirm, Prompt
|
||||
|
||||
from ....anilist import AniList
|
||||
from ...config import Config
|
||||
from ...utils.tools import exit_app
|
||||
|
||||
|
||||
@click.command(help="Login to your anilist account")
|
||||
@click.option("--status", "-s", help="Whether you are logged in or not", is_flag=True)
|
||||
@click.pass_obj
|
||||
def login(config: Config, status):
|
||||
if status:
|
||||
is_logged_in = True if config.user else False
|
||||
message = (
|
||||
"You are logged in :happy:" if is_logged_in else "You arent logged in :cry"
|
||||
)
|
||||
print(message)
|
||||
print(config.user)
|
||||
exit_app()
|
||||
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] )",
|
||||
)
|
||||
webbrowser.open(config.fastanime_anilist_app_login_url)
|
||||
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()
|
||||
return
|
||||
user["token"] = token
|
||||
config.update_user(user)
|
||||
print("Successfully saved credentials")
|
||||
print(user)
|
||||
exit_app()
|
||||
101
fastanime/cli/commands/anilist/notifier.py
Normal file
101
fastanime/cli/commands/anilist/notifier.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import click
|
||||
import requests
|
||||
from plyer import notification
|
||||
|
||||
from ....anilist import AniList
|
||||
from ....constants import APP_CACHE_DIR, APP_DATA_DIR, APP_NAME, PLATFORM
|
||||
from ..config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# plyer.notification(title="anime",message="Update",app_name=APP_NAME)
|
||||
@click.command(help="Check for notifications on anime you currently watching")
|
||||
@click.pass_obj
|
||||
def notifier(config: Config):
|
||||
notified = os.path.join(APP_DATA_DIR, "last_notification.json")
|
||||
anime_image = os.path.join(APP_CACHE_DIR, "notification_image")
|
||||
notification_duration = config.notification_duration * 60
|
||||
|
||||
if not config.user:
|
||||
print("Not Authenticated")
|
||||
print("Run the following to get started: fastanime anilist loggin")
|
||||
return
|
||||
run = True
|
||||
timeout = 2
|
||||
if os.path.exists(notified):
|
||||
with open(notified, "r") as f:
|
||||
past_notifications = json.load(f)
|
||||
else:
|
||||
past_notifications = {}
|
||||
with open(notified, "w") as f:
|
||||
json.dump(past_notifications, f)
|
||||
|
||||
while run:
|
||||
try:
|
||||
logger.info("checking for notifications")
|
||||
result = AniList.get_notification()
|
||||
if not result[0]:
|
||||
print(result)
|
||||
logger.warning(
|
||||
"Something went wrong this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
logger.info("sleeping...")
|
||||
time.sleep(timeout * 60)
|
||||
continue
|
||||
data = result[1]
|
||||
# pyright:ignore
|
||||
notifications = data["data"]["Page"]["notifications"]
|
||||
if not notifications:
|
||||
logger.info("Nothing to notify")
|
||||
else:
|
||||
for notification_ in notifications:
|
||||
anime_episode = notification_["episode"]
|
||||
title = f"Episode {anime_episode} just aired"
|
||||
anime_title = notification_["media"]["title"][
|
||||
config.preferred_language
|
||||
]
|
||||
# pyright:ignore
|
||||
message = f"{anime_title}\nBe sure to watch so you are not left out of the loop."
|
||||
# message = str(textwrap.wrap(message, width=50))
|
||||
|
||||
id = notification_["media"]["id"]
|
||||
if past_notifications.get(str(id)) == notification_["episode"]:
|
||||
logger.info(
|
||||
f"skipping id={id} title={anime_title} episode={anime_episode} already notified"
|
||||
)
|
||||
|
||||
else:
|
||||
if PLATFORM != "Windows":
|
||||
image_link = notification_["media"]["coverImage"]["medium"]
|
||||
print(image_link)
|
||||
logger.info("Downloading image")
|
||||
|
||||
resp = requests.get(image_link)
|
||||
if resp.status_code == 200:
|
||||
with open(anime_image, "wb") as f:
|
||||
f.write(resp.content)
|
||||
ICON_PATH = anime_image
|
||||
|
||||
past_notifications[f"{id}"] = notification_["episode"]
|
||||
with open(notified, "w") as f:
|
||||
json.dump(past_notifications, f)
|
||||
logger.info(message)
|
||||
notification.notify( # pyright:ignore
|
||||
title=title,
|
||||
message=message,
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
hints={"image-path": ICON_PATH},
|
||||
timeout=notification_duration,
|
||||
)
|
||||
time.sleep(30)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
logger.info("sleeping...")
|
||||
time.sleep(timeout * 60)
|
||||
29
fastanime/cli/commands/anilist/paused.py
Normal file
29
fastanime/cli/commands/anilist/paused.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import click
|
||||
|
||||
from fastanime.cli.config import Config
|
||||
from fastanime.cli.interfaces import anilist_interfaces
|
||||
from fastanime.cli.utils.tools import QueryDict, exit_app
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
|
||||
@click.command(help="View anime you paused on watching")
|
||||
@click.pass_obj
|
||||
def paused(config: Config):
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("PAUSED")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
29
fastanime/cli/commands/anilist/planning.py
Normal file
29
fastanime/cli/commands/anilist/planning.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import click
|
||||
|
||||
from fastanime.cli.config import Config
|
||||
from fastanime.cli.interfaces import anilist_interfaces
|
||||
from fastanime.cli.utils.tools import QueryDict, exit_app
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
|
||||
@click.command(help="View anime you are planning on watching")
|
||||
@click.pass_obj
|
||||
def planning(config: Config):
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("PLANNING")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
29
fastanime/cli/commands/anilist/rewatching.py
Normal file
29
fastanime/cli/commands/anilist/rewatching.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import click
|
||||
|
||||
from fastanime.cli.config import Config
|
||||
from fastanime.cli.interfaces import anilist_interfaces
|
||||
from fastanime.cli.utils.tools import QueryDict, exit_app
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
|
||||
@click.command(help="View anime you are rewatching")
|
||||
@click.pass_obj
|
||||
def rewatching(config: Config):
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("REPEATING")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
29
fastanime/cli/commands/anilist/watching.py
Normal file
29
fastanime/cli/commands/anilist/watching.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import click
|
||||
|
||||
from fastanime.cli.config import Config
|
||||
from fastanime.cli.interfaces import anilist_interfaces
|
||||
from fastanime.cli.utils.tools import QueryDict, exit_app
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
|
||||
@click.command(help="View anime you are watching")
|
||||
@click.pass_obj
|
||||
def watching(config: Config):
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("CURRENT")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = QueryDict()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.select_anime(config, anilist_config)
|
||||
@@ -4,7 +4,9 @@ import subprocess
|
||||
import click
|
||||
from rich import print
|
||||
|
||||
from ... import USER_CONFIG_PATH
|
||||
from fastanime.cli.config import Config
|
||||
|
||||
from ...constants import USER_CONFIG_PATH
|
||||
from ..utils.tools import exit_app
|
||||
|
||||
|
||||
@@ -13,7 +15,8 @@ from ..utils.tools import exit_app
|
||||
short_help="Edit your config",
|
||||
)
|
||||
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
|
||||
def configure(path):
|
||||
@click.pass_obj
|
||||
def configure(config: Config, path):
|
||||
if path:
|
||||
print(USER_CONFIG_PATH)
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import click
|
||||
from rich import print
|
||||
from rich.progress import Progress
|
||||
from thefuzz import fuzz
|
||||
|
||||
from ...libs.anime_provider.types import Anime
|
||||
@@ -28,9 +29,11 @@ def download(config: Config, anime_title, episode_range):
|
||||
anime_provider = config.anime_provider
|
||||
translation_type = config.translation_type
|
||||
download_dir = config.downloads_dir
|
||||
search_results = anime_provider.search_for_anime(
|
||||
anime_title, translation_type=translation_type
|
||||
)
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Search Results...", total=None)
|
||||
search_results = anime_provider.search_for_anime(
|
||||
anime_title, translation_type=translation_type
|
||||
)
|
||||
if not search_results:
|
||||
print("Search results failed")
|
||||
input("Enter to retry")
|
||||
@@ -51,7 +54,11 @@ def download(config: Config, anime_title, episode_range):
|
||||
list(search_results_.keys()), "Please Select title: ", "FastAnime"
|
||||
)
|
||||
|
||||
anime: Anime | None = anime_provider.get_anime(search_results_[search_result]["id"])
|
||||
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...")
|
||||
@@ -70,20 +77,24 @@ def download(config: Config, anime_title, episode_range):
|
||||
if episode not in episodes:
|
||||
print(f"[cyan]Warning[/]: Episode {episode} not found, skipping")
|
||||
continue
|
||||
streams = anime_provider.get_episode_streams(
|
||||
anime, episode, config.translation_type
|
||||
)
|
||||
if not streams:
|
||||
print("No streams 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
|
||||
|
||||
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]
|
||||
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
|
||||
|
||||
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]
|
||||
downloader._download_file(
|
||||
link,
|
||||
download_dir,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import click
|
||||
from rich import print
|
||||
from rich.progress import Progress
|
||||
from thefuzz import fuzz
|
||||
|
||||
from ...cli.config import Config
|
||||
@@ -23,9 +24,11 @@ from ..utils.utils import clear
|
||||
@click.pass_obj
|
||||
def search(config: Config, anime_title: str, episode_range: str):
|
||||
anime_provider = config.anime_provider
|
||||
search_results = anime_provider.search_for_anime(
|
||||
anime_title, config.translation_type
|
||||
)
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Search Results...", total=None)
|
||||
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")
|
||||
@@ -50,7 +53,12 @@ def search(config: Config, anime_title: str, episode_range: str):
|
||||
list(search_results_.keys()), "Please Select title: ", "FastAnime"
|
||||
)
|
||||
|
||||
anime: Anime | None = anime_provider.get_anime(search_results_[search_result]["id"])
|
||||
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...")
|
||||
@@ -82,17 +90,20 @@ def search(config: Config, anime_title: str, episode_range: str):
|
||||
|
||||
if not episode or episode not in episodes:
|
||||
episode = fzf.run(episodes, "Select an episode: ", header=search_result)
|
||||
streams = anime_provider.get_episode_streams(
|
||||
anime, episode, config.translation_type
|
||||
)
|
||||
if not streams:
|
||||
print("Failed to get streams")
|
||||
return
|
||||
links = [link["link"] for server in streams for link in server["links"]]
|
||||
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("Failed to get streams")
|
||||
return
|
||||
links = [link["link"] for server in streams for link in server["links"]]
|
||||
|
||||
# TODO: Come up with way to know quality and better server interface
|
||||
link = links[config.quality]
|
||||
# TODO: Come up with way to know quality and better server interface
|
||||
link = links[config.quality]
|
||||
# link = fzf.run(links, "Select stream", "Streams")
|
||||
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
|
||||
|
||||
mpv(link, search_result)
|
||||
stream_anime()
|
||||
|
||||
@@ -3,14 +3,17 @@ from configparser import ConfigParser
|
||||
|
||||
from rich import print
|
||||
|
||||
from .. import USER_CONFIG_PATH, USER_VIDEOS_DIR
|
||||
from ..AnimeProvider import AnimeProvider
|
||||
from ..constants import USER_CONFIG_PATH, USER_VIDEOS_DIR
|
||||
from ..Utility.user_data_helper import user_data_helper
|
||||
|
||||
|
||||
class Config(object):
|
||||
anime_list: list
|
||||
watch_history: dict
|
||||
fastanime_anilist_app_login_url = (
|
||||
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.load_config()
|
||||
@@ -31,6 +34,10 @@ class Config(object):
|
||||
"preview": "False",
|
||||
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
||||
"provider": "allanime",
|
||||
"error": "3",
|
||||
"icons": "false",
|
||||
"notification_duration": "2",
|
||||
"skip": "false",
|
||||
}
|
||||
)
|
||||
self.configparser.add_section("stream")
|
||||
@@ -45,6 +52,8 @@ class Config(object):
|
||||
self.downloads_dir = self.get_downloads_dir()
|
||||
self.provider = self.get_provider()
|
||||
self.use_fzf = self.get_use_fzf()
|
||||
self.skip = self.get_skip()
|
||||
self.icons = self.get_icons()
|
||||
self.preview = self.get_preview()
|
||||
self.translation_type = self.get_translation_type()
|
||||
self.sort_by = self.get_sort_by()
|
||||
@@ -52,6 +61,8 @@ class Config(object):
|
||||
self.auto_next = self.get_auto_next()
|
||||
self.auto_select = self.get_auto_select()
|
||||
self.quality = self.get_quality()
|
||||
self.notification_duration = self.get_notification_duration()
|
||||
self.error = self.get_error()
|
||||
self.server = self.get_server()
|
||||
self.format = self.get_format()
|
||||
self.preferred_language = self.get_preferred_language()
|
||||
@@ -59,11 +70,26 @@ class Config(object):
|
||||
# ---- setup user data ------
|
||||
self.watch_history: dict = user_data_helper.user_data.get("watch_history", {})
|
||||
self.anime_list: list = user_data_helper.user_data.get("animelist", [])
|
||||
self.user: dict = user_data_helper.user_data.get("user", {})
|
||||
|
||||
self.anime_provider = AnimeProvider(self.provider)
|
||||
|
||||
def update_watch_history(self, anime_id: int, episode: str | None):
|
||||
self.watch_history.update({str(anime_id): episode})
|
||||
def update_user(self, user):
|
||||
self.user = user
|
||||
user_data_helper.update_user_info(user)
|
||||
|
||||
def update_watch_history(
|
||||
self, anime_id: int, episode: str | None, start_time="0", total_time="0"
|
||||
):
|
||||
self.watch_history.update(
|
||||
{
|
||||
str(anime_id): {
|
||||
"episode": episode,
|
||||
"start_time": start_time,
|
||||
"total_time": total_time,
|
||||
}
|
||||
}
|
||||
)
|
||||
user_data_helper.update_watch_history(self.watch_history)
|
||||
|
||||
def update_anime_list(self, anime_id: int, remove=False):
|
||||
@@ -88,6 +114,12 @@ class Config(object):
|
||||
def get_use_fzf(self):
|
||||
return self.configparser.getboolean("general", "use_fzf")
|
||||
|
||||
def get_skip(self):
|
||||
return self.configparser.getboolean("stream", "skip")
|
||||
|
||||
def get_icons(self):
|
||||
return self.configparser.getboolean("general", "icons")
|
||||
|
||||
def get_preview(self):
|
||||
return self.configparser.getboolean("general", "preview")
|
||||
|
||||
@@ -112,6 +144,12 @@ class Config(object):
|
||||
def get_quality(self):
|
||||
return self.configparser.getint("stream", "quality")
|
||||
|
||||
def get_notification_duration(self):
|
||||
return self.configparser.getint("general", "notification_duration")
|
||||
|
||||
def get_error(self):
|
||||
return self.configparser.getint("stream", "error")
|
||||
|
||||
def get_server(self):
|
||||
return self.configparser.get("stream", "server")
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
from InquirerPy import inquirer
|
||||
from InquirerPy.validator import EmptyInputValidator
|
||||
from rich import print
|
||||
from rich.prompt import Prompt
|
||||
from rich.progress import Progress
|
||||
from rich.prompt import Confirm, Prompt
|
||||
|
||||
from ... import USER_CONFIG_PATH
|
||||
from ...anilist import AniList
|
||||
from ...constants import USER_CONFIG_PATH
|
||||
from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from ...libs.anime_provider.types import Anime, SearchResult, Server
|
||||
from ...libs.fzf import fzf
|
||||
@@ -17,6 +21,20 @@ from ..config import Config
|
||||
from ..utils.mpv import mpv
|
||||
from ..utils.tools import QueryDict, exit_app
|
||||
from ..utils.utils import clear, fuzzy_inquirer
|
||||
from .utils import aniskip
|
||||
|
||||
|
||||
def calculate_time_delta(start_time, end_time):
|
||||
time_format = "%H:%M:%S"
|
||||
|
||||
# Convert string times to datetime objects
|
||||
start = datetime.strptime(start_time, time_format)
|
||||
end = datetime.strptime(end_time, time_format)
|
||||
|
||||
# Calculate the difference
|
||||
delta = end - start
|
||||
|
||||
return delta
|
||||
|
||||
|
||||
def player_controls(config: Config, anilist_config: QueryDict):
|
||||
@@ -45,8 +63,34 @@ def player_controls(config: Config, anilist_config: QueryDict):
|
||||
current_episode,
|
||||
)
|
||||
|
||||
mpv(current_link, selected_server["episode_title"])
|
||||
start_time = config.watch_history[str(anime_id)]["start_time"]
|
||||
print("[green]Continuing from:[/] ", start_time)
|
||||
custom_args = []
|
||||
if config.skip:
|
||||
if args := aniskip(
|
||||
anilist_config.selected_anime_anilist["idMal"], current_episode
|
||||
):
|
||||
custom_args = args
|
||||
stop_time, total_time = mpv(
|
||||
current_link,
|
||||
selected_server["episode_title"],
|
||||
start_time=start_time,
|
||||
custom_args=custom_args,
|
||||
)
|
||||
if stop_time == "0":
|
||||
episode = str(int(current_episode) + 1)
|
||||
else:
|
||||
error = 5 * 60
|
||||
delta = calculate_time_delta(stop_time, total_time)
|
||||
if delta.total_seconds() > error:
|
||||
episode = current_episode
|
||||
else:
|
||||
episode = str(int(current_episode) + 1)
|
||||
stop_time = "0"
|
||||
total_time = "0"
|
||||
|
||||
clear()
|
||||
config.update_watch_history(anime_id, episode, stop_time, total_time)
|
||||
player_controls(config, anilist_config)
|
||||
|
||||
def _next_episode():
|
||||
@@ -54,7 +98,7 @@ def player_controls(config: Config, anilist_config: QueryDict):
|
||||
if next_episode >= len(episodes):
|
||||
next_episode = len(episodes) - 1
|
||||
|
||||
# update internal config
|
||||
# updateinternal config
|
||||
anilist_config.episode_number = episodes[next_episode]
|
||||
|
||||
# update user config
|
||||
@@ -115,18 +159,23 @@ def player_controls(config: Config, anilist_config: QueryDict):
|
||||
# reload to controls
|
||||
player_controls(config, anilist_config)
|
||||
|
||||
icons = config.icons
|
||||
options = {
|
||||
"Replay": _replay,
|
||||
"Next Episode": _next_episode,
|
||||
"Previous Episode": _previous_episode,
|
||||
"Episodes": _episodes,
|
||||
"Change Quality": _change_quality,
|
||||
"Change Translation Type": _change_translation_type,
|
||||
"Servers": _servers,
|
||||
"Main Menu": lambda: anilist(config, anilist_config),
|
||||
"Anime Options Menu": lambda: anilist_options(config, anilist_config),
|
||||
"Search Results": lambda: select_anime(config, anilist_config),
|
||||
"Exit": exit_app,
|
||||
f"{'🔂 ' if icons else ''}Replay": _replay,
|
||||
f"{'⏭ ' if icons else ''}Next Episode": _next_episode,
|
||||
f"{'⏮ ' if icons else ''}Previous Episode": _previous_episode,
|
||||
f"{'🗃️ ' if icons else ''}Episodes": _episodes,
|
||||
f"{'📀 ' if icons else ''}Change Quality": _change_quality,
|
||||
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
||||
f"{'💽 ' if icons else ''}Servers": _servers,
|
||||
f"{'📱 ' if icons else ''}Main Menu": lambda: anilist(config, anilist_config),
|
||||
f"{'📜 ' if icons else ''}Anime Options Menu": lambda: anilist_options(
|
||||
config, anilist_config
|
||||
),
|
||||
f"{'🔎 ' if icons else ''}Search Results": lambda: select_anime(
|
||||
config, anilist_config
|
||||
),
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
|
||||
if config.auto_next:
|
||||
@@ -154,17 +203,20 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
|
||||
anime_provider = config.anime_provider
|
||||
|
||||
# get streams for episode from provider
|
||||
episode_streams = anime_provider.get_episode_streams(
|
||||
anime, episode_number, translation_type
|
||||
)
|
||||
if not episode_streams:
|
||||
print("Failed to fetch :cry:")
|
||||
input("Enter to retry...")
|
||||
return fetch_streams(config, anilist_config)
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Episode Streams...", total=None)
|
||||
episode_streams = anime_provider.get_episode_streams(
|
||||
anime, episode_number, translation_type
|
||||
)
|
||||
if not episode_streams:
|
||||
print("Failed to fetch :cry:")
|
||||
input("Enter to retry...")
|
||||
return fetch_streams(config, anilist_config)
|
||||
|
||||
episode_streams = {
|
||||
episode_stream["server"]: episode_stream for episode_stream in episode_streams
|
||||
}
|
||||
episode_streams = {
|
||||
episode_stream["server"]: episode_stream
|
||||
for episode_stream in episode_streams
|
||||
}
|
||||
|
||||
# prompt for preferred server
|
||||
server = None
|
||||
@@ -210,11 +262,50 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
|
||||
"[bold magenta] Episode: [/]",
|
||||
episode_number,
|
||||
)
|
||||
# -- update anilist info if user --
|
||||
if config.user and episode_number:
|
||||
AniList.update_anime_list(
|
||||
{
|
||||
"mediaId": anime_id,
|
||||
"status": "CURRENT",
|
||||
"progress": episode_number,
|
||||
}
|
||||
)
|
||||
|
||||
mpv(stream_link, selected_server["episode_title"])
|
||||
start_time = config.watch_history.get(str(anime_id), {}).get("start_time", "0")
|
||||
if start_time != "0":
|
||||
print("[green]Continuing from:[/] ", start_time)
|
||||
custom_args = []
|
||||
if config.skip:
|
||||
if args := aniskip(
|
||||
anilist_config.selected_anime_anilist["idMal"], episode_number
|
||||
):
|
||||
custom_args = args
|
||||
|
||||
stop_time, total_time = mpv(
|
||||
stream_link,
|
||||
selected_server["episode_title"],
|
||||
start_time=start_time,
|
||||
custom_args=custom_args,
|
||||
)
|
||||
print("Finished at: ", stop_time)
|
||||
|
||||
# update_watch_history
|
||||
config.update_watch_history(anime_id, str(int(episode_number) + 1))
|
||||
if stop_time == "0":
|
||||
episode = str(int(episode_number) + 1)
|
||||
else:
|
||||
error = config.error * 60
|
||||
delta = calculate_time_delta(stop_time, total_time)
|
||||
if delta.total_seconds() > error:
|
||||
episode = episode_number
|
||||
else:
|
||||
episode = str(int(episode_number) + 1)
|
||||
stop_time = "0"
|
||||
total_time = "0"
|
||||
|
||||
config.update_watch_history(
|
||||
anime_id, episode, start_time=stop_time, total_time=total_time
|
||||
)
|
||||
|
||||
# switch to controls
|
||||
clear()
|
||||
@@ -236,8 +327,11 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
|
||||
|
||||
# prompt for episode number
|
||||
episodes = anime["availableEpisodesDetail"][translation_type]
|
||||
if continue_from_history and user_watch_history.get(str(anime_id)) in episodes:
|
||||
episode_number = user_watch_history[str(anime_id)]
|
||||
if (
|
||||
continue_from_history
|
||||
and user_watch_history.get(str(anime_id), {}).get("episode") in episodes
|
||||
):
|
||||
episode_number = user_watch_history[str(anime_id)]["episode"]
|
||||
print(f"[bold cyan]Continuing from Episode:[/] [bold]{episode_number}[/]")
|
||||
else:
|
||||
choices = [*episodes, "Back"]
|
||||
@@ -253,7 +347,8 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
|
||||
if episode_number == "Back":
|
||||
anilist_options(config, anilist_config)
|
||||
return
|
||||
config.update_watch_history(anime_id, episode_number)
|
||||
start_time = user_watch_history.get(str(anime_id), {}).get("start_time", "0")
|
||||
config.update_watch_history(anime_id, episode_number, start_time=start_time)
|
||||
|
||||
# update internal config
|
||||
anilist_config.episodes = episodes
|
||||
@@ -267,7 +362,9 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
|
||||
def fetch_anime_episode(config, anilist_config: QueryDict):
|
||||
selected_anime: SearchResult = anilist_config._anime
|
||||
anime_provider = config.anime_provider
|
||||
anilist_config.anime = anime_provider.get_anime(selected_anime["id"])
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Anime Info...", total=None)
|
||||
anilist_config.anime = anime_provider.get_anime(selected_anime["id"])
|
||||
if not anilist_config.anime:
|
||||
|
||||
print(
|
||||
@@ -291,9 +388,11 @@ def provide_anime(config: Config, anilist_config: QueryDict):
|
||||
anime_provider = config.anime_provider
|
||||
|
||||
# search and get the requested title from provider
|
||||
search_results = anime_provider.search_for_anime(
|
||||
selected_anime_title, translation_type
|
||||
)
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Search Results...", total=None)
|
||||
search_results = anime_provider.search_for_anime(
|
||||
selected_anime_title, translation_type
|
||||
)
|
||||
if not search_results:
|
||||
print(
|
||||
"Sth went wrong :cry: while fetching this could mean you have poor internet connection or the provider is down"
|
||||
@@ -347,7 +446,10 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
if trailer := selected_anime.get("trailer"):
|
||||
trailer_url = "https://youtube.com/watch?v=" + trailer["id"]
|
||||
print("[bold magenta]Watching Trailer of:[/]", selected_anime_title)
|
||||
mpv(trailer_url, selected_anime_title, f"--ytdl-format={config.format}")
|
||||
mpv(
|
||||
trailer_url,
|
||||
ytdl_format=config.format,
|
||||
)
|
||||
anilist_options(config, anilist_config)
|
||||
else:
|
||||
print("no trailer available :confused:")
|
||||
@@ -355,11 +457,70 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _add_to_list(config: Config, anilist_config: QueryDict):
|
||||
config.update_anime_list(anilist_config.anime_id)
|
||||
# config.update_anime_list(anilist_config.anime_id)
|
||||
anime_lists = {
|
||||
"Watching": "CURRENT",
|
||||
"Paused": "PAUSED",
|
||||
"Planning": "PLANNING",
|
||||
"Dropped": "DROPPED",
|
||||
"Rewatching": "REPEATING",
|
||||
"Completed": "COMPLETED",
|
||||
}
|
||||
if config.use_fzf:
|
||||
anime_list = fzf.run(
|
||||
list(anime_lists.keys()),
|
||||
"Choose the list you want to add to",
|
||||
"Add your animelist",
|
||||
)
|
||||
else:
|
||||
anime_list = fuzzy_inquirer(
|
||||
"Choose the list you want to add to", list(anime_lists.keys())
|
||||
)
|
||||
result = AniList.update_anime_list(
|
||||
{"status": anime_lists[anime_list], "mediaId": selected_anime["id"]}
|
||||
)
|
||||
if not result[0]:
|
||||
print("Failed to update", result)
|
||||
else:
|
||||
print(
|
||||
f"Successfully added {selected_anime_title} to your {anime_list} list :smile:"
|
||||
)
|
||||
input("Enter to continue...")
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _score_anime(config: Config, anilist_config: QueryDict):
|
||||
score = inquirer.number(
|
||||
message="Enter the score:",
|
||||
min_allowed=0,
|
||||
max_allowed=100,
|
||||
validate=EmptyInputValidator(),
|
||||
).execute()
|
||||
|
||||
result = AniList.update_anime_list(
|
||||
{"scoreRaw": score, "mediaId": selected_anime["id"]}
|
||||
)
|
||||
if not result[0]:
|
||||
print("Failed to update", result)
|
||||
else:
|
||||
print(f"Successfully scored {selected_anime_title}; score: {score}")
|
||||
input("Enter to continue...")
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _remove_from_list(config: Config, anilist_config: QueryDict):
|
||||
config.update_anime_list(anilist_config.anime_id, True)
|
||||
if Confirm.ask(
|
||||
f"Are you sure you want to procede, the folowing action will permanently remove {selected_anime_title} from your list and your progress will be erased",
|
||||
default=False,
|
||||
):
|
||||
success, data = AniList.delete_medialist_entry(selected_anime["id"])
|
||||
if not success:
|
||||
print("Failed to delete", data)
|
||||
elif not data.get("deleted"):
|
||||
print("Failed to delete", data)
|
||||
else:
|
||||
print("Successfully deleted :cry:", selected_anime_title)
|
||||
else:
|
||||
print(selected_anime_title, ":relieved:")
|
||||
input("Enter to continue...")
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _change_translation_type(config: Config, anilist_config: QueryDict):
|
||||
@@ -430,15 +591,17 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
anilist_options(config, anilist_config)
|
||||
return
|
||||
|
||||
icons = config.icons
|
||||
options = {
|
||||
"Stream": provide_anime,
|
||||
"Watch Trailer": _watch_trailer,
|
||||
"Add to List": _add_to_list,
|
||||
"Remove from List": _remove_from_list,
|
||||
"View Info": _view_info,
|
||||
"Change Translation Type": _change_translation_type,
|
||||
"Back": select_anime,
|
||||
"Exit": exit_app,
|
||||
f"{'📽️ ' if icons else ''}Stream": provide_anime,
|
||||
f"{'📼 ' if icons else ''}Watch Trailer": _watch_trailer,
|
||||
f"{'✨ ' if icons else ''}Score Anime": _score_anime,
|
||||
f"{'📥 ' if icons else ''}Add to List": _add_to_list,
|
||||
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 ''}Back": select_anime,
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
if config.use_fzf:
|
||||
action = fzf.run(
|
||||
@@ -493,6 +656,46 @@ def select_anime(config: Config, anilist_config: QueryDict):
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
|
||||
def handle_animelist(anilist_config, config: Config, list_type: str):
|
||||
if not config.user:
|
||||
print("You haven't logged in please run: fastanime anilist login")
|
||||
input("Enter to continue...")
|
||||
anilist(config, anilist_config)
|
||||
return
|
||||
match list_type:
|
||||
case "Watching":
|
||||
status = "CURRENT"
|
||||
case "Planned":
|
||||
status = "PLANNING"
|
||||
case "Completed":
|
||||
status = "COMPLETED"
|
||||
case "Dropped":
|
||||
status = "DROPPED"
|
||||
case "Paused":
|
||||
status = "PAUSED"
|
||||
case "Repeating":
|
||||
status = "REPEATING"
|
||||
case _:
|
||||
return
|
||||
anime_list = AniList.get_anime_list(status)
|
||||
if not anime_list:
|
||||
print("Sth went wrong", anime_list)
|
||||
input("Enter to continue")
|
||||
anilist(config, anilist_config)
|
||||
return
|
||||
if not anime_list[0]:
|
||||
print("Sth went wrong", anime_list)
|
||||
input("Enter to continue")
|
||||
anilist(config, anilist_config)
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
return anime_list
|
||||
|
||||
|
||||
def anilist(config: Config, anilist_config: QueryDict):
|
||||
def _anilist_search():
|
||||
search_term = Prompt.ask("[cyan]Search for[/]")
|
||||
@@ -521,19 +724,38 @@ def anilist(config: Config, anilist_config: QueryDict):
|
||||
|
||||
anilist(config, anilist_config)
|
||||
|
||||
icons = config.icons
|
||||
options = {
|
||||
"Trending": AniList.get_trending,
|
||||
"Recently Updated Anime": AniList.get_most_recently_updated,
|
||||
"Search": _anilist_search,
|
||||
"Watch History": _watch_history,
|
||||
"AnimeList": _anime_list,
|
||||
"Random Anime": _anilist_random,
|
||||
"Most Popular Anime": AniList.get_most_popular,
|
||||
"Most Favourite Anime": AniList.get_most_favourite,
|
||||
"Most Scored Anime": AniList.get_most_scored,
|
||||
"Upcoming Anime": AniList.get_upcoming_anime,
|
||||
"Edit Config": edit_config,
|
||||
"Exit": exit_app,
|
||||
f"{'🔥 ' if icons else ''}Trending": AniList.get_trending,
|
||||
f"{'📺 ' if icons else ''}Watching": lambda x="Watching": handle_animelist(
|
||||
anilist_config, config, x
|
||||
),
|
||||
f"{'⏸ ' if icons else ''}Paused": lambda x="Paused": handle_animelist(
|
||||
anilist_config, config, x
|
||||
),
|
||||
f"{'🚮 ' if icons else ''}Dropped": lambda x="Dropped": handle_animelist(
|
||||
anilist_config, config, x
|
||||
),
|
||||
f"{'📑 ' if icons else ''}Planned": lambda x="Planned": handle_animelist(
|
||||
anilist_config, config, x
|
||||
),
|
||||
f"{'✅ ' if icons else ''}Completed": lambda x="Completed": handle_animelist(
|
||||
anilist_config, config, x
|
||||
),
|
||||
f"{'🔁 ' if icons else ''}Rewatching": lambda x="Repeating": handle_animelist(
|
||||
anilist_config, config, x
|
||||
),
|
||||
f"{'🔔 ' if icons else ''}Recently Updated Anime": AniList.get_most_recently_updated,
|
||||
f"{'🔎 ' if icons else ''}Search": _anilist_search,
|
||||
f"{'🎞️ ' if icons else ''}Watch History": _watch_history,
|
||||
# "AnimeList": _anime_list💯,
|
||||
f"{'🎲 ' if icons else ''}Random Anime": _anilist_random,
|
||||
f"{'🌟 ' if icons else ''}Most Popular Anime": AniList.get_most_popular,
|
||||
f"{'💖 ' if icons else ''}Most Favourite Anime": AniList.get_most_favourite,
|
||||
f"{'✨ ' if icons else ''}Most Scored Anime": AniList.get_most_scored,
|
||||
f"{'🎬 ' if icons else ''}Upcoming Anime": AniList.get_upcoming_anime,
|
||||
f"{'📝 ' if icons else ''}Edit Config": edit_config,
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
if config.use_fzf:
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
from threading import Thread
|
||||
|
||||
import requests
|
||||
|
||||
from ... import APP_CACHE_DIR
|
||||
from ...constants import APP_CACHE_DIR
|
||||
from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from ...Utility import anilist_data_helper
|
||||
from ...Utility.utils import remove_html_tags, sanitize_filename
|
||||
from ..config import Config
|
||||
|
||||
fzf_preview = """
|
||||
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.
|
||||
@@ -92,6 +93,19 @@ fzf-preview(){
|
||||
SEARCH_RESULTS_CACHE = os.path.join(APP_CACHE_DIR, "search_results")
|
||||
|
||||
|
||||
def aniskip(mal_id, episode):
|
||||
ANISKIP = shutil.which("ani-skip")
|
||||
if not ANISKIP:
|
||||
print("Aniskip not found, please install and try again")
|
||||
return
|
||||
args = [ANISKIP, "-q", str(mal_id), "-e", str(episode)]
|
||||
aniskip_result = subprocess.run(args, text=True, stdout=subprocess.PIPE)
|
||||
if aniskip_result.returncode != 0:
|
||||
return
|
||||
mpv_skip_args = aniskip_result.stdout.strip()
|
||||
return mpv_skip_args.split(" ")
|
||||
|
||||
|
||||
def write_search_results(
|
||||
search_results: list[AnilistBaseMediaDataSchema], config: Config
|
||||
):
|
||||
@@ -126,7 +140,8 @@ def write_search_results(
|
||||
Favourites: {anime['favourites']}
|
||||
Status: {anime['status']}
|
||||
Episodes: {anime['episodes']}
|
||||
Genres: {anilist_data_helper.format_list_data_with_comma(anime['genres'])}
|
||||
Genres: {anilist_data_helper.format_list_data_with_comma(
|
||||
anime['genres'])}
|
||||
Next Episode: {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])}
|
||||
Start Date: {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
|
||||
End Date: {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
|
||||
@@ -136,7 +151,8 @@ def write_search_results(
|
||||
template = textwrap.dedent(template)
|
||||
template = f"""
|
||||
{template}
|
||||
{textwrap.fill(remove_html_tags(str(anime['description'])),width=45)}
|
||||
{textwrap.fill(remove_html_tags(
|
||||
str(anime['description'])), width=45)}
|
||||
"""
|
||||
f.write(template)
|
||||
|
||||
|
||||
@@ -1,23 +1,141 @@
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
# legacy
|
||||
# def mpv(link, title: None | str = "anime", *custom_args):
|
||||
# MPV = shutil.which("mpv")
|
||||
# if not MPV:
|
||||
# args = [
|
||||
# "nohup",
|
||||
# "am",
|
||||
# "start",
|
||||
# "--user",
|
||||
# "0",
|
||||
# "-a",
|
||||
# "android.intent.action.VIEW",
|
||||
# "-d",
|
||||
# link,
|
||||
# "-n",
|
||||
# "is.xyz.mpv/.MPVActivity",
|
||||
# ]
|
||||
# subprocess.run(args)
|
||||
# else:
|
||||
# subprocess.run([MPV, *custom_args, f"--title={title}", link])
|
||||
#
|
||||
|
||||
|
||||
def mpv(link, title: None | str = "anime", *custom_args):
|
||||
def stream_video(url, mpv_args, custom_args):
|
||||
process = subprocess.Popen(
|
||||
["mpv", url, *mpv_args, *custom_args],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
last_time = None
|
||||
av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)")
|
||||
last_time = "0"
|
||||
total_time = "0"
|
||||
|
||||
try:
|
||||
while True:
|
||||
output = process.stderr.readline()
|
||||
|
||||
if output:
|
||||
# Match the timestamp in the output
|
||||
match = av_time_pattern.search(output.strip())
|
||||
if match:
|
||||
current_time = match.group(1)
|
||||
total_time = match.group(2)
|
||||
match.group(3)
|
||||
last_time = current_time
|
||||
# print(f"Current stream time: {current_time}, Total time: {total_time}, Progress: {percentage}%")
|
||||
|
||||
# Check if the process has terminated
|
||||
retcode = process.poll()
|
||||
if retcode is not None:
|
||||
print("Finshed at: ", last_time)
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
finally:
|
||||
process.terminate()
|
||||
|
||||
return last_time, total_time
|
||||
|
||||
|
||||
def mpv(
|
||||
link: str,
|
||||
title: Optional[str] = "",
|
||||
start_time: str = "0",
|
||||
ytdl_format="",
|
||||
custom_args=[],
|
||||
):
|
||||
# Determine if mpv is available
|
||||
MPV = shutil.which("mpv")
|
||||
|
||||
# If title is None, set a default value
|
||||
|
||||
# Regex to check if the link is a YouTube URL
|
||||
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
|
||||
|
||||
if not MPV:
|
||||
args = [
|
||||
"nohup",
|
||||
"am",
|
||||
"start",
|
||||
"--user",
|
||||
"0",
|
||||
"-a",
|
||||
"android.intent.action.VIEW",
|
||||
"-d",
|
||||
link,
|
||||
"-n",
|
||||
"is.xyz.mpv/.MPVActivity",
|
||||
]
|
||||
# Determine if the link is a YouTube URL
|
||||
if re.match(youtube_regex, link):
|
||||
# Android specific commands to launch mpv with a YouTube URL
|
||||
args = [
|
||||
"nohup",
|
||||
"am",
|
||||
"start",
|
||||
"--user",
|
||||
"0",
|
||||
"-a",
|
||||
"android.intent.action.VIEW",
|
||||
"-d",
|
||||
link,
|
||||
"-n",
|
||||
"com.google.android.youtube/.UrlActivity",
|
||||
]
|
||||
return "0"
|
||||
else:
|
||||
# Android specific commands to launch mpv with a regular URL
|
||||
args = [
|
||||
"nohup",
|
||||
"am",
|
||||
"start",
|
||||
"--user",
|
||||
"0",
|
||||
"-a",
|
||||
"android.intent.action.VIEW",
|
||||
"-d",
|
||||
link,
|
||||
"-n",
|
||||
"is.xyz.mpv/.MPVActivity",
|
||||
]
|
||||
|
||||
subprocess.run(args)
|
||||
return "0"
|
||||
else:
|
||||
subprocess.run([MPV, *custom_args, f"--title={title}", link])
|
||||
# General mpv command with custom arguments
|
||||
mpv_args = []
|
||||
if start_time != "0":
|
||||
mpv_args.append(f"--start={start_time}")
|
||||
if title:
|
||||
mpv_args.append(f"--title={title}")
|
||||
if ytdl_format:
|
||||
mpv_args.append(f"--ytdl-format={ytdl_format}")
|
||||
stop_time, total_time = stream_video(link, mpv_args, custom_args)
|
||||
return stop_time, total_time
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
mpv(
|
||||
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"Example Video",
|
||||
"--fullscreen",
|
||||
"--volume=50",
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ def exit_app(*args):
|
||||
|
||||
from rich import print
|
||||
|
||||
from ... import USER_NAME
|
||||
from ...constants import USER_NAME
|
||||
|
||||
print("Have a good day :smile:", USER_NAME)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
from InquirerPy import inquirer
|
||||
from thefuzz import fuzz
|
||||
|
||||
from ... import PLATFORM
|
||||
from ...constants import PLATFORM
|
||||
from ...Utility.data import anime_normalizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -54,20 +54,3 @@ def anime_title_percentage_match(
|
||||
fuzz.ratio(title[1].lower(), possible_user_requested_anime_title.lower()),
|
||||
)
|
||||
return percentage_ratio
|
||||
|
||||
|
||||
def get_selected_anime(anime_title, results):
|
||||
def _get_result(result, compare):
|
||||
return result["name"] == compare
|
||||
|
||||
return list(
|
||||
filter(lambda x: _get_result(x, anime_title), results["shows"]["edges"])
|
||||
)
|
||||
|
||||
|
||||
def get_selected_server(_server, servers):
|
||||
def _get_server(server, server_name):
|
||||
return server[0] == server_name
|
||||
|
||||
server = list(filter(lambda x: _get_server(x, _server), servers)).pop()
|
||||
return server
|
||||
|
||||
38
fastanime/constants.py
Normal file
38
fastanime/constants.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
from platform import system
|
||||
|
||||
from platformdirs import PlatformDirs
|
||||
|
||||
from . import APP_NAME, AUTHOR
|
||||
|
||||
PLATFORM = system()
|
||||
dirs = PlatformDirs(appname=APP_NAME, appauthor=AUTHOR, ensure_exists=True)
|
||||
|
||||
|
||||
# ---- app deps ----
|
||||
APP_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
CONFIGS_DIR = os.path.join(APP_DIR, "configs")
|
||||
ASSETS_DIR = os.path.join(APP_DIR, "assets")
|
||||
|
||||
if PLATFORM == "Windows":
|
||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
|
||||
else:
|
||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.png")
|
||||
|
||||
# ----- user configs and data -----
|
||||
APP_DATA_DIR = dirs.user_config_dir
|
||||
if not APP_DATA_DIR:
|
||||
APP_DATA_DIR = dirs.user_data_dir
|
||||
|
||||
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
|
||||
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
|
||||
NOTIFIER_LOG_FILE_PATH = os.path.join(APP_DATA_DIR, "notifier.log")
|
||||
|
||||
# cache dir
|
||||
APP_CACHE_DIR = dirs.user_cache_dir
|
||||
|
||||
# video dir
|
||||
USER_VIDEOS_DIR = os.path.join(dirs.user_videos_dir, APP_NAME)
|
||||
|
||||
|
||||
USER_NAME = os.environ.get("USERNAME", f"{APP_NAME} user")
|
||||
@@ -17,6 +17,14 @@ class AnilistImage(TypedDict):
|
||||
large: str
|
||||
|
||||
|
||||
class AnilistUser(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
bannerImage: str | None
|
||||
avatar: AnilistImage
|
||||
token: str
|
||||
|
||||
|
||||
class AnilistMediaTrailer(TypedDict):
|
||||
id: str
|
||||
site: str
|
||||
@@ -49,11 +57,6 @@ class AnilistMediaNextAiringEpisode(TypedDict):
|
||||
episode: int
|
||||
|
||||
|
||||
class AnilistUser(TypedDict):
|
||||
name: str
|
||||
avatar: AnilistImage
|
||||
|
||||
|
||||
class AnilistReview(TypedDict):
|
||||
summary: str
|
||||
user: AnilistUser
|
||||
@@ -110,7 +113,8 @@ class AnilistBaseMediaDataSchema(TypedDict):
|
||||
This a convenience class is used to type the received Anilist data to enhance dev experience
|
||||
"""
|
||||
|
||||
id: str
|
||||
id: int
|
||||
idMal: int
|
||||
title: AnilistMediaTitle
|
||||
coverImage: AnilistImage
|
||||
trailer: AnilistMediaTrailer | None
|
||||
|
||||
@@ -2,25 +2,37 @@
|
||||
This is the core module availing all the abstractions of the anilist api
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import requests
|
||||
|
||||
from .anilist_data_schema import AnilistDataSchema
|
||||
from .anilist_data_schema import AnilistDataSchema, AnilistUser
|
||||
from .queries_graphql import (
|
||||
airing_schedule_query,
|
||||
anime_characters_query,
|
||||
anime_query,
|
||||
anime_relations_query,
|
||||
delete_list_entry_query,
|
||||
get_logged_in_user_query,
|
||||
get_medialist_item_query,
|
||||
mark_as_read_mutation,
|
||||
media_list_mutation,
|
||||
media_list_query,
|
||||
most_favourite_query,
|
||||
most_popular_query,
|
||||
most_recently_updated_query,
|
||||
most_scored_query,
|
||||
notification_query,
|
||||
recommended_query,
|
||||
search_query,
|
||||
trending_query,
|
||||
upcoming_anime_query,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# from kivy.network.urlrequest import UrlRequestRequests
|
||||
ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
||||
|
||||
|
||||
class AniListApi:
|
||||
@@ -28,6 +40,130 @@ class AniListApi:
|
||||
This class provides an abstraction for the anilist api
|
||||
"""
|
||||
|
||||
def login_user(self, token: str):
|
||||
self.token = token
|
||||
self.headers = {"Authorization": f"Bearer {self.token}"}
|
||||
user = self.get_logged_in_user()
|
||||
if not user:
|
||||
return
|
||||
if not user[0]:
|
||||
return
|
||||
user_info: AnilistUser = user[1]["data"]["Viewer"] # pyright:ignore
|
||||
self.user_id = user_info["id"] # pyright:ignore
|
||||
return user_info
|
||||
|
||||
def get_notification(self):
|
||||
return self._make_authenticated_request(notification_query)
|
||||
|
||||
def reset_notification_count(self):
|
||||
return self._make_authenticated_request(mark_as_read_mutation)
|
||||
|
||||
def update_login_info(self, user: AnilistUser, token: str):
|
||||
self.token = token
|
||||
self.headers = {"Authorization": f"Bearer {self.token}"}
|
||||
self.user_id = user["id"]
|
||||
|
||||
def get_logged_in_user(self):
|
||||
if not self.headers:
|
||||
return
|
||||
return self._make_authenticated_request(get_logged_in_user_query)
|
||||
|
||||
def update_anime_list(self, values_to_update: dict):
|
||||
variables = {"userId": self.user_id, **values_to_update}
|
||||
return self._make_authenticated_request(media_list_mutation, variables)
|
||||
|
||||
def get_anime_list(
|
||||
self,
|
||||
status: Literal[
|
||||
"CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"
|
||||
],
|
||||
):
|
||||
variables = {"status": status, "userId": self.user_id}
|
||||
return self._make_authenticated_request(media_list_query, variables)
|
||||
|
||||
def get_medialist_entry(self, mediaId: int):
|
||||
variables = {"mediaId": mediaId}
|
||||
return self._make_authenticated_request(get_medialist_item_query, variables)
|
||||
|
||||
def delete_medialist_entry(self, mediaId: int):
|
||||
result = self.get_medialist_entry(mediaId)
|
||||
if not result[0]:
|
||||
return result
|
||||
id = result[1]["data"]["MediaList"]["id"]
|
||||
variables = {"id": id}
|
||||
return self._make_authenticated_request(delete_list_entry_query, variables)
|
||||
|
||||
def _make_authenticated_request(self, query: str, variables: dict = {}):
|
||||
"""
|
||||
The core abstraction for getting authenticated data from the anilist api
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
query:str
|
||||
a valid anilist graphql query
|
||||
variables:dict
|
||||
variables to pass to the anilist api
|
||||
"""
|
||||
# req=UrlRequestRequests(url, self.got_data,)
|
||||
try:
|
||||
# TODO: check if data is as expected
|
||||
response = requests.post(
|
||||
ANILIST_ENDPOINT,
|
||||
json={"query": query, "variables": variables},
|
||||
timeout=10,
|
||||
headers=self.headers,
|
||||
)
|
||||
anilist_data = response.json()
|
||||
|
||||
# ensuring you dont get blocked
|
||||
if (
|
||||
int(response.headers.get("X-RateLimit-Remaining", 0)) < 5
|
||||
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:
|
||||
return (True, anilist_data)
|
||||
else:
|
||||
return (False, anilist_data)
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning(
|
||||
"Timeout has been exceeded this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
return (
|
||||
False,
|
||||
{
|
||||
"Error": "Timeout Exceeded for connection there might be a problem with your internet or anilist is down."
|
||||
},
|
||||
) # type: ignore
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warning(
|
||||
"ConnectionError this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
|
||||
return (
|
||||
False,
|
||||
{
|
||||
"Error": "There might be a problem with your internet or anilist is down."
|
||||
},
|
||||
) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
return (False, {"Error": f"{e}"}) # type: ignore
|
||||
|
||||
def get_watchlist(self):
|
||||
variables = {"status": "CURRENT", "userId": self.user_id}
|
||||
return self._make_authenticated_request(media_list_query, variables)
|
||||
|
||||
def get_data(
|
||||
self, query: str, variables: dict = {}
|
||||
) -> tuple[bool, AnilistDataSchema]:
|
||||
@@ -41,16 +177,40 @@ class AniListApi:
|
||||
variables:dict
|
||||
variables to pass to the anilist api
|
||||
"""
|
||||
url = "https://graphql.anilist.co"
|
||||
# req=UrlRequestRequests(url, self.got_data,)
|
||||
try:
|
||||
# TODO: check if data is as expected
|
||||
response = requests.post(
|
||||
url, json={"query": query, "variables": variables}, timeout=10
|
||||
ANILIST_ENDPOINT,
|
||||
json={"query": query, "variables": variables},
|
||||
timeout=10,
|
||||
)
|
||||
anilist_data: AnilistDataSchema = response.json()
|
||||
return (True, anilist_data)
|
||||
|
||||
# ensuring you dont get blocked
|
||||
if (
|
||||
int(response.headers.get("X-RateLimit-Remaining", 0)) < 5
|
||||
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:
|
||||
return (True, anilist_data)
|
||||
else:
|
||||
return (False, anilist_data)
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning(
|
||||
"Timeout has been exceeded this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
return (
|
||||
False,
|
||||
{
|
||||
@@ -58,6 +218,9 @@ class AniListApi:
|
||||
},
|
||||
) # type: ignore
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warning(
|
||||
"ConnectionError this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
return (
|
||||
False,
|
||||
{
|
||||
@@ -65,6 +228,7 @@ class AniListApi:
|
||||
},
|
||||
) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
return (False, {"Error": f"{e}"}) # type: ignore
|
||||
|
||||
def search(
|
||||
|
||||
@@ -3,6 +3,203 @@ This module contains all the preset queries for the sake of neatness and convini
|
||||
Mostly for internal usage
|
||||
"""
|
||||
|
||||
mark_as_read_mutation = """
|
||||
mutation{
|
||||
UpdateUser{
|
||||
unreadNotificationCount
|
||||
}
|
||||
}
|
||||
"""
|
||||
reviews_query = """
|
||||
query($id:Int){
|
||||
Page{
|
||||
pageInfo{
|
||||
total
|
||||
}
|
||||
|
||||
reviews(mediaId:$id){
|
||||
summary
|
||||
user{
|
||||
name
|
||||
avatar {
|
||||
large
|
||||
medium
|
||||
}
|
||||
}
|
||||
body
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
notification_query = """
|
||||
query{
|
||||
Page {
|
||||
pageInfo {
|
||||
total
|
||||
}
|
||||
notifications(resetNotificationCount:true,type:AIRING) {
|
||||
... on AiringNotification {
|
||||
id
|
||||
type
|
||||
episode
|
||||
contexts
|
||||
createdAt
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
coverImage{
|
||||
medium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
get_medialist_item_query = """
|
||||
query($mediaId:Int){
|
||||
MediaList(mediaId:$mediaId){
|
||||
id
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
delete_list_entry_query = """
|
||||
mutation($id:Int){
|
||||
DeleteMediaListEntry(id:$id){
|
||||
deleted
|
||||
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
get_logged_in_user_query = """
|
||||
query{
|
||||
Viewer{
|
||||
id
|
||||
name
|
||||
bannerImage
|
||||
avatar {
|
||||
large
|
||||
medium
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
media_list_mutation = """
|
||||
mutation($mediaId:Int,$scoreRaw:Int,$repeat:Int,$progress:Int,$status:MediaListStatus){
|
||||
SaveMediaListEntry(mediaId:$mediaId,scoreRaw:$scoreRaw,progress:$progress,repeat:$repeat,status:$status){
|
||||
id
|
||||
status
|
||||
mediaId
|
||||
score
|
||||
progress
|
||||
repeat
|
||||
startedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
completedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
media_list_query = """
|
||||
query ($userId: Int, $status: MediaListStatus) {
|
||||
Page {
|
||||
pageInfo {
|
||||
currentPage
|
||||
total
|
||||
}
|
||||
mediaList(userId: $userId, status: $status) {
|
||||
mediaId
|
||||
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
trailer {
|
||||
site
|
||||
id
|
||||
}
|
||||
popularity
|
||||
favourites
|
||||
averageScore
|
||||
episodes
|
||||
genres
|
||||
studios {
|
||||
nodes {
|
||||
name
|
||||
isAnimationStudio
|
||||
}
|
||||
}
|
||||
tags {
|
||||
name
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
status
|
||||
description
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
status
|
||||
progress
|
||||
score
|
||||
repeat
|
||||
notes
|
||||
startedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
completedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
createdAt
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
optional_variables = "\
|
||||
$page:Int,\
|
||||
$sort:[MediaSort],\
|
||||
@@ -57,6 +254,7 @@ query($query:String,%s){
|
||||
)
|
||||
{
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
romaji
|
||||
english
|
||||
@@ -114,6 +312,7 @@ query{
|
||||
|
||||
media(sort:TRENDING_DESC,type:ANIME,genre_not_in:["hentai"]){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
romaji
|
||||
english
|
||||
@@ -168,6 +367,7 @@ query{
|
||||
Page(perPage:15){
|
||||
media(sort:FAVOURITES_DESC,type:ANIME,genre_not_in:["hentai"]){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
romaji
|
||||
english
|
||||
@@ -222,6 +422,7 @@ query{
|
||||
Page(perPage:15){
|
||||
media(sort:SCORE_DESC,type:ANIME,genre_not_in:["hentai"]){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
romaji
|
||||
english
|
||||
@@ -276,6 +477,7 @@ query{
|
||||
Page(perPage:15){
|
||||
media(sort:POPULARITY_DESC,type:ANIME,genre_not_in:["hentai"]){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
romaji
|
||||
english
|
||||
@@ -330,6 +532,7 @@ query{
|
||||
Page(perPage:15){
|
||||
media(sort:UPDATED_AT_DESC,type:ANIME,averageScore_greater:50,genre_not_in:["hentai"],status:RELEASING){
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
romaji
|
||||
english
|
||||
@@ -386,6 +589,7 @@ query {
|
||||
nodes{
|
||||
media{
|
||||
id
|
||||
idMal
|
||||
title{
|
||||
english
|
||||
romaji
|
||||
@@ -475,6 +679,7 @@ query ($id: Int) {
|
||||
relations {
|
||||
nodes {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
@@ -548,6 +753,7 @@ query ($page: Int) {
|
||||
}
|
||||
media(type: ANIME, status: NOT_YET_RELEASED,sort:POPULARITY_DESC,genre_not_in:["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
@@ -601,6 +807,7 @@ query($id:Int){
|
||||
Page{
|
||||
media(id:$id) {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from .allanime.api import AllAnimeAPI
|
||||
from .animepahe.api import AnimePaheApi
|
||||
|
||||
anime_sources = {"allanime": AllAnimeAPI}
|
||||
anime_sources = {"allanime": AllAnimeAPI, "animepahe": AnimePaheApi}
|
||||
|
||||
|
||||
class Anime_Provider:
|
||||
|
||||
@@ -4,8 +4,6 @@ from typing import Iterator
|
||||
|
||||
import requests
|
||||
from requests.exceptions import Timeout
|
||||
from rich import print
|
||||
from rich.progress import Progress
|
||||
|
||||
from ....libs.anime_provider.allanime.types import AllAnimeEpisode
|
||||
from ....libs.anime_provider.types import Anime, Server
|
||||
@@ -24,6 +22,7 @@ Logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO: create tests for the api
|
||||
#
|
||||
# ** Based on ani-cli **
|
||||
class AllAnimeAPI:
|
||||
"""
|
||||
Provides a fast and effective interface to AllAnime site.
|
||||
@@ -42,15 +41,17 @@ class AllAnimeAPI:
|
||||
headers={"Referer": ALLANIME_REFERER, "User-Agent": USER_AGENT},
|
||||
timeout=10,
|
||||
)
|
||||
return response.json()["data"]
|
||||
except Timeout as e:
|
||||
print(
|
||||
"Timeout has been exceeded :cry:. This could mean allanime is down or your internet is down"
|
||||
if response.status_code == 200:
|
||||
return response.json()["data"]
|
||||
else:
|
||||
Logger.error("allanime(ERROR): ", response.text)
|
||||
return {}
|
||||
except Timeout:
|
||||
Logger.error(
|
||||
"allanime(Error):Timeout exceeded this could mean allanime is down or you have lost internet connection"
|
||||
)
|
||||
Logger.error(f"allanime(Error): {e}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print("sth went wrong :confused:")
|
||||
Logger.error(f"allanime:Error: {e}")
|
||||
return {}
|
||||
|
||||
@@ -75,22 +76,20 @@ class AllAnimeAPI:
|
||||
"countryorigin": countryorigin,
|
||||
}
|
||||
try:
|
||||
with Progress() as progress:
|
||||
progress.add_task("[cyan]searching..", start=False, total=None)
|
||||
|
||||
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
|
||||
return normalize_search_results(search_results) # pyright:ignore
|
||||
except Exception:
|
||||
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
|
||||
return normalize_search_results(search_results) # pyright:ignore
|
||||
except Exception as e:
|
||||
Logger.error(f"FA(AllAnime): {e}")
|
||||
return {}
|
||||
|
||||
def get_anime(self, allanime_show_id: str):
|
||||
variables = {"showId": allanime_show_id}
|
||||
try:
|
||||
with Progress() as progress:
|
||||
progress.add_task("[cyan]fetching anime..", start=False, total=None)
|
||||
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
|
||||
return normalize_anime(anime["show"])
|
||||
except Exception:
|
||||
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
|
||||
return normalize_anime(anime["show"])
|
||||
except Exception as e:
|
||||
Logger.error(f"FA(AllAnime): {e}")
|
||||
return None
|
||||
|
||||
def get_anime_episode(
|
||||
@@ -102,11 +101,10 @@ class AllAnimeAPI:
|
||||
"episodeString": episode_string,
|
||||
}
|
||||
try:
|
||||
with Progress() as progress:
|
||||
progress.add_task("[cyan]fetching episode..", start=False, total=None)
|
||||
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
|
||||
return episode["episode"] # pyright: ignore
|
||||
except Exception:
|
||||
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
|
||||
return episode["episode"] # pyright: ignore
|
||||
except Exception as e:
|
||||
Logger.error(f"FA(AllAnime): {e}")
|
||||
return {}
|
||||
|
||||
def get_episode_streams(
|
||||
@@ -121,99 +119,97 @@ class AllAnimeAPI:
|
||||
|
||||
embeds = allanime_episode["sourceUrls"]
|
||||
try:
|
||||
with Progress() as progress:
|
||||
progress.add_task("[cyan]fetching streams..", start=False, total=None)
|
||||
for embed in embeds:
|
||||
try:
|
||||
# filter the working streams
|
||||
if embed.get("sourceName", "") not in (
|
||||
"Sak",
|
||||
"Kir",
|
||||
"S-mp4",
|
||||
"Luf-mp4",
|
||||
):
|
||||
continue
|
||||
url = embed.get("sourceUrl")
|
||||
for embed in embeds:
|
||||
try:
|
||||
# filter the working streams
|
||||
if embed.get("sourceName", "") not in (
|
||||
"Sak",
|
||||
"Kir",
|
||||
"S-mp4",
|
||||
"Luf-mp4",
|
||||
"Default",
|
||||
):
|
||||
continue
|
||||
url = embed.get("sourceUrl")
|
||||
|
||||
if not url:
|
||||
continue
|
||||
if url.startswith("--"):
|
||||
url = url[2:]
|
||||
if not url:
|
||||
continue
|
||||
if url.startswith("--"):
|
||||
url = url[2:]
|
||||
|
||||
# get the stream url for an episode of the defined source names
|
||||
parsed_url = decode_hex_string(url)
|
||||
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock','clock.json')}"
|
||||
resp = requests.get(
|
||||
embed_url,
|
||||
headers={
|
||||
"Referer": ALLANIME_REFERER,
|
||||
"User-Agent": USER_AGENT,
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
match embed["sourceName"]:
|
||||
case "Luf-mp4":
|
||||
Logger.debug(
|
||||
"allanime:Found streams from gogoanime"
|
||||
# get the stream url for an episode of the defined source names
|
||||
parsed_url = decode_hex_string(url)
|
||||
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock', 'clock.json')}"
|
||||
resp = requests.get(
|
||||
embed_url,
|
||||
headers={
|
||||
"Referer": ALLANIME_REFERER,
|
||||
"User-Agent": USER_AGENT,
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
match embed["sourceName"]:
|
||||
case "Luf-mp4":
|
||||
Logger.debug("allanime:Found streams from gogoanime")
|
||||
yield {
|
||||
"server": "gogoanime",
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
print("[yellow]GogoAnime Fetched")
|
||||
yield {
|
||||
"server": "gogoanime",
|
||||
"episode_title": (
|
||||
allanime_episode["notes"]
|
||||
or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
} # pyright:ignore
|
||||
case "Kir":
|
||||
Logger.debug(
|
||||
"allanime:Found streams from wetransfer"
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
} # pyright:ignore
|
||||
case "Kir":
|
||||
Logger.debug("allanime:Found streams from wetransfer")
|
||||
yield {
|
||||
"server": "wetransfer",
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
print("[yellow]WeTransfer Fetched")
|
||||
yield {
|
||||
"server": "wetransfer",
|
||||
"episode_title": (
|
||||
allanime_episode["notes"]
|
||||
or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
} # pyright:ignore
|
||||
case "S-mp4":
|
||||
Logger.debug(
|
||||
"allanime:Found streams from sharepoint"
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
} # pyright:ignore
|
||||
case "S-mp4":
|
||||
Logger.debug("allanime:Found streams from sharepoint")
|
||||
yield {
|
||||
"server": "sharepoint",
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
print("[yellow]Sharepoint Fetched")
|
||||
yield {
|
||||
"server": "sharepoint",
|
||||
"episode_title": (
|
||||
allanime_episode["notes"]
|
||||
or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
} # pyright:ignore
|
||||
case "Sak":
|
||||
Logger.debug("allanime:Found streams from dropbox")
|
||||
print("[yellow]Dropbox Fetched")
|
||||
yield {
|
||||
"server": "dropbox",
|
||||
"episode_title": (
|
||||
allanime_episode["notes"]
|
||||
or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
} # pyright:ignore
|
||||
except Timeout:
|
||||
print(
|
||||
"Timeout has been exceeded :cry: this could mean allanime is down or your internet connection is poor"
|
||||
)
|
||||
except Exception as e:
|
||||
print("Sth went wrong :confused:", e)
|
||||
except Exception:
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
} # pyright:ignore
|
||||
case "Sak":
|
||||
Logger.debug("allanime:Found streams from dropbox")
|
||||
yield {
|
||||
"server": "dropbox",
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
} # pyright:ignore
|
||||
case "Default":
|
||||
Logger.debug("allanime:Found streams from wixmp")
|
||||
yield {
|
||||
"server": "wixmp",
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f'{anime["title"]}'
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": resp.json()["links"],
|
||||
} # pyright:ignore
|
||||
except Timeout:
|
||||
Logger.error(
|
||||
"Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
|
||||
)
|
||||
return []
|
||||
except Exception as e:
|
||||
Logger.error(f"FA(Allanime): {e}")
|
||||
return []
|
||||
except Exception as e:
|
||||
Logger.error(f"FA(Allanime): {e}")
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -4,4 +4,4 @@ ALLANIME_BASE = "allanime.day"
|
||||
ALLANIME_REFERER = "https://allanime.to/"
|
||||
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
|
||||
USER_AGENT = random_user_agent()
|
||||
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer"]
|
||||
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp"]
|
||||
|
||||
62
fastanime/libs/anime_provider/animepahe/api.py
Normal file
62
fastanime/libs/anime_provider/animepahe/api.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import requests
|
||||
|
||||
from .constants import ANIMEPAHE_BASE, ANIMEPAHE_ENDPOINT, REQUEST_HEADERS
|
||||
|
||||
|
||||
class AnimePaheApi:
|
||||
def search_for_anime(self, user_query, *args):
|
||||
try:
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
||||
headers = {**REQUEST_HEADERS}
|
||||
response = requests.get(url, headers=headers)
|
||||
if not response.status_code == 200:
|
||||
return
|
||||
data = response.json()
|
||||
return {
|
||||
"pageInfo": {"total": data["total"]},
|
||||
"results": [
|
||||
{
|
||||
"id": result["session"],
|
||||
"title": result["title"],
|
||||
"availableEpisodes": result["episodes"],
|
||||
"type": result["type"],
|
||||
}
|
||||
for result in data["data"]
|
||||
],
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
input()
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
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()
|
||||
22
fastanime/libs/anime_provider/animepahe/constants.py
Normal file
22
fastanime/libs/anime_provider/animepahe/constants.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from yt_dlp.utils.networking import random_user_agent
|
||||
|
||||
USER_AGENT = random_user_agent()
|
||||
ANIMEPAHE = "animepahe.ru"
|
||||
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}/"
|
||||
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
|
||||
|
||||
REQUEST_HEADERS = {
|
||||
"Cookie": "__ddgid_=VvX0ebHrH2DsFZo4; __ddgmark_=3savRpSVFhvZcn5x; __ddg2_=buBJ3c4pNBYKFZNp; __ddg1_=rbVADKr9URtt55zoIGFa; SERVERID=janna; XSRF-TOKEN=eyJpdiI6IjV5bFNtd0phUHgvWGJxc25wL0VJSUE9PSIsInZhbHVlIjoicEJTZktlR2hxR2JZTWhnL0JzazlvZU5TQTR2bjBWZ2dDb0RwUXVUUWNSclhQWUhLRStYSmJmWmUxWkpiYkFRYU12RjFWejlSWHorME1wZG5qQ1U0TnFlNnBFR2laQjN1MjdyNjc5TjVPdXdJb2o5VkU1bEduRW9pRHNDTHh6Sy8iLCJtYWMiOiI0OTc0ZmNjY2UwMGJkOWY2MWNkM2NlMjk2ZGMyZGJmMWE0NTdjZTdkNGI2Y2IwNTIzZmFiZWU5ZTE2OTk0YmU4IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImxvdlpqREFnTjdaeFJubUlXQWlJVWc9PSIsInZhbHVlIjoiQnE4R3VHdjZ4M1NDdEVWM1ZqMUxtNnVERnJCcmtCUHZKNzRPR2RFbzNFcStTL29xdnVTbWhsNVRBUXEybVZWNU1UYVlTazFqYlN5UjJva1k4czNGaXBTbkJJK01oTUd3VHRYVHBoc3dGUWxHYnFlS2NJVVNFbTFqMVBWdFpuVUgiLCJtYWMiOiI1NDdjZTVkYmNhNjUwZTMxZmRlZmVmMmRlMGNiYjAwYjlmYjFjY2U0MDc1YTQzZThiMTIxMjJlYTg1NTA4YjBmIiwidGFnIjoiIn0%3D; latest=5592 ",
|
||||
"Host": ANIMEPAHE,
|
||||
"User-Agent": USER_AGENT,
|
||||
"Accept": "application , text/javascript, */*; q=0.01",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
"Referer": ANIMEPAHE_BASE,
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"DNT": "1",
|
||||
"Connection": "keep-alive",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"TE": "trailers",
|
||||
}
|
||||
0
fastanime/libs/anime_provider/animepahe/types.py
Normal file
0
fastanime/libs/anime_provider/animepahe/types.py
Normal file
0
fastanime/libs/aniskip/__init__.py
Normal file
0
fastanime/libs/aniskip/__init__.py
Normal file
22
fastanime/libs/aniskip/api.py
Normal file
22
fastanime/libs/aniskip/api.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import requests
|
||||
|
||||
ANISKIP_ENDPOINT = "https://api.aniskip.com/v1/skip-times"
|
||||
|
||||
|
||||
# TODO: Finish own implementation of aniskip script
|
||||
class AniSkip:
|
||||
@classmethod
|
||||
def get_skip_times(
|
||||
cls, mal_id: int, episode_number: float | int, types=["op", "ed"]
|
||||
):
|
||||
url = f"{ANISKIP_ENDPOINT}/{mal_id}/{episode_number}?types=op&types=ed"
|
||||
response = requests.get(url)
|
||||
print(response.text)
|
||||
return response.json()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mal_id = input("Mal id: ")
|
||||
episode_number = input("episode_number: ")
|
||||
skip_times = AniSkip.get_skip_times(int(mal_id), float(episode_number))
|
||||
print(skip_times)
|
||||
@@ -8,7 +8,7 @@ from typing import Callable, List
|
||||
from art import text2art
|
||||
from rich import print
|
||||
|
||||
from ... import PLATFORM
|
||||
from ...constants import PLATFORM
|
||||
from .config import FZF_DEFAULT_OPTS, FzfOptions
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
101
poetry.lock
generated
101
poetry.lock
generated
@@ -206,6 +206,17 @@ files = [
|
||||
[package.dependencies]
|
||||
cffi = ">=1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "5.4.0"
|
||||
description = "Extensible memoizing collections and decorators"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"},
|
||||
{file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.7.4"
|
||||
@@ -292,6 +303,17 @@ files = [
|
||||
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "5.2.0"
|
||||
description = "Universal encoding detector for Python 3"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"},
|
||||
{file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.3.2"
|
||||
@@ -660,6 +682,23 @@ files = [
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "plyer"
|
||||
version = "2.1.0"
|
||||
description = "Platform-independent wrapper for platform-dependent APIs"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "plyer-2.1.0-py2.py3-none-any.whl", hash = "sha256:1b1772060df8b3045ed4f08231690ec8f7de30f5a004aa1724665a9074eed113"},
|
||||
{file = "plyer-2.1.0.tar.gz", hash = "sha256:65b7dfb7e11e07af37a8487eb2aa69524276ef70dad500b07228ce64736baa61"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
android = ["pyjnius"]
|
||||
dev = ["flake8", "mock"]
|
||||
ios = ["pyobjus"]
|
||||
macosx = ["pyobjus"]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "3.7.1"
|
||||
@@ -769,15 +808,34 @@ files = [
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyproject-api"
|
||||
version = "1.7.1"
|
||||
description = "API to interact with the python pyproject.toml based projects"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"},
|
||||
{file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=24.1"
|
||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2024.5.6)", "sphinx-autodoc-typehints (>=2.2.1)"]
|
||||
testing = ["covdefaults (>=2.3)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=70.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.1"
|
||||
version = "8.3.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"},
|
||||
{file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"},
|
||||
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
|
||||
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1060,6 +1118,33 @@ files = [
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tox"
|
||||
version = "4.16.0"
|
||||
description = "tox is a generic virtualenv management and test command line tool"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tox-4.16.0-py3-none-any.whl", hash = "sha256:61e101061b977b46cf00093d4319438055290ad0009f84497a07bf2d2d7a06d0"},
|
||||
{file = "tox-4.16.0.tar.gz", hash = "sha256:43499656f9949edb681c0f907f86fbfee98677af9919d8b11ae5ad77cb800748"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cachetools = ">=5.3.3"
|
||||
chardet = ">=5.2"
|
||||
colorama = ">=0.4.6"
|
||||
filelock = ">=3.15.4"
|
||||
packaging = ">=24.1"
|
||||
platformdirs = ">=4.2.2"
|
||||
pluggy = ">=1.5"
|
||||
pyproject-api = ">=1.7.1"
|
||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
virtualenv = ">=20.26.3"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2024.5.6)", "sphinx (>=7.3.7)", "sphinx-argparse-cli (>=1.16)", "sphinx-autodoc-typehints (>=2.2.2)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.11)"]
|
||||
testing = ["build[virtualenv] (>=1.2.1)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=9.1)", "distlib (>=0.3.8)", "flaky (>=3.8.1)", "hatch-vcs (>=0.4)", "hatchling (>=1.25)", "psutil (>=6)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-xdist (>=3.6.1)", "re-assert (>=1.1)", "setuptools (>=70.2)", "time-machine (>=2.14.2)", "wheel (>=0.43)"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
@@ -1202,13 +1287,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "yt-dlp"
|
||||
version = "2024.7.16"
|
||||
version = "2024.7.25"
|
||||
description = "A feature-rich command-line audio/video downloader"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "yt_dlp-2024.7.16-py3-none-any.whl", hash = "sha256:424805a112e757b141e767bc938d49db56d13d6415a92fa4cd8acadd55790be0"},
|
||||
{file = "yt_dlp-2024.7.16.tar.gz", hash = "sha256:c5bd517a49dea1923ec8e14f51858f10fd89dfece14cb701392b480b41b2f516"},
|
||||
{file = "yt_dlp-2024.7.25-py3-none-any.whl", hash = "sha256:f44b5f33776b4f718900c670fe6e4698fb6fcd426455cd837cf25a1d6d4d9560"},
|
||||
{file = "yt_dlp-2024.7.25.tar.gz", hash = "sha256:7587aa25e236cf7b14bdb9378bbffff51202d901b04202be0cf62cbb56d3b52c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1222,7 +1307,7 @@ urllib3 = ">=1.26.17,<3"
|
||||
websockets = ">=12.0"
|
||||
|
||||
[package.extras]
|
||||
build = ["build", "hatchling", "pip", "setuptools", "wheel"]
|
||||
build = ["build", "hatchling", "pip", "setuptools (>=71.0.2)", "wheel"]
|
||||
curl-cffi = ["curl-cffi (==0.5.10)", "curl-cffi (>=0.5.10,<0.6.dev0 || ==0.7.*)"]
|
||||
dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "ruff (>=0.5.0,<0.6.0)"]
|
||||
py2exe = ["py2exe (>=0.12)"]
|
||||
@@ -1234,4 +1319,4 @@ test = ["pytest (>=8.1,<9.0)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "b6f2f120c8a562e8c8d98aae75f1e5fc4dd779d2da60fdcff6b98bf88008f23b"
|
||||
content-hash = "871d39c0e2481614146804d675aafa7b1b79c736ccf12a8e749655f574881670"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "0.31.2"
|
||||
description = "A fast and efficient GUI and CLI anime scrapper"
|
||||
authors = ["Benex254 <benedictx855@gmail.com>"]
|
||||
version = "0.40.1"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -18,6 +18,7 @@ python-dotenv = "^1.0.1"
|
||||
thefuzz = "^0.22.1"
|
||||
requests = "^2.32.3"
|
||||
|
||||
plyer = "^2.1.0"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.4.2"
|
||||
isort = "^5.13.2"
|
||||
@@ -26,10 +27,10 @@ ruff = "^0.4.10"
|
||||
pre-commit = "^3.7.1"
|
||||
autoflake = "^2.3.1"
|
||||
|
||||
tox = "^4.16.0"
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
fastanime = 'fastanime:FastAnime'
|
||||
|
||||
|
||||
26
tox.ini
Normal file
26
tox.ini
Normal file
@@ -0,0 +1,26 @@
|
||||
[tox]
|
||||
requires =
|
||||
tox>=4
|
||||
env_list = lint, type, py{310,311}
|
||||
|
||||
; [testenv]
|
||||
; description = run unit tests
|
||||
; deps =
|
||||
; pytest>=7
|
||||
; pytest-sugar
|
||||
; commands =
|
||||
; pytest {posargs:tests}
|
||||
;
|
||||
[testenv:lint]
|
||||
description = run linters
|
||||
skip_install = true
|
||||
deps =
|
||||
black==22.12
|
||||
commands = black {posargs:.}
|
||||
;
|
||||
; [testenv:type]
|
||||
; description = run type checks
|
||||
; deps =
|
||||
; mypy>=0.991
|
||||
; commands =
|
||||
; mypy {posargs:src tests}
|
||||
Reference in New Issue
Block a user