Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c1f8d09e6 | ||
|
|
6bb2c89a8c | ||
|
|
9f56b74ff0 | ||
|
|
4d03b86498 | ||
|
|
fab86090a3 | ||
|
|
71d258385c | ||
|
|
bc55ed6e81 | ||
|
|
197bfa9f8a | ||
|
|
f84c60e6bc | ||
|
|
d8b94cbbca | ||
|
|
dd4462f42a | ||
|
|
0f9e08b9fa | ||
|
|
01333ab1d1 | ||
|
|
d8bf9e18c4 | ||
|
|
bc909397d5 | ||
|
|
f3d88f9825 | ||
|
|
eb7bef72b3 | ||
|
|
f6ec094bc7 | ||
|
|
3f1bf1781a | ||
|
|
21167fc208 | ||
|
|
c7c6ff92c4 | ||
|
|
78319731c0 | ||
|
|
b619a11db1 | ||
|
|
022420aa4c | ||
|
|
a7e46d9c18 | ||
|
|
5e2826be4e | ||
|
|
5e314e2bca | ||
|
|
3d23854d89 | ||
|
|
80a25d24a3 | ||
|
|
1ad7929c66 | ||
|
|
0670bd735c |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 971 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 690 KiB |
|
Before Width: | Height: | Size: 718 KiB |
|
Before Width: | Height: | Size: 813 KiB |
|
Before Width: | Height: | Size: 763 KiB |
|
Before Width: | Height: | Size: 518 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 578 KiB |
|
Before Width: | Height: | Size: 644 KiB |
|
Before Width: | Height: | Size: 566 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
2
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: build
|
||||
name: debug_build
|
||||
on: push
|
||||
jobs:
|
||||
ci:
|
||||
|
||||
66
.github/workflows/publish.yml
vendored
Normal 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
|
||||
83
README.md
@@ -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
|
||||
|
||||
452
buildozer.spec
@@ -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
|
||||
58
fastanime/AnimeProvider.py
Normal 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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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]"
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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)
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -0,0 +1,3 @@
|
||||
from .libs.anilist.api import AniListApi
|
||||
|
||||
AniList = AniListApi()
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
27
fastanime/cli/commands/anilist/random_anime.py
Normal 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])
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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"]
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
kivy
|
||||
yt-dlp
|
||||
ffpyplayer
|
||||
plyer
|
||||
https://github.com/kivymd/KivyMD/archive/master.zip
|
||||
fuzzywuzzy
|
||||
python-Levenshtein
|
||||
rich
|
||||
click
|
||||