Compare commits

..

40 Commits

Author SHA1 Message Date
Benex254
2ca5985b9a fix: wrong code formating 2024-07-30 18:09:28 +03:00
Benex254
684f8c57a8 build: add tox to ensure compatibility 2024-07-30 18:09:02 +03:00
Benex254
324fef36ac chore: bump app version 2024-07-30 17:34:14 +03:00
Benex254
9d62915f2b chore: bump version 2024-07-30 17:14:19 +03:00
Benex254
a4e9e5f29e chore: bump version 2024-07-30 17:12:27 +03:00
Benex254
d00c958ff2 docs: update readme 2024-07-30 17:12:11 +03:00
Benex254
bc2ac69b9a fix(interface): escape sequence warning 2024-07-30 17:01:59 +03:00
Benex254
01fa96c27a feat: update fa script 2024-07-30 16:53:36 +03:00
Benex254
6c1bbfe50a feat: add aniskip intergration and scoring of anime 2024-07-30 16:52:33 +03:00
Benex254
ecc4e85079 feat(anilist): ensure rate limit is not exceeded 2024-07-30 16:34:18 +03:00
Benex254
1cd743acdf feat(anilist): include mal ids in queries 2024-07-30 16:27:43 +03:00
Benex254
23dd969d37 feat?: create custom aniskip functionality 2024-07-30 16:26:43 +03:00
Benex254
d21f6b5ab0 feat: ensure correct python version 2024-07-30 16:25:58 +03:00
Benex254
640bb12c44 feat: remove unused functions 2024-07-30 16:25:28 +03:00
Benex254
453e4c1b74 feat(notifier): improve error handling 2024-07-30 16:24:34 +03:00
Benex254
4dc3d1b0bb feat: rename watchlist to watching and repeating to rewatching 2024-07-30 16:23:52 +03:00
Benex254
4df57f9410 feat: remove unused print statement 2024-07-30 16:22:53 +03:00
Benex254
baa94efc24 docs: update readme 2024-07-30 10:33:06 +03:00
Benex254
f5d18512f8 feat(cli): add top as an option for servers 2024-07-30 10:32:53 +03:00
Benex254
72037eea07 feat: show anime cover image for notifications on none windows systems 2024-07-30 09:37:17 +03:00
Benex254
f5c120ebb8 feat: handle none logged in user 2024-07-30 09:36:23 +03:00
Benex254
5f2b88bd9b feat(anilist_api): handle none 200 status code 2024-07-30 09:35:28 +03:00
Benex254
b346801dba feat(allanime): handle none 200 status code 2024-07-30 09:34:45 +03:00
benex
1b1a05e2b3 feat(notifier): add icon 2024-07-29 13:21:32 +03:00
benex
8716fb2e1d fix: use platform.system to correctly detect the os 2024-07-29 12:38:37 +03:00
benex
12a38d6d48 feat(anilist): make icons optional 2024-07-29 12:29:27 +03:00
Benex254
e6aa508644 chore: update pyproject.toml 2024-07-29 13:42:45 +03:00
Benex254
584a2ee3f1 feat(allanime): add server 2024-07-29 13:42:45 +03:00
Benex254
385dd4337d feat(anilist): add notifier command 2024-07-29 13:42:45 +03:00
Benex254
1c70a2122d feat(anilist): add notifier command 2024-07-29 13:42:45 +03:00
BenedictX
46b9b844d4 Update README.md 2024-07-29 13:11:25 +03:00
Benex254
272042ec35 fix(anilist_interface): trailer not loading 2024-07-28 16:43:38 +03:00
Benex254
56632cf77c feat(tui): improve the ui 2024-07-28 15:32:31 +03:00
Benex254
e8dacf0722 feat(anilist): only update episode progress in their is actual progress 2024-07-28 11:24:31 +03:00
Benex254
b95d49429c feat(anilist): add update your anilist feature 2024-07-28 10:42:32 +03:00
Benex254
ca087b2e94 feat(player): implement continue from timestamp 2024-07-28 02:23:54 +03:00
Benex254
3f33ae3738 feat(anilist): change media animelist status for anime you currently watching 2024-07-28 00:32:24 +03:00
Benex254
94a282a320 feat(anilist): implement viewing of your anilist animelist 2024-07-28 00:08:44 +03:00
Benex254
0b379ec813 feat(anilist): add account intergration 2024-07-27 22:57:40 +03:00
Benex254
6b0a013705 feat(provider): add animepahe as new provider 2024-07-27 22:54:17 +03:00
37 changed files with 1499 additions and 127 deletions

133
README.md
View File

@@ -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
View File

@@ -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" "$@"

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
fastanime/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -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

View File

@@ -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)

View 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)

View 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)

View 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()

View 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)

View 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)

View 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)

View 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)

View 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)

View File

@@ -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")

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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:

View File

@@ -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"

View File

@@ -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"]

View 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()

View 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",
}

View File

View 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
View File

@@ -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"

View File

@@ -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
View 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}