Compare commits

..

31 Commits

Author SHA1 Message Date
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
BenedictX
bc909397d5 Update pyproject.toml 2024-07-25 19:29:33 +03:00
Benex254
f3d88f9825 feat(cli): switch to using AnimeProvider obj 2024-07-25 18:12:27 +03:00
Benex254
eb7bef72b3 feat(anilist_interface): remove legacy methods 2024-07-25 18:10:11 +03:00
Benex254
f6ec094bc7 feat(allanime): change typing from generator to iterator 2024-07-25 18:03:29 +03:00
Benex254
3f1bf1781a feat: implement AnimeProvider obj to manage providers 2024-07-25 17:59:32 +03:00
Benex254
21167fc208 docs: update readme 2024-07-25 14:23:34 +03:00
Benex254
c7c6ff92c4 feat(config): set default format to accept .mp4 2024-07-25 13:22:53 +03:00
Benex254
78319731c0 feat: add yt-dlp format option 2024-07-25 13:10:21 +03:00
Benex254
b619a11db1 chore: use latest version of python for publising 2024-07-25 11:25:28 +03:00
Benex254
022420aa4c feat(anilist): implement anilist random subcommand 2024-07-25 11:23:24 +03:00
Benex254
a7e46d9c18 feat(anilist_interface): ensure the app does not exit when trailer not found 2024-07-25 11:23:24 +03:00
Benex254
5e2826be4e feat(mpv): add typing for mpv title option 2024-07-25 11:23:24 +03:00
Benex254
5e314e2bca docs: update readme 2024-07-25 11:23:24 +03:00
Benex254
3d23854d89 feat(cli): rename translation_type option to translation-type 2024-07-25 11:23:24 +03:00
Benex254
80a25d24a3 chore: renamed build action 2024-07-25 11:23:24 +03:00
BenedictX
1ad7929c66 ci: Update publish.yml 2024-07-25 02:21:33 +03:00
BenedictX
0670bd735c ci: Create publish.yml 2024-07-25 02:14:53 +03:00
68 changed files with 655 additions and 1081 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

View File

@@ -1,4 +1,4 @@
name: build
name: debug_build
on: push
jobs:
ci:

66
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.
name: Upload Python Package
on:
release:
types: [published]
permissions:
contents: read
jobs:
release-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build release distributions
run: |
# NOTE: put your own distribution build steps here.
python -m pip install build
python -m build
- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: release-dists
path: dist/
pypi-publish:
runs-on: ubuntu-latest
needs:
- release-build
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
# Dedicated environments with protections for publishing are strongly recommended.
environment:
name: pypi
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
# url: https://pypi.org/p/YOURPROJECT
steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v4
with:
name: release-dists
path: dist/
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

View File

@@ -12,14 +12,15 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
> [!NOTE]
>
> Also the docs are still being worked on and are far from completion.
> The docs are still being worked on and are far from completion.
## Table of Contents
- [Installation](#installation)
- [Using pip](#using-pip)
- [Using pipx](#using-pipx)
- [Pre-built binaries](#pre-built-binaries)
- [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)
@@ -36,22 +37,46 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
## Installation
### Using pip
The app can run wherever python can run. So all you need to have is python installed on your device.
On android you can use [termux](https://github.com/termux/termux-app).
If you have any difficulty consult for help on the [discord channel](https://discord.gg/HRjySFjQ)
```bash
pip install https://github.com/Benex254/FastAnime/releases/download/v0.30.0/fastanime-0.3.0.tar.gz
```
### Installation using your favourite package manager
### Using pipx
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
The app is published approximately after every 14 days, which will include accumulative changes during that period.
#### Using pipx
Preferred method of installation since [Pipx](https://github.com/pypa/pipx) creates an isolated environment for each app it installs.
```bash
pipx install https://github.com/Benex254/FastAnime/releases/download/v0.30.0/fastanime-0.3.0.tar.gz
pipx install fastanime
```
### Pre-built binaries
#### Using pip
We will soon release pre-built binaries for Linux and Windows.
```bash
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.
Then:
```bash
unzip fastanime_debug_build
# outputs fastanime<version>.tar.gz
pipx install fastanime<version>.tar.gz
# --- or ---
pip install fastanime<version>.tar.gz
```
### Building from the source
@@ -87,6 +112,7 @@ fastanime --version
> [!Tip]
>
> Download the completions from [here](https://github.com/Benex254/FastAnime/tree/master/completions) for your shell.
> To add completions:
>
> - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/`
@@ -107,7 +133,6 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
**Other dependecies that will just make your experience better:**
- [fzf](https://github.com/junegunn/fzf) :fire: which is used as a better alternative to the ui.
- [fzf-preview](https://github.com/junegunn/fzf/blob/master/bin/fzf-preview.sh) a script that is used to preview images and is maintained by the devs of fzf.
- [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]()!!
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
@@ -136,6 +161,21 @@ Overview of main commands:
Configuration is directly passed into this command at run time to overide your config.
Available options include:
- `--server;-s <server>` set the default server to auto select
- `--continue;-c/--no-continue;-no-c` whether to continue from the last episode you were watching
- `--quality;-q <0|1|2|3>` the link to choose from server
- `--translation-type;- <dub|sub` what language for anime
- `--auto-select;-a/--no-auto-select;-no-a` auto select title from provider results
- `--auto-next;-A;/--no-auto-next;-no-A` auto select next episode
- `-downloads-dir;-d <path>` set the folder to download anime into
- `--fzf` use fzf for the ui
- `--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`
#### The anilist command
Stream, browse, and discover anime efficiently from the terminal using the [AniList API](https://github.com/AniList/ApiV2-GraphQL-Docs).
@@ -154,6 +194,7 @@ The subcommands are mainly their as convenience. Since all the features already
- `fastanime anilist upcoming`: Top 15 upcoming anime.
- `fastanime anilist popular`: Top 15 popular anime.
- `fastanime anilist favourites`: Top 15 favorite anime.
- `fastanime anilist random`: get random anime
#### download subcommand
@@ -203,6 +244,10 @@ View and stream the anime you downloaded using MPV.
```bash
fastanime downloads
# to get the path to the downloads folder set
fastanime downloads --path
# useful when you want to use the value for other programs
```
#### config subcommand
@@ -213,6 +258,9 @@ Edit FastAnime configuration settings using your preferred editor (based on `$ED
```bash
fastanime config
# to get config path which is useful if you want to use it for another program.
fastanime config --path
```
> [!Note]
@@ -233,10 +281,19 @@ auto_next = False # Auto-select next episode
# Note this wont always be correct.But 99% of the time will be.
auto_select=True
# the format of downloaded anime and trailer
# based on yt-dlp format and passed directly to it
# learn more by looking it up on their site
# only works for downloaded anime if server=gogoanime
# since its the only one that offers different formats
# the others tend not to
format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
[general]
preferred_language = romaji # Display language (options: english, romaji)
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
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
[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

View File

@@ -0,0 +1,58 @@
from typing import Iterator
from .libs.anime_provider import anime_sources
from .libs.anime_provider.types import Anime, SearchResults, Server
class AnimeProvider:
"""
Class that manages all anime sources adding some extra functionality to them.
"""
PROVIDERS = list(anime_sources.keys())
provider = PROVIDERS[0]
def __init__(self, provider, dynamic=False, retries=0) -> None:
self.provider = provider
self.dynamic = dynamic
self.retries = retries
self.load_provider_obj()
def load_provider_obj(self):
anime_provider = anime_sources[self.provider]()
self.anime_provider = anime_provider
def search_for_anime(
self, user_query, translation_type: str = "sub", nsfw=True, unknown=True
) -> SearchResults | None:
anime_provider = self.anime_provider
try:
results = anime_provider.search_for_anime(
user_query, translation_type, nsfw, unknown
)
except Exception as e:
print(e)
results = None
return results # pyright:ignore
def get_anime(self, anime_id: str) -> Anime | None:
anime_provider = self.anime_provider
try:
results = anime_provider.get_anime(anime_id)
except Exception as e:
print(e)
results = None
return results
def get_episode_streams(
self, anime, episode: str, translation_type: str
) -> Iterator[Server] | None:
anime_provider = self.anime_provider
try:
results = anime_provider.get_episode_streams(
anime, episode, translation_type
)
except Exception as e:
print(e)
results = None
return results # pyright:ignore

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

@@ -49,7 +49,7 @@ class YtDLPDownloader:
self._thread.start()
# Function to download the file
def _download_file(self, url: str, download_dir, title, silent):
def _download_file(self, url: str, download_dir, title, silent, vid_format="best"):
anime_title = sanitize_filename(title[0])
episode_title = sanitize_filename(title[1])
ydl_opts = {
@@ -59,6 +59,7 @@ class YtDLPDownloader:
], # Progress hook
"silent": silent,
"verbose": False,
"format": vid_format,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:

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,7 +2,7 @@ import json
import logging
import os
from .. import USER_DATA_PATH
from ..constants import USER_DATA_PATH
logger = logging.getLogger(__name__)

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,8 @@
import logging
import os
import sys
from platform import platform
from dotenv import load_dotenv
from platformdirs import PlatformDirs
load_dotenv()
@@ -15,43 +13,12 @@ if os.environ.get("FA_RICH_TRACEBACK", False):
# initiate constants
__version__ = "v0.30.0"
__version__ = "v0.32.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():

3
fastanime/anilist.py Normal file
View File

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

View File

@@ -3,6 +3,7 @@ import signal
import click
from .. import __version__
from ..libs.anime_provider import anime_sources
from ..libs.anime_provider.allanime.constants import SERVERS_AVAILABLE
from ..Utility.data import anilist_sort_normalizer
from .commands.anilist import anilist
@@ -40,12 +41,24 @@ signal.signal(signal.SIGINT, handle_exit)
short_help="Stream Anime",
)
@click.version_option(__version__, "--version")
@click.option(
"-p",
"--provider",
type=click.Choice(list(anime_sources.keys()), case_sensitive=False),
help="Provider of your choice",
)
@click.option(
"-s",
"--server",
type=click.Choice(SERVERS_AVAILABLE, case_sensitive=False),
help="Server of choice",
)
@click.option(
"-f",
"--format",
type=str,
help="yt-dlp format to use",
)
@click.option(
"-c/-no-c",
"--continue/--no-continue",
@@ -61,7 +74,7 @@ signal.signal(signal.SIGINT, handle_exit)
)
@click.option(
"-t",
"--translation_type",
"--translation-type",
type=click.Choice(["dub", "sub"]),
help="Anime language[dub/sub]",
)
@@ -90,7 +103,9 @@ signal.signal(signal.SIGINT, handle_exit)
@click.pass_context
def run_cli(
ctx: click.Context,
provider,
server,
format,
continue_,
translation_type,
quality,
@@ -104,8 +119,13 @@ def run_cli(
no_preview,
):
ctx.obj = Config()
if provider:
ctx.obj.provider = provider
ctx.obj.load_config()
if server:
ctx.obj.server = server
if format:
ctx.obj.format = format
if ctx.get_parameter_source("continue_") == click.core.ParameterSource.COMMANDLINE:
ctx.obj.continue_from_history = continue_
if quality:

View File

@@ -4,6 +4,7 @@ from ...interfaces.anilist_interfaces import anilist as anilist_interface
from ...utils.tools import QueryDict
from .favourites import favourites
from .popular import popular
from .random_anime import random_anime
from .recent import recent
from .scores import scores
from .search import search
@@ -18,6 +19,7 @@ commands = {
"scores": scores,
"popular": popular,
"favourites": favourites,
"random": random_anime,
}

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,27 @@
import random
import click
from ....anilist import AniList
from ...interfaces.anilist_interfaces import select_anime
from ...utils.tools import QueryDict
@click.command(
help="Get random anime from anilist based on a range of anilist anime ids that are seected at random",
short_help="View random anime",
)
@click.pass_obj
def random_anime(config):
random_anime = range(1, 15000)
random_anime = random.sample(random_anime, k=50)
anime_data = AniList.search(id_in=list(random_anime))
if anime_data[0]:
anilist_config = QueryDict()
anilist_config.data = anime_data[1]
select_anime(config, anilist_config)
else:
print(anime_data[1])

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

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

@@ -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,8 +1,8 @@
import click
from rich import print
from rich.progress import Progress
from thefuzz import fuzz
from ...libs.anime_provider.allanime.api import anime_provider
from ...libs.anime_provider.types import Anime
from ...libs.fzf import fzf
from ...Utility.downloader.downloader import downloader
@@ -26,11 +26,19 @@ from ..utils.utils import clear
)
@click.pass_obj
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")
download(config, anime_title, episode_range)
return
search_results = search_results["results"]
search_results_ = {
search_result["title"]: search_result for search_result in search_results
@@ -46,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...")
@@ -65,22 +77,30 @@ 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, (anime["title"], streams[0]["episode_title"]), True
link,
download_dir,
(anime["title"], streams[0]["episode_title"]),
True,
config.format,
)
except Exception as e:
print(e)

View File

@@ -1,9 +1,9 @@
import click
from rich import print
from rich.progress import Progress
from thefuzz import fuzz
from ...cli.config import Config
from ...libs.anime_provider.allanime.api import anime_provider
from ...libs.anime_provider.types import Anime
from ...libs.fzf import fzf
from ..utils.mpv import mpv
@@ -23,9 +23,17 @@ from ..utils.utils import clear
@click.argument("anime_title", required=True, type=str)
@click.pass_obj
def search(config: Config, anime_title: str, episode_range: str):
search_results = anime_provider.search_for_anime(
anime_title, config.translation_type
)
anime_provider = config.anime_provider
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")
search(config, anime_title, episode_range)
return
search_results = search_results["results"]
if not search_results:
print("Anime not found :cry:")
@@ -45,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...")
@@ -77,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,7 +3,8 @@ 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
@@ -28,6 +29,8 @@ class Config(object):
"preferred_language": "english",
"use_fzf": "False",
"preview": "False",
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
"provider": "allanime",
}
)
self.configparser.add_section("stream")
@@ -40,6 +43,7 @@ class Config(object):
# --- set defaults ---
self.downloads_dir = self.get_downloads_dir()
self.provider = self.get_provider()
self.use_fzf = self.get_use_fzf()
self.preview = self.get_preview()
self.translation_type = self.get_translation_type()
@@ -49,12 +53,15 @@ class Config(object):
self.auto_select = self.get_auto_select()
self.quality = self.get_quality()
self.server = self.get_server()
self.format = self.get_format()
self.preferred_language = self.get_preferred_language()
# ---- 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.anime_provider = AnimeProvider(self.provider)
def update_watch_history(self, anime_id: int, episode: str | None):
self.watch_history.update({str(anime_id): episode})
user_data_helper.update_watch_history(self.watch_history)
@@ -72,6 +79,9 @@ class Config(object):
print("Succesfully added :smile:")
input("Enter to continue...")
def get_provider(self):
return self.configparser.get("general", "provider")
def get_downloads_dir(self):
return self.configparser.get("general", "downloads_dir")
@@ -105,6 +115,9 @@ class Config(object):
def get_server(self):
return self.configparser.get("stream", "server")
def get_format(self):
return self.configparser.get("stream", "format")
def update_config(self, section: str, key: str, value: str):
self.configparser.set(section, key, value)
with open(USER_CONFIG_PATH, "w") as config:

View File

@@ -4,12 +4,12 @@ import os
import random
from rich import print
from rich.progress import Progress
from rich.prompt import 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.allanime.api import anime_provider
from ...libs.anime_provider.types import Anime, SearchResult, Server
from ...libs.fzf import fzf
from ...Utility.data import anime_normalizer
@@ -22,10 +22,9 @@ from ..utils.utils import clear, fuzzy_inquirer
def player_controls(config: Config, anilist_config: QueryDict):
# user config
translation_type: str = config.translation_type.lower()
config.translation_type.lower()
# internal config
anime: Anime = anilist_config.anime
current_episode: str = anilist_config.episode_number
episodes: list = sorted(anilist_config.episodes, key=float)
links: list = anilist_config.current_stream_links
@@ -55,19 +54,8 @@ def player_controls(config: Config, anilist_config: QueryDict):
next_episode = episodes.index(current_episode) + 1
if next_episode >= len(episodes):
next_episode = len(episodes) - 1
episode = anime_provider.get_anime_episode(
anime["id"], episodes[next_episode], translation_type
)
if not episode:
print(
"Sth went wrong :cry: this could mean the provider is down or your internet"
)
input("Enter to continue...")
_next_episode()
return
# update internal config
anilist_config.episode = episode
anilist_config.episode_number = episodes[next_episode]
# update user config
@@ -87,19 +75,6 @@ def player_controls(config: Config, anilist_config: QueryDict):
prev_episode = episodes.index(current_episode) - 1
if prev_episode <= 0:
prev_episode = 0
episode = anime_provider.get_anime_episode(
anime["id"], episodes[prev_episode], config.translation_type.lower()
)
if not episode:
print(
"Sth went wrong :cry: this could mean the provider is down or your internet"
)
input("Enter to continue...")
_previous_episode()
return
# update internal config
anilist_config.episode = episode
# anilist_config.episode_title = episode["title"]
anilist_config.episode_number = episodes[prev_episode]
@@ -177,19 +152,23 @@ def fetch_streams(config: Config, anilist_config: QueryDict):
anime_id: int = anilist_config.anime_id
anime: Anime = anilist_config.anime
translation_type = config.translation_type
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
@@ -280,22 +259,8 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
return
config.update_watch_history(anime_id, episode_number)
# get the episode info from provider
episode = anime_provider.get_anime_episode(
_anime["id"], episode_number, translation_type
)
if not episode:
print(
"Sth went wrong :cry: this could mean the provider is down or your internet"
)
input("Enter to continue...")
fetch_episode(config, anilist_config)
return
# update internal config
anilist_config.episodes = episodes
anilist_config.episode = episode
# anilist_config.episode_title = episode["title"]
anilist_config.episode_number = episode_number
@@ -305,7 +270,10 @@ def fetch_episode(config: Config, anilist_config: QueryDict):
def fetch_anime_episode(config, anilist_config: QueryDict):
selected_anime: SearchResult = anilist_config._anime
anilist_config.anime = anime_provider.get_anime(selected_anime["id"])
anime_provider = config.anime_provider
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(
@@ -326,11 +294,14 @@ def provide_anime(config: Config, anilist_config: QueryDict):
selected_anime_title = anilist_config.selected_anime_title
anime_data: AnilistBaseMediaDataSchema = anilist_config.selected_anime_anilist
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"
@@ -384,7 +355,11 @@ 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)
mpv(trailer_url, selected_anime_title, f"--ytdl-format={config.format}")
anilist_options(config, anilist_config)
else:
print("no trailer available :confused:")
input("Enter to continue...")
anilist_options(config, anilist_config)
def _add_to_list(config: Config, anilist_config: QueryDict):

View File

@@ -1,7 +1,11 @@
import os
import shutil
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
@@ -91,10 +95,6 @@ SEARCH_RESULTS_CACHE = os.path.join(APP_CACHE_DIR, "search_results")
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)
@@ -142,7 +142,6 @@ def write_search_results(
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,87 @@
import re
import shutil
import subprocess
from typing import Optional
def mpv(link, title="anime", *custom_args):
# 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: str, title: Optional[str] = "anime", *custom_args):
# Determine if mpv is available
MPV = shutil.which("mpv")
# If title is None, set a default value
if title is None:
title = "anime"
# Regex to check if the link is a YouTube URL
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
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",
]
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)
else:
subprocess.run([MPV, *custom_args, f"--title={title}", link])
# General mpv command with custom arguments
mpv_args = [MPV, *custom_args, f"--title={title}", link]
subprocess.run(mpv_args)
# 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

32
fastanime/constants.py Normal file
View File

@@ -0,0 +1,32 @@
import os
from platform import platform
from platformdirs import PlatformDirs
from . import APP_NAME, AUTHOR
PLATFORM = platform()
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)
USER_NAME = os.environ.get("USERNAME", f"{APP_NAME} user")

View File

@@ -23,14 +23,13 @@ from .queries_graphql import (
# from kivy.network.urlrequest import UrlRequestRequests
class AniList:
class AniListApi:
"""
This class provides an abstraction for the anilist api
"""
@classmethod
def get_data(
cls, query: str, variables: dict = {}
self, query: str, variables: dict = {}
) -> tuple[bool, AnilistDataSchema]:
"""
The core abstraction for getting data from the anilist api
@@ -43,7 +42,7 @@ class AniList:
variables to pass to the anilist api
"""
url = "https://graphql.anilist.co"
# req=UrlRequestRequests(url, cls.got_data,)
# req=UrlRequestRequests(url, self.got_data,)
try:
# TODO: check if data is as expected
response = requests.post(
@@ -68,9 +67,8 @@ class AniList:
except Exception as e:
return (False, {"Error": f"{e}"}) # type: ignore
@classmethod
def search(
cls,
self,
query: str | None = None,
sort: str | None = None,
genre_in: list[str] | None = None,
@@ -99,86 +97,75 @@ class AniList:
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)
search_results = self.get_data(search_query, variables=variables)
return search_results
@classmethod
def get_anime(cls, id: int):
def get_anime(self, id: int):
"""
Gets a single anime by a valid anilist anime id
"""
variables = {"id": id}
return cls.get_data(anime_query, variables)
return self.get_data(anime_query, variables)
@classmethod
def get_trending(cls, *_, **kwargs):
def get_trending(self, *_, **kwargs):
"""
Gets the currently trending anime
"""
trending = cls.get_data(trending_query)
trending = self.get_data(trending_query)
return trending
@classmethod
def get_most_favourite(cls, *_, **kwargs):
def get_most_favourite(self, *_, **kwargs):
"""
Gets the most favoured anime on anilist
"""
most_favourite = cls.get_data(most_favourite_query)
most_favourite = self.get_data(most_favourite_query)
return most_favourite
@classmethod
def get_most_scored(cls, *_, **kwargs):
def get_most_scored(self, *_, **kwargs):
"""
Gets most scored anime on anilist
"""
most_scored = cls.get_data(most_scored_query)
most_scored = self.get_data(most_scored_query)
return most_scored
@classmethod
def get_most_recently_updated(cls, *_, **kwargs):
def get_most_recently_updated(self, *_, **kwargs):
"""
Gets most recently updated anime from anilist
"""
most_recently_updated = cls.get_data(most_recently_updated_query)
most_recently_updated = self.get_data(most_recently_updated_query)
return most_recently_updated
@classmethod
def get_most_popular(cls, *_, **kwargs):
def get_most_popular(self):
"""
Gets most popular anime on anilist
"""
most_popular = cls.get_data(most_popular_query)
most_popular = self.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)
def get_recommended_anime_for(self, id: int, *_, **kwargs):
recommended_anime = self.get_data(recommended_query)
return recommended_anime
@classmethod
def get_charcters_of(cls, id: int, *_, **kwargs):
def get_charcters_of(self, id: int, *_, **kwargs):
variables = {"id": id}
characters = cls.get_data(anime_characters_query, variables)
characters = self.get_data(anime_characters_query, variables)
return characters
@classmethod
def get_related_anime_for(cls, id: int, *_, **kwargs):
def get_related_anime_for(self, id: int, *_, **kwargs):
variables = {"id": id}
related_anime = cls.get_data(anime_relations_query, variables)
related_anime = self.get_data(anime_relations_query, variables)
return related_anime
@classmethod
def get_airing_schedule_for(cls, id: int, *_, **kwargs):
def get_airing_schedule_for(self, id: int, *_, **kwargs):
variables = {"id": id}
airing_schedule = cls.get_data(airing_schedule_query, variables)
airing_schedule = self.get_data(airing_schedule_query, variables)
return airing_schedule
@classmethod
def get_upcoming_anime(cls, page: int = 1, *_, **kwargs):
def get_upcoming_anime(self, page: int = 1, *_, **kwargs):
"""
Gets upcoming anime from anilist
"""
variables = {"page": page}
upcoming_anime = cls.get_data(upcoming_anime_query, variables)
upcoming_anime = self.get_data(upcoming_anime_query, variables)
return upcoming_anime

View File

@@ -0,0 +1,14 @@
from .allanime.api import AllAnimeAPI
anime_sources = {"allanime": AllAnimeAPI}
class Anime_Provider:
def search_for_anime(self):
pass
def get_anime(self):
pass
def get_episode_streams(self):
pass

View File

@@ -1,11 +1,9 @@
import json
import logging
from typing import Generator
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
@@ -43,19 +41,22 @@ class AllAnimeAPI:
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"
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 {}
def search_for_anime(
self, user_query: str, translation_type: str = "sub", nsfw=True, unknown=True
self,
user_query: str,
translation_type: str = "sub",
nsfw=True,
unknown=True,
**kwargs,
):
search = {"allowAdult": nsfw, "allowUnknown": unknown, "query": user_query}
limit = 40
@@ -70,22 +71,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(
@@ -97,23 +96,15 @@ 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(
self, anime: Anime, episode_number: str, translation_type="sub"
) -> (
Generator[
Server,
Server,
Server,
]
| None
):
) -> Iterator[Server] | None:
anime_id = anime["id"]
allanime_episode = self.get_anime_episode(
anime_id, episode_number, translation_type
@@ -123,106 +114,91 @@ 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",
):
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
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 []
anime_provider = AllAnimeAPI()
if __name__ == "__main__":
anime_provider = AllAnimeAPI()
# lets see if it works :)
import subprocess
import sys

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

16
poetry.lock generated
View File

@@ -771,13 +771,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 +1202,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 +1222,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 +1234,4 @@ test = ["pytest (>=8.1,<9.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "b6f2f120c8a562e8c8d98aae75f1e5fc4dd779d2da60fdcff6b98bf88008f23b"
content-hash = "38fed68d89077d348221af9eb8e2d0ea6c9585bd4c5de16d6e5974664c562f73"

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,7 +1,7 @@
[tool.poetry]
name = "fastanime"
version = "0.3.0"
description = "A fast and efficient GUI and CLI anime scrapper"
version = "0.32.0"
description = "A fast and efficient anime scrapper and exploration tool"
authors = ["Benex254 <benedictx855@gmail.com>"]
license = "UNLICENSE"
readme = "README.md"
@@ -17,8 +17,7 @@ art = "^6.2"
python-dotenv = "^1.0.1"
thefuzz = "^0.22.1"
requests = "^2.32.3"
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
black = "^24.4.2"
isort = "^5.13.2"
@@ -26,7 +25,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 +32,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