mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-06 04:41:06 -08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d921b86c4 | ||
|
|
603179673d | ||
|
|
0ea3cc87ee | ||
|
|
ab9f237c19 | ||
|
|
1dd7c72b4c | ||
|
|
f7cef0eb25 | ||
|
|
07900b3bf8 | ||
|
|
6479012072 | ||
|
|
0808b8dd38 | ||
|
|
3e2a22612d | ||
|
|
45ff21b1af | ||
|
|
affed01840 | ||
|
|
71e707400a | ||
|
|
f0133f718c | ||
|
|
0dc5bfc06b | ||
|
|
a95a118e27 | ||
|
|
ace11bc63e | ||
|
|
cc5d65eee3 | ||
|
|
ba0de50925 | ||
|
|
d373ba3bf6 | ||
|
|
36ce504873 | ||
|
|
47420bedc9 | ||
|
|
0f60324fd0 | ||
|
|
1431edb52a | ||
|
|
f9fc210264 | ||
|
|
68fdb7835f | ||
|
|
531929aab5 | ||
|
|
2ca5985b9a | ||
|
|
684f8c57a8 | ||
|
|
324fef36ac | ||
|
|
9d62915f2b | ||
|
|
a4e9e5f29e | ||
|
|
d00c958ff2 | ||
|
|
bc2ac69b9a | ||
|
|
01fa96c27a | ||
|
|
6c1bbfe50a | ||
|
|
ecc4e85079 | ||
|
|
1cd743acdf | ||
|
|
23dd969d37 | ||
|
|
d21f6b5ab0 | ||
|
|
640bb12c44 | ||
|
|
453e4c1b74 | ||
|
|
4dc3d1b0bb | ||
|
|
4df57f9410 | ||
|
|
baa94efc24 | ||
|
|
f5d18512f8 | ||
|
|
72037eea07 | ||
|
|
f5c120ebb8 | ||
|
|
5f2b88bd9b | ||
|
|
b346801dba | ||
|
|
1b1a05e2b3 | ||
|
|
8716fb2e1d | ||
|
|
12a38d6d48 | ||
|
|
e6aa508644 | ||
|
|
584a2ee3f1 | ||
|
|
385dd4337d | ||
|
|
1c70a2122d | ||
|
|
46b9b844d4 | ||
|
|
272042ec35 | ||
|
|
56632cf77c | ||
|
|
e8dacf0722 | ||
|
|
b95d49429c | ||
|
|
ca087b2e94 | ||
|
|
3f33ae3738 | ||
|
|
94a282a320 | ||
|
|
0b379ec813 | ||
|
|
6b0a013705 |
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Build release distributions
|
||||
run: |
|
||||
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM ubuntu
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install python3
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install pipx
|
||||
RUN pipx ensurepath
|
||||
COPY . /fastanime
|
||||
WORKDIR /fastanime
|
||||
RUN pipx install .
|
||||
CMD ["bash"]
|
||||
148
README.md
148
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,15 @@ 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.
|
||||
- `--rofi` use rofi for the ui
|
||||
- `--rofi-theme <path>` theme to use with rofi
|
||||
- `--rofi-theme-input <path>` theme to use with rofi input
|
||||
- `--rofi-theme-confirm <path>` theme to use with rofi confirm
|
||||
|
||||
#### 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 +207,47 @@ The subcommands are mainly their as convenience. Since all the features already
|
||||
- `fastanime anilist favourites`: Top 15 favorite anime.
|
||||
- `fastanime anilist random`: get random anime
|
||||
|
||||
The following are commands you can only run if you are signed in to your AniList account:
|
||||
|
||||
- `fastanime anilist watching`
|
||||
- `fastanime anilist planning`
|
||||
- `fastanime anilist rewatching`
|
||||
- `fastanime anilist dropped`
|
||||
- `fastanime anilist paused`
|
||||
- `fastanime anilist completed`
|
||||
|
||||
Plus: `fastanime anilist notifier` :fire:
|
||||
|
||||
```bash
|
||||
# basic form
|
||||
fastanime anilist notifier
|
||||
|
||||
# with logging to stdout
|
||||
fastanime --log anilist notifier
|
||||
|
||||
# with logging to a file. stored in the same place as your config
|
||||
fastanime --log-file anilist notifier
|
||||
```
|
||||
|
||||
The above commands will start a loop that checks every 2 minutes if any of the anime in your watch list that are aireing has just released a new episode.
|
||||
|
||||
The notification will consist of a cover image of the anime in none windows systems.
|
||||
|
||||
You can place the command among your machines startup scripts.
|
||||
|
||||
For fish users for example you can decide to put this in your `~/.config/fish/config.fish`:
|
||||
|
||||
```fish
|
||||
if ! ps aux | grep -q '[f]astanime .* notifier'
|
||||
echo initializing fastanime anilist notifier
|
||||
nohup 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 +321,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
|
||||
@@ -292,8 +350,22 @@ format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
|
||||
[general]
|
||||
preferred_language = romaji # Display language (options: english, romaji)
|
||||
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
|
||||
preview=false # whether to show a preview window when using fzf or rofi
|
||||
|
||||
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
|
||||
|
||||
use_rofi=false # whether to use rofi for the ui
|
||||
rofi_theme=<path-to-rofi-theme-file>
|
||||
rofi_theme_input=<path-to-rofi-theme-file>
|
||||
rofi_theme_confirm=<path-to-rofi-theme-file>
|
||||
|
||||
|
||||
# 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
|
||||
@@ -311,7 +383,7 @@ For inquiries, join our [Discord Server](https://discord.gg/4NUTj5Pt).
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/HRjySFjQ">
|
||||
<img src="https://invidget.switchblade.xyz/HRjySFjQ">
|
||||
<img src="https://invidget.switchblade.xyz/HRjySFjQ"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
6
fa
6
fa
@@ -1,2 +1,4 @@
|
||||
#! /usr/bin/bash
|
||||
poetry run fastanime $*
|
||||
#!/usr/bin/env sh
|
||||
# exec "${PYTHON:-python3}" -Werror -Xdev -m "$(dirname "$(realpath "$0")")/fastanime" "$@"
|
||||
cd "$(dirname "$(realpath "$0")")" || exit 1
|
||||
exec python -m fastanime "$@"
|
||||
|
||||
@@ -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.50.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,25 @@ 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.option("--dub", help="Set the translation type to dub", is_flag=True)
|
||||
@click.option("--sub", help="Set the translation type to sub", is_flag=True)
|
||||
@click.option("--rofi", help="Use rofi for the ui", is_flag=True)
|
||||
@click.option("--rofi-theme", help="Rofi theme to use", type=click.Path())
|
||||
@click.option(
|
||||
"--rofi-theme-confirm",
|
||||
help="Rofi theme to use for the confirm prompt",
|
||||
type=click.Path(),
|
||||
)
|
||||
@click.option(
|
||||
"--rofi-theme-input",
|
||||
help="Rofi theme to use for the user input prompt",
|
||||
type=click.Path(),
|
||||
)
|
||||
@click.pass_context
|
||||
def run_cli(
|
||||
ctx: click.Context,
|
||||
@@ -107,6 +131,7 @@ def run_cli(
|
||||
server,
|
||||
format,
|
||||
continue_,
|
||||
skip,
|
||||
translation_type,
|
||||
quality,
|
||||
auto_next,
|
||||
@@ -117,6 +142,13 @@ def run_cli(
|
||||
default,
|
||||
preview,
|
||||
no_preview,
|
||||
icons,
|
||||
dub,
|
||||
sub,
|
||||
rofi,
|
||||
rofi_theme,
|
||||
rofi_theme_confirm,
|
||||
rofi_theme_input,
|
||||
):
|
||||
ctx.obj = Config()
|
||||
if provider:
|
||||
@@ -128,12 +160,17 @@ 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:
|
||||
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")
|
||||
ctx.get_parameter_source("--auto_select")
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
):
|
||||
ctx.obj.auto_select = auto_select
|
||||
@@ -151,3 +188,24 @@ def run_cli(
|
||||
ctx.obj.preview = True
|
||||
if no_preview:
|
||||
ctx.obj.preview = False
|
||||
if dub:
|
||||
ctx.obj.translation_type = "dub"
|
||||
if sub:
|
||||
ctx.obj.translation_type = "sub"
|
||||
if rofi:
|
||||
ctx.obj.use_fzf = False
|
||||
ctx.obj.use_rofi = True
|
||||
if rofi:
|
||||
from ..libs.rofi import Rofi
|
||||
|
||||
if rofi_theme:
|
||||
ctx.obj.rofi_theme = rofi_theme
|
||||
Rofi.rofi_theme = rofi_theme
|
||||
|
||||
if rofi_theme_input:
|
||||
ctx.obj.rofi_theme_input = rofi_theme_input
|
||||
Rofi.rofi_theme_input = rofi_theme_input
|
||||
|
||||
if rofi_theme_confirm:
|
||||
ctx.obj.rofi_theme_confirm = rofi_theme_confirm
|
||||
Rofi.rofi_theme_confirm = rofi_theme_confirm
|
||||
|
||||
@@ -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 or not anime_list[1]:
|
||||
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] or not anime_list[1]:
|
||||
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()
|
||||
124
fastanime/cli/commands/anilist/notifier.py
Normal file
124
fastanime/cli/commands/anilist/notifier.py
Normal file
@@ -0,0 +1,124 @@
|
||||
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,
|
||||
ICON_PATH,
|
||||
NOTIFICATION_BELL,
|
||||
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
|
||||
app_icon = ""
|
||||
|
||||
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]
|
||||
if not data:
|
||||
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
|
||||
|
||||
# pyright:ignore
|
||||
notifications = data["data"]["Page"]["notifications"]
|
||||
if not notifications:
|
||||
logger.info("Nothing to notify")
|
||||
else:
|
||||
for notification_ in notifications:
|
||||
anime_episode = notification_["episode"]
|
||||
anime_title = notification_["media"]["title"][
|
||||
config.preferred_language
|
||||
]
|
||||
title = f"{anime_title} Episode {anime_episode} just aired"
|
||||
# pyright:ignore
|
||||
message = "Be 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)
|
||||
app_icon = anime_image
|
||||
else:
|
||||
app_icon = ICON_PATH
|
||||
|
||||
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=app_icon,
|
||||
hints={
|
||||
"image-path": app_icon,
|
||||
"sound-file": NOTIFICATION_BELL,
|
||||
},
|
||||
timeout=notification_duration,
|
||||
)
|
||||
# os.system(f"play {NOTIFICATION_BELL}")
|
||||
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] or not anime_list[1]:
|
||||
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] or not anime_list[1]:
|
||||
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] or not anime_list[1]:
|
||||
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] or not anime_list[1]:
|
||||
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)
|
||||
@@ -5,12 +5,16 @@ from rich import print
|
||||
|
||||
from ..AnimeProvider import AnimeProvider
|
||||
from ..constants import USER_CONFIG_PATH, USER_VIDEOS_DIR
|
||||
from ..libs.rofi import Rofi
|
||||
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 +35,14 @@ 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",
|
||||
"use_rofi": "false",
|
||||
"rofi_theme": "",
|
||||
"rofi_theme_input": "",
|
||||
"rofi_theme_confirm": "",
|
||||
}
|
||||
)
|
||||
self.configparser.add_section("stream")
|
||||
@@ -39,12 +51,16 @@ class Config(object):
|
||||
if not os.path.exists(USER_CONFIG_PATH):
|
||||
with open(USER_CONFIG_PATH, "w") as config:
|
||||
self.configparser.write(config)
|
||||
|
||||
self.configparser.read(USER_CONFIG_PATH)
|
||||
|
||||
# --- set defaults ---
|
||||
self.downloads_dir = self.get_downloads_dir()
|
||||
self.provider = self.get_provider()
|
||||
self.use_fzf = self.get_use_fzf()
|
||||
self.use_rofi = self.get_use_rofi()
|
||||
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,18 +68,40 @@ 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()
|
||||
|
||||
self.rofi_theme = self.get_rofi_theme()
|
||||
Rofi.rofi_theme = self.rofi_theme
|
||||
self.rofi_theme_input = self.get_rofi_theme_input()
|
||||
Rofi.rofi_theme_input = self.rofi_theme_input
|
||||
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
|
||||
Rofi.rofi_theme_confirm = self.rofi_theme_confirm
|
||||
# ---- 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):
|
||||
@@ -82,12 +120,30 @@ class Config(object):
|
||||
def get_provider(self):
|
||||
return self.configparser.get("general", "provider")
|
||||
|
||||
def get_rofi_theme(self):
|
||||
return self.configparser.get("general", "rofi_theme")
|
||||
|
||||
def get_rofi_theme_input(self):
|
||||
return self.configparser.get("general", "rofi_theme_input")
|
||||
|
||||
def get_rofi_theme_confirm(self):
|
||||
return self.configparser.get("general", "rofi_theme_confirm")
|
||||
|
||||
def get_downloads_dir(self):
|
||||
return self.configparser.get("general", "downloads_dir")
|
||||
|
||||
def get_use_fzf(self):
|
||||
return self.configparser.getboolean("general", "use_fzf")
|
||||
|
||||
def get_use_rofi(self):
|
||||
return self.configparser.getboolean("general", "use_rofi")
|
||||
|
||||
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 +168,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,22 +2,40 @@ 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
|
||||
from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from ...libs.anime_provider.types import Anime, SearchResult, Server
|
||||
from ...libs.fzf import fzf
|
||||
from ...libs.rofi import Rofi
|
||||
from ...Utility.data import anime_normalizer
|
||||
from ...Utility.utils import anime_title_percentage_match, sanitize_filename
|
||||
from ..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,16 +64,77 @@ 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():
|
||||
# ensures you dont accidentally erase your progress for an in complete episode
|
||||
stop_time = config.watch_history.get(str(anime_id), {}).get("start_time", "0")
|
||||
|
||||
total_time = config.watch_history.get(str(anime_id), {}).get("total_time", "0")
|
||||
|
||||
error = config.error * 60
|
||||
if stop_time == "0" or total_time == "0":
|
||||
dt = 0
|
||||
else:
|
||||
delta = calculate_time_delta(stop_time, total_time)
|
||||
dt = delta.total_seconds()
|
||||
if dt > error:
|
||||
if config.auto_next:
|
||||
if config.use_rofi:
|
||||
if not Rofi.confirm(
|
||||
"Are you sure you wish to continue to the next episode you haven't completed the current episode?"
|
||||
):
|
||||
anilist_options(config, anilist_config)
|
||||
return
|
||||
else:
|
||||
if not Confirm.ask(
|
||||
"Are you sure you wish to continue to the next episode you haven't completed the current episode?",
|
||||
default=False,
|
||||
):
|
||||
anilist_options(config, anilist_config)
|
||||
return
|
||||
elif not config.use_rofi:
|
||||
if not Confirm.ask(
|
||||
"Are you sure you wish to continue to the next episode, your progress for the current episodes will be erased?",
|
||||
default=True,
|
||||
):
|
||||
player_controls(config, anilist_config)
|
||||
return
|
||||
|
||||
# all checks have passed lets go to the next episode
|
||||
next_episode = episodes.index(current_episode) + 1
|
||||
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
|
||||
@@ -93,6 +172,8 @@ def player_controls(config: Config, anilist_config: QueryDict):
|
||||
quality = fzf.run(
|
||||
options, prompt="Select Quality:", header="Quality Options"
|
||||
)
|
||||
elif config.use_rofi:
|
||||
quality = Rofi.run(options, "Select Quality")
|
||||
else:
|
||||
quality = fuzzy_inquirer("Select Quality", options)
|
||||
config.quality = options.index(quality) # set quality
|
||||
@@ -105,6 +186,8 @@ def player_controls(config: Config, anilist_config: QueryDict):
|
||||
translation_type = fzf.run(
|
||||
options, prompt="Select Translation Type: ", header="Lang Options"
|
||||
).lower()
|
||||
elif config.use_rofi:
|
||||
translation_type = Rofi.run(options, "Select Translation Type")
|
||||
else:
|
||||
translation_type = fuzzy_inquirer(
|
||||
"Select Translation Type", options
|
||||
@@ -116,27 +199,35 @@ 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:
|
||||
print("Auto selecting next episode")
|
||||
_next_episode()
|
||||
return
|
||||
if config.use_fzf:
|
||||
action = fzf.run(
|
||||
list(options.keys()), prompt="Select Action:", header="Player Controls"
|
||||
)
|
||||
elif config.use_rofi:
|
||||
action = Rofi.run(list(options.keys()), "Select Action")
|
||||
else:
|
||||
action = fuzzy_inquirer("Select Action", options.keys())
|
||||
options[action]()
|
||||
@@ -161,8 +252,12 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
|
||||
anime, episode_number, translation_type
|
||||
)
|
||||
if not episode_streams:
|
||||
print("Failed to fetch :cry:")
|
||||
input("Enter to retry...")
|
||||
if not config.use_rofi:
|
||||
print("Failed to fetch :cry:")
|
||||
input("Enter to retry...")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
return fetch_streams(config, anilist_config)
|
||||
|
||||
episode_streams = {
|
||||
@@ -184,6 +279,8 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
|
||||
prompt="Select Server: ",
|
||||
header="Servers",
|
||||
)
|
||||
elif config.use_rofi:
|
||||
server = Rofi.run(choices, "Select Server")
|
||||
else:
|
||||
server = fuzzy_inquirer("Select Server", choices)
|
||||
if server == "Back":
|
||||
@@ -214,11 +311,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 +376,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"]
|
||||
@@ -251,13 +390,16 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
|
||||
prompt="Select Episode:",
|
||||
header=anime_title,
|
||||
)
|
||||
elif config.use_rofi:
|
||||
episode_number = Rofi.run(choices, "Select Episode")
|
||||
else:
|
||||
episode_number = fuzzy_inquirer("Select Episode", choices)
|
||||
|
||||
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
|
||||
@@ -275,11 +417,14 @@ def fetch_anime_episode(config, anilist_config: QueryDict):
|
||||
progress.add_task("Fetching Anime Info...", total=None)
|
||||
anilist_config.anime = anime_provider.get_anime(selected_anime["id"])
|
||||
if not anilist_config.anime:
|
||||
|
||||
print(
|
||||
"Sth went wrong :cry: this could mean the provider is down or your internet"
|
||||
)
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
fetch_anime_episode(config, anilist_config)
|
||||
return
|
||||
|
||||
@@ -306,7 +451,11 @@ def provide_anime(config: Config, anilist_config: QueryDict):
|
||||
print(
|
||||
"Sth went wrong :cry: while fetching this could mean you have poor internet connection or the provider is down"
|
||||
)
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
provide_anime(config, anilist_config)
|
||||
return
|
||||
|
||||
@@ -337,6 +486,8 @@ def provide_anime(config: Config, anilist_config: QueryDict):
|
||||
header="Anime Search Results",
|
||||
)
|
||||
|
||||
elif config.use_rofi:
|
||||
anime_title = Rofi.run(choices, "Select Search Result")
|
||||
else:
|
||||
anime_title = fuzzy_inquirer("Select Search Result", choices)
|
||||
if anime_title == "Back":
|
||||
@@ -355,19 +506,96 @@ 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:")
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
print("no trailer available :confused:")
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("No trailler found!!Enter to continue"):
|
||||
exit(0)
|
||||
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",
|
||||
)
|
||||
elif config.use_rofi:
|
||||
anime_list = Rofi.run(
|
||||
list(anime_lists.keys()), "Choose list you want to add to"
|
||||
)
|
||||
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:"
|
||||
)
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _score_anime(config: Config, anilist_config: QueryDict):
|
||||
if config.use_rofi:
|
||||
score = Rofi.ask("Enter Score", is_int=True)
|
||||
score = max(100, min(0, score))
|
||||
else:
|
||||
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}")
|
||||
if not config.use_rofi:
|
||||
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 or not data:
|
||||
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:")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _change_translation_type(config: Config, anilist_config: QueryDict):
|
||||
@@ -377,6 +605,8 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
translation_type = fzf.run(
|
||||
options, prompt="Select Translation Type:", header="Language Options"
|
||||
)
|
||||
elif config.use_rofi:
|
||||
translation_type = Rofi.run(options, "Select Translation Type")
|
||||
else:
|
||||
translation_type = fuzzy_inquirer("Select translation type", options)
|
||||
|
||||
@@ -438,20 +668,34 @@ def anilist_options(config, anilist_config: QueryDict):
|
||||
anilist_options(config, anilist_config)
|
||||
return
|
||||
|
||||
def _toggle_auto_select(config, anilist_config):
|
||||
config.auto_select = not config.auto_select
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
def _toggle_auto_next(config, anilist_config):
|
||||
config.auto_select = not config.auto_select
|
||||
anilist_options(config, anilist_config)
|
||||
|
||||
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 ''}Toggle auto select anime": _toggle_auto_select, # problematic if you choose an anime that doesnt match id
|
||||
f"{'💠 ' if icons else ''}Toggle auto next episode": _toggle_auto_next,
|
||||
f"{'🔙 ' if icons else ''}Back": select_anime,
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
if config.use_fzf:
|
||||
action = fzf.run(
|
||||
list(options.keys()), prompt="Select Action:", header="Anime Menu"
|
||||
)
|
||||
elif config.use_rofi:
|
||||
action = Rofi.run(list(options.keys()), "Select Action")
|
||||
else:
|
||||
action = fuzzy_inquirer("Select Action", options.keys())
|
||||
options[action](config, anilist_config)
|
||||
@@ -484,6 +728,26 @@ def select_anime(config: Config, anilist_config: QueryDict):
|
||||
prompt="Select Anime: ",
|
||||
header="Search Results",
|
||||
)
|
||||
elif config.use_rofi:
|
||||
# TODO: Make this faster
|
||||
if config.preview:
|
||||
from .utils import IMAGES_DIR, get_icons
|
||||
|
||||
get_icons(search_results, config)
|
||||
choices = []
|
||||
for anime in search_results:
|
||||
title = sanitize_filename(
|
||||
str(
|
||||
anime["title"][config.preferred_language]
|
||||
or anime["title"]["romaji"]
|
||||
)
|
||||
)
|
||||
icon_path = os.path.join(IMAGES_DIR, title)
|
||||
choices.append(f"{title}\0icon\x1f{icon_path}")
|
||||
choices.append("Back")
|
||||
selected_anime_title = Rofi.run_with_icons(choices, "Select Anime")
|
||||
else:
|
||||
selected_anime_title = Rofi.run(choices, "Select Anime")
|
||||
else:
|
||||
selected_anime_title = fuzzy_inquirer("Select Anime", choices)
|
||||
# "bat %s/{}" % SEARCH_RESULTS_CACHE
|
||||
@@ -501,9 +765,65 @@ 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:
|
||||
if not config.use_rofi:
|
||||
print("You haven't logged in please run: fastanime anilist login")
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("You haven't logged in!!Enter to continue"):
|
||||
exit(1)
|
||||
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)
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
anilist(config, anilist_config)
|
||||
return
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
print("Sth went wrong", anime_list)
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
|
||||
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[/]")
|
||||
if config.use_rofi:
|
||||
search_term = str(Rofi.ask("Search for"))
|
||||
else:
|
||||
search_term = Prompt.ask("[cyan]Search for[/]")
|
||||
|
||||
return AniList.search(query=search_term)
|
||||
|
||||
@@ -525,31 +845,56 @@ def anilist(config: Config, anilist_config: QueryDict):
|
||||
import subprocess
|
||||
|
||||
subprocess.run([os.environ.get("EDITOR", "open"), USER_CONFIG_PATH])
|
||||
config.load_config()
|
||||
if config.use_rofi:
|
||||
config.load_config()
|
||||
config.use_rofi = True
|
||||
config.use_fzf = False
|
||||
else:
|
||||
config.load_config()
|
||||
|
||||
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:
|
||||
|
||||
action = fzf.run(
|
||||
list(options.keys()),
|
||||
prompt="Select Action: ",
|
||||
header="Anilist Menu",
|
||||
)
|
||||
elif config.use_rofi:
|
||||
action = Rofi.run(list(options.keys()), "Select Action")
|
||||
else:
|
||||
action = fuzzy_inquirer("Select Action", options.keys())
|
||||
anilist_data = options[action]()
|
||||
@@ -559,5 +904,9 @@ def anilist(config: Config, anilist_config: QueryDict):
|
||||
|
||||
else:
|
||||
print(anilist_data[1])
|
||||
input("Enter to continue...")
|
||||
if not config.use_rofi:
|
||||
input("Enter to continue...")
|
||||
else:
|
||||
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
|
||||
exit(1)
|
||||
anilist(config, anilist_config)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import concurrent.futures
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
from threading import Thread
|
||||
|
||||
@@ -10,8 +13,11 @@ from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
|
||||
from ...Utility import anilist_data_helper
|
||||
from ...Utility.utils import remove_html_tags, sanitize_filename
|
||||
from ..config import Config
|
||||
from ..utils.utils import get_true_fg
|
||||
|
||||
fzf_preview = """
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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.
|
||||
@@ -89,81 +95,147 @@ fzf-preview(){
|
||||
"""
|
||||
|
||||
|
||||
SEARCH_RESULTS_CACHE = os.path.join(APP_CACHE_DIR, "search_results")
|
||||
# ---- aniskip intergration ----
|
||||
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(" ")
|
||||
|
||||
|
||||
# ---- prevew stuff ----
|
||||
# import tempfile
|
||||
|
||||
WORKING_DIR = APP_CACHE_DIR # tempfile.gettempdir()
|
||||
IMAGES_DIR = os.path.join(WORKING_DIR, "images")
|
||||
if not os.path.exists(IMAGES_DIR):
|
||||
os.mkdir(IMAGES_DIR)
|
||||
INFO_DIR = os.path.join(WORKING_DIR, "info")
|
||||
if not os.path.exists(INFO_DIR):
|
||||
os.mkdir(INFO_DIR)
|
||||
|
||||
|
||||
def save_image_from_url(url: str, file_name: str):
|
||||
image = requests.get(url)
|
||||
with open(f"{IMAGES_DIR}/{file_name}", "wb") as f:
|
||||
f.write(image.content)
|
||||
|
||||
|
||||
def save_info_from_str(info: str, file_name: str):
|
||||
with open(f"{INFO_DIR}/{file_name}", "w") as f:
|
||||
f.write(info)
|
||||
|
||||
|
||||
def write_search_results(
|
||||
search_results: list[AnilistBaseMediaDataSchema], config: Config
|
||||
search_results: list[AnilistBaseMediaDataSchema], config: Config, workers=None
|
||||
):
|
||||
for anime in search_results:
|
||||
if not os.path.exists(SEARCH_RESULTS_CACHE):
|
||||
os.mkdir(SEARCH_RESULTS_CACHE)
|
||||
anime_title = (
|
||||
anime["title"][config.preferred_language] or anime["title"]["romaji"]
|
||||
)
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
ANIME_CACHE = os.path.join(SEARCH_RESULTS_CACHE, anime_title)
|
||||
if not os.path.exists(ANIME_CACHE):
|
||||
os.mkdir(ANIME_CACHE)
|
||||
with open(
|
||||
f"{ANIME_CACHE}/image",
|
||||
"wb",
|
||||
) as f:
|
||||
try:
|
||||
image = requests.get(anime["coverImage"]["large"], timeout=5)
|
||||
f.write(image.content)
|
||||
except Exception:
|
||||
pass
|
||||
H_COLOR = 215, 0, 95
|
||||
S_COLOR = 208, 208, 208
|
||||
S_WIDTH = 45
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
future_to_task = {}
|
||||
for anime in search_results:
|
||||
anime_title = (
|
||||
anime["title"][config.preferred_language] or anime["title"]["romaji"]
|
||||
)
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_task[
|
||||
executor.submit(save_image_from_url, image_url, anime_title)
|
||||
] = image_url
|
||||
|
||||
with open(f"{ANIME_CACHE}/data", "w") as f:
|
||||
# data = json.dumps(anime, sort_keys=True, indent=2, separators=(',', ': '))
|
||||
# handle the text data
|
||||
template = f"""
|
||||
{"-"*40}
|
||||
Anime Title(jp): {anime['title']['romaji']}
|
||||
Anime Title(eng): {anime['title']['english']}
|
||||
{"-"*40}
|
||||
Popularity: {anime['popularity']}
|
||||
Favourites: {anime['favourites']}
|
||||
Status: {anime['status']}
|
||||
Episodes: {anime['episodes']}
|
||||
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'])}
|
||||
{"-"*40}
|
||||
Description:
|
||||
{get_true_fg("-"*S_WIDTH,*S_COLOR,bold=False)}
|
||||
{get_true_fg('Title(jp):',*H_COLOR)} {anime['title']['romaji']}
|
||||
{get_true_fg('Title(eng):',*H_COLOR)} {anime['title']['english']}
|
||||
{get_true_fg('Popularity:',*H_COLOR)} {anime['popularity']}
|
||||
{get_true_fg('Favourites:',*H_COLOR)} {anime['favourites']}
|
||||
{get_true_fg('Status:',*H_COLOR)} {anime['status']}
|
||||
{get_true_fg('Episodes:',*H_COLOR)} {anime['episodes']}
|
||||
{get_true_fg('Genres:',*H_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime['genres'])}
|
||||
{get_true_fg('Next Episode:',*H_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])}
|
||||
{get_true_fg('Start Date:',*H_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
|
||||
{get_true_fg('End Date:',*H_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
|
||||
{get_true_fg("-"*S_WIDTH,*S_COLOR,bold=False)}
|
||||
{get_true_fg('Description:',*H_COLOR)}
|
||||
"""
|
||||
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)
|
||||
future_to_task[
|
||||
executor.submit(save_info_from_str, template, anime_title)
|
||||
] = anime_title
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_task):
|
||||
task = future_to_task[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as exc:
|
||||
logger.error("%r generated an exception: %s" % (task, exc))
|
||||
|
||||
|
||||
def get_preview(search_results: list[AnilistBaseMediaDataSchema], config: Config):
|
||||
# get rofi icons
|
||||
def get_icons(search_results: list[AnilistBaseMediaDataSchema], config, workers=None):
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
for anime in search_results:
|
||||
anime_title = (
|
||||
anime["title"][config.preferred_language] or anime["title"]["romaji"]
|
||||
)
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_url[
|
||||
executor.submit(save_image_from_url, image_url, anime_title)
|
||||
] = image_url
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as exc:
|
||||
logger.error("%r generated an exception: %s" % (url, exc))
|
||||
|
||||
|
||||
def get_preview(
|
||||
search_results: list[AnilistBaseMediaDataSchema], config: Config, wait=False
|
||||
):
|
||||
# ensure images and info exists
|
||||
background_worker = Thread(
|
||||
target=write_search_results, args=(search_results, config)
|
||||
)
|
||||
background_worker.daemon = True
|
||||
background_worker.start()
|
||||
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
os.environ["SHELL"] = shutil.which("bash") or "sh"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{}/image ]; then fzf-preview %s/{}/image
|
||||
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||
else echo Loading...
|
||||
fi
|
||||
if [ -s %s/{}/data ]; then cat %s/{}/data
|
||||
if [ -s %s/{} ]; then cat %s/{}
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
SEARCH_RESULTS_CACHE,
|
||||
SEARCH_RESULTS_CACHE,
|
||||
SEARCH_RESULTS_CACHE,
|
||||
SEARCH_RESULTS_CACHE,
|
||||
IMAGES_DIR,
|
||||
IMAGES_DIR,
|
||||
INFO_DIR,
|
||||
INFO_DIR,
|
||||
)
|
||||
# preview.replace("\n", ";")
|
||||
if wait:
|
||||
background_worker.join()
|
||||
return preview
|
||||
|
||||
@@ -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(MPV, 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", "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", "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(MPV, link, mpv_args, custom_args)
|
||||
return stop_time, total_time
|
||||
|
||||
|
||||
# Example usage
|
||||
|
||||
@@ -14,13 +14,36 @@ class QueryDict(dict):
|
||||
|
||||
|
||||
def exit_app(*args):
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from rich import print
|
||||
from ...constants import APP_NAME, ICON_PATH, USER_NAME
|
||||
|
||||
from ...constants import USER_NAME
|
||||
def is_running_in_terminal():
|
||||
try:
|
||||
shutil.get_terminal_size()
|
||||
return (
|
||||
sys.stdin.isatty()
|
||||
and sys.stdout.isatty()
|
||||
and os.getenv("TERM") is not None
|
||||
)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
print("Have a good day :smile:", USER_NAME)
|
||||
if not is_running_in_terminal():
|
||||
from plyer import notification
|
||||
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message=f"Have a good day {USER_NAME}",
|
||||
title="Shutting down",
|
||||
) # pyright:ignore
|
||||
else:
|
||||
from rich import print
|
||||
|
||||
print("Have a good day :smile:", USER_NAME)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,29 @@ from ...Utility.data import anime_normalizer
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Define ANSI escape codes as constants
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
INVISIBLE_CURSOR = "\033[?25l"
|
||||
VISIBLE_CURSOR = "\033[?25h"
|
||||
UNDERLINE = "\033[4m"
|
||||
|
||||
# ESC[38;2;{r};{g};{b}m
|
||||
BG_GREEN = "\033[48;2;120;233;12;m"
|
||||
GREEN = "\033[38;2;45;24;45;m"
|
||||
|
||||
|
||||
def get_true_fg(string: str, r: int, g: int, b: int, bold=True) -> str:
|
||||
if bold:
|
||||
return f"{BOLD}\033[38;2;{r};{g};{b};m{string}{RESET}"
|
||||
else:
|
||||
return f"\033[38;2;{r};{g};{b};m{string}{RESET}"
|
||||
|
||||
|
||||
def get_true_bg(string, r: int, g: int, b: int) -> str:
|
||||
return f"\033[48;2;{r};{g};{b};m{string}{RESET}"
|
||||
|
||||
|
||||
def clear():
|
||||
if PLATFORM == "Windows":
|
||||
os.system("cls")
|
||||
|
||||
@@ -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,15 @@ 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")
|
||||
|
||||
# --- notification bell ---
|
||||
NOTIFICATION_BELL = os.path.join(ASSETS_DIR, "tut_turu.mp3")
|
||||
|
||||
# --- icon stuff ---
|
||||
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 +30,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
|
||||
@@ -29,4 +39,4 @@ APP_CACHE_DIR = dirs.user_cache_dir
|
||||
USER_VIDEOS_DIR = os.path.join(dirs.user_videos_dir, APP_NAME)
|
||||
|
||||
|
||||
USER_NAME = os.environ.get("USERNAME", f"{APP_NAME} user")
|
||||
USER_NAME = os.environ.get("USERNAME", "Anime fun")
|
||||
|
||||
@@ -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
|
||||
@@ -161,3 +165,50 @@ class AnilistPages(TypedDict):
|
||||
class AnilistDataSchema(TypedDict):
|
||||
data: AnilistPages
|
||||
Error: str
|
||||
|
||||
|
||||
class AnilistNotification(TypedDict):
|
||||
id: int
|
||||
type: str
|
||||
episode: int
|
||||
context: str
|
||||
createdAt: str
|
||||
media: AnilistBaseMediaDataSchema
|
||||
|
||||
|
||||
class AnilistNotificationPage(TypedDict):
|
||||
pageInfo: AnilistPageInfo
|
||||
notifications: list[AnilistNotification]
|
||||
|
||||
|
||||
class AnilistNotificationPages(TypedDict):
|
||||
Page: AnilistNotificationPage
|
||||
|
||||
|
||||
class AnilistNotifications(TypedDict):
|
||||
data: AnilistNotificationPages
|
||||
|
||||
|
||||
class AnilistMediaList(TypedDict):
|
||||
media: AnilistBaseMediaDataSchema
|
||||
status: str
|
||||
progress: int
|
||||
score: int
|
||||
repeat: int
|
||||
notes: str
|
||||
startDate: AnilistDateObject
|
||||
completedAt: AnilistDateObject
|
||||
createdAt: str
|
||||
|
||||
|
||||
class AnilistMediaListPage(TypedDict):
|
||||
pageInfo: AnilistPageInfo
|
||||
mediaList: list[AnilistMediaList]
|
||||
|
||||
|
||||
class AnilistMediaListPages(TypedDict):
|
||||
Page: AnilistMediaListPage
|
||||
|
||||
|
||||
class AnilistMediaLists(TypedDict):
|
||||
data: AnilistMediaListPages
|
||||
|
||||
@@ -2,25 +2,42 @@
|
||||
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,
|
||||
AnilistMediaLists,
|
||||
AnilistNotifications,
|
||||
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 +45,125 @@ 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,
|
||||
) -> tuple[bool, AnilistNotifications] | tuple[bool, None]:
|
||||
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"
|
||||
],
|
||||
) -> tuple[bool, AnilistMediaLists] | tuple[bool, None]:
|
||||
variables = {"status": status, "userId": self.user_id}
|
||||
return self._make_authenticated_request(media_list_query, variables)
|
||||
|
||||
def get_medialist_entry(
|
||||
self, mediaId: int
|
||||
) -> tuple[bool, dict] | tuple[bool, None]:
|
||||
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)
|
||||
data = result[1]
|
||||
if not result[0] or not data:
|
||||
return result
|
||||
id = data["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)) < 30
|
||||
and not response.status_code == 500
|
||||
):
|
||||
print(
|
||||
"Warning you are exceeding the allowed number of calls per minute"
|
||||
)
|
||||
logger.warning(
|
||||
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
|
||||
)
|
||||
print("Forced timeout will now be initiated")
|
||||
import time
|
||||
|
||||
print("sleeping...")
|
||||
time.sleep(1 * 60)
|
||||
if response.status_code == 200:
|
||||
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, None)
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.warning(
|
||||
"ConnectionError this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
return (False, None)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
return (False, None) # 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)) < 30
|
||||
and not response.status_code == 500
|
||||
):
|
||||
print(
|
||||
"Warning you are exceeding the allowed number of calls per minute"
|
||||
)
|
||||
logger.warning(
|
||||
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
|
||||
)
|
||||
print("Forced timeout will now be initiated")
|
||||
import time
|
||||
|
||||
print("sleeping...")
|
||||
time.sleep(1 * 60)
|
||||
if response.status_code == 200:
|
||||
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(perPage:5){
|
||||
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)
|
||||
138
fastanime/libs/rofi/__init__.py
Normal file
138
fastanime/libs/rofi/__init__.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import subprocess
|
||||
from shutil import which
|
||||
from sys import exit
|
||||
|
||||
from plyer import notification
|
||||
|
||||
from fastanime import APP_NAME
|
||||
|
||||
from ...constants import ICON_PATH
|
||||
|
||||
|
||||
class RofiApi:
|
||||
ROFI_EXECUTABLE = which("rofi")
|
||||
|
||||
rofi_theme = ""
|
||||
rofi_theme_confirm = ""
|
||||
rofi_theme_input = ""
|
||||
|
||||
def run_with_icons(self, options: list[str], prompt_text: str) -> str:
|
||||
rofi_input = "\n".join(options)
|
||||
|
||||
if not self.ROFI_EXECUTABLE:
|
||||
raise Exception("Rofi not found")
|
||||
|
||||
args = [self.ROFI_EXECUTABLE]
|
||||
if self.rofi_theme:
|
||||
args.extend(["-no-config", "-theme", self.rofi_theme])
|
||||
args.extend(["-p", prompt_text, "-i", "-show-icons", "-dmenu"])
|
||||
result = subprocess.run(
|
||||
args,
|
||||
input=rofi_input,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
choice = result.stdout.strip()
|
||||
if not choice:
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message="FastAnime is shutting down",
|
||||
title="No Valid Input Provided",
|
||||
) # pyright:ignore
|
||||
exit(1)
|
||||
|
||||
return choice
|
||||
|
||||
def run(self, options: list[str], prompt_text: str) -> str:
|
||||
rofi_input = "\n".join(options)
|
||||
|
||||
if not self.ROFI_EXECUTABLE:
|
||||
raise Exception("Rofi not found")
|
||||
|
||||
args = [self.ROFI_EXECUTABLE]
|
||||
if self.rofi_theme:
|
||||
args.extend(["-no-config", "-theme", self.rofi_theme])
|
||||
args.extend(["-p", prompt_text, "-i", "-dmenu"])
|
||||
result = subprocess.run(
|
||||
args,
|
||||
input=rofi_input,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
choice = result.stdout.strip()
|
||||
if not choice or choice not in options:
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message="FastAnime is shutting down",
|
||||
title="No Valid Input Provided",
|
||||
) # pyright:ignore
|
||||
exit(1)
|
||||
|
||||
return choice
|
||||
|
||||
def confirm(self, prompt_text: str) -> bool:
|
||||
rofi_choices = "Yes\nNo"
|
||||
if not self.ROFI_EXECUTABLE:
|
||||
raise Exception("Rofi not found")
|
||||
args = [self.ROFI_EXECUTABLE]
|
||||
if self.rofi_theme_confirm:
|
||||
args.extend(["-no-config", "-theme", self.rofi_theme_confirm])
|
||||
args.extend(["-p", prompt_text, "-i", "", "-no-fixed-num-lines", "-dmenu"])
|
||||
result = subprocess.run(
|
||||
args,
|
||||
input=rofi_choices,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
choice = result.stdout.strip()
|
||||
if not choice:
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message="FastAnime is shutting down",
|
||||
title="No Valid Input Provided",
|
||||
) # pyright:ignore
|
||||
exit(1)
|
||||
if choice == "Yes":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def ask(
|
||||
self, prompt_text: str, is_int: bool = False, is_float: bool = False
|
||||
) -> str | float | int:
|
||||
if not self.ROFI_EXECUTABLE:
|
||||
raise Exception("Rofi not found")
|
||||
args = [self.ROFI_EXECUTABLE]
|
||||
if self.rofi_theme_input:
|
||||
args.extend(["-no-config", "-theme", self.rofi_theme_input])
|
||||
args.extend(["-p", prompt_text, "-i", "-no-fixed-num-lines", "-dmenu"])
|
||||
result = subprocess.run(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
user_input = result.stdout.strip()
|
||||
if not user_input:
|
||||
notification.notify(
|
||||
app_name=APP_NAME,
|
||||
app_icon=ICON_PATH,
|
||||
message="FastAnime is shutting down",
|
||||
title="No Valid Input Provided",
|
||||
) # pyright:ignore
|
||||
exit(1)
|
||||
if is_float:
|
||||
user_input = float(user_input)
|
||||
elif is_int:
|
||||
user_input = int(user_input)
|
||||
|
||||
return user_input
|
||||
|
||||
|
||||
Rofi = RofiApi()
|
||||
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"
|
||||
description = "A fast and efficient anime scrapper and exploration tool"
|
||||
authors = ["Benex254 <benedictx855@gmail.com>"]
|
||||
version = "0.50.0"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -18,6 +18,7 @@ python-dotenv = "^1.0.1"
|
||||
thefuzz = "^0.22.1"
|
||||
requests = "^2.32.3"
|
||||
|
||||
plyer = "^2.1.0"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.4.2"
|
||||
isort = "^5.13.2"
|
||||
@@ -26,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