Compare commits

..

51 Commits

Author SHA1 Message Date
Benex254
9d62915f2b chore: bump version 2024-07-30 17:14:19 +03:00
Benex254
a4e9e5f29e chore: bump version 2024-07-30 17:12:27 +03:00
Benex254
d00c958ff2 docs: update readme 2024-07-30 17:12:11 +03:00
Benex254
bc2ac69b9a fix(interface): escape sequence warning 2024-07-30 17:01:59 +03:00
Benex254
01fa96c27a feat: update fa script 2024-07-30 16:53:36 +03:00
Benex254
6c1bbfe50a feat: add aniskip intergration and scoring of anime 2024-07-30 16:52:33 +03:00
Benex254
ecc4e85079 feat(anilist): ensure rate limit is not exceeded 2024-07-30 16:34:18 +03:00
Benex254
1cd743acdf feat(anilist): include mal ids in queries 2024-07-30 16:27:43 +03:00
Benex254
23dd969d37 feat?: create custom aniskip functionality 2024-07-30 16:26:43 +03:00
Benex254
d21f6b5ab0 feat: ensure correct python version 2024-07-30 16:25:58 +03:00
Benex254
640bb12c44 feat: remove unused functions 2024-07-30 16:25:28 +03:00
Benex254
453e4c1b74 feat(notifier): improve error handling 2024-07-30 16:24:34 +03:00
Benex254
4dc3d1b0bb feat: rename watchlist to watching and repeating to rewatching 2024-07-30 16:23:52 +03:00
Benex254
4df57f9410 feat: remove unused print statement 2024-07-30 16:22:53 +03:00
Benex254
baa94efc24 docs: update readme 2024-07-30 10:33:06 +03:00
Benex254
f5d18512f8 feat(cli): add top as an option for servers 2024-07-30 10:32:53 +03:00
Benex254
72037eea07 feat: show anime cover image for notifications on none windows systems 2024-07-30 09:37:17 +03:00
Benex254
f5c120ebb8 feat: handle none logged in user 2024-07-30 09:36:23 +03:00
Benex254
5f2b88bd9b feat(anilist_api): handle none 200 status code 2024-07-30 09:35:28 +03:00
Benex254
b346801dba feat(allanime): handle none 200 status code 2024-07-30 09:34:45 +03:00
benex
1b1a05e2b3 feat(notifier): add icon 2024-07-29 13:21:32 +03:00
benex
8716fb2e1d fix: use platform.system to correctly detect the os 2024-07-29 12:38:37 +03:00
benex
12a38d6d48 feat(anilist): make icons optional 2024-07-29 12:29:27 +03:00
Benex254
e6aa508644 chore: update pyproject.toml 2024-07-29 13:42:45 +03:00
Benex254
584a2ee3f1 feat(allanime): add server 2024-07-29 13:42:45 +03:00
Benex254
385dd4337d feat(anilist): add notifier command 2024-07-29 13:42:45 +03:00
Benex254
1c70a2122d feat(anilist): add notifier command 2024-07-29 13:42:45 +03:00
BenedictX
46b9b844d4 Update README.md 2024-07-29 13:11:25 +03:00
Benex254
272042ec35 fix(anilist_interface): trailer not loading 2024-07-28 16:43:38 +03:00
Benex254
56632cf77c feat(tui): improve the ui 2024-07-28 15:32:31 +03:00
Benex254
e8dacf0722 feat(anilist): only update episode progress in their is actual progress 2024-07-28 11:24:31 +03:00
Benex254
b95d49429c feat(anilist): add update your anilist feature 2024-07-28 10:42:32 +03:00
Benex254
ca087b2e94 feat(player): implement continue from timestamp 2024-07-28 02:23:54 +03:00
Benex254
3f33ae3738 feat(anilist): change media animelist status for anime you currently watching 2024-07-28 00:32:24 +03:00
Benex254
94a282a320 feat(anilist): implement viewing of your anilist animelist 2024-07-28 00:08:44 +03:00
Benex254
0b379ec813 feat(anilist): add account intergration 2024-07-27 22:57:40 +03:00
Benex254
6b0a013705 feat(provider): add animepahe as new provider 2024-07-27 22:54:17 +03:00
Benex254
6c1f8d09e6 chore: update package info 2024-07-26 14:13:50 +03:00
Benex254
6bb2c89a8c feat(mpv): improve streaming on mobile 2024-07-26 14:10:49 +03:00
Benex254
9f56b74ff0 feat(utils): add logging 2024-07-26 14:10:12 +03:00
Benex254
4d03b86498 chore(anime_provider): remove print statements from provider and switch to logging 2024-07-26 14:09:44 +03:00
Benex254
fab86090a3 chore: remove legacy code 2024-07-26 14:07:57 +03:00
Benex254
71d258385c chore(constants): create constants module to store useful constants 2024-07-26 14:07:37 +03:00
Benex254
bc55ed6e81 chore(updater): update updater info 2024-07-26 14:04:53 +03:00
Benex254
197bfa9f8a chore: update pyproject.toml 2024-07-26 09:27:56 +03:00
Benex254
f84c60e6bc chore: update dependencies 2024-07-26 09:24:15 +03:00
Benex254
d8b94cbbca update pyproject.toml file 2024-07-26 09:19:49 +03:00
Benex254
dd4462f42a chore: reorganize imports 2024-07-26 09:07:49 +03:00
Benex254
0f9e08b9fa chore: reorganize codebase to make anilist top level 2024-07-26 09:07:09 +03:00
Benex254
01333ab1d1 chore: clean up legacy files 2024-07-26 08:42:19 +03:00
Benex254
d8bf9e18c4 chore: clean up legacy code 2024-07-26 08:41:39 +03:00
86 changed files with 1893 additions and 1262 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 971 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

133
README.md
View File

@@ -1,11 +1,36 @@
# Fast Anime
# FastAnime
Welcome to **FastAnime**, an anime scrapper that brings a browser experience to the terminal.
Welcome to **FastAnime**, anime site experience from the terminal.
[intro.webm](https://github.com/user-attachments/assets/036af7fc-83ff-4f9b-bda6-0c913f7d0f38)
[fa_demo.webm](https://github.com/user-attachments/assets/bb46642c-176e-42b3-a533-ff55d4dac111)
Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magic-tape](https://gitlab.com/christosangel/magic-tape/-/tree/main?ref_type=heads) and [ani-cli](https://github.com/pystardust/ani-cli).
<!--toc:start-->
- [FastAnime](#fastanime)
- [Installation](#installation)
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
- [Using pipx](#using-pipx)
- [Using pip](#using-pip)
- [Installing the bleeding edge version](#installing-the-bleeding-edge-version)
- [Building from the source](#building-from-the-source)
- [External Dependencies](#external-dependencies)
- [Usage](#usage)
- [The Commandline interface :fire:](#the-commandline-interface-fire)
- [The anilist command](#the-anilist-command)
- [Running without any subcommand](#running-without-any-subcommand)
- [Subcommands](#subcommands)
- [download subcommand](#download-subcommand)
- [search subcommand](#search-subcommand)
- [downloads subcommand](#downloads-subcommand)
- [config subcommand](#config-subcommand)
- [Configuration](#configuration)
- [Contributing](#contributing)
- [Receiving Support](#receiving-support)
- [Supporting the Project](#supporting-the-project)
<!--toc:end-->
> [!IMPORTANT]
>
> This project currently scrapes allanime and is in no way related to them. The site is in the public domain and can be access by any one with a browser.
@@ -14,27 +39,6 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
>
> The docs are still being worked on and are far from completion.
## Table of Contents
- [Installation](#installation)
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
- [Using pipx](#using-pipx)
- [Using pip](#using-pip)
- [Installing the building edge version](#installing-the-bleeding-edge-version)
- [Building from the source](#building-from-the-source)
- [External Dependencies](#external-dependencies)
- [Usage](#usage)
- [The Commandline interface](#the-commandline-interface-fire)
- [The anilist command](#the-anilist-command)
- [download subcommand](#download-subcommand)
- [search subcommand](#search-subcommand)
- [downloads subcommand](#downloads-subcommand)
- [config subcommand](#config-subcommand)
- [Configuration](#configuration)
- [Contributing](#contributing)
- [Receiving Support](#receiving-support)
- [Supporting the Project](#supporting-the-project)
## Installation
The app can run wherever python can run. So all you need to have is python installed on your device.
@@ -63,7 +67,7 @@ pip install fastanime
### Installing the bleeding edge version
To install the latest build which are created on every push by Github actions, download the [fastanime_debug_build](https://github.com/Benex254/FastAnime/actions) of your choosing from the Github actions page.
To install the latest build which are created on every push by GitHub actions, download the [fastanime_debug_build](https://github.com/Benex254/FastAnime/actions) of your choosing from the GitHub actions page.
Then:
```bash
@@ -130,12 +134,13 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
> everything you could ever need with a small footprint.
> But if you have a reason feel free to encourage as to do so.
**Other dependecies that will just make your experience better:**
**Other dependencies that will just make your experience better:**
- [fzf](https://github.com/junegunn/fzf) :fire: which is used as a better alternative to the ui.
- [chafa](https://github.com/hpjansson/chafa) currently the best cross platform and cross terminal image viewer for the terminal.
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it]()!!
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it](https://github.com/chase/awrit?tab=readme-ov-file)!!
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
- [ani-skip](https://github.com/synacktraa/ani-skip) :fire: used for skipping the opening and ending theme songs
## Usage
@@ -143,7 +148,7 @@ The app offers both a graphical interface (under development) and a robust comma
> [!NOTE]
>
> The GUI is in development; use the CLI for now.
> The GUI is mostly in hiatus; use the CLI for now.
> However, you can try it out before i decided to change my objective by checking out this [release](https://github.com/Benex254/FastAnime/tree/v0.20.0).
> But be reassured for those who aren't terminal chads, i will still complete the GUI for the fun of it
@@ -153,13 +158,13 @@ Designed for power users who prefer efficiency over browser-based streaming and
Overview of main commands:
- `fastanime anilist`: Powerful command for browsing and exploring anime due to Anilist intergration.
- `fastanime anilist`: Powerful command for browsing and exploring anime due to AniList integration.
- `fastanime download`: Download anime.
- `fastanime search`: Powerful command meant for binging since it doesn't require the interfaces
- `fastanime downloads`: View downloaded anime and watch with mpv.
- `fastanime downloads`: View downloaded anime and watch with MPV.
- `fastanime config`: Quickly edit configuration settings.
Configuration is directly passed into this command at run time to overide your config.
Configuration is directly passed into this command at run time to override your config.
Available options include:
@@ -174,9 +179,11 @@ Available options include:
- `--default` use the default ui
- `--preview` show a preview when using fzf
- `--no-preview` dont show a preview when using fzf
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. works when `--server gogoanime`
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
- `--icons/--no-icons` toggle the visibility of the icons
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
#### The anilist command
#### The anilist command :fire: :fire: :fire:
Stream, browse, and discover anime efficiently from the terminal using the [AniList API](https://github.com/AniList/ApiV2-GraphQL-Docs).
@@ -196,6 +203,47 @@ The subcommands are mainly their as convenience. Since all the features already
- `fastanime anilist favourites`: Top 15 favorite anime.
- `fastanime anilist random`: get random anime
The following are commands you can only run if you are signed in to your AniList account:
- `fastanime anilist watching`
- `fastanime anilist planning`
- `fastanime anilist repeating`
- `fastanime anilist dropped`
- `fastanime anilist paused`
- `fastanime anilist completed`
Plus: `fastanime anilist notifier` :fire:
```bash
# basic form
fastanime anilist notifier
# with logging to stdout
fastanime --log anilist notifier
# with logging to a file. stored in the same place as your config
fastanime --log-file anilist notifier
```
The above commands will start a loop that checks every 2 minutes if any of the anime in your watch list that are aireing has just released a new episode.
The notification will consist of a cover image of the anime in none windows systems.
You can place the command among your machines startup scripts.
For fish users for example you can decide to put this in your `~/.config/fish/config.fish`:
```fish
if ! ps aux | grep -q '[f]astanime .* notifier'
echo initializing fastanime anilist notifier
fastanime --log-file anilist notifier>/dev/null &
end
```
> [!NOTE]
> To sign in just run `fastanime anilist login` and follow the instructions.
> To view your login status `fastanime anilist login --status`
#### download subcommand
Download anime to watch later dub or sub with this one command.
@@ -269,17 +317,23 @@ fastanime config --path
## Configuration
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on linux and mac or somewhere on windows.
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on Linux and mac or somewhere on windows; you can check by running `fastanime config --path`.
```ini
[stream]
continue_from_history = True # Auto continue from watch history
translation_type = sub # Preferred language for anime (options: dub, sub)
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top)
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
auto_next = False # Auto-select next episode
# Auto select the anime provider results with fuzzyfind.
# Auto select the anime provider results with fuzzy find.
# Note this wont always be correct.But 99% of the time will be.
auto_select=True
# whether to skip the opening and ending theme songs
# note requires ani-skip to be in path
skip=false
# the maximum delta time in minutes after which the episode should be considered as completed
# used in the continue from time stamp
error=3
# the format of downloaded anime and trailer
# based on yt-dlp format and passed directly to it
@@ -295,6 +349,13 @@ downloads_dir = <Default-videos-dir>/FastAnime # Download directory
use_fzf=False # whether to use fzf as the interface for the anilist command and others.
preview=false # whether to show a preview window when using fzf
# whether to show the icons
icons=false
# the duration in minutes a notification will stay in the screen
# used by notifier command
notification_duration=2
[anilist]
# Not implemented yet
```

View File

@@ -1,452 +0,0 @@
[app]
# (str) Title of your application
title = FastAnime
# (str) Package name
package.name = FastAnime
# (str) Package domain (needed for android/ios packaging)
package.domain = org.test
# (str) Source code where the main.py live
source.dir = ./fastanime/
# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,kv,atlas
# (list) List of inclusions using pattern matching
#source.include_patterns = assets/*,images/*.png
# (list) Source files to exclude (let empty to not exclude anything)
#source.exclude_exts = spec
# (list) List of directory to exclude (let empty to not exclude anything)
#source.exclude_dirs = tests, bin, venv
# (list) List of exclusions using pattern matching
# Do not prefix with './'
#source.exclude_patterns = license,images/*/*.jpg
# (str) Application versioning (method 1)
version = 0.30.0
# (str) Application versioning (method 2)
# version.regex = __version__ = ['"](.*)['"]
# version.filename = %(source.dir)s/main.py
# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
requirements = python3,click,rich,curl_cffi,yt-dlp,python-dotenv,art,inquirerpy,platformdirs,thefuzz
# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy
# (str) Presplash of the application
#presplash.filename = %(source.dir)s/data/presplash.png
# (str) Icon of the application
#icon.filename = %(source.dir)s/data/icon.png
# (list) Supported orientations
# Valid options are: landscape, portrait, portrait-reverse or landscape-reverse
orientation = portrait
# (list) List of service to declare
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY
#
# OSX Specific
#
#
# author = © Copyright Info
# change the major version of python used by the app
osx.python_version = 3
# Kivy version to use
#osx.kivy_version = 1.9.1
#
# Android specific
#
# (bool) Indicate if the application should be fullscreen or not
fullscreen = 0
# (string) Presplash background color (for android toolchain)
# Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
# olive, purple, silver, teal.
#android.presplash_color = #FFFFFF
# (string) Presplash animation using Lottie format.
# see https://lottiefiles.com/ for examples and https://airbnb.design/lottie/
# for general documentation.
# Lottie files can be created using various tools, like Adobe After Effect or Synfig.
#android.presplash_lottie = "path/to/lottie/file.json"
# (str) Adaptive icon of the application (used if Android API level is 26+ at runtime)
#icon.adaptive_foreground.filename = %(source.dir)s/data/icon_fg.png
#icon.adaptive_background.filename = %(source.dir)s/data/icon_bg.png
# (list) Permissions
# (See https://python-for-android.readthedocs.io/en/latest/buildoptions/#build-options-1 for all the supported syntaxes and properties)
#android.permissions = android.permission.INTERNET, (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)
# (list) features (adds uses-feature -tags to manifest)
#android.features = android.hardware.usb.host
# (int) Target Android API, should be as high as possible.
#android.api = 31
# (int) Minimum API your APK / AAB will support.
#android.minapi = 21
# (int) Android SDK version to use
#android.sdk = 20
# (str) Android NDK version to use
#android.ndk = 23b
# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi.
#android.ndk_api = 21
# (bool) Use --private data storage (True) or --dir public storage (False)
#android.private_storage = True
# (str) Android NDK directory (if empty, it will be automatically downloaded.)
#android.ndk_path =
# (str) Android SDK directory (if empty, it will be automatically downloaded.)
#android.sdk_path =
# (str) ANT directory (if empty, it will be automatically downloaded.)
#android.ant_path =
# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
# android.skip_update = False
# (bool) If True, then automatically accept SDK license
# agreements. This is intended for automation only. If set to False,
# the default, you will be shown the license when first running
# buildozer.
# android.accept_sdk_license = False
# (str) Android entry point, default is ok for Kivy-based app
#android.entrypoint = org.kivy.android.PythonActivity
# (str) Full name including package path of the Java class that implements Android Activity
# use that parameter together with android.entrypoint to set custom Java class instead of PythonActivity
#android.activity_class_name = org.kivy.android.PythonActivity
# (str) Extra xml to write directly inside the <manifest> element of AndroidManifest.xml
# use that parameter to provide a filename from where to load your custom XML code
#android.extra_manifest_xml = ./src/android/extra_manifest.xml
# (str) Extra xml to write directly inside the <manifest><application> tag of AndroidManifest.xml
# use that parameter to provide a filename from where to load your custom XML arguments:
#android.extra_manifest_application_arguments = ./src/android/extra_manifest_application_arguments.xml
# (str) Full name including package path of the Java class that implements Python Service
# use that parameter to set custom Java class which extends PythonService
#android.service_class_name = org.kivy.android.PythonService
# (str) Android app theme, default is ok for Kivy-based app
# android.apptheme = "@android:style/Theme.NoTitleBar"
# (list) Pattern to whitelist for the whole project
#android.whitelist =
# (str) Path to a custom whitelist file
#android.whitelist_src =
# (str) Path to a custom blacklist file
#android.blacklist_src =
# (list) List of Java .jar files to add to the libs so that pyjnius can access
# their classes. Don't add jars that you do not need, since extra jars can slow
# down the build process. Allows wildcards matching, for example:
# OUYA-ODK/libs/*.jar
#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar
# (list) List of Java files to add to the android project (can be java or a
# directory containing the files)
#android.add_src =
# (list) Android AAR archives to add
#android.add_aars =
# (list) Put these files or directories in the apk assets directory.
# Either form may be used, and assets need not be in 'source.include_exts'.
# 1) android.add_assets = source_asset_relative_path
# 2) android.add_assets = source_asset_path:destination_asset_relative_path
#android.add_assets =
# (list) Put these files or directories in the apk res directory.
# The option may be used in three ways, the value may contain one or zero ':'
# Some examples:
# 1) A file to add to resources, legal resource names contain ['a-z','0-9','_']
# android.add_resources = my_icons/all-inclusive.png:drawable/all_inclusive.png
# 2) A directory, here 'legal_icons' must contain resources of one kind
# android.add_resources = legal_icons:drawable
# 3) A directory, here 'legal_resources' must contain one or more directories,
# each of a resource kind: drawable, xml, etc...
# android.add_resources = legal_resources
#android.add_resources =
# (list) Gradle dependencies to add
#android.gradle_dependencies =
# (bool) Enable AndroidX support. Enable when 'android.gradle_dependencies'
# contains an 'androidx' package, or any package from Kotlin source.
# android.enable_androidx requires android.api >= 28
#android.enable_androidx = True
# (list) add java compile options
# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option
# see https://developer.android.com/studio/write/java8-support for further information
# android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8"
# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies}
# please enclose in double quotes
# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }"
#android.add_gradle_repositories =
# (list) packaging options to add
# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
# can be necessary to solve conflicts in gradle_dependencies
# please enclose in double quotes
# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'"
#android.add_packaging_options =
# (list) Java classes to add as activities to the manifest.
#android.add_activities = com.example.ExampleActivity
# (str) OUYA Console category. Should be one of GAME or APP
# If you leave this blank, OUYA support will not be enabled
#android.ouya.category = GAME
# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png
# (str) XML file to include as an intent filters in <activity> tag
#android.manifest.intent_filters =
# (list) Copy these files to src/main/res/xml/ (used for example with intent-filters)
#android.res_xml = PATH_TO_FILE,
# (str) launchMode to set for the main activity
#android.manifest.launch_mode = standard
# (str) screenOrientation to set for the main activity.
# Valid values can be found at https://developer.android.com/guide/topics/manifest/activity-element
#android.manifest.orientation = fullSensor
# (list) Android additional libraries to copy into libs/armeabi
#android.add_libs_armeabi = libs/android/*.so
#android.add_libs_armeabi_v7a = libs/android-v7/*.so
#android.add_libs_arm64_v8a = libs/android-v8/*.so
#android.add_libs_x86 = libs/android-x86/*.so
#android.add_libs_mips = libs/android-mips/*.so
# (bool) Indicate whether the screen should stay on
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False
# (list) Android application meta-data to set (key=value format)
#android.meta_data =
# (list) Android library project to add (will be added in the
# project.properties automatically.)
#android.library_references =
# (list) Android shared libraries which will be added to AndroidManifest.xml using <uses-library> tag
#android.uses_library =
# (str) Android logcat filters to use
#android.logcat_filters = *:S python:D
# (bool) Android logcat only display log for activity's pid
#android.logcat_pid_only = False
# (str) Android additional adb arguments
#android.adb_args = -H host.docker.internal
# (bool) Copy library instead of making a libpymodules.so
#android.copy_libs = 1
# (list) The Android archs to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
# In past, was `android.arch` as we weren't supporting builds for multiple archs at the same time.
android.archs = arm64-v8a, armeabi-v7a
# (int) overrides automatic versionCode computation (used in build.gradle)
# this is not the same as app version and should only be edited if you know what you're doing
# android.numeric_version = 1
# (bool) enables Android auto backup feature (Android API >=23)
android.allow_backup = True
# (str) XML file for custom backup rules (see official auto backup documentation)
# android.backup_rules =
# (str) If you need to insert variables into your AndroidManifest.xml file,
# you can do so with the manifestPlaceholders property.
# This property takes a map of key-value pairs. (via a string)
# Usage example : android.manifest_placeholders = [myCustomUrl:\"org.kivy.customurl\"]
# android.manifest_placeholders = [:]
# (bool) Skip byte compile for .py files
# android.no-byte-compile-python = False
# (str) The format used to package the app for release mode (aab or apk or aar).
# android.release_artifact = aab
# (str) The format used to package the app for debug mode (apk or aar).
# android.debug_artifact = apk
#
# Python for android (p4a) specific
#
# (str) python-for-android URL to use for checkout
#p4a.url =
# (str) python-for-android fork to use in case if p4a.url is not specified, defaults to upstream (kivy)
#p4a.fork = kivy
# (str) python-for-android branch to use, defaults to master
#p4a.branch = master
# (str) python-for-android specific commit to use, defaults to HEAD, must be within p4a.branch
#p4a.commit = HEAD
# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
#p4a.source_dir =
# (str) The directory in which python-for-android should look for your own build recipes (if any)
#p4a.local_recipes =
# (str) Filename to the hook for p4a
#p4a.hook =
# (str) Bootstrap to use for android builds
# p4a.bootstrap = sdl2
# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
#p4a.port =
# Control passing the --use-setup-py vs --ignore-setup-py to p4a
# "in the future" --use-setup-py is going to be the default behaviour in p4a, right now it is not
# Setting this to false will pass --ignore-setup-py, true will pass --use-setup-py
# NOTE: this is general setuptools integration, having pyproject.toml is enough, no need to generate
# setup.py if you're using Poetry, but you need to add "toml" to source.include_exts.
#p4a.setup_py = false
# (str) extra command line arguments to pass when invoking pythonforandroid.toolchain
#p4a.extra_args =
#
# iOS specific
#
# (str) Path to a custom kivy-ios folder
#ios.kivy_ios_dir = ../kivy-ios
# Alternately, specify the URL and branch of a git checkout:
ios.kivy_ios_url = https://github.com/kivy/kivy-ios
ios.kivy_ios_branch = master
# Another platform dependency: ios-deploy
# Uncomment to use a custom checkout
#ios.ios_deploy_dir = ../ios_deploy
# Or specify URL and branch
ios.ios_deploy_url = https://github.com/phonegap/ios-deploy
ios.ios_deploy_branch = 1.10.0
# (bool) Whether or not to sign the code
ios.codesign.allowed = false
# (str) Name of the certificate to use for signing the debug version
# Get a list of available identities: buildozer ios list_identities
#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"
# (str) The development team to use for signing the debug version
#ios.codesign.development_team.debug = <hexstring>
# (str) Name of the certificate to use for signing the release version
#ios.codesign.release = %(ios.codesign.debug)s
# (str) The development team to use for signing the release version
#ios.codesign.development_team.release = <hexstring>
# (str) URL pointing to .ipa file to be installed
# This option should be defined along with `display_image_url` and `full_size_image_url` options.
#ios.manifest.app_url =
# (str) URL pointing to an icon (57x57px) to be displayed during download
# This option should be defined along with `app_url` and `full_size_image_url` options.
#ios.manifest.display_image_url =
# (str) URL pointing to a large icon (512x512px) to be used by iTunes
# This option should be defined along with `app_url` and `display_image_url` options.
#ios.manifest.full_size_image_url =
[buildozer]
# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2
# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 1
# (str) Path to build artifact storage, absolute or relative to spec file
# build_dir = ./.buildozer
# (str) Path to build output (i.e. .apk, .aab, .ipa) storage
# bin_dir = ./bin
# -----------------------------------------------------------------------------
# List as sections
#
# You can define all the "list" as [section:key].
# Each line will be considered as a option to the list.
# Let's take [app] / source.exclude_patterns.
# Instead of doing:
#
#[app]
#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
# This can be translated into:
#
#[app:source.exclude_patterns]
#license
#data/audio/*.wav
#data/images/original/*
#
# -----------------------------------------------------------------------------
# Profiles
#
# You can extend section / key with a profile
# For example, you want to deploy a demo version of your application without
# HD content. You could first change the title to add "(demo)" in the name
# and extend the excluded directories to remove the HD content.
#
#[app@demo]
#title = My Application (demo)
#
#[app:source.exclude_patterns@demo]
#images/hd/*
#
# Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug

4
fa
View File

@@ -1,2 +1,2 @@
#! /usr/bin/bash
poetry run fastanime $*
#!/usr/bin/env sh
exec "${PYTHON:-python3}" -Werror -Xdev "$(dirname "$(realpath "$0")")/fastanime/__main__.py" "$@"

View File

@@ -1,32 +0,0 @@
import os
import shutil
from pyshortcuts import make_shortcut
from .. import ASSETS_DIR, PLATFORM
def create_desktop_shortcut():
app = "_ -m fastanime --gui"
logo = os.path.join(ASSETS_DIR, "logo.png")
if PLATFORM == "Windows":
logo = os.path.join(ASSETS_DIR, "logo.ico")
if fastanime := shutil.which("fastanime"):
app = f"{fastanime} --gui"
make_shortcut(
app,
name="FastAnime",
description="Download and watch anime",
terminal=False,
icon=logo,
executable=fastanime,
)
else:
make_shortcut(
app,
name="FastAnime",
description="Download and watch anime",
terminal=False,
icon=logo,
)

View File

@@ -8,7 +8,7 @@ from subprocess import PIPE, Popen
import requests
from rich import print
from .. import APP_NAME, AUTHOR, GIT_REPO, REPO, __version__
from .. import APP_NAME, AUTHOR, GIT_REPO, __version__
API_URL = f"https://api.{GIT_REPO}/repos/{AUTHOR}/{APP_NAME}/releases/latest"
@@ -91,13 +91,12 @@ def update_app():
else:
executable = sys.executable
app_package_url = f"https://{REPO}/releases/download/{tag_name}/fastanime-{tag_name.replace("v","")}.tar.gz"
args = [
executable,
"-m",
"pip",
"install",
app_package_url,
APP_NAME,
"--user",
"--no-warn-script-location",
]

View File

@@ -53,7 +53,8 @@ class YtDLPDownloader:
anime_title = sanitize_filename(title[0])
episode_title = sanitize_filename(title[1])
ydl_opts = {
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s", # Specify the output path and template
# Specify the output path and template
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
"progress_hooks": [
main_progress_hook,
], # Progress hook

View File

@@ -1,39 +0,0 @@
import yt_dlp
class MyLogger:
def debug(self, msg):
print(msg)
def warning(self, msg):
print(msg)
def error(self, msg):
print(msg)
def my_hook(d):
if d["status"] == "finished":
print("Done downloading, now converting ...")
# URL of the HLS stream
url = "https://example.com/path/to/stream.m3u8"
# Options for yt-dlp
ydl_opts = {
"format": "best", # Choose the best quality available
"outtmpl": "/path/to/downloaded/video.%(ext)s", # Specify the output path and template
"logger": MyLogger(), # Custom logger
"progress_hooks": [my_hook], # Progress hook
}
# Function to download the HLS video
def download_hls_video(url, options):
with yt_dlp.YoutubeDL(options) as ydl:
ydl.download([url])
# Call the function
download_hls_video(url, ydl_opts)

View File

@@ -1,54 +0,0 @@
"""
Contains helper functions to make your life easy when adding kivy markup to text
"""
from kivy.utils import get_hex_from_color
def bolden(text: str):
return f"[b]{text}[/b]"
def italicize(text: str):
return f"[i]{text}[/i]"
def underline(text: str):
return f"[u]{text}[/u]"
def strike_through(text: str):
return f"[s]{text}[/s]"
def sub_script(text: str):
return f"[sub]{text}[/sub]"
def super_script(text: str):
return f"[sup]{text}[/sup]"
def color_text(text: str, color: tuple):
hex_color = get_hex_from_color(color)
return f"[color={hex_color}]{text}[/color]"
def font(text: str, font_name: str):
return f"[font={font_name}]{text}[/font]"
def font_family(text: str, family: str):
return f"[font_family={family}]{text}[/font_family]"
def font_context(text: str, context: str):
return f"[font_context={context}]{text}[/font_context]"
def font_size(text: str, size: int):
return f"[size={size}]{text}[/size]"
def text_ref(text: str, ref: str):
return f"[ref={ref}]{text}[/ref]"

View File

@@ -1,16 +0,0 @@
# Of course, "very flexible Python" allows you to do without an abstract
# superclass at all or use the clever exception `NotImplementedError`. In my
# opinion, this can negatively affect the architecture of the application.
# I would like to point out that using Kivy, one could use the on-signaling
# model. In this case, when the state changes, the model will send a signal
# that can be received by all attached observers. This approach seems less
# universal - you may want to use a different library in the future.
class Observer:
"""Abstract superclass for all observers."""
def model_is_changed(self):
"""
The method that will be called on the observer when the model changes.
"""

View File

@@ -1,29 +0,0 @@
from kivy.clock import Clock
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarSupportingText, MDSnackbarText
def show_notification(title, details):
"""helper function to display notifications
Args:
title (str): the title of your message
details (str): the details of your message
"""
def _show(dt):
MDSnackbar(
MDSnackbarText(
text=title,
adaptive_height=True,
),
MDSnackbarSupportingText(
text=details, shorten=False, max_lines=0, adaptive_height=True
),
duration=5,
y="10dp",
pos_hint={"bottom": 1, "right": 0.99},
padding=[0, 0, "8dp", "8dp"],
size_hint_x=0.4,
).open()
Clock.schedule_once(_show, 1)

View File

@@ -2,13 +2,13 @@ import json
import logging
import os
from .. import USER_DATA_PATH
from ..constants import USER_DATA_PATH
logger = logging.getLogger(__name__)
class UserData:
user_data = {"watch_history": {}, "animelist": []}
user_data = {"watch_history": {}, "animelist": [], "user": {}}
def __init__(self):
try:
@@ -23,6 +23,10 @@ class UserData:
self.user_data["watch_history"] = watch_history
self._update_user_data()
def update_user_info(self, user: dict):
self.user_data["user"] = user
self._update_user_data()
def update_animelist(self, anime_list: list):
self.user_data["animelist"] = list(set(anime_list))
self._update_user_data()

View File

@@ -1,3 +1,4 @@
import logging
import os
import re
import shutil
@@ -10,6 +11,7 @@ from fastanime.libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchem
from .data import anime_normalizer
logger = logging.getLogger(__name__)
# TODO: make it use color_text instead of fixed vals
# from .kivy_markup_helper import color_text
@@ -127,6 +129,7 @@ def anime_title_percentage_match(
fuzz.ratio(title_a.lower(), possible_user_requested_anime_title.lower()),
fuzz.ratio(title_b.lower(), possible_user_requested_anime_title.lower()),
)
logger.info(f"{locals()}")
return percentage_ratio

View File

@@ -1,10 +1,14 @@
import sys
if sys.version_info < (3, 10):
raise ImportError(
"You are using an unsupported version of Python. Only Python versions 3.8 and above are supported by yt-dlp"
) # noqa: F541
import logging
import os
import sys
from platform import platform
from dotenv import load_dotenv
from platformdirs import PlatformDirs
load_dotenv()
@@ -15,43 +19,12 @@ if os.environ.get("FA_RICH_TRACEBACK", False):
# initiate constants
__version__ = "v0.30.0"
__version__ = "v0.4.0"
PLATFORM = platform()
APP_NAME = "FastAnime"
AUTHOR = "Benex254"
GIT_REPO = "github.com"
REPO = f"{GIT_REPO}/{AUTHOR}/{APP_NAME}"
USER_NAME = os.environ.get("USERNAME", f"{APP_NAME} user")
dirs = PlatformDirs(appname=APP_NAME, appauthor=AUTHOR, ensure_exists=True)
# ---- app deps ----
APP_DIR = os.path.abspath(os.path.dirname(__file__))
CONFIGS_DIR = os.path.join(APP_DIR, "configs")
ASSETS_DIR = os.path.join(APP_DIR, "assets")
# ----- user configs and data -----
APP_DATA_DIR = dirs.user_config_dir
if not APP_DATA_DIR:
APP_DATA_DIR = dirs.user_data_dir
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
# cache dir
APP_CACHE_DIR = dirs.user_cache_dir
# video dir
USER_VIDEOS_DIR = os.path.join(dirs.user_videos_dir, APP_NAME)
# web dirs
WEB_DIR = os.path.join(APP_DIR, "web")
FRONTEND_DIR = os.path.join(WEB_DIR, "frontend")
BACKEND_DIR = os.path.join(WEB_DIR, "backend")
def FastAnime():
@@ -72,6 +45,22 @@ def FastAnime():
handlers=[RichHandler()], # Use RichHandler to format the logs
)
sys.argv.remove("--log")
if "--log-file" in sys.argv:
# Configure logging
from rich.logging import RichHandler
from .constants import NOTIFIER_LOG_FILE_PATH
logging.getLogger(__name__)
logging.basicConfig(
level=logging.DEBUG, # Set the logging level to DEBUG
# Use a simple message format
format="%(asctime)s%(levelname)s: %(message)s",
datefmt="[%d/%m/%Y@%H:%M:%S]", # Use a custom date format
filename=NOTIFIER_LOG_FILE_PATH,
filemode="a", # Use RichHandler to format the logs
)
sys.argv.remove("--log-file")
from .cli import run_cli

View File

@@ -1,4 +1,3 @@
import os
import sys
if __package__ is None and not getattr(sys, "frozen", False):
@@ -10,15 +9,6 @@ if __package__ is None and not getattr(sys, "frozen", False):
if __name__ == "__main__":
in_development = bool(os.environ.get("FA_DEVELOPMENT", False))
from . import FastAnime
if in_development:
FastAnime()
else:
try:
FastAnime()
except Exception as e:
from .Utility.utils import write_crash
write_crash(e)
FastAnime()

3
fastanime/anilist.py Normal file
View File

@@ -0,0 +1,3 @@
from .libs.anilist.api import AniListApi
AniList = AniListApi()

BIN
fastanime/assets/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
fastanime/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -50,7 +50,7 @@ signal.signal(signal.SIGINT, handle_exit)
@click.option(
"-s",
"--server",
type=click.Choice(SERVERS_AVAILABLE, case_sensitive=False),
type=click.Choice([*SERVERS_AVAILABLE, "top"], case_sensitive=False),
help="Server of choice",
)
@click.option(
@@ -66,6 +66,11 @@ signal.signal(signal.SIGINT, handle_exit)
type=bool,
help="Continue from last episode?",
)
@click.option(
"--skip/--no-skip",
type=bool,
help="Skip opening and ending theme songs?",
)
@click.option(
"-q",
"--quality",
@@ -100,6 +105,11 @@ signal.signal(signal.SIGINT, handle_exit)
@click.option("--default", is_flag=True, help="Use the default interface")
@click.option("--preview", is_flag=True, help="Show preview when using fzf")
@click.option("--no-preview", is_flag=True, help="Dont show preview when using fzf")
@click.option(
"--icons/--no-icons",
type=bool,
help="Use icons in the interfaces",
)
@click.pass_context
def run_cli(
ctx: click.Context,
@@ -107,6 +117,7 @@ def run_cli(
server,
format,
continue_,
skip,
translation_type,
quality,
auto_next,
@@ -117,6 +128,7 @@ def run_cli(
default,
preview,
no_preview,
icons,
):
ctx.obj = Config()
if provider:
@@ -128,10 +140,15 @@ def run_cli(
ctx.obj.format = format
if ctx.get_parameter_source("continue_") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.continue_from_history = continue_
if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.skip = skip
if quality:
ctx.obj.quality = quality
if ctx.get_parameter_source("auto-next") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.auto_next = auto_next
if ctx.get_parameter_source("icons") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.icons = icons
if (
ctx.get_parameter_source("auto_select")
== click.core.ParameterSource.COMMANDLINE

View File

@@ -1,15 +1,24 @@
import click
from ....anilist import AniList
from ...interfaces.anilist_interfaces import anilist as anilist_interface
from ...utils.tools import QueryDict
from .completed import completed
from .dropped import dropped
from .favourites import favourites
from .login import login
from .notifier import notifier
from .paused import paused
from .planning import planning
from .popular import popular
from .random_anime import random_anime
from .recent import recent
from .rewatching import rewatching
from .scores import scores
from .search import search
from .trending import trending
from .upcoming import upcoming
from .watching import watching
commands = {
"trending": trending,
@@ -20,6 +29,14 @@ commands = {
"popular": popular,
"favourites": favourites,
"random": random_anime,
"login": login,
"watching": watching,
"paused": paused,
"rewatching": rewatching,
"dropped": dropped,
"completed": completed,
"planning": planning,
"notifier": notifier,
}
@@ -31,6 +48,8 @@ commands = {
)
@click.pass_context
def anilist(ctx: click.Context):
if user := ctx.obj.user:
AniList.update_login_info(user, user["token"])
if ctx.invoked_subcommand is None:
anilist_config = QueryDict()
anilist_interface(ctx.obj, anilist_config)

View File

@@ -0,0 +1,29 @@
import click
from fastanime.cli.config import Config
from fastanime.cli.interfaces import anilist_interfaces
from fastanime.cli.utils.tools import QueryDict, exit_app
from ....anilist import AniList
@click.command(help="View anime you completed")
@click.pass_obj
def completed(config: Config):
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("COMPLETED")
if not anime_list:
return
if not anime_list[0]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
anilist_config = QueryDict()
anilist_config.data = anime_list[1]
anilist_interfaces.select_anime(config, anilist_config)

View File

@@ -0,0 +1,29 @@
import click
from fastanime.cli.config import Config
from fastanime.cli.interfaces import anilist_interfaces
from fastanime.cli.utils.tools import QueryDict, exit_app
from ....anilist import AniList
@click.command(help="View anime you dropped")
@click.pass_obj
def dropped(config: Config):
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("DROPPED")
if not anime_list:
return
if not anime_list[0]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
anilist_config = QueryDict()
anilist_config.data = anime_list[1]
anilist_interfaces.select_anime(config, anilist_config)

View File

@@ -1,6 +1,6 @@
import click
from ....libs.anilist.anilist import AniList
from ....anilist import AniList
from ...interfaces.anilist_interfaces import select_anime
from ...utils.tools import QueryDict

View File

@@ -0,0 +1,45 @@
import webbrowser
import click
from rich import print
from rich.prompt import Confirm, Prompt
from ....anilist import AniList
from ...config import Config
from ...utils.tools import exit_app
@click.command(help="Login to your anilist account")
@click.option("--status", "-s", help="Whether you are logged in or not", is_flag=True)
@click.pass_obj
def login(config: Config, status):
if status:
is_logged_in = True if config.user else False
message = (
"You are logged in :happy:" if is_logged_in else "You arent logged in :cry"
)
print(message)
print(config.user)
exit_app()
if config.user:
print("Already logged in :confused:")
if not Confirm.ask("or would you like to reloggin", default=True):
exit_app()
# ---- new loggin -----
print(
f"A browser session will be opened ( [link]{
config.fastanime_anilist_app_login_url}[/link] )",
)
webbrowser.open(config.fastanime_anilist_app_login_url)
print("Please paste the token provided here")
token = Prompt.ask("Enter token")
user = AniList.login_user(token)
if not user:
print("Sth went wrong", user)
exit_app()
return
user["token"] = token
config.update_user(user)
print("Successfully saved credentials")
print(user)
exit_app()

View File

@@ -0,0 +1,102 @@
import json
import logging
import os
import time
import click
import requests
from plyer import notification
from ....anilist import AniList
from ....constants import APP_CACHE_DIR, APP_DATA_DIR, APP_NAME, PLATFORM
from ..config import Config
logger = logging.getLogger(__name__)
# plyer.notification(title="anime",message="Update",app_name=APP_NAME)
@click.command(help="Check for notifications on anime you currently watching")
@click.pass_obj
def notifier(config: Config):
notified = os.path.join(APP_DATA_DIR, "last_notification.json")
anime_image = os.path.join(APP_CACHE_DIR, "notification_image")
notification_duration = config.notification_duration * 60
if not config.user:
print("Not Authenticated")
print("Run the following to get started: fastanime anilist loggin")
return
run = True
timeout = 2
if os.path.exists(notified):
with open(notified, "r") as f:
past_notifications = json.load(f)
else:
past_notifications = {}
with open(notified, "w") as f:
json.dump(past_notifications, f)
while run:
try:
logger.info("checking for notifications")
result = AniList.get_notification()
if not result[0]:
print(result)
logger.warning(
"Something went wrong this could mean anilist is down or you have lost internet connection"
)
logger.info("sleeping...")
time.sleep(timeout * 60)
continue
data = result[1]
# pyright:ignore
notifications = data["data"]["Page"]["notifications"]
if not notifications:
logger.info("Nothing to notify")
else:
for notification_ in notifications:
anime_episode = notification_["episode"]
title = f"Episode {anime_episode} just aired"
anime_title = notification_["media"]["title"][
config.preferred_language
]
# pyright:ignore
message = f"{anime_title}\nBe sure to watch so you are not left out of the loop."
# message = str(textwrap.wrap(message, width=50))
id = notification_["media"]["id"]
if past_notifications.get(str(id)) == notification_["episode"]:
logger.info(
f"skipping id={id} title={anime_title} episode={
anime_episode} already notified"
)
else:
if PLATFORM != "Windows":
image_link = notification_["media"]["coverImage"]["medium"]
print(image_link)
logger.info("Downloading image")
resp = requests.get(image_link)
if resp.status_code == 200:
with open(anime_image, "wb") as f:
f.write(resp.content)
ICON_PATH = anime_image
past_notifications[f"{id}"] = notification_["episode"]
with open(notified, "w") as f:
json.dump(past_notifications, f)
logger.info(message)
notification.notify( # pyright:ignore
title=title,
message=message,
app_name=APP_NAME,
app_icon=ICON_PATH,
hints={"image-path": ICON_PATH},
timeout=notification_duration,
)
time.sleep(30)
except Exception as e:
logger.error(e)
logger.info("sleeping...")
time.sleep(timeout * 60)

View File

@@ -0,0 +1,29 @@
import click
from fastanime.cli.config import Config
from fastanime.cli.interfaces import anilist_interfaces
from fastanime.cli.utils.tools import QueryDict, exit_app
from ....anilist import AniList
@click.command(help="View anime you paused on watching")
@click.pass_obj
def paused(config: Config):
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("PAUSED")
if not anime_list:
return
if not anime_list[0]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
anilist_config = QueryDict()
anilist_config.data = anime_list[1]
anilist_interfaces.select_anime(config, anilist_config)

View File

@@ -0,0 +1,29 @@
import click
from fastanime.cli.config import Config
from fastanime.cli.interfaces import anilist_interfaces
from fastanime.cli.utils.tools import QueryDict, exit_app
from ....anilist import AniList
@click.command(help="View anime you are planning on watching")
@click.pass_obj
def planning(config: Config):
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("PLANNING")
if not anime_list:
return
if not anime_list[0]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
anilist_config = QueryDict()
anilist_config.data = anime_list[1]
anilist_interfaces.select_anime(config, anilist_config)

View File

@@ -1,6 +1,6 @@
import click
from ....libs.anilist.anilist import AniList
from ....anilist import AniList
from ...interfaces.anilist_interfaces import select_anime
from ...utils.tools import QueryDict

View File

@@ -2,7 +2,7 @@ import random
import click
from ....libs.anilist.anilist import AniList
from ....anilist import AniList
from ...interfaces.anilist_interfaces import select_anime
from ...utils.tools import QueryDict

View File

@@ -1,6 +1,6 @@
import click
from ....libs.anilist.anilist import AniList
from ....anilist import AniList
from ...interfaces.anilist_interfaces import select_anime
from ...utils.tools import QueryDict

View File

@@ -0,0 +1,29 @@
import click
from fastanime.cli.config import Config
from fastanime.cli.interfaces import anilist_interfaces
from fastanime.cli.utils.tools import QueryDict, exit_app
from ....anilist import AniList
@click.command(help="View anime you are rewatching")
@click.pass_obj
def rewatching(config: Config):
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("REPEATING")
if not anime_list:
return
if not anime_list[0]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
anilist_config = QueryDict()
anilist_config.data = anime_list[1]
anilist_interfaces.select_anime(config, anilist_config)

View File

@@ -1,6 +1,6 @@
import click
from ....libs.anilist.anilist import AniList
from ....anilist import AniList
from ...interfaces.anilist_interfaces import select_anime
from ...utils.tools import QueryDict

View File

@@ -1,6 +1,6 @@
import click
from ....libs.anilist.anilist import AniList
from ....anilist import AniList
from ...interfaces.anilist_interfaces import select_anime
from ...utils.tools import QueryDict

View File

@@ -1,6 +1,6 @@
import click
from ....libs.anilist.anilist import AniList
from ....anilist import AniList
from ...interfaces.anilist_interfaces import select_anime
from ...utils.tools import QueryDict

View File

@@ -1,6 +1,6 @@
import click
from ....libs.anilist.anilist import AniList
from ....anilist import AniList
from ...interfaces.anilist_interfaces import select_anime
from ...utils.tools import QueryDict

View File

@@ -0,0 +1,29 @@
import click
from fastanime.cli.config import Config
from fastanime.cli.interfaces import anilist_interfaces
from fastanime.cli.utils.tools import QueryDict, exit_app
from ....anilist import AniList
@click.command(help="View anime you are watching")
@click.pass_obj
def watching(config: Config):
if not config.user:
print("Not authenticated")
print("Please run: fastanime anilist loggin")
exit_app()
anime_list = AniList.get_anime_list("CURRENT")
if not anime_list:
return
if not anime_list[0]:
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
anilist_config = QueryDict()
anilist_config.data = anime_list[1]
anilist_interfaces.select_anime(config, anilist_config)

View File

@@ -4,7 +4,9 @@ import subprocess
import click
from rich import print
from ... import USER_CONFIG_PATH
from fastanime.cli.config import Config
from ...constants import USER_CONFIG_PATH
from ..utils.tools import exit_app
@@ -13,7 +15,8 @@ from ..utils.tools import exit_app
short_help="Edit your config",
)
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
def configure(path):
@click.pass_obj
def configure(config: Config, path):
if path:
print(USER_CONFIG_PATH)
else:

View File

@@ -1,5 +1,6 @@
import click
from rich import print
from rich.progress import Progress
from thefuzz import fuzz
from ...libs.anime_provider.types import Anime
@@ -28,9 +29,11 @@ def download(config: Config, anime_title, episode_range):
anime_provider = config.anime_provider
translation_type = config.translation_type
download_dir = config.downloads_dir
search_results = anime_provider.search_for_anime(
anime_title, translation_type=translation_type
)
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
search_results = anime_provider.search_for_anime(
anime_title, translation_type=translation_type
)
if not search_results:
print("Search results failed")
input("Enter to retry")
@@ -51,7 +54,11 @@ def download(config: Config, anime_title, episode_range):
list(search_results_.keys()), "Please Select title: ", "FastAnime"
)
anime: Anime | None = anime_provider.get_anime(search_results_[search_result]["id"])
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime: Anime | None = anime_provider.get_anime(
search_results_[search_result]["id"]
)
if not anime:
print("Sth went wring anime no found")
input("Enter to continue...")
@@ -70,20 +77,24 @@ def download(config: Config, anime_title, episode_range):
if episode not in episodes:
print(f"[cyan]Warning[/]: Episode {episode} not found, skipping")
continue
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("No streams skipping")
continue
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("No streams skipping")
continue
streams = list(streams)
links = [
(link.get("priority", 0), link["link"])
for server in streams
for link in server["links"]
]
link = max(links, key=lambda x: x[0])[1]
print(f"[purple]Now Downloading:[/] {search_result} Episode {episode}")
streams = list(streams)
links = [
(link.get("priority", 0), link["link"])
for server in streams
for link in server["links"]
]
link = max(links, key=lambda x: x[0])[1]
downloader._download_file(
link,
download_dir,

View File

@@ -1,5 +1,6 @@
import click
from rich import print
from rich.progress import Progress
from thefuzz import fuzz
from ...cli.config import Config
@@ -23,9 +24,11 @@ from ..utils.utils import clear
@click.pass_obj
def search(config: Config, anime_title: str, episode_range: str):
anime_provider = config.anime_provider
search_results = anime_provider.search_for_anime(
anime_title, config.translation_type
)
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
search_results = anime_provider.search_for_anime(
anime_title, config.translation_type
)
if not search_results:
print("Search results not found")
input("Enter to retry")
@@ -50,7 +53,12 @@ def search(config: Config, anime_title: str, episode_range: str):
list(search_results_.keys()), "Please Select title: ", "FastAnime"
)
anime: Anime | None = anime_provider.get_anime(search_results_[search_result]["id"])
with Progress() as progress:
progress.add_task("Fetching Anime...", total=None)
anime: Anime | None = anime_provider.get_anime(
search_results_[search_result]["id"]
)
if not anime:
print("Sth went wring anime no found")
input("Enter to continue...")
@@ -82,17 +90,20 @@ def search(config: Config, anime_title: str, episode_range: str):
if not episode or episode not in episodes:
episode = fzf.run(episodes, "Select an episode: ", header=search_result)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("Failed to get streams")
return
links = [link["link"] for server in streams for link in server["links"]]
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
streams = anime_provider.get_episode_streams(
anime, episode, config.translation_type
)
if not streams:
print("Failed to get streams")
return
links = [link["link"] for server in streams for link in server["links"]]
# TODO: Come up with way to know quality and better server interface
link = links[config.quality]
# TODO: Come up with way to know quality and better server interface
link = links[config.quality]
# link = fzf.run(links, "Select stream", "Streams")
print(f"[purple]Now Playing:[/] {search_result} Episode {episode}")
mpv(link, search_result)
stream_anime()

View File

@@ -3,14 +3,17 @@ from configparser import ConfigParser
from rich import print
from .. import USER_CONFIG_PATH, USER_VIDEOS_DIR
from ..AnimeProvider import AnimeProvider
from ..constants import USER_CONFIG_PATH, USER_VIDEOS_DIR
from ..Utility.user_data_helper import user_data_helper
class Config(object):
anime_list: list
watch_history: dict
fastanime_anilist_app_login_url = (
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
)
def __init__(self) -> None:
self.load_config()
@@ -31,6 +34,10 @@ class Config(object):
"preview": "False",
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
"provider": "allanime",
"error": "3",
"icons": "false",
"notification_duration": "2",
"skip": "false",
}
)
self.configparser.add_section("stream")
@@ -45,6 +52,8 @@ class Config(object):
self.downloads_dir = self.get_downloads_dir()
self.provider = self.get_provider()
self.use_fzf = self.get_use_fzf()
self.skip = self.get_skip()
self.icons = self.get_icons()
self.preview = self.get_preview()
self.translation_type = self.get_translation_type()
self.sort_by = self.get_sort_by()
@@ -52,6 +61,8 @@ class Config(object):
self.auto_next = self.get_auto_next()
self.auto_select = self.get_auto_select()
self.quality = self.get_quality()
self.notification_duration = self.get_notification_duration()
self.error = self.get_error()
self.server = self.get_server()
self.format = self.get_format()
self.preferred_language = self.get_preferred_language()
@@ -59,11 +70,26 @@ class Config(object):
# ---- setup user data ------
self.watch_history: dict = user_data_helper.user_data.get("watch_history", {})
self.anime_list: list = user_data_helper.user_data.get("animelist", [])
self.user: dict = user_data_helper.user_data.get("user", {})
self.anime_provider = AnimeProvider(self.provider)
def update_watch_history(self, anime_id: int, episode: str | None):
self.watch_history.update({str(anime_id): episode})
def update_user(self, user):
self.user = user
user_data_helper.update_user_info(user)
def update_watch_history(
self, anime_id: int, episode: str | None, start_time="0", total_time="0"
):
self.watch_history.update(
{
str(anime_id): {
"episode": episode,
"start_time": start_time,
"total_time": total_time,
}
}
)
user_data_helper.update_watch_history(self.watch_history)
def update_anime_list(self, anime_id: int, remove=False):
@@ -88,6 +114,12 @@ class Config(object):
def get_use_fzf(self):
return self.configparser.getboolean("general", "use_fzf")
def get_skip(self):
return self.configparser.getboolean("stream", "skip")
def get_icons(self):
return self.configparser.getboolean("general", "icons")
def get_preview(self):
return self.configparser.getboolean("general", "preview")
@@ -112,6 +144,12 @@ class Config(object):
def get_quality(self):
return self.configparser.getint("stream", "quality")
def get_notification_duration(self):
return self.configparser.getint("general", "notification_duration")
def get_error(self):
return self.configparser.getint("stream", "error")
def get_server(self):
return self.configparser.get("stream", "server")

View File

@@ -2,12 +2,16 @@ from __future__ import annotations
import os
import random
from datetime import datetime
from InquirerPy import inquirer
from InquirerPy.validator import EmptyInputValidator
from rich import print
from rich.prompt import Prompt
from rich.progress import Progress
from rich.prompt import Confirm, Prompt
from ... import USER_CONFIG_PATH
from ...libs.anilist.anilist import AniList
from ...anilist import AniList
from ...constants import USER_CONFIG_PATH
from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
from ...libs.anime_provider.types import Anime, SearchResult, Server
from ...libs.fzf import fzf
@@ -17,6 +21,20 @@ from ..config import Config
from ..utils.mpv import mpv
from ..utils.tools import QueryDict, exit_app
from ..utils.utils import clear, fuzzy_inquirer
from .utils import aniskip
def calculate_time_delta(start_time, end_time):
time_format = "%H:%M:%S"
# Convert string times to datetime objects
start = datetime.strptime(start_time, time_format)
end = datetime.strptime(end_time, time_format)
# Calculate the difference
delta = end - start
return delta
def player_controls(config: Config, anilist_config: QueryDict):
@@ -45,8 +63,34 @@ def player_controls(config: Config, anilist_config: QueryDict):
current_episode,
)
mpv(current_link, selected_server["episode_title"])
start_time = config.watch_history[str(anime_id)]["start_time"]
print("[green]Continuing from:[/] ", start_time)
custom_args = []
if config.skip:
if args := aniskip(
anilist_config.selected_anime_anilist["idMal"], current_episode
):
custom_args = args
stop_time, total_time = mpv(
current_link,
selected_server["episode_title"],
start_time=start_time,
custom_args=custom_args,
)
if stop_time == "0":
episode = str(int(current_episode) + 1)
else:
error = 5 * 60
delta = calculate_time_delta(stop_time, total_time)
if delta.total_seconds() > error:
episode = current_episode
else:
episode = str(int(current_episode) + 1)
stop_time = "0"
total_time = "0"
clear()
config.update_watch_history(anime_id, episode, stop_time, total_time)
player_controls(config, anilist_config)
def _next_episode():
@@ -54,7 +98,7 @@ def player_controls(config: Config, anilist_config: QueryDict):
if next_episode >= len(episodes):
next_episode = len(episodes) - 1
# update internal config
# updateinternal config
anilist_config.episode_number = episodes[next_episode]
# update user config
@@ -115,18 +159,23 @@ def player_controls(config: Config, anilist_config: QueryDict):
# reload to controls
player_controls(config, anilist_config)
icons = config.icons
options = {
"Replay": _replay,
"Next Episode": _next_episode,
"Previous Episode": _previous_episode,
"Episodes": _episodes,
"Change Quality": _change_quality,
"Change Translation Type": _change_translation_type,
"Servers": _servers,
"Main Menu": lambda: anilist(config, anilist_config),
"Anime Options Menu": lambda: anilist_options(config, anilist_config),
"Search Results": lambda: select_anime(config, anilist_config),
"Exit": exit_app,
f"{'🔂 ' if icons else ''}Replay": _replay,
f"{'' if icons else ''}Next Episode": _next_episode,
f"{'' if icons else ''}Previous Episode": _previous_episode,
f"{'🗃️ ' if icons else ''}Episodes": _episodes,
f"{'📀 ' if icons else ''}Change Quality": _change_quality,
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
f"{'💽 ' if icons else ''}Servers": _servers,
f"{'📱 ' if icons else ''}Main Menu": lambda: anilist(config, anilist_config),
f"{'📜 ' if icons else ''}Anime Options Menu": lambda: anilist_options(
config, anilist_config
),
f"{'🔎 ' if icons else ''}Search Results": lambda: select_anime(
config, anilist_config
),
f"{'' if icons else ''}Exit": exit_app,
}
if config.auto_next:
@@ -154,17 +203,20 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
anime_provider = config.anime_provider
# get streams for episode from provider
episode_streams = anime_provider.get_episode_streams(
anime, episode_number, translation_type
)
if not episode_streams:
print("Failed to fetch :cry:")
input("Enter to retry...")
return fetch_streams(config, anilist_config)
with Progress() as progress:
progress.add_task("Fetching Episode Streams...", total=None)
episode_streams = anime_provider.get_episode_streams(
anime, episode_number, translation_type
)
if not episode_streams:
print("Failed to fetch :cry:")
input("Enter to retry...")
return fetch_streams(config, anilist_config)
episode_streams = {
episode_stream["server"]: episode_stream for episode_stream in episode_streams
}
episode_streams = {
episode_stream["server"]: episode_stream
for episode_stream in episode_streams
}
# prompt for preferred server
server = None
@@ -210,11 +262,50 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
"[bold magenta] Episode: [/]",
episode_number,
)
# -- update anilist info if user --
if config.user and episode_number:
AniList.update_anime_list(
{
"mediaId": anime_id,
"status": "CURRENT",
"progress": episode_number,
}
)
mpv(stream_link, selected_server["episode_title"])
start_time = config.watch_history.get(str(anime_id), {}).get("start_time", "0")
if start_time != "0":
print("[green]Continuing from:[/] ", start_time)
custom_args = []
if config.skip:
if args := aniskip(
anilist_config.selected_anime_anilist["idMal"], episode_number
):
custom_args = args
stop_time, total_time = mpv(
stream_link,
selected_server["episode_title"],
start_time=start_time,
custom_args=custom_args,
)
print("Finished at: ", stop_time)
# update_watch_history
config.update_watch_history(anime_id, str(int(episode_number) + 1))
if stop_time == "0":
episode = str(int(episode_number) + 1)
else:
error = config.error * 60
delta = calculate_time_delta(stop_time, total_time)
if delta.total_seconds() > error:
episode = episode_number
else:
episode = str(int(episode_number) + 1)
stop_time = "0"
total_time = "0"
config.update_watch_history(
anime_id, episode, start_time=stop_time, total_time=total_time
)
# switch to controls
clear()
@@ -236,8 +327,11 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
# prompt for episode number
episodes = anime["availableEpisodesDetail"][translation_type]
if continue_from_history and user_watch_history.get(str(anime_id)) in episodes:
episode_number = user_watch_history[str(anime_id)]
if (
continue_from_history
and user_watch_history.get(str(anime_id), {}).get("episode") in episodes
):
episode_number = user_watch_history[str(anime_id)]["episode"]
print(f"[bold cyan]Continuing from Episode:[/] [bold]{episode_number}[/]")
else:
choices = [*episodes, "Back"]
@@ -253,7 +347,8 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
if episode_number == "Back":
anilist_options(config, anilist_config)
return
config.update_watch_history(anime_id, episode_number)
start_time = user_watch_history.get(str(anime_id), {}).get("start_time", "0")
config.update_watch_history(anime_id, episode_number, start_time=start_time)
# update internal config
anilist_config.episodes = episodes
@@ -267,7 +362,9 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
def fetch_anime_episode(config, anilist_config: QueryDict):
selected_anime: SearchResult = anilist_config._anime
anime_provider = config.anime_provider
anilist_config.anime = anime_provider.get_anime(selected_anime["id"])
with Progress() as progress:
progress.add_task("Fetching Anime Info...", total=None)
anilist_config.anime = anime_provider.get_anime(selected_anime["id"])
if not anilist_config.anime:
print(
@@ -291,9 +388,11 @@ def provide_anime(config: Config, anilist_config: QueryDict):
anime_provider = config.anime_provider
# search and get the requested title from provider
search_results = anime_provider.search_for_anime(
selected_anime_title, translation_type
)
with Progress() as progress:
progress.add_task("Fetching Search Results...", total=None)
search_results = anime_provider.search_for_anime(
selected_anime_title, translation_type
)
if not search_results:
print(
"Sth went wrong :cry: while fetching this could mean you have poor internet connection or the provider is down"
@@ -347,7 +446,10 @@ def anilist_options(config, anilist_config: QueryDict):
if trailer := selected_anime.get("trailer"):
trailer_url = "https://youtube.com/watch?v=" + trailer["id"]
print("[bold magenta]Watching Trailer of:[/]", selected_anime_title)
mpv(trailer_url, selected_anime_title, f"--ytdl-format={config.format}")
mpv(
trailer_url,
ytdl_format=config.format,
)
anilist_options(config, anilist_config)
else:
print("no trailer available :confused:")
@@ -355,11 +457,71 @@ def anilist_options(config, anilist_config: QueryDict):
anilist_options(config, anilist_config)
def _add_to_list(config: Config, anilist_config: QueryDict):
config.update_anime_list(anilist_config.anime_id)
# config.update_anime_list(anilist_config.anime_id)
anime_lists = {
"Watching": "CURRENT",
"Paused": "PAUSED",
"Planning": "PLANNING",
"Dropped": "DROPPED",
"Rewatching": "REPEATING",
"Completed": "COMPLETED",
}
if config.use_fzf:
anime_list = fzf.run(
list(anime_lists.keys()),
"Choose the list you want to add to",
"Add your animelist",
)
else:
anime_list = fuzzy_inquirer(
"Choose the list you want to add to", list(anime_lists.keys())
)
result = AniList.update_anime_list(
{"status": anime_lists[anime_list], "mediaId": selected_anime["id"]}
)
if not result[0]:
print("Failed to update", result)
else:
print(
f"Successfully added {selected_anime_title} to your {anime_list} list :smile:"
)
input("Enter to continue...")
anilist_options(config, anilist_config)
def _score_anime(config: Config, anilist_config: QueryDict):
score = inquirer.number(
message="Enter the score:",
min_allowed=0,
max_allowed=100,
validate=EmptyInputValidator(),
).execute()
result = AniList.update_anime_list(
{"scoreRaw": score, "mediaId": selected_anime["id"]}
)
if not result[0]:
print("Failed to update", result)
else:
print(f"Successfully scored {selected_anime_title}; score: {score}")
input("Enter to continue...")
anilist_options(config, anilist_config)
def _remove_from_list(config: Config, anilist_config: QueryDict):
config.update_anime_list(anilist_config.anime_id, True)
if Confirm.ask(
f"Are you sure you want to procede, the folowing action will permanently remove {
selected_anime_title} from your list and your progress will be erased",
default=False,
):
success, data = AniList.delete_medialist_entry(selected_anime["id"])
if not success:
print("Failed to delete", data)
elif not data.get("deleted"):
print("Failed to delete", data)
else:
print("Successfully deleted :cry:", selected_anime_title)
else:
print(selected_anime_title, ":relieved:")
input("Enter to continue...")
anilist_options(config, anilist_config)
def _change_translation_type(config: Config, anilist_config: QueryDict):
@@ -430,15 +592,17 @@ def anilist_options(config, anilist_config: QueryDict):
anilist_options(config, anilist_config)
return
icons = config.icons
options = {
"Stream": provide_anime,
"Watch Trailer": _watch_trailer,
"Add to List": _add_to_list,
"Remove from List": _remove_from_list,
"View Info": _view_info,
"Change Translation Type": _change_translation_type,
"Back": select_anime,
"Exit": exit_app,
f"{'📽️ ' if icons else ''}Stream": provide_anime,
f"{'📼 ' if icons else ''}Watch Trailer": _watch_trailer,
f"{'' if icons else ''}Score Anime": _score_anime,
f"{'📥 ' if icons else ''}Add to List": _add_to_list,
f"{'📤 ' if icons else ''}Remove from List": _remove_from_list,
f"{'📖 ' if icons else ''}View Info": _view_info,
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
f"{'🔙 ' if icons else ''}Back": select_anime,
f"{'' if icons else ''}Exit": exit_app,
}
if config.use_fzf:
action = fzf.run(
@@ -493,6 +657,46 @@ def select_anime(config: Config, anilist_config: QueryDict):
anilist_options(config, anilist_config)
def handle_animelist(anilist_config, config: Config, list_type: str):
if not config.user:
print("You haven't logged in please run: fastanime anilist login")
input("Enter to continue...")
anilist(config, anilist_config)
return
match list_type:
case "Watching":
status = "CURRENT"
case "Planned":
status = "PLANNING"
case "Completed":
status = "COMPLETED"
case "Dropped":
status = "DROPPED"
case "Paused":
status = "PAUSED"
case "Repeating":
status = "REPEATING"
case _:
return
anime_list = AniList.get_anime_list(status)
if not anime_list:
print("Sth went wrong", anime_list)
input("Enter to continue")
anilist(config, anilist_config)
return
if not anime_list[0]:
print("Sth went wrong", anime_list)
input("Enter to continue")
anilist(config, anilist_config)
return
media = [
mediaListItem["media"]
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
] # pyright:ignore
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
return anime_list
def anilist(config: Config, anilist_config: QueryDict):
def _anilist_search():
search_term = Prompt.ask("[cyan]Search for[/]")
@@ -521,19 +725,38 @@ def anilist(config: Config, anilist_config: QueryDict):
anilist(config, anilist_config)
icons = config.icons
options = {
"Trending": AniList.get_trending,
"Recently Updated Anime": AniList.get_most_recently_updated,
"Search": _anilist_search,
"Watch History": _watch_history,
"AnimeList": _anime_list,
"Random Anime": _anilist_random,
"Most Popular Anime": AniList.get_most_popular,
"Most Favourite Anime": AniList.get_most_favourite,
"Most Scored Anime": AniList.get_most_scored,
"Upcoming Anime": AniList.get_upcoming_anime,
"Edit Config": edit_config,
"Exit": exit_app,
f"{'🔥 ' if icons else ''}Trending": AniList.get_trending,
f"{'📺 ' if icons else ''}Watching": lambda x="Watching": handle_animelist(
anilist_config, config, x
),
f"{'' if icons else ''}Paused": lambda x="Paused": handle_animelist(
anilist_config, config, x
),
f"{'🚮 ' if icons else ''}Dropped": lambda x="Dropped": handle_animelist(
anilist_config, config, x
),
f"{'📑 ' if icons else ''}Planned": lambda x="Planned": handle_animelist(
anilist_config, config, x
),
f"{'' if icons else ''}Completed": lambda x="Completed": handle_animelist(
anilist_config, config, x
),
f"{'🔁 ' if icons else ''}Rewatching": lambda x="Repeating": handle_animelist(
anilist_config, config, x
),
f"{'🔔 ' if icons else ''}Recently Updated Anime": AniList.get_most_recently_updated,
f"{'🔎 ' if icons else ''}Search": _anilist_search,
f"{'🎞️ ' if icons else ''}Watch History": _watch_history,
# "AnimeList": _anime_list💯,
f"{'🎲 ' if icons else ''}Random Anime": _anilist_random,
f"{'🌟 ' if icons else ''}Most Popular Anime": AniList.get_most_popular,
f"{'💖 ' if icons else ''}Most Favourite Anime": AniList.get_most_favourite,
f"{'' if icons else ''}Most Scored Anime": AniList.get_most_scored,
f"{'🎬 ' if icons else ''}Upcoming Anime": AniList.get_upcoming_anime,
f"{'📝 ' if icons else ''}Edit Config": edit_config,
f"{'' if icons else ''}Exit": exit_app,
}
if config.use_fzf:

View File

@@ -1,13 +1,18 @@
import os
import shutil
import subprocess
import textwrap
from threading import Thread
from ... import APP_CACHE_DIR
import requests
from ...constants import APP_CACHE_DIR
from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema
from ...Utility import anilist_data_helper
from ...Utility.utils import remove_html_tags, sanitize_filename
from ..config import Config
fzf_preview = """
fzf_preview = r"""
#
# The purpose of this script is to demonstrate how to preview a file or an
# image in the preview window of fzf.
@@ -88,13 +93,22 @@ fzf-preview(){
SEARCH_RESULTS_CACHE = os.path.join(APP_CACHE_DIR, "search_results")
def aniskip(mal_id, episode):
ANISKIP = shutil.which("ani-skip")
if not ANISKIP:
print("Aniskip not found, please install and try again")
return
args = [ANISKIP, "-q", str(mal_id), "-e", str(episode)]
aniskip_result = subprocess.run(args, text=True, stdout=subprocess.PIPE)
if aniskip_result.returncode != 0:
return
mpv_skip_args = aniskip_result.stdout.strip()
return mpv_skip_args.split(" ")
def write_search_results(
search_results: list[AnilistBaseMediaDataSchema], config: Config
):
import textwrap
import requests
for anime in search_results:
if not os.path.exists(SEARCH_RESULTS_CACHE):
os.mkdir(SEARCH_RESULTS_CACHE)
@@ -126,7 +140,8 @@ def write_search_results(
Favourites: {anime['favourites']}
Status: {anime['status']}
Episodes: {anime['episodes']}
Genres: {anilist_data_helper.format_list_data_with_comma(anime['genres'])}
Genres: {anilist_data_helper.format_list_data_with_comma(
anime['genres'])}
Next Episode: {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])}
Start Date: {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
End Date: {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
@@ -136,13 +151,13 @@ def write_search_results(
template = textwrap.dedent(template)
template = f"""
{template}
{textwrap.fill(remove_html_tags(str(anime['description'])),width=45)}
{textwrap.fill(remove_html_tags(
str(anime['description'])), width=45)}
"""
f.write(template)
def get_preview(search_results: list[AnilistBaseMediaDataSchema], config: Config):
from threading import Thread
background_worker = Thread(
target=write_search_results, args=(search_results, config)

View File

@@ -1,23 +1,141 @@
import re
import shutil
import subprocess
from typing import Optional
# legacy
# def mpv(link, title: None | str = "anime", *custom_args):
# MPV = shutil.which("mpv")
# if not MPV:
# args = [
# "nohup",
# "am",
# "start",
# "--user",
# "0",
# "-a",
# "android.intent.action.VIEW",
# "-d",
# link,
# "-n",
# "is.xyz.mpv/.MPVActivity",
# ]
# subprocess.run(args)
# else:
# subprocess.run([MPV, *custom_args, f"--title={title}", link])
#
def mpv(link, title: None | str = "anime", *custom_args):
def stream_video(url, mpv_args, custom_args):
process = subprocess.Popen(
["mpv", url, *mpv_args, *custom_args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
last_time = None
av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)")
last_time = "0"
total_time = "0"
try:
while True:
output = process.stderr.readline()
if output:
# Match the timestamp in the output
match = av_time_pattern.search(output.strip())
if match:
current_time = match.group(1)
total_time = match.group(2)
match.group(3)
last_time = current_time
# print(f"Current stream time: {current_time}, Total time: {total_time}, Progress: {percentage}%")
# Check if the process has terminated
retcode = process.poll()
if retcode is not None:
print("Finshed at: ", last_time)
break
except Exception as e:
print(f"An error occurred: {e}")
finally:
process.terminate()
return last_time, total_time
def mpv(
link: str,
title: Optional[str] = "",
start_time: str = "0",
ytdl_format="",
custom_args=[],
):
# Determine if mpv is available
MPV = shutil.which("mpv")
# If title is None, set a default value
# Regex to check if the link is a YouTube URL
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
if not MPV:
args = [
"nohup",
"am",
"start",
"--user",
"0",
"-a",
"android.intent.action.VIEW",
"-d",
link,
"-n",
"is.xyz.mpv/.MPVActivity",
]
# Determine if the link is a YouTube URL
if re.match(youtube_regex, link):
# Android specific commands to launch mpv with a YouTube URL
args = [
"nohup",
"am",
"start",
"--user",
"0",
"-a",
"android.intent.action.VIEW",
"-d",
link,
"-n",
"com.google.android.youtube/.UrlActivity",
]
return "0"
else:
# Android specific commands to launch mpv with a regular URL
args = [
"nohup",
"am",
"start",
"--user",
"0",
"-a",
"android.intent.action.VIEW",
"-d",
link,
"-n",
"is.xyz.mpv/.MPVActivity",
]
subprocess.run(args)
return "0"
else:
subprocess.run([MPV, *custom_args, f"--title={title}", link])
# General mpv command with custom arguments
mpv_args = []
if start_time != "0":
mpv_args.append(f"--start={start_time}")
if title:
mpv_args.append(f"--title={title}")
if ytdl_format:
mpv_args.append(f"--ytdl-format={ytdl_format}")
stop_time, total_time = stream_video(link, mpv_args, custom_args)
return stop_time, total_time
# Example usage
if __name__ == "__main__":
mpv(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"Example Video",
"--fullscreen",
"--volume=50",
)

View File

@@ -18,7 +18,7 @@ def exit_app(*args):
from rich import print
from ... import USER_NAME
from ...constants import USER_NAME
print("Have a good day :smile:", USER_NAME)
sys.exit(0)

View File

@@ -4,7 +4,7 @@ import os
from InquirerPy import inquirer
from thefuzz import fuzz
from ... import PLATFORM
from ...constants import PLATFORM
from ...Utility.data import anime_normalizer
logger = logging.getLogger(__name__)
@@ -54,20 +54,3 @@ def anime_title_percentage_match(
fuzz.ratio(title[1].lower(), possible_user_requested_anime_title.lower()),
)
return percentage_ratio
def get_selected_anime(anime_title, results):
def _get_result(result, compare):
return result["name"] == compare
return list(
filter(lambda x: _get_result(x, anime_title), results["shows"]["edges"])
)
def get_selected_server(_server, servers):
def _get_server(server, server_name):
return server[0] == server_name
server = list(filter(lambda x: _get_server(x, _server), servers)).pop()
return server

38
fastanime/constants.py Normal file
View File

@@ -0,0 +1,38 @@
import os
from platform import system
from platformdirs import PlatformDirs
from . import APP_NAME, AUTHOR
PLATFORM = system()
dirs = PlatformDirs(appname=APP_NAME, appauthor=AUTHOR, ensure_exists=True)
# ---- app deps ----
APP_DIR = os.path.abspath(os.path.dirname(__file__))
CONFIGS_DIR = os.path.join(APP_DIR, "configs")
ASSETS_DIR = os.path.join(APP_DIR, "assets")
if PLATFORM == "Windows":
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
else:
ICON_PATH = os.path.join(ASSETS_DIR, "logo.png")
# ----- user configs and data -----
APP_DATA_DIR = dirs.user_config_dir
if not APP_DATA_DIR:
APP_DATA_DIR = dirs.user_data_dir
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
NOTIFIER_LOG_FILE_PATH = os.path.join(APP_DATA_DIR, "notifier.log")
# cache dir
APP_CACHE_DIR = dirs.user_cache_dir
# video dir
USER_VIDEOS_DIR = os.path.join(dirs.user_videos_dir, APP_NAME)
USER_NAME = os.environ.get("USERNAME", f"{APP_NAME} user")

View File

@@ -1,184 +0,0 @@
"""
This is the core module availing all the abstractions of the anilist api
"""
import requests
from .anilist_data_schema import AnilistDataSchema
from .queries_graphql import (
airing_schedule_query,
anime_characters_query,
anime_query,
anime_relations_query,
most_favourite_query,
most_popular_query,
most_recently_updated_query,
most_scored_query,
recommended_query,
search_query,
trending_query,
upcoming_anime_query,
)
# from kivy.network.urlrequest import UrlRequestRequests
class AniList:
"""
This class provides an abstraction for the anilist api
"""
@classmethod
def get_data(
cls, query: str, variables: dict = {}
) -> tuple[bool, AnilistDataSchema]:
"""
The core abstraction for getting data from the anilist api
Parameters:
----------
query:str
a valid anilist graphql query
variables:dict
variables to pass to the anilist api
"""
url = "https://graphql.anilist.co"
# req=UrlRequestRequests(url, cls.got_data,)
try:
# TODO: check if data is as expected
response = requests.post(
url, json={"query": query, "variables": variables}, timeout=10
)
anilist_data: AnilistDataSchema = response.json()
return (True, anilist_data)
except requests.exceptions.Timeout:
return (
False,
{
"Error": "Timeout Exceeded for connection there might be a problem with your internet or anilist is down."
},
) # type: ignore
except requests.exceptions.ConnectionError:
return (
False,
{
"Error": "There might be a problem with your internet or anilist is down."
},
) # type: ignore
except Exception as e:
return (False, {"Error": f"{e}"}) # type: ignore
@classmethod
def search(
cls,
query: str | None = None,
sort: str | None = None,
genre_in: list[str] | None = None,
id_in: list[int] | None = None,
genre_not_in: list[str] = ["hentai"],
popularity_greater: int | None = None,
popularity_lesser: int | None = None,
averageScore_greater: int | None = None,
averageScore_lesser: int | None = None,
tag_in: list[str] | None = None,
tag_not_in: list[str] | None = None,
status: str | None = None,
status_in: list[str] | None = None,
status_not_in: list[str] | None = None,
endDate_greater: int | None = None,
endDate_lesser: int | None = None,
start_greater: int | None = None,
start_lesser: int | None = None,
page: int | None = None,
**kwargs,
):
"""
A powerful method for searching anime using the anilist api availing most of its options
"""
variables = {}
for key, val in list(locals().items())[1:]:
if val is not None and key not in ["variables"]:
variables[key] = val
search_results = cls.get_data(search_query, variables=variables)
return search_results
@classmethod
def get_anime(cls, id: int):
"""
Gets a single anime by a valid anilist anime id
"""
variables = {"id": id}
return cls.get_data(anime_query, variables)
@classmethod
def get_trending(cls, *_, **kwargs):
"""
Gets the currently trending anime
"""
trending = cls.get_data(trending_query)
return trending
@classmethod
def get_most_favourite(cls, *_, **kwargs):
"""
Gets the most favoured anime on anilist
"""
most_favourite = cls.get_data(most_favourite_query)
return most_favourite
@classmethod
def get_most_scored(cls, *_, **kwargs):
"""
Gets most scored anime on anilist
"""
most_scored = cls.get_data(most_scored_query)
return most_scored
@classmethod
def get_most_recently_updated(cls, *_, **kwargs):
"""
Gets most recently updated anime from anilist
"""
most_recently_updated = cls.get_data(most_recently_updated_query)
return most_recently_updated
@classmethod
def get_most_popular(cls, *_, **kwargs):
"""
Gets most popular anime on anilist
"""
most_popular = cls.get_data(most_popular_query)
return most_popular
# FIXME:dont know why its not giving useful data
@classmethod
def get_recommended_anime_for(cls, id: int, *_, **kwargs):
recommended_anime = cls.get_data(recommended_query)
return recommended_anime
@classmethod
def get_charcters_of(cls, id: int, *_, **kwargs):
variables = {"id": id}
characters = cls.get_data(anime_characters_query, variables)
return characters
@classmethod
def get_related_anime_for(cls, id: int, *_, **kwargs):
variables = {"id": id}
related_anime = cls.get_data(anime_relations_query, variables)
return related_anime
@classmethod
def get_airing_schedule_for(cls, id: int, *_, **kwargs):
variables = {"id": id}
airing_schedule = cls.get_data(airing_schedule_query, variables)
return airing_schedule
@classmethod
def get_upcoming_anime(cls, page: int = 1, *_, **kwargs):
"""
Gets upcoming anime from anilist
"""
variables = {"page": page}
upcoming_anime = cls.get_data(upcoming_anime_query, variables)
return upcoming_anime

View File

@@ -17,6 +17,14 @@ class AnilistImage(TypedDict):
large: str
class AnilistUser(TypedDict):
id: int
name: str
bannerImage: str | None
avatar: AnilistImage
token: str
class AnilistMediaTrailer(TypedDict):
id: str
site: str
@@ -49,11 +57,6 @@ class AnilistMediaNextAiringEpisode(TypedDict):
episode: int
class AnilistUser(TypedDict):
name: str
avatar: AnilistImage
class AnilistReview(TypedDict):
summary: str
user: AnilistUser
@@ -110,7 +113,8 @@ class AnilistBaseMediaDataSchema(TypedDict):
This a convenience class is used to type the received Anilist data to enhance dev experience
"""
id: str
id: int
idMal: int
title: AnilistMediaTitle
coverImage: AnilistImage
trailer: AnilistMediaTrailer | None

View File

@@ -0,0 +1,335 @@
"""
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, 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:
"""
This class provides an abstraction for the anilist api
"""
def login_user(self, token: str):
self.token = token
self.headers = {"Authorization": f"Bearer {self.token}"}
user = self.get_logged_in_user()
if not user:
return
if not user[0]:
return
user_info: AnilistUser = user[1]["data"]["Viewer"] # pyright:ignore
self.user_id = user_info["id"] # pyright:ignore
return user_info
def get_notification(self):
return self._make_authenticated_request(notification_query)
def reset_notification_count(self):
return self._make_authenticated_request(mark_as_read_mutation)
def update_login_info(self, user: AnilistUser, token: str):
self.token = token
self.headers = {"Authorization": f"Bearer {self.token}"}
self.user_id = user["id"]
def get_logged_in_user(self):
if not self.headers:
return
return self._make_authenticated_request(get_logged_in_user_query)
def update_anime_list(self, values_to_update: dict):
variables = {"userId": self.user_id, **values_to_update}
return self._make_authenticated_request(media_list_mutation, variables)
def get_anime_list(
self,
status: Literal[
"CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"
],
):
variables = {"status": status, "userId": self.user_id}
return self._make_authenticated_request(media_list_query, variables)
def get_medialist_entry(self, mediaId: int):
variables = {"mediaId": mediaId}
return self._make_authenticated_request(get_medialist_item_query, variables)
def delete_medialist_entry(self, mediaId: int):
result = self.get_medialist_entry(mediaId)
if not result[0]:
return result
id = result[1]["data"]["MediaList"]["id"]
variables = {"id": id}
return self._make_authenticated_request(delete_list_entry_query, variables)
def _make_authenticated_request(self, query: str, variables: dict = {}):
"""
The core abstraction for getting authenticated data from the anilist api
Parameters:
----------
query:str
a valid anilist graphql query
variables:dict
variables to pass to the anilist api
"""
# req=UrlRequestRequests(url, self.got_data,)
try:
# TODO: check if data is as expected
response = requests.post(
ANILIST_ENDPOINT,
json={"query": query, "variables": variables},
timeout=10,
headers=self.headers,
)
anilist_data = response.json()
# ensuring you dont get blocked
if (
int(response.headers.get("X-RateLimit-Remaining", 0)) < 5
and not response.status_code == 500
):
print(
"Warning you are exceeding the allowed number of calls per minute"
)
logger.warning(
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
)
print("Forced timeout will now be initiated")
import time
print("sleeping...")
time.sleep(1 * 60)
if response.status_code == 200:
return (True, anilist_data)
else:
return (False, anilist_data)
except requests.exceptions.Timeout:
logger.warning(
"Timeout has been exceeded this could mean anilist is down or you have lost internet connection"
)
return (
False,
{
"Error": "Timeout Exceeded for connection there might be a problem with your internet or anilist is down."
},
) # type: ignore
except requests.exceptions.ConnectionError:
logger.warning(
"ConnectionError this could mean anilist is down or you have lost internet connection"
)
return (
False,
{
"Error": "There might be a problem with your internet or anilist is down."
},
) # type: ignore
except Exception as e:
logger.error(f"Something unexpected occured {e}")
return (False, {"Error": f"{e}"}) # type: ignore
def get_watchlist(self):
variables = {"status": "CURRENT", "userId": self.user_id}
return self._make_authenticated_request(media_list_query, variables)
def get_data(
self, query: str, variables: dict = {}
) -> tuple[bool, AnilistDataSchema]:
"""
The core abstraction for getting 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,
)
anilist_data: AnilistDataSchema = response.json()
# ensuring you dont get blocked
if (
int(response.headers.get("X-RateLimit-Remaining", 0)) < 5
and not response.status_code == 500
):
print(
"Warning you are exceeding the allowed number of calls per minute"
)
logger.warning(
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
)
print("Forced timeout will now be initiated")
import time
print("sleeping...")
time.sleep(1 * 60)
if response.status_code == 200:
return (True, anilist_data)
else:
return (False, anilist_data)
except requests.exceptions.Timeout:
logger.warning(
"Timeout has been exceeded this could mean anilist is down or you have lost internet connection"
)
return (
False,
{
"Error": "Timeout Exceeded for connection there might be a problem with your internet or anilist is down."
},
) # type: ignore
except requests.exceptions.ConnectionError:
logger.warning(
"ConnectionError this could mean anilist is down or you have lost internet connection"
)
return (
False,
{
"Error": "There might be a problem with your internet or anilist is down."
},
) # type: ignore
except Exception as e:
logger.error(f"Something unexpected occured {e}")
return (False, {"Error": f"{e}"}) # type: ignore
def search(
self,
query: str | None = None,
sort: str | None = None,
genre_in: list[str] | None = None,
id_in: list[int] | None = None,
genre_not_in: list[str] = ["hentai"],
popularity_greater: int | None = None,
popularity_lesser: int | None = None,
averageScore_greater: int | None = None,
averageScore_lesser: int | None = None,
tag_in: list[str] | None = None,
tag_not_in: list[str] | None = None,
status: str | None = None,
status_in: list[str] | None = None,
status_not_in: list[str] | None = None,
endDate_greater: int | None = None,
endDate_lesser: int | None = None,
start_greater: int | None = None,
start_lesser: int | None = None,
page: int | None = None,
**kwargs,
):
"""
A powerful method for searching anime using the anilist api availing most of its options
"""
variables = {}
for key, val in list(locals().items())[1:]:
if val is not None and key not in ["variables"]:
variables[key] = val
search_results = self.get_data(search_query, variables=variables)
return search_results
def get_anime(self, id: int):
"""
Gets a single anime by a valid anilist anime id
"""
variables = {"id": id}
return self.get_data(anime_query, variables)
def get_trending(self, *_, **kwargs):
"""
Gets the currently trending anime
"""
trending = self.get_data(trending_query)
return trending
def get_most_favourite(self, *_, **kwargs):
"""
Gets the most favoured anime on anilist
"""
most_favourite = self.get_data(most_favourite_query)
return most_favourite
def get_most_scored(self, *_, **kwargs):
"""
Gets most scored anime on anilist
"""
most_scored = self.get_data(most_scored_query)
return most_scored
def get_most_recently_updated(self, *_, **kwargs):
"""
Gets most recently updated anime from anilist
"""
most_recently_updated = self.get_data(most_recently_updated_query)
return most_recently_updated
def get_most_popular(self):
"""
Gets most popular anime on anilist
"""
most_popular = self.get_data(most_popular_query)
return most_popular
# FIXME:dont know why its not giving useful data
def get_recommended_anime_for(self, id: int, *_, **kwargs):
recommended_anime = self.get_data(recommended_query)
return recommended_anime
def get_charcters_of(self, id: int, *_, **kwargs):
variables = {"id": id}
characters = self.get_data(anime_characters_query, variables)
return characters
def get_related_anime_for(self, id: int, *_, **kwargs):
variables = {"id": id}
related_anime = self.get_data(anime_relations_query, variables)
return related_anime
def get_airing_schedule_for(self, id: int, *_, **kwargs):
variables = {"id": id}
airing_schedule = self.get_data(airing_schedule_query, variables)
return airing_schedule
def get_upcoming_anime(self, page: int = 1, *_, **kwargs):
"""
Gets upcoming anime from anilist
"""
variables = {"page": page}
upcoming_anime = self.get_data(upcoming_anime_query, variables)
return upcoming_anime

View File

@@ -3,6 +3,203 @@ This module contains all the preset queries for the sake of neatness and convini
Mostly for internal usage
"""
mark_as_read_mutation = """
mutation{
UpdateUser{
unreadNotificationCount
}
}
"""
reviews_query = """
query($id:Int){
Page{
pageInfo{
total
}
reviews(mediaId:$id){
summary
user{
name
avatar {
large
medium
}
}
body
}
}
}
"""
notification_query = """
query{
Page {
pageInfo {
total
}
notifications(resetNotificationCount:true,type:AIRING) {
... on AiringNotification {
id
type
episode
contexts
createdAt
media {
id
idMal
title {
romaji
english
}
coverImage{
medium
}
}
}
}
}
}
"""
get_medialist_item_query = """
query($mediaId:Int){
MediaList(mediaId:$mediaId){
id
}
}
"""
delete_list_entry_query = """
mutation($id:Int){
DeleteMediaListEntry(id:$id){
deleted
}
}
"""
get_logged_in_user_query = """
query{
Viewer{
id
name
bannerImage
avatar {
large
medium
}
}
}
"""
media_list_mutation = """
mutation($mediaId:Int,$scoreRaw:Int,$repeat:Int,$progress:Int,$status:MediaListStatus){
SaveMediaListEntry(mediaId:$mediaId,scoreRaw:$scoreRaw,progress:$progress,repeat:$repeat,status:$status){
id
status
mediaId
score
progress
repeat
startedAt {
year
month
day
}
completedAt {
year
month
day
}
}
}
"""
media_list_query = """
query ($userId: Int, $status: MediaListStatus) {
Page {
pageInfo {
currentPage
total
}
mediaList(userId: $userId, status: $status) {
mediaId
media {
id
idMal
title {
romaji
english
}
coverImage {
medium
large
}
trailer {
site
id
}
popularity
favourites
averageScore
episodes
genres
studios {
nodes {
name
isAnimationStudio
}
}
tags {
name
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
description
nextAiringEpisode {
timeUntilAiring
airingAt
episode
}
}
status
progress
score
repeat
notes
startedAt {
year
month
day
}
completedAt {
year
month
day
}
createdAt
}
}
}
"""
optional_variables = "\
$page:Int,\
$sort:[MediaSort],\
@@ -57,6 +254,7 @@ query($query:String,%s){
)
{
id
idMal
title{
romaji
english
@@ -114,6 +312,7 @@ query{
media(sort:TRENDING_DESC,type:ANIME,genre_not_in:["hentai"]){
id
idMal
title{
romaji
english
@@ -168,6 +367,7 @@ query{
Page(perPage:15){
media(sort:FAVOURITES_DESC,type:ANIME,genre_not_in:["hentai"]){
id
idMal
title{
romaji
english
@@ -222,6 +422,7 @@ query{
Page(perPage:15){
media(sort:SCORE_DESC,type:ANIME,genre_not_in:["hentai"]){
id
idMal
title{
romaji
english
@@ -276,6 +477,7 @@ query{
Page(perPage:15){
media(sort:POPULARITY_DESC,type:ANIME,genre_not_in:["hentai"]){
id
idMal
title{
romaji
english
@@ -330,6 +532,7 @@ query{
Page(perPage:15){
media(sort:UPDATED_AT_DESC,type:ANIME,averageScore_greater:50,genre_not_in:["hentai"],status:RELEASING){
id
idMal
title{
romaji
english
@@ -386,6 +589,7 @@ query {
nodes{
media{
id
idMal
title{
english
romaji
@@ -475,6 +679,7 @@ query ($id: Int) {
relations {
nodes {
id
idMal
title {
english
romaji
@@ -548,6 +753,7 @@ query ($page: Int) {
}
media(type: ANIME, status: NOT_YET_RELEASED,sort:POPULARITY_DESC,genre_not_in:["hentai"]) {
id
idMal
title {
romaji
english
@@ -601,6 +807,7 @@ query($id:Int){
Page{
media(id:$id) {
id
idMal
title {
romaji
english

View File

@@ -1,6 +1,7 @@
from .allanime.api import AllAnimeAPI
from .animepahe.api import AnimePaheApi
anime_sources = {"allanime": AllAnimeAPI}
anime_sources = {"allanime": AllAnimeAPI, "animepahe": AnimePaheApi}
class Anime_Provider:

View File

@@ -4,8 +4,6 @@ from typing import Iterator
import requests
from requests.exceptions import Timeout
from rich import print
from rich.progress import Progress
from ....libs.anime_provider.allanime.types import AllAnimeEpisode
from ....libs.anime_provider.types import Anime, Server
@@ -24,6 +22,7 @@ Logger = logging.getLogger(__name__)
# TODO: create tests for the api
#
# ** Based on ani-cli **
class AllAnimeAPI:
"""
Provides a fast and effective interface to AllAnime site.
@@ -42,15 +41,17 @@ class AllAnimeAPI:
headers={"Referer": ALLANIME_REFERER, "User-Agent": USER_AGENT},
timeout=10,
)
return response.json()["data"]
except Timeout as e:
print(
"Timeout has been exceeded :cry:. This could mean allanime is down or your internet is down"
if response.status_code == 200:
return response.json()["data"]
else:
Logger.error("allanime(ERROR): ", response.text)
return {}
except Timeout:
Logger.error(
"allanime(Error):Timeout exceeded this could mean allanime is down or you have lost internet connection"
)
Logger.error(f"allanime(Error): {e}")
return {}
except Exception as e:
print("sth went wrong :confused:")
Logger.error(f"allanime:Error: {e}")
return {}
@@ -75,22 +76,20 @@ class AllAnimeAPI:
"countryorigin": countryorigin,
}
try:
with Progress() as progress:
progress.add_task("[cyan]searching..", start=False, total=None)
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
return normalize_search_results(search_results) # pyright:ignore
except Exception:
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
return normalize_search_results(search_results) # pyright:ignore
except Exception as e:
Logger.error(f"FA(AllAnime): {e}")
return {}
def get_anime(self, allanime_show_id: str):
variables = {"showId": allanime_show_id}
try:
with Progress() as progress:
progress.add_task("[cyan]fetching anime..", start=False, total=None)
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
return normalize_anime(anime["show"])
except Exception:
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
return normalize_anime(anime["show"])
except Exception as e:
Logger.error(f"FA(AllAnime): {e}")
return None
def get_anime_episode(
@@ -102,11 +101,10 @@ class AllAnimeAPI:
"episodeString": episode_string,
}
try:
with Progress() as progress:
progress.add_task("[cyan]fetching episode..", start=False, total=None)
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
return episode["episode"] # pyright: ignore
except Exception:
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
return episode["episode"] # pyright: ignore
except Exception as e:
Logger.error(f"FA(AllAnime): {e}")
return {}
def get_episode_streams(
@@ -121,99 +119,107 @@ class AllAnimeAPI:
embeds = allanime_episode["sourceUrls"]
try:
with Progress() as progress:
progress.add_task("[cyan]fetching streams..", start=False, total=None)
for embed in embeds:
try:
# filter the working streams
if embed.get("sourceName", "") not in (
"Sak",
"Kir",
"S-mp4",
"Luf-mp4",
):
continue
url = embed.get("sourceUrl")
for embed in embeds:
try:
# filter the working streams
if embed.get("sourceName", "") not in (
"Sak",
"Kir",
"S-mp4",
"Luf-mp4",
"Default",
):
continue
url = embed.get("sourceUrl")
if not url:
continue
if url.startswith("--"):
url = url[2:]
if not url:
continue
if url.startswith("--"):
url = url[2:]
# get the stream url for an episode of the defined source names
parsed_url = decode_hex_string(url)
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock','clock.json')}"
resp = requests.get(
embed_url,
headers={
"Referer": ALLANIME_REFERER,
"User-Agent": USER_AGENT,
},
timeout=10,
)
if resp.status_code == 200:
match embed["sourceName"]:
case "Luf-mp4":
Logger.debug(
"allanime:Found streams from gogoanime"
# get the stream url for an episode of the defined source names
parsed_url = decode_hex_string(url)
embed_url = f"https://{ALLANIME_BASE}{parsed_url.replace('clock', 'clock.json')}"
resp = requests.get(
embed_url,
headers={
"Referer": ALLANIME_REFERER,
"User-Agent": USER_AGENT,
},
timeout=10,
)
if resp.status_code == 200:
match embed["sourceName"]:
case "Luf-mp4":
Logger.debug("allanime:Found streams from gogoanime")
yield {
"server": "gogoanime",
"episode_title": (
allanime_episode["notes"]
or f'{
anime["title"]}'
)
print("[yellow]GogoAnime Fetched")
yield {
"server": "gogoanime",
"episode_title": (
allanime_episode["notes"]
or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "Kir":
Logger.debug(
"allanime:Found streams from wetransfer"
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "Kir":
Logger.debug("allanime:Found streams from wetransfer")
yield {
"server": "wetransfer",
"episode_title": (
allanime_episode["notes"]
or f'{
anime["title"]}'
)
print("[yellow]WeTransfer Fetched")
yield {
"server": "wetransfer",
"episode_title": (
allanime_episode["notes"]
or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "S-mp4":
Logger.debug(
"allanime:Found streams from sharepoint"
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "S-mp4":
Logger.debug("allanime:Found streams from sharepoint")
yield {
"server": "sharepoint",
"episode_title": (
allanime_episode["notes"]
or f'{
anime["title"]}'
)
print("[yellow]Sharepoint Fetched")
yield {
"server": "sharepoint",
"episode_title": (
allanime_episode["notes"]
or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "Sak":
Logger.debug("allanime:Found streams from dropbox")
print("[yellow]Dropbox Fetched")
yield {
"server": "dropbox",
"episode_title": (
allanime_episode["notes"]
or f'{anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
except Timeout:
print(
"Timeout has been exceeded :cry: this could mean allanime is down or your internet connection is poor"
)
except Exception as e:
print("Sth went wrong :confused:", e)
except Exception:
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "Sak":
Logger.debug("allanime:Found streams from dropbox")
yield {
"server": "dropbox",
"episode_title": (
allanime_episode["notes"]
or f'{
anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
case "Default":
Logger.debug("allanime:Found streams from wixmp")
yield {
"server": "wixmp",
"episode_title": (
allanime_episode["notes"]
or f'{
anime["title"]}'
)
+ f"; Episode {episode_number}",
"links": resp.json()["links"],
} # pyright:ignore
except Timeout:
Logger.error(
"Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
)
return []
except Exception as e:
Logger.error(f"FA(Allanime): {e}")
return []
except Exception as e:
Logger.error(f"FA(Allanime): {e}")
return []

View File

@@ -4,4 +4,4 @@ ALLANIME_BASE = "allanime.day"
ALLANIME_REFERER = "https://allanime.to/"
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
USER_AGENT = random_user_agent()
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer"]
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp"]

View File

@@ -0,0 +1,64 @@
import requests
from .constants import ANIMEPAHE_BASE, ANIMEPAHE_ENDPOINT, REQUEST_HEADERS
class AnimePaheApi:
def search_for_anime(self, user_query, *args):
try:
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
headers = {**REQUEST_HEADERS}
response = requests.get(url, headers=headers)
if not response.status_code == 200:
return
data = response.json()
return {
"pageInfo": {"total": data["total"]},
"results": [
{
"id": result["session"],
"title": result["title"],
"availableEpisodes": result["episodes"],
"type": result["type"],
}
for result in data["data"]
],
}
except Exception as e:
print(e)
input()
def get_anime(self, session_id: str, *args):
url = "https://animepahe.ru/api?m=release&id=&sort=episode_asc&page=1"
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={
session_id}&sort=episode_asc&page=1"
response = requests.get(url, headers=REQUEST_HEADERS)
if not response.status_code == 200:
return
data = response.json()
self.current = data
episodes = list(map(str, range(data["total"])))
return {
"id": session_id,
"title": "none",
"availableEpisodesDetail": {
"sub": episodes,
"dub": episodes,
"raw": episodes,
},
}
def get_episode_streams(self, anime, episode, *args):
episode_id = self.current["data"][int(episode)]["session"]
anime_id = anime["id"]
url = f"{ANIMEPAHE_BASE}play/{anime_id}{episode_id}"
response = requests.get(url, headers=REQUEST_HEADERS)
print(response.status_code)
input()
if not response.status_code == 200:
print(response.text)
return
print(response.text)
input()

View File

@@ -0,0 +1,22 @@
from yt_dlp.utils.networking import random_user_agent
USER_AGENT = random_user_agent()
ANIMEPAHE = "animepahe.ru"
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}/"
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
REQUEST_HEADERS = {
"Cookie": "__ddgid_=VvX0ebHrH2DsFZo4; __ddgmark_=3savRpSVFhvZcn5x; __ddg2_=buBJ3c4pNBYKFZNp; __ddg1_=rbVADKr9URtt55zoIGFa; SERVERID=janna; XSRF-TOKEN=eyJpdiI6IjV5bFNtd0phUHgvWGJxc25wL0VJSUE9PSIsInZhbHVlIjoicEJTZktlR2hxR2JZTWhnL0JzazlvZU5TQTR2bjBWZ2dDb0RwUXVUUWNSclhQWUhLRStYSmJmWmUxWkpiYkFRYU12RjFWejlSWHorME1wZG5qQ1U0TnFlNnBFR2laQjN1MjdyNjc5TjVPdXdJb2o5VkU1bEduRW9pRHNDTHh6Sy8iLCJtYWMiOiI0OTc0ZmNjY2UwMGJkOWY2MWNkM2NlMjk2ZGMyZGJmMWE0NTdjZTdkNGI2Y2IwNTIzZmFiZWU5ZTE2OTk0YmU4IiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6ImxvdlpqREFnTjdaeFJubUlXQWlJVWc9PSIsInZhbHVlIjoiQnE4R3VHdjZ4M1NDdEVWM1ZqMUxtNnVERnJCcmtCUHZKNzRPR2RFbzNFcStTL29xdnVTbWhsNVRBUXEybVZWNU1UYVlTazFqYlN5UjJva1k4czNGaXBTbkJJK01oTUd3VHRYVHBoc3dGUWxHYnFlS2NJVVNFbTFqMVBWdFpuVUgiLCJtYWMiOiI1NDdjZTVkYmNhNjUwZTMxZmRlZmVmMmRlMGNiYjAwYjlmYjFjY2U0MDc1YTQzZThiMTIxMjJlYTg1NTA4YjBmIiwidGFnIjoiIn0%3D; latest=5592 ",
"Host": ANIMEPAHE,
"User-Agent": USER_AGENT,
"Accept": "application , text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Referer": ANIMEPAHE_BASE,
"X-Requested-With": "XMLHttpRequest",
"DNT": "1",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"TE": "trailers",
}

View File

View File

@@ -0,0 +1,22 @@
import requests
ANISKIP_ENDPOINT = "https://api.aniskip.com/v1/skip-times"
# TODO: Finish own implementation of aniskip script
class AniSkip:
@classmethod
def get_skip_times(
cls, mal_id: int, episode_number: float | int, types=["op", "ed"]
):
url = f"{ANISKIP_ENDPOINT}/{mal_id}/{episode_number}?types=op&types=ed"
response = requests.get(url)
print(response.text)
return response.json()
if __name__ == "__main__":
mal_id = input("Mal id: ")
episode_number = input("episode_number: ")
skip_times = AniSkip.get_skip_times(int(mal_id), float(episode_number))
print(skip_times)

View File

@@ -8,7 +8,7 @@ from typing import Callable, List
from art import text2art
from rich import print
from ... import PLATFORM
from ...constants import PLATFORM
from .config import FZF_DEFAULT_OPTS, FzfOptions
logger = logging.getLogger(__name__)

33
poetry.lock generated
View File

@@ -660,6 +660,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"
@@ -771,13 +788,13 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pytest"
version = "8.3.1"
version = "8.3.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"},
{file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"},
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
]
[package.dependencies]
@@ -1202,13 +1219,13 @@ files = [
[[package]]
name = "yt-dlp"
version = "2024.7.16"
version = "2024.7.25"
description = "A feature-rich command-line audio/video downloader"
optional = false
python-versions = ">=3.8"
files = [
{file = "yt_dlp-2024.7.16-py3-none-any.whl", hash = "sha256:424805a112e757b141e767bc938d49db56d13d6415a92fa4cd8acadd55790be0"},
{file = "yt_dlp-2024.7.16.tar.gz", hash = "sha256:c5bd517a49dea1923ec8e14f51858f10fd89dfece14cb701392b480b41b2f516"},
{file = "yt_dlp-2024.7.25-py3-none-any.whl", hash = "sha256:f44b5f33776b4f718900c670fe6e4698fb6fcd426455cd837cf25a1d6d4d9560"},
{file = "yt_dlp-2024.7.25.tar.gz", hash = "sha256:7587aa25e236cf7b14bdb9378bbffff51202d901b04202be0cf62cbb56d3b52c"},
]
[package.dependencies]
@@ -1222,7 +1239,7 @@ urllib3 = ">=1.26.17,<3"
websockets = ">=12.0"
[package.extras]
build = ["build", "hatchling", "pip", "setuptools", "wheel"]
build = ["build", "hatchling", "pip", "setuptools (>=71.0.2)", "wheel"]
curl-cffi = ["curl-cffi (==0.5.10)", "curl-cffi (>=0.5.10,<0.6.dev0 || ==0.7.*)"]
dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "ruff (>=0.5.0,<0.6.0)"]
py2exe = ["py2exe (>=0.12)"]
@@ -1234,4 +1251,4 @@ test = ["pytest (>=8.1,<9.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "b6f2f120c8a562e8c8d98aae75f1e5fc4dd779d2da60fdcff6b98bf88008f23b"
content-hash = "e954480b704eff2f72debbdd91c97f607262e8d9df1adfb382d33e584036788f"

View File

@@ -1,72 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
import sys
import os
from kivy_deps import sdl2, glew
from kivymd.icon_definitions import md_icons
from kivymd import hooks_path as kivymd_hooks_path
path = os.path.abspath(".")
kv_file_paths = []
app_dir = os.path.join(os.getcwd(),"anixstream")
print(app_dir)
views_folder = os.path.join(app_dir,"View")
for dirpath,dirnames,filenames in os.walk(views_folder):
for filename in filenames:
if os.path.splitext(filename)[1]==".kv":
kv_file = os.path.join(dirpath,filename)
kv_file_paths.append((kv_file,"./Views/"))
a = Analysis(
['./anixstream/__main__.py'],
datas=[ *kv_file_paths,
(f'{app_dir}./assets/*', './assets/'),(f"{app_dir}./data/*","./data/"),(f"{app_dir}./configs/*","./configs/")
],
pathex=[path],
hiddenimports=["kivymd.icon_definitions.md_icons","plyer.platforms","plyer.platforms.win","plyer.platforms.win.storagepath","win32timezone"],
hookspath=[kivymd_hooks_path],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
*[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
debug=False,
strip=False,
upx=True,
name="AniXStream",
console=False,
icon=f"{app_dir}./assets/logo.ico",
exclude_binaries=True,
bootloader_ignore_signals=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='AniXStream',
)

View File

@@ -1,8 +1,8 @@
[tool.poetry]
name = "fastanime"
version = "0.31.0"
description = "A fast and efficient GUI and CLI anime scrapper"
authors = ["Benex254 <benedictx855@gmail.com>"]
version = "0.4.0"
description = "A fast and efficient anime scrapper and exploration tool"
authors = ["Benextempest <benextempest@gmail.com>"]
license = "UNLICENSE"
readme = "README.md"
@@ -17,8 +17,8 @@ art = "^6.2"
python-dotenv = "^1.0.1"
thefuzz = "^0.22.1"
requests = "^2.32.3"
[tool.poetry.group.dev]
optional = true
plyer = "^2.1.0"
[tool.poetry.group.dev.dependencies]
black = "^24.4.2"
isort = "^5.13.2"
@@ -26,7 +26,6 @@ pytest = "^8.2.2"
ruff = "^0.4.10"
pre-commit = "^3.7.1"
autoflake = "^2.3.1"
# bandit = "^1.7.9"
[build-system]
requires = ["poetry-core"]
@@ -34,9 +33,3 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
fastanime = 'fastanime:FastAnime'
# FILE: .bandit
# [tool.bandit]
#exclude = tests,path/to/file
#tests = B201,B301
# skips = ["B311", "B603", "B607", "B404"]

View File

@@ -1,9 +0,0 @@
kivy
yt-dlp
ffpyplayer
plyer
https://github.com/kivymd/KivyMD/archive/master.zip
fuzzywuzzy
python-Levenshtein
rich
click