mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-10 06:40:39 -08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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 repeating`
|
||||
- `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" "$@"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ 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,6 +1,12 @@
|
||||
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 dotenv import load_dotenv
|
||||
|
||||
@@ -13,7 +19,7 @@ if os.environ.get("FA_RICH_TRACEBACK", False):
|
||||
|
||||
|
||||
# initiate constants
|
||||
__version__ = "v0.32.0"
|
||||
__version__ = "v0.40.0"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
@@ -39,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)
|
||||
@@ -11,6 +11,9 @@ 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,10 +2,13 @@ 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.progress import Progress
|
||||
from rich.prompt import Prompt
|
||||
from rich.prompt import Confirm, Prompt
|
||||
|
||||
from ...anilist import AniList
|
||||
from ...constants import USER_CONFIG_PATH
|
||||
@@ -18,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):
|
||||
@@ -46,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():
|
||||
@@ -55,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
|
||||
@@ -116,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:
|
||||
@@ -214,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()
|
||||
@@ -240,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"]
|
||||
@@ -257,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
|
||||
@@ -355,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:")
|
||||
@@ -363,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):
|
||||
@@ -438,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(
|
||||
@@ -501,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[/]")
|
||||
@@ -529,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,5 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
from threading import Thread
|
||||
|
||||
@@ -11,7 +12,7 @@ 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)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import shutil
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# legacy
|
||||
# def mpv(link, title: None | str = "anime", *custom_args):
|
||||
# MPV = shutil.which("mpv")
|
||||
@@ -25,14 +24,60 @@ from typing import Optional
|
||||
# else:
|
||||
# subprocess.run([MPV, *custom_args, f"--title={title}", link])
|
||||
#
|
||||
#
|
||||
def mpv(link: str, title: Optional[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
|
||||
if title is None:
|
||||
title = "anime"
|
||||
|
||||
# Regex to check if the link is a YouTube URL
|
||||
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
|
||||
@@ -54,6 +99,7 @@ def mpv(link: str, title: Optional[str] = "anime", *custom_args):
|
||||
"-n",
|
||||
"com.google.android.youtube/.UrlActivity",
|
||||
]
|
||||
return "0"
|
||||
else:
|
||||
# Android specific commands to launch mpv with a regular URL
|
||||
args = [
|
||||
@@ -71,10 +117,18 @@ def mpv(link: str, title: Optional[str] = "anime", *custom_args):
|
||||
]
|
||||
|
||||
subprocess.run(args)
|
||||
return "0"
|
||||
else:
|
||||
# General mpv command with custom arguments
|
||||
mpv_args = [MPV, *custom_args, f"--title={title}", link]
|
||||
subprocess.run(mpv_args)
|
||||
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
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import os
|
||||
from platform import platform
|
||||
from platform import system
|
||||
|
||||
from platformdirs import PlatformDirs
|
||||
|
||||
from . import APP_NAME, AUTHOR
|
||||
|
||||
PLATFORM = platform()
|
||||
PLATFORM = system()
|
||||
dirs = PlatformDirs(appname=APP_NAME, appauthor=AUTHOR, ensure_exists=True)
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ 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:
|
||||
@@ -21,6 +26,7 @@ if not APP_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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -22,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.
|
||||
@@ -40,7 +41,11 @@ class AllAnimeAPI:
|
||||
headers={"Referer": ALLANIME_REFERER, "User-Agent": USER_AGENT},
|
||||
timeout=10,
|
||||
)
|
||||
return response.json()["data"]
|
||||
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"
|
||||
@@ -122,6 +127,7 @@ class AllAnimeAPI:
|
||||
"Kir",
|
||||
"S-mp4",
|
||||
"Luf-mp4",
|
||||
"Default",
|
||||
):
|
||||
continue
|
||||
url = embed.get("sourceUrl")
|
||||
@@ -133,7 +139,7 @@ class AllAnimeAPI:
|
||||
|
||||
# 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')}"
|
||||
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock', 'clock.json')}"
|
||||
resp = requests.get(
|
||||
embed_url,
|
||||
headers={
|
||||
@@ -184,6 +190,16 @@ class AllAnimeAPI:
|
||||
+ 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"
|
||||
|
||||
@@ -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)
|
||||
87
poetry.lock
generated
87
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,6 +808,25 @@ 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.2"
|
||||
@@ -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"
|
||||
@@ -1234,4 +1319,4 @@ test = ["pytest (>=8.1,<9.0)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "38fed68d89077d348221af9eb8e2d0ea6c9585bd4c5de16d6e5974664c562f73"
|
||||
content-hash = "871d39c0e2481614146804d675aafa7b1b79c736ccf12a8e749655f574881670"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "0.32.0"
|
||||
version = "0.40.0"
|
||||
description = "A fast and efficient anime scrapper and exploration tool"
|
||||
authors = ["Benex254 <benedictx855@gmail.com>"]
|
||||
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,6 +27,7 @@ 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"
|
||||
|
||||
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