Compare commits

...

44 Commits

Author SHA1 Message Date
benex
3ac4e1ac71 chore: bump version (v2.7.4) 2024-11-16 22:10:15 +03:00
benex
d62f580d7a docs: update readme 2024-11-16 22:09:41 +03:00
benex
02e35b66cb fix: implement another way to get timestamps from mpv due to issues on nixos 2024-11-16 22:09:41 +03:00
benex
7b11e0a301 feat: add rofi-theme-preview option 2024-11-16 22:09:41 +03:00
benex
aa8b91aed3 chore: remove unnecessary yt-dlp extras 2024-11-16 22:09:41 +03:00
benex
fe0fa97576 fix(player): workaround weird problem with mpv in nixos 2024-11-16 22:09:41 +03:00
Benedict Xavier
92059cd5ed Update README.md 2024-11-16 12:01:55 +03:00
benex
ed3064e3b1 feat(anime_provider): add yugen provider 2024-11-13 20:53:18 +03:00
Benex254
441d1e5e6c chore: bump version (v2.7.3) 2024-11-11 12:57:50 +03:00
Benex254
653b2cf4eb chore: add pyinstaller as dev dep 2024-11-11 12:57:29 +03:00
Benex254
8d4b71e0c8 feat: add entry point for pyinstaller executable 2024-11-11 12:57:29 +03:00
Benex254
29cc6cad09 build: pyinstaller spec 2024-11-11 12:57:28 +03:00
Benex254
8119eef263 docs(readme): update table of contents 2024-11-11 12:57:28 +03:00
Benex254
912c8674cf refactor(player): remove unnecessary orint statement 2024-11-11 12:57:28 +03:00
Benex254
6b3ca236dd fix: auto next episode 2024-11-11 12:57:28 +03:00
benex
f1c352d4ff chore: bump version (v2.7.2) 2024-11-10 12:29:46 +03:00
benex
714533d845 refactor(cli): update config options and set fastanime config environs 2024-11-10 12:06:44 +03:00
benex
56dd25df8d refactor(cli): update config options
This commit updates the configuration options in the CLI module. Specifically, it modifies the "image_previews" option to be platform-dependent, setting it to "True" for non-Windows platforms and "False" for Windows. Additionally, it sets the "normalize_titles" option to "True". These changes improve the behavior and user experience of the CLI.
2024-11-10 12:06:17 +03:00
benex
8248dc53df fix: text preview not showing on windows 2024-11-10 12:05:32 +03:00
Benex254
1a8a187de6 docs: update readme 2024-11-09 00:27:49 +03:00
Benex254
bc86be8c93 chore: bump version 2024-11-09 00:11:20 +03:00
Benex254
75026d4fc5 feat(api): add watch endpoint 2024-11-09 00:11:20 +03:00
Benedict Xavier
f8a5ccb8d2 Update README.md 2024-11-08 22:33:16 +03:00
Benex254
719d1bd187 chore: update deps 2024-11-08 16:26:43 +03:00
Benex254
0dd83463c6 docs: update readme 2024-10-20 11:28:39 +03:00
Benex254
1ee50e8a55 chore: bump version (v2.6.9) 2024-10-20 10:06:02 +03:00
Benex254
ae95c5ea3d docs: update readme 2024-10-20 10:04:58 +03:00
Benex254
d64ad5e11d fix: move quality to stream section in config 2024-10-20 10:03:49 +03:00
Benex254
d1a47c6d44 chore: bump version (v2.6.8) 2024-10-18 22:59:18 +03:00
Benex254
51a834a62f chore: update deps 2024-10-18 22:53:39 +03:00
Benex254
3a030bf6f7 feat: add ability to update fastanime uv installations 2024-10-18 22:53:26 +03:00
Benex254
eb6a6fc82c chore: use uv in fa script 2024-10-18 22:46:44 +03:00
Benex254
437ccd94e4 ci: update to use uv 2024-10-18 22:37:14 +03:00
Benex254
d65868cc30 chore: update workflows to work with uv 2024-10-18 21:50:20 +03:00
Benex254
8678aa6544 Merge branch 'master' into uv 2024-10-18 20:26:55 +03:00
Benex254
00e5141152 chore: bump version (v2.6.7) 2024-10-12 01:08:14 +03:00
Benex254
90e757dfe1 feat: init switch to uv 2024-10-11 11:57:29 +03:00
Benex254
8b471b08e8 chore: init switch to uv 2024-10-11 10:52:18 +03:00
Benex254
158bc5710f docs: update readme 2024-10-11 10:49:53 +03:00
Benex254
a0b946a13d feat: add recent menu 2024-10-11 10:22:23 +03:00
Benex254
b547b75f03 feat: add environment variable that force updating of the cache db 2024-10-11 09:34:40 +03:00
Benex254
58c7427a47 feat(cli:serve): use the full executable path to python 2024-10-06 01:25:22 +03:00
Benex254
6220b9c55d chore: bump version (v2.6.6) 2024-10-06 01:15:15 +03:00
Benex254
6b9b5c131c fix(cli): use str instead of ints in serve 2024-10-06 01:15:05 +03:00
28 changed files with 2549 additions and 2261 deletions

View File

@@ -8,31 +8,24 @@ jobs:
debug_build: debug_build:
if: ${{ github.event.workflow_run.conclusion == 'success' }} if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Python
- name: "Set up Python"
uses: actions/setup-python@v5 uses: actions/setup-python@v5
- name: Install poetry
uses: abatilo/actions-poetry@v2 - name: Install uv
- name: Setup a local virtual environment (if no poetry.toml file) uses: astral-sh/setup-uv@v3
run: |
poetry config virtualenvs.create true --local
poetry config virtualenvs.in-project true --local
- uses: actions/cache@v3
name: Define a cache for the virtual environment based on the dependencies lock file
with: with:
path: ./.venv enable-cache: true
key: venv-${{ hashFiles('poetry.lock') }}
- name: Install the project dependencies - name: Build fastanime
run: poetry install --all-extras run: uv build
- name: build app
run: poetry build
- name: Archive production artifacts - name: Archive production artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: fastanime_debug_build name: fastanime_debug_build
path: | path: |
dist dist
!dist/*.whl
# - name: Run the automated tests (for example)
# run: poetry run pytest -v

View File

@@ -27,11 +27,13 @@ jobs:
with: with:
python-version: "3.10" python-version: "3.10"
- name: Build release distributions - name: Install uv
run: | uses: astral-sh/setup-uv@v3
# NOTE: put your own distribution build steps here. with:
python -m pip install build enable-cache: true
python -m build
- name: Build fastanime
run: uv build
- name: Upload distributions - name: Upload distributions
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@@ -6,37 +6,35 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.10", "3.11"] # List the Python versions you want to test python-version: ["3.10", "3.11"] # List the Python versions you want to test
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Python ${{ matrix.python-version }} - name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install poetry
uses: abatilo/actions-poetry@v2 - name: Install uv
- name: Setup a local virtual environment (if no poetry.toml file) uses: astral-sh/setup-uv@v3
run: |
poetry config virtualenvs.create true --local
poetry config virtualenvs.in-project true --local
- uses: actions/cache@v3
name: Define a cache for the virtual environment based on the dependencies lock file
with: with:
path: ./.venv enable-cache: true
key: venv-${{ hashFiles('poetry.lock') }}
- name: Install the project dependencies - name: Install the project
run: poetry install --all-extras run: uv sync --all-extras --dev
- name: run linter, formatters and sort imports
run: | - name: Run linter and formater
poetry run black . run: uv run ruff check --output-format=github
poetry run ruff check --output-format=github . --fix
poetry run isort . --profile black - name: Run type checking
- name: run type checking run: uv run pyright
run: poetry run pyright
- name: run tests - name: Run tests
run: poetry run pytest run: uv run pytest tests

View File

@@ -1,10 +1,7 @@
FROM ubuntu FROM python:3.12-slim-bookworm
RUN apt-get update COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
RUN apt-get -y install python3
RUN apt-get update
RUN apt-get -y install pipx
RUN pipx ensurepath
COPY . /fastanime COPY . /fastanime
ENV PATH=/root/.local/bin:$PATH
WORKDIR /fastanime WORKDIR /fastanime
RUN pipx install . RUN uv tool install .
CMD ["bash"] CMD ["bash"]

697
README.md
View File

@@ -1,9 +1,9 @@
# **FastAnime** # **FastAnime**
![PyPI - Downloads](https://img.shields.io/pypi/dm/fastanime) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Benex254/FastAnime/test.yml?label=Tests) ![PyPI - Downloads](https://img.shields.io/pypi/dm/fastanime) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/FastAnime/FastAnime/test.yml?label=Tests)
![Discord](https://img.shields.io/discord/1250887070906323096?label=Discord) ![Discord](https://img.shields.io/discord/1250887070906323096?label=Discord)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/Benex254/FastAnime) ![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/FastAnime/FastAnime)
![GitHub deployments](https://img.shields.io/github/deployments/Benex254/fastanime/pypi?label=PyPi%20Publish) ![GitHub deployments](https://img.shields.io/github/deployments/FastAnime/fastanime/pypi?label=PyPi%20Publish)
![PyPI - License](https://img.shields.io/pypi/l/fastanime) ![PyPI - License](https://img.shields.io/pypi/l/fastanime)
Welcome to **FastAnime**, anime site experience from the terminal. Welcome to **FastAnime**, anime site experience from the terminal.
@@ -38,6 +38,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [jerr
- [**FastAnime**](#fastanime) - [**FastAnime**](#fastanime)
- [Installation](#installation) - [Installation](#installation)
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager) - [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
- [Using uv](#using-uv)
- [Using pipx](#using-pipx) - [Using pipx](#using-pipx)
- [Using pip](#using-pip) - [Using pip](#using-pip)
- [Installing the bleeding edge version](#installing-the-bleeding-edge-version) - [Installing the bleeding edge version](#installing-the-bleeding-edge-version)
@@ -56,6 +57,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [jerr
- [cache subcommand](#cache-subcommand) - [cache subcommand](#cache-subcommand)
- [update subcommand](#update-subcommand) - [update subcommand](#update-subcommand)
- [completions subcommand](#completions-subcommand) - [completions subcommand](#completions-subcommand)
- [fastanime serve](#fastanime-serve)
- [MPV specific commands](#mpv-specific-commands) - [MPV specific commands](#mpv-specific-commands)
- [Key Bindings](#key-bindings) - [Key Bindings](#key-bindings)
- [Script Messages](#script-messages) - [Script Messages](#script-messages)
@@ -85,11 +87,31 @@ If you have any difficulty consult for help on the [discord channel](https://dis
### Installation using your favourite package manager ### Installation using your favourite package manager
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/). Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
With the following extras available:
- standard -which installs all dependencies
- api - which installs dependencies required to use `fastanime serve`
- mpv - which installs python mpv
- notifications - which installs plyer required for desktop notifications
#### Using uv
Recommended method of installation is using [uv](https://docs.astral.sh/uv/).
```bash
# generally:
uv tool install fastanime[standard]
# or stripped down installations:
uv tool install fastanime
uv tool install fastanime[api]
uv tool install fastanime[mpv]
uv tool install fastanime[notifications]
```
#### Using pipx #### Using pipx
Preferred method of installation since [Pipx](https://github.com/pypa/pipx) creates an isolated environment for each app it installs.
```bash ```bash
pipx install fastanime pipx install fastanime
@@ -112,7 +134,7 @@ pip install 'fastanime==<latest-pre-release-tag>.dev1'
### Installing the bleeding edge version ### Installing the bleeding edge version
To install the latest build which are created on every push by GitHub actions, download the [fastanime_debug_build](https://github.com/Benex254/FastAnime/actions) of your choosing from the GitHub actions page. To install the latest build which are created on every push by GitHub actions, download the [fastanime_debug_build](https://github.com/FastAnime/FastAnime/actions) of your choosing from the GitHub actions page.
Then: Then:
```bash ```bash
@@ -133,24 +155,17 @@ Requirements:
- [git](https://git-scm.com/) - [git](https://git-scm.com/)
- [python 3.10 and above](https://www.python.org/) - [python 3.10 and above](https://www.python.org/)
- [poetry](https://python-poetry.org/docs/#installation) - [uv](https://astral.sh/blog/uv)
To build from the source, follow these steps: To build from the source, follow these steps:
1. Clone the repository: `git clone https://github.com/Benex254/FastAnime.git --depth 1` 1. Clone the repository: `git clone https://github.com/FastAnime/FastAnime.git --depth 1`
2. Navigate into the folder: `cd FastAnime` 2. Navigate into the folder: `cd FastAnime`
3. Then build and Install the app: 3. Then build and Install the app:
```bash ```bash
# Normal Installation # build and install fastanime with uv
poetry build uv tool install .
cd dist
pip install fastanime<version>.whl
# Editable installation (easiest for updates)
# just do a git pull in the Project dir
# the latter will require rebuilding the app
pip install -e .
``` ```
4. Enjoy! Verify installation with: 4. Enjoy! Verify installation with:
@@ -161,12 +176,13 @@ fastanime --version
> [!Tip] > [!Tip]
> >
> Download the completions from [here](https://github.com/Benex254/FastAnime/tree/master/completions) for your shell. > Download the completions from [here](https://github.com/FastAnime/FastAnime/tree/master/completions) for your shell.
> To add completions: > To add completions:
> >
> - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/` > - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/`
> - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc` > - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc`
> - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc` > - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc`
> or using the built in command `fastanime completions`
### External Dependencies ### External Dependencies
@@ -344,7 +360,7 @@ fastanime anilist search -f MOVIE -s FAVOURITES_DESC
For more details visit the anilist docs or just get the completions which will improve the experience. For more details visit the anilist docs or just get the completions which will improve the experience.
Like seriously **[get the completions](https://github.com/Benex254/FastAnime#completions-subcommand)** and the experience will be a 💯 💯 better. Like seriously **[get the completions](https://github.com/FastAnime/FastAnime#completions-subcommand)** and the experience will be a 💯 💯 better.
The following are commands you can only run if you are signed in to your AniList account: The following are commands you can only run if you are signed in to your AniList account:
@@ -637,6 +653,645 @@ fastanime completions --bash
fastanime completions --zsh fastanime completions --zsh
``` ```
#### fastanime serve
Helper command that starts a rest server.
This requires you to install fastanime with the api extra or standard extra.
```bash
# default options
fastanime serve
# specify host and port
fastanime serve --host <host> --port <port>
```
An example instance is hosted by [render](https://fastanime.onrender.com/)
Examples:
**search for anime by title:**
```bash
curl 'https://fastanime.onrender.com/search?title=dragon&translation_type=sub'
```
<details>
<summary>
Result
</summary>
```json
{
"pageInfo": {
"total": 22839
},
"results": [
{
"id": "ju2pgynxn9o9DZvse",
"title": "Dragon Ball Daima",
"type": "Show",
"availableEpisodes": {
"sub": 5,
"dub": 0,
"raw": 0
}
},
{
"id": "qpnhxfarTHfP7kjgR",
"title": "My WeChat connects to the Dragon Palace",
"type": "Show",
"availableEpisodes": {
"sub": 26,
"dub": 0,
"raw": 0
}
},
{
"id": "8aM5BBoEGLvjG3MZm",
"title": "Sayounara Ryuusei, Konnichiwa Jinsei",
"type": "Show",
"availableEpisodes": {
"sub": 6,
"dub": 0,
"raw": 0
}
},
{
"id": "Sg9Q9FyqBnJ9qtv5n",
"title": "Yarinaoshi Reijou wa Ryuutei Heika wo Kouryakuchuu",
"type": "Show",
"availableEpisodes": {
"sub": 5,
"dub": 0,
"raw": 0
}
},
{
"id": "gF2mKbWBatQudcF6A",
"title": "Throne of the Dragon King",
"type": "Show",
"availableEpisodes": {
"sub": 3,
"dub": 0,
"raw": 0
}
},
{
"id": "SXLNNoorPifT5ZStw",
"title": "Shi Cao Lao Long Bei Guan Yi E Long Zhi Ming Season 2",
"type": "Show",
"availableEpisodes": {
"sub": 7,
"dub": 0,
"raw": 0
}
},
{
"id": "v4ZkjtyftscNzYF2A",
"title": "I Have a Dragon in My Body Episode122-133",
"type": "Show",
"availableEpisodes": {
"sub": 77,
"dub": 0,
"raw": 0
}
},
{
"id": "9RSQCRJ3d554sBzoz",
"title": "City Immortal Emperor: Dragon King Temple",
"type": "Show",
"availableEpisodes": {
"sub": 20,
"dub": 0,
"raw": 0
}
},
{
"id": "t8C6zvsdJE5JJKDLE",
"title": "It Turns Out I Am the Peerless Dragon God",
"type": "Show",
"availableEpisodes": {
"sub": 2,
"dub": 0,
"raw": 0
}
},
{
"id": "xyDt3mJieZkD76P7S",
"title": "Urban Hidden Dragon",
"type": "Show",
"availableEpisodes": {
"sub": 13,
"dub": 0,
"raw": 0
}
},
{
"id": "8PoJiTEDAswkw8b3u",
"title": "The Collected Animations of ICAF (2001-2006)",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 0,
"raw": 0
}
},
{
"id": "KZeMmRSsyJgz37EmH",
"title": "Dragon Master",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 0,
"raw": 0
}
},
{
"id": "7a33i9m26poonyNLg",
"title": "I Have a Dragon in My Body",
"type": "Show",
"availableEpisodes": {
"sub": 79,
"dub": 0,
"raw": 0
}
},
{
"id": "uwwvBujGRsjCQ8kKM",
"title": "Cong Gu Huo Niao Kaishi: Long Cheng Fengyun",
"type": "Show",
"availableEpisodes": {
"sub": 16,
"dub": 0,
"raw": 0
}
},
{
"id": "RoexdZwHSTDwyzEzd",
"title": "Super Dragon Ball Heroes Meteor Mission",
"type": "Show",
"availableEpisodes": {
"sub": 6,
"dub": 0,
"raw": 0
}
},
{
"id": "gAcGCcMENjbWhBnR9",
"title": "Dungeon Meshi",
"type": "Show",
"availableEpisodes": {
"sub": 24,
"dub": 24,
"raw": 0
}
},
{
"id": "ZGh2QHiaCY5T5Mhi4",
"title": "Long Shidai",
"type": "Show",
"availableEpisodes": {
"sub": 9,
"dub": 0,
"raw": 1
}
},
{
"id": "gZSHt98fQpHRfJJXw",
"title": "Xanadu Dragonslayer Densetsu",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 0,
"raw": 0
}
},
{
"id": "wo8pX4Sba97mFCAkc",
"title": "Vanguard Dragon God",
"type": "Show",
"availableEpisodes": {
"sub": 86,
"dub": 0,
"raw": 0
}
},
{
"id": "rrbCftmca3Y2TEiBX",
"title": "Super Dragon Ball Heroes Ultra God Mission",
"type": "Show",
"availableEpisodes": {
"sub": 10,
"dub": 0,
"raw": 0
}
},
{
"id": "JzSeXC2WtBBhn3guN",
"title": "Dragon King's Son-In-Law",
"type": "Show",
"availableEpisodes": {
"sub": 11,
"dub": 0,
"raw": 0
}
},
{
"id": "eE3txJGGk9atw7k2v",
"title": "Majutsushi Orphen Hagure Tabi: Seiiki-hen",
"type": "Show",
"availableEpisodes": {
"sub": 12,
"dub": 0,
"raw": 0
}
},
{
"id": "4X2JbZgiQrb2PTzex",
"title": "Yowai 5000-nen no Soushoku Dragon, Iwarenaki Jaryuu Nintei (Japanese Dub)",
"type": "Show",
"availableEpisodes": {
"sub": 12,
"dub": 0,
"raw": 0
}
},
{
"id": "SHp5NFDakKjPT5nJE",
"title": "Starting from Gu Huoniao: Dragon City Hegemony",
"type": "Show",
"availableEpisodes": {
"sub": 22,
"dub": 0,
"raw": 0
}
},
{
"id": "8LgaCGrz7Gz35LRpk",
"title": "Yuan Zun",
"type": "Show",
"availableEpisodes": {
"sub": 5,
"dub": 0,
"raw": 0
}
},
{
"id": "4GKHyjFC7Dyc7fBpT",
"title": "Shen Ji Long Wei",
"type": "Show",
"availableEpisodes": {
"sub": 26,
"dub": 0,
"raw": 0
}
},
{
"id": "2PQiuXiuJoTQTdgy4",
"title": "Long Zu",
"type": "Show",
"availableEpisodes": {
"sub": 15,
"dub": 0,
"raw": 0
}
},
{
"id": "rE47AepmBFRvZ6cne",
"title": "Jidao Long Shen",
"type": "Show",
"availableEpisodes": {
"sub": 40,
"dub": 0,
"raw": 0
}
},
{
"id": "c4JcjPbRfiuoJPB4F",
"title": "Dragon Quest: Dai no Daibouken (2020)",
"type": "Show",
"availableEpisodes": {
"sub": 101,
"dub": 100,
"raw": 0
}
},
{
"id": "nGRTwG7kj5rCPiAX4",
"title": "Dragon Quest: Dai no Daibouken Tachiagare!! Aban no Shito",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 0,
"raw": 0
}
},
{
"id": "6LJBjT4RzJaucdmX3",
"title": "Dragon Slayer Eiyuu Densetsu: Ouji no Tabidachi",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 1,
"raw": 0
}
},
{
"id": "JKbtxdw2cRqqmZgnS",
"title": "Dragon Quest: Dai no Daibouken Buchiyabure!! Shinsei 6 Daishougun",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 0,
"raw": 0
}
},
{
"id": "pn32RijEHPfuTYt4h",
"title": "Dragon Quest Retsuden: Roto no Monshou",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 0,
"raw": 0
}
},
{
"id": "xHwk6oo7jaDrMG9to",
"title": "Dragon Fist",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 0,
"raw": 0
}
},
{
"id": "ugFXPFQW8kvLocZgx",
"title": "Yowai 5000-nen no Soushoku Dragon, Iwarenaki Jaryuu Nintei",
"type": "Show",
"availableEpisodes": {
"sub": 12,
"dub": 0,
"raw": 0
}
},
{
"id": "qSFMEcT4SufEhLZnq",
"title": "Doraemon Movie 8: Nobita to Ryuu no Kishi",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 0,
"raw": 0
}
},
{
"id": "LTzXFSmQR878MdJaS",
"title": "Dragon Ball Specials",
"type": "Show",
"availableEpisodes": {
"sub": 2,
"dub": 0,
"raw": 0
}
},
{
"id": "XuTNNzF7DfapLFMFJ",
"title": "Dragon Ball Super: Super Hero",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 1,
"raw": 0
}
},
{
"id": "n4S2spjyTHXHNAMDW",
"title": "Shin Ikkitousen",
"type": "Show",
"availableEpisodes": {
"sub": 3,
"dub": 3,
"raw": 0
}
},
{
"id": "srMRCkMEJA9Rmt7do",
"title": "Dragon Ball Z: Atsumare! Goku World",
"type": "Show",
"availableEpisodes": {
"sub": 1,
"dub": 0,
"raw": 0
}
}
]
}
```
</details>
**Get anime by id:**
```bash
curl 'https://fastanime.onrender.com/anime/8aM5BBoEGLvjG3MZm'
```
<details>
<summary>
Result
</summary>
```json
{
"id": "8aM5BBoEGLvjG3MZm",
"title": "Sayounara Ryuusei, Konnichiwa Jinsei",
"availableEpisodesDetail": {
"sub": ["6", "5", "4", "3", "2", "1"],
"dub": [],
"raw": []
},
"type": null
}
```
</details>
**Get episode streams by translation_type:**
```bash
curl 'https://fastanime.onrender.com/anime/8aM5BBoEGLvjG3MZm/watch?episode=3&translation_type=sub'
```
<details>
<summary>
Result
</summary>
```json
[
{
"server": "Yt",
"episode_title": "Sayounara Ryuusei, Konnichiwa Jinsei; Episode 3",
"headers": {
"Referer": "https://allanime.day/"
},
"subtitles": [],
"links": [
{
"link": "https://tools.fast4speed.rsvp//media9/videos/8aM5BBoEGLvjG3MZm/sub/3",
"quality": "1080"
}
]
},
{
"server": "sharepoint",
"headers": {},
"subtitles": [],
"episode_title": "Sayounara Ryuusei, Konnichiwa Jinsei; Episode 3",
"links": [
{
"link": "https://myanime.sharepoint.com/sites/chartlousty/_layouts/15/download.aspx?share=ERpIT0CTmOVHmO8386bNGZMBf7Emtoda_3bUMzCleWhp4g",
"mp4": true,
"resolutionStr": "Mp4",
"src": "https://myanime.sharepoint.com/sites/chartlousty/_layouts/15/download.aspx?share=ERpIT0CTmOVHmO8386bNGZMBf7Emtoda_3bUMzCleWhp4g",
"quality": "1080"
}
]
},
{
"server": "gogoanime",
"headers": {},
"subtitles": [],
"episode_title": "Sayounara Ryuusei, Konnichiwa Jinsei; Episode 3",
"links": [
{
"link": "https://www114.anzeat.pro/streamhls/6454b50a557e9fa52a60cfdee0b0906e/ep.3.1729188150.m3u8",
"hls": true,
"mp4": false,
"resolutionStr": "hls P",
"priority": 3,
"quality": "1080"
},
{
"link": "https://www114.anicdnstream.info/videos/hls/h1IUtAefmoWTc8hJhtr8OQ/1731106912/235294/6454b50a557e9fa52a60cfdee0b0906e/ep.3.1729188150.m3u8",
"hls": true,
"mp4": false,
"resolutionStr": "HLS1",
"priority": 2,
"quality": "720"
},
{
"link": "https://workfields.maverickki.lol/7d2473746a243c246e727276753c29297171713737322867686f65626875727463676b286f68606929706f62636975296e6a75296e374f53724763606b695152653e6e4c6e72743e495729373135373736303f373429343533343f32293032333264333667333331633f6067333467303665606263633664363f3630632963762835283731343f373e3e373336286b35733e242a2476677475634e6a75243c727473632a2462677263243c373135373634363236363636367b",
"hls": true,
"resolutionStr": "Alt",
"src": "https://workfields.maverickki.lol/7d2473746a243c246e727276753c29297171713737322867686f65626875727463676b286f68606929706f62636975296e6a75296e374f53724763606b695152653e6e4c6e72743e495729373135373736303f373429343533343f32293032333264333667333331633f6067333467303665606263633664363f3630632963762835283731343f373e3e373336286b35733e242a2476677475634e6a75243c727473632a2462677263243c373135373634363236363636367b",
"priority": 1,
"quality": "480"
}
]
}
]
```
</details>
**Get Episode Streams by AniList Id:**
```bash
curl 'https://fastanime.onrender.com/watch/269?episode=1&translation_type=dub'
```
<details>
<summary>
Results
</summary>
```json
[
{
"server": "gogoanime",
"headers": {},
"subtitles": [],
"episode_title": "Bleach; Episode 1",
"links": [
{
"link": "",
"hls": true,
"mp4": false,
"resolutionStr": "hls P",
"priority": 3,
"quality": "1080"
},
{
"link": "",
"hls": true,
"mp4": false,
"resolutionStr": "HLS1",
"priority": 2,
"quality": "720"
},
{
"link": "",
"hls": true,
"resolutionStr": "Alt",
"src": "",
"priority": 1,
"quality": "480"
}
]
},
{
"server": "Yt",
"episode_title": "Bleach; Episode 1",
"headers": {
"Referer": "https://allanime.day/"
},
"subtitles": [],
"links": [
{
"link": "",
"quality": "1080"
}
]
},
{
"server": "wixmp",
"headers": {},
"subtitles": [],
"episode_title": "Bleach; Episode 1",
"links": [
{
"link": "",
"hls": true,
"resolutionStr": "Hls",
"quality": "1080"
}
]
},
{
"server": "sharepoint",
"headers": {},
"subtitles": [],
"episode_title": "Bleach; Episode 1",
"links": [
{
"link": "",
"mp4": true,
"resolutionStr": "Mp4",
"src": "",
"quality": "1080"
}
]
}
]
```
</details>
### MPV specific commands ### MPV specific commands
The project now allows on the fly media controls directly from mpv. This means you can go to the next or previous episode without the window ever closing thus offering a seamless experience. The project now allows on the fly media controls directly from mpv. This means you can go to the next or previous episode without the window ever closing thus offering a seamless experience.
@@ -728,6 +1383,8 @@ cache_requests = True
use_persistent_provider_store = False use_persistent_provider_store = False
recent = 50
[stream] [stream]
continue_from_history = True continue_from_history = True
@@ -761,7 +1418,7 @@ We welcome your issues and feature requests. However, due to time constraints, w
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed or if you are in a rush for the feature to be merged just open a pr. If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed or if you are in a rush for the feature to be merged just open a pr.
If you find an anime title that does not correspond with a provider or is just weird just [edit the data file](https://github.com/Benex254/FastAnime/blob/master/fastanime/Utility/data.py) and open a pr or if you don't want to do that open an issue. If you find an anime title that does not correspond with a provider or is just weird just [edit the data file](https://github.com/FastAnime/FastAnime/blob/master/fastanime/Utility/data.py) and open a pr or if you don't want to do that open an issue.
## Receiving Support ## Receiving Support

2
fa
View File

@@ -1,3 +1,3 @@
#!/usr/bin/env sh #!/usr/bin/env sh
CLI_DIR="$(dirname "$(realpath "$0")")" CLI_DIR="$(dirname "$(realpath "$0")")"
exec python -m "$CLI_DIR/fastanime" "$@" exec uv run --directory "$CLI_DIR/../" fastanime "$@"

View File

@@ -14,6 +14,7 @@ anime_normalizer_raw = {
"hianime": {"My Star": "Oshi no Ko"}, "hianime": {"My Star": "Oshi no Ko"},
"animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"}, "animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
"nyaa": {}, "nyaa": {},
"yugen": {},
} }

View File

@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
) # noqa: F541 ) # noqa: F541
__version__ = "v2.6.5" __version__ = "v2.7.4"
APP_NAME = "FastAnime" APP_NAME = "FastAnime"
AUTHOR = "Benex254" AUTHOR = "Benex254"

View File

@@ -1,11 +1,15 @@
from typing import Literal from typing import Literal
from fastapi import FastAPI from fastapi import FastAPI
from requests import post
from thefuzz import fuzz
from ..AnimeProvider import AnimeProvider from ..AnimeProvider import AnimeProvider
from ..Utility.data import anime_normalizer
app = FastAPI() app = FastAPI()
anime_provider = AnimeProvider("allanime", "true", "true") anime_provider = AnimeProvider("allanime", "true", "true")
ANILIST_ENDPOINT = "https://graphql.anilist.co"
@app.get("/search") @app.get("/search")
@@ -23,3 +27,67 @@ def get_episode_streams(
anime_id: str, episode: str, translation_type: Literal["sub", "dub"] anime_id: str, episode: str, translation_type: Literal["sub", "dub"]
): ):
return anime_provider.get_episode_streams(anime_id, episode, translation_type) return anime_provider.get_episode_streams(anime_id, episode, translation_type)
def get_anime_by_anilist_id(anilist_id: int):
query = f"""
query {{
Media(id: {anilist_id}) {{
id
title {{
romaji
english
native
}}
synonyms
episodes
duration
}}
}}
"""
response = post(ANILIST_ENDPOINT, json={"query": query}).json()
return response["data"]["Media"]
@app.get("/watch/{anilist_id}")
def get_episode_streams_by_anilist_id(
anilist_id: int, episode: str, translation_type: Literal["sub", "dub"]
):
anime = get_anime_by_anilist_id(anilist_id)
if not anime:
return
if search_results := anime_provider.search_for_anime(
str(anime["title"]["romaji"] or anime["title"]["english"]), translation_type
):
if not search_results["results"]:
return
def match_title(possible_user_requested_anime_title):
possible_user_requested_anime_title = anime_normalizer.get(
possible_user_requested_anime_title, possible_user_requested_anime_title
)
title_a = str(anime["title"]["romaji"])
title_b = str(anime["title"]["english"])
percentage_ratio = max(
*[
fuzz.ratio(
title.lower(), possible_user_requested_anime_title.lower()
)
for title in anime["synonyms"]
],
fuzz.ratio(
title_a.lower(), possible_user_requested_anime_title.lower()
),
fuzz.ratio(
title_b.lower(), possible_user_requested_anime_title.lower()
),
)
return percentage_ratio
provider_anime = max(
search_results["results"], key=lambda x: match_title(x["title"])
)
anime_provider.get_anime(provider_anime["id"])
return anime_provider.get_episode_streams(
provider_anime["id"], episode, translation_type
)

View File

@@ -158,6 +158,9 @@ signal.signal(signal.SIGINT, handle_exit)
@click.option("--sub", help="Set the translation type to sub", is_flag=True) @click.option("--sub", help="Set the translation type to sub", is_flag=True)
@click.option("--rofi", help="Use rofi for the ui", is_flag=True) @click.option("--rofi", help="Use rofi for the ui", is_flag=True)
@click.option("--rofi-theme", help="Rofi theme to use", type=click.Path()) @click.option("--rofi-theme", help="Rofi theme to use", type=click.Path())
@click.option(
"--rofi-theme-preview", help="Rofi theme to use for previews", type=click.Path()
)
@click.option( @click.option(
"--rofi-theme-confirm", "--rofi-theme-confirm",
help="Rofi theme to use for the confirm prompt", help="Rofi theme to use for the confirm prompt",
@@ -178,6 +181,9 @@ signal.signal(signal.SIGINT, handle_exit)
help="the player to use when streaming", help="the player to use when streaming",
type=click.Choice(["mpv", "vlc"]), type=click.Choice(["mpv", "vlc"]),
) )
@click.option(
"--fresh-requests", is_flag=True, help="Force the requests cache to be updated"
)
@click.pass_context @click.pass_context
def run_cli( def run_cli(
ctx: click.Context, ctx: click.Context,
@@ -207,12 +213,16 @@ def run_cli(
sub, sub,
rofi, rofi,
rofi_theme, rofi_theme,
rofi_theme_preview,
rofi_theme_confirm, rofi_theme_confirm,
rofi_theme_input, rofi_theme_input,
use_python_mpv, use_python_mpv,
sync_play, sync_play,
player, player,
fresh_requests,
): ):
import os
from .config import Config from .config import Config
ctx.obj = Config() ctx.obj = Config()
@@ -251,6 +261,8 @@ def run_cli(
install() install()
if fresh_requests:
os.environ["FASTANIME_FRESH_REQUESTS"] = "1"
if sync_play: if sync_play:
ctx.obj.sync_play = sync_play ctx.obj.sync_play = sync_play
if provider: if provider:
@@ -317,6 +329,10 @@ def run_cli(
if rofi: if rofi:
from ..libs.rofi import Rofi from ..libs.rofi import Rofi
if rofi_theme_preview:
ctx.obj.rofi_theme_preview = rofi_theme_preview
Rofi.rofi_theme_preview = rofi_theme_preview
if rofi_theme: if rofi_theme:
ctx.obj.rofi_theme = rofi_theme ctx.obj.rofi_theme = rofi_theme
Rofi.rofi_theme = rofi_theme Rofi.rofi_theme = rofi_theme

View File

@@ -75,9 +75,9 @@ def is_git_repo(author, repository):
return bool(match) and match.group(1) == f"{author}/{repository}" return bool(match) and match.group(1) == f"{author}/{repository}"
def update_app(): def update_app(force=False):
is_latest, release_json = check_for_updates() is_latest, release_json = check_for_updates()
if is_latest: if is_latest and not force:
print("[green]App is up to date[/]") print("[green]App is up to date[/]")
return False, release_json return False, release_json
tag_name = release_json["tag_name"] tag_name = release_json["tag_name"]
@@ -101,8 +101,10 @@ def update_app():
) )
else: else:
if PIPX_EXECUTABLE := shutil.which("pipx"): if UV := shutil.which("uv"):
process = subprocess.run([PIPX_EXECUTABLE, "upgrade", APP_NAME]) process = subprocess.run([UV, "tool", "upgrade", APP_NAME])
elif PIPX := shutil.which("pipx"):
process = subprocess.run([PIPX, "upgrade", APP_NAME])
else: else:
PYTHON_EXECUTABLE = sys.executable PYTHON_EXECUTABLE = sys.executable

View File

@@ -14,14 +14,14 @@ fastanime serve --host 127.0.0.1 --port 8080
""", """,
) )
@click.option("--host", "-H", help="Specify the host to run the server on") @click.option("--host", "-H", help="Specify the host to run the server on")
@click.option("--port", "-p", help="Check for the latest release", type=int) @click.option("--port", "-p", help="Specify the port to run the server on")
def serve(host, port): def serve(host, port):
import os import os
import sys import sys
from ...constants import APP_DIR from ...constants import APP_DIR
args = ["python", "-m", "fastapi", "run"] args = [sys.executable, "-m", "fastapi", "run"]
if host: if host:
args.extend(["--host", host]) args.extend(["--host", host])

View File

@@ -11,12 +11,14 @@ import click
\b \b
# check for latest release # check for latest release
fastanime update --check fastanime update --check
# Force an update regardless of the current version
fastanime update --force
""", """,
) )
@click.option("--check", "-c", help="Check for the latest release", is_flag=True) @click.option("--check", "-c", help="Check for the latest release", is_flag=True)
def update( @click.option("--force", "-c", help="Force update", is_flag=True)
check, def update(check, force):
):
from rich.console import Console from rich.console import Console
from rich.markdown import Markdown from rich.markdown import Markdown
@@ -45,7 +47,7 @@ def update(
print(f"You are running the latest version ({__version__}) of fastanime") print(f"You are running the latest version ({__version__}) of fastanime")
_print_release(github_release_data) _print_release(github_release_data)
else: else:
success, github_release_data = update_app() success, github_release_data = update_app(force)
_print_release(github_release_data) _print_release(github_release_data)
if success: if success:
print("Successfully updated") print("Successfully updated")

View File

@@ -9,6 +9,7 @@ from ..constants import (
USER_DATA_PATH, USER_DATA_PATH,
USER_VIDEOS_DIR, USER_VIDEOS_DIR,
USER_WATCH_HISTORY_PATH, USER_WATCH_HISTORY_PATH,
S_PLATFORM,
) )
from ..libs.rofi import Rofi from ..libs.rofi import Rofi
@@ -26,7 +27,7 @@ class Config(object):
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token" "https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
) )
anime_provider: "AnimeProvider" anime_provider: "AnimeProvider"
user_data = {"watch_history": {}, "animelist": [], "user": {}} user_data = {"recent_anime": [], "animelist": [], "user": {}}
default_config = { default_config = {
"auto_next": "False", "auto_next": "False",
"auto_select": "True", "auto_select": "True",
@@ -34,14 +35,15 @@ class Config(object):
"continue_from_history": "True", "continue_from_history": "True",
"default_media_list_tracking": "None", "default_media_list_tracking": "None",
"downloads_dir": USER_VIDEOS_DIR, "downloads_dir": USER_VIDEOS_DIR,
"disable_mpv_popen": "True",
"episode_complete_at": "80", "episode_complete_at": "80",
"ffmpegthumbnailer_seek_time": "-1", "ffmpegthumbnailer_seek_time": "-1",
"force_forward_tracking": "true", "force_forward_tracking": "true",
"force_window": "immediate", "force_window": "immediate",
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best", "format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
"icons": "false", "icons": "false",
"image_previews": "true", "image_previews": "True" if S_PLATFORM != "win32" else "False",
"normalize_titles": "true", "normalize_titles": "True",
"notification_duration": "2", "notification_duration": "2",
"player": "mpv", "player": "mpv",
"preferred_history": "local", "preferred_history": "local",
@@ -49,7 +51,9 @@ class Config(object):
"preview": "False", "preview": "False",
"provider": "allanime", "provider": "allanime",
"quality": "1080", "quality": "1080",
"recent": "50",
"rofi_theme": "", "rofi_theme": "",
"rofi_theme_preview": "",
"rofi_theme_confirm": "", "rofi_theme_confirm": "",
"rofi_theme_input": "", "rofi_theme_input": "",
"server": "top", "server": "top",
@@ -64,7 +68,7 @@ class Config(object):
} }
def __init__(self) -> None: def __init__(self) -> None:
self.initialize_user_data_and_watch_history() self.initialize_user_data_and_watch_history_recent_anime()
self.load_config() self.load_config()
def load_config(self): def load_config(self):
@@ -77,11 +81,17 @@ class Config(object):
if os.path.exists(USER_CONFIG_PATH): if os.path.exists(USER_CONFIG_PATH):
self.configparser.read(USER_CONFIG_PATH, encoding="utf-8") self.configparser.read(USER_CONFIG_PATH, encoding="utf-8")
# TODO: rewrite all this removing the useless functions
# hate technical debt
# why did i do this lol
self.auto_next = self.get_auto_next() self.auto_next = self.get_auto_next()
self.auto_select = self.get_auto_select() self.auto_select = self.get_auto_select()
self.cache_requests = self.get_cache_requests() self.cache_requests = self.get_cache_requests()
self.continue_from_history = self.get_continue_from_history() self.continue_from_history = self.get_continue_from_history()
self.default_media_list_tracking = self.get_default_media_list_tracking() self.default_media_list_tracking = self.get_default_media_list_tracking()
self.disable_mpv_popen = self.configparser.getboolean(
"stream", "disable_mpv_popen"
)
self.downloads_dir = self.get_downloads_dir() self.downloads_dir = self.get_downloads_dir()
self.episode_complete_at = self.get_episode_complete_at() self.episode_complete_at = self.get_episode_complete_at()
self.ffmpegthumbnailer_seek_time = self.get_ffmpegthumnailer_seek_time() self.ffmpegthumbnailer_seek_time = self.get_ffmpegthumnailer_seek_time()
@@ -99,13 +109,16 @@ class Config(object):
self.provider = self.get_provider() self.provider = self.get_provider()
self.quality = self.get_quality() self.quality = self.get_quality()
self.recent = self.get_recent()
self.rofi_theme_confirm = self.get_rofi_theme_confirm() self.rofi_theme_confirm = self.get_rofi_theme_confirm()
self.rofi_theme_input = self.get_rofi_theme_input() self.rofi_theme_input = self.get_rofi_theme_input()
self.rofi_theme = self.get_rofi_theme() self.rofi_theme = self.get_rofi_theme()
self.rofi_theme_preview = self.get_rofi_theme_preview()
Rofi.rofi_theme_confirm = self.rofi_theme_confirm Rofi.rofi_theme_confirm = self.rofi_theme_confirm
Rofi.rofi_theme_input = self.rofi_theme_input Rofi.rofi_theme_input = self.rofi_theme_input
Rofi.rofi_theme = self.rofi_theme Rofi.rofi_theme = self.rofi_theme
Rofi.rofi_theme_preview = self.rofi_theme_preview
self.server = self.get_server() self.server = self.get_server()
self.skip = self.get_skip() self.skip = self.get_skip()
@@ -136,6 +149,20 @@ class Config(object):
self.user_data["user"] = user self.user_data["user"] = user
self._update_user_data() self._update_user_data()
def update_recent(self, recent_anime: list):
recent_anime_ids = []
_recent_anime = []
for anime in recent_anime[::-1]:
if (
anime["id"] not in recent_anime_ids
and len(recent_anime_ids) <= self.recent
):
_recent_anime.append(anime)
recent_anime_ids.append(anime["id"])
self.user_data["recent_anime"] = _recent_anime
self._update_user_data()
def media_list_track( def media_list_track(
self, self,
anime_id: int, anime_id: int,
@@ -157,7 +184,7 @@ class Config(object):
with open(USER_WATCH_HISTORY_PATH, "w") as f: with open(USER_WATCH_HISTORY_PATH, "w") as f:
json.dump(self.watch_history, f) json.dump(self.watch_history, f)
def initialize_user_data_and_watch_history(self): def initialize_user_data_and_watch_history_recent_anime(self):
try: try:
if os.path.isfile(USER_DATA_PATH): if os.path.isfile(USER_DATA_PATH):
with open(USER_DATA_PATH, "r") as f: with open(USER_DATA_PATH, "r") as f:
@@ -218,6 +245,9 @@ class Config(object):
def get_rofi_theme(self): def get_rofi_theme(self):
return self.configparser.get("general", "rofi_theme") return self.configparser.get("general", "rofi_theme")
def get_rofi_theme_preview(self):
return self.configparser.get("general", "rofi_theme_preview")
def get_rofi_theme_input(self): def get_rofi_theme_input(self):
return self.configparser.get("general", "rofi_theme_input") return self.configparser.get("general", "rofi_theme_input")
@@ -236,6 +266,9 @@ class Config(object):
def get_normalize_titles(self): def get_normalize_titles(self):
return self.configparser.getboolean("general", "normalize_titles") return self.configparser.getboolean("general", "normalize_titles")
def get_recent(self):
return self.configparser.getint("general", "recent")
# --- stream section --- # --- stream section ---
def get_skip(self): def get_skip(self):
return self.configparser.getboolean("stream", "skip") return self.configparser.getboolean("stream", "skip")
@@ -306,13 +339,6 @@ class Config(object):
# be sure to also give the replacement emoji # be sure to also give the replacement emoji
icons = {self.icons} icons = {self.icons}
# the quality of the stream [1080,720,480,360]
# this option is usually only reliable when:
# provider=animepahe
# since it provides links that actually point to streams of different qualities
# while the rest just point to another link that can provide the anime from the same server
quality = {self.quality}
# whether to normalize provider titles [True/False] # whether to normalize provider titles [True/False]
# basically takes the provider titles and finds the corresponding anilist title then changes the title to that # basically takes the provider titles and finds the corresponding anilist title then changes the title to that
# useful for uniformity especially when downloading from different providers # useful for uniformity especially when downloading from different providers
@@ -367,6 +393,8 @@ use_rofi = {self.use_rofi}
# by the way i recommend getting the rofi themes from this project; # by the way i recommend getting the rofi themes from this project;
rofi_theme = {self.rofi_theme} rofi_theme = {self.rofi_theme}
rofi_theme = {self.rofi_theme_preview}
rofi_theme_input = {self.rofi_theme_input} rofi_theme_input = {self.rofi_theme_input}
rofi_theme_confirm = {self.rofi_theme_confirm} rofi_theme_confirm = {self.rofi_theme_confirm}
@@ -408,8 +436,19 @@ cache_requests = {self.cache_requests}
# leave it as is # leave it as is
use_persistent_provider_store = {self.use_persistent_provider_store} use_persistent_provider_store = {self.use_persistent_provider_store}
# no of recent anime to keep [0-50]
# 0 will disable recent anime tracking
recent = {self.recent}
[stream] [stream]
# the quality of the stream [1080,720,480,360]
# this option is usually only reliable when:
# provider=animepahe
# since it provides links that actually point to streams of different qualities
# while the rest just point to another link that can provide the anime from the same server
quality = {self.quality}
# Auto continue from watch history [True/False] # Auto continue from watch history [True/False]
# this will make fastanime to choose the episode that you last watched to completion # this will make fastanime to choose the episode that you last watched to completion
# and increment it by one # and increment it by one
@@ -482,6 +521,15 @@ episode_complete_at = {self.episode_complete_at}
# or just switch to arch linux # or just switch to arch linux
use_python_mpv = {self.use_python_mpv} use_python_mpv = {self.use_python_mpv}
# whether to use popen to get the timestamps for continue_from_history
# implemented because popen does not work for some reason in nixos
# if you are on nixos and you have a solution to this problem please share
# i will be glad to hear it 😄
# So for now ignore this option
# and anyways the new method of getting timestamps is better
disable_mpv_popen = {self.disable_mpv_popen}
# force mpv window # force mpv window
# the default 'immediate' just makes mpv to open the window even if the video has not yet loaded # the default 'immediate' just makes mpv to open the window even if the video has not yet loaded
# done for asthetics # done for asthetics

View File

@@ -539,6 +539,14 @@ def provider_anime_episode_servers_menu(
episode_title = episode_detail["title"] episode_title = episode_detail["title"]
break break
if config.recent:
config.update_recent(
[
*config.user_data["recent_anime"],
fastanime_runtime_state.selected_anime_anilist,
]
)
print("Updating recent anime...")
if config.sync_play: if config.sync_play:
from ..utils.syncplay import SyncPlayer from ..utils.syncplay import SyncPlayer
@@ -585,22 +593,18 @@ def provider_anime_episode_servers_menu(
# this will try to update the episode to be the next episode if delta has reached a specific threshhold # this will try to update the episode to be the next episode if delta has reached a specific threshhold
# this update will only apply locally # this update will only apply locally
# the remote(anilist) is only updated when its certain you are going to open the player # the remote(anilist) is only updated when its certain you are going to open the player
available_episodes: list[str] = sorted(
fastanime_runtime_state.provider_available_episodes, key=float
)
if stop_time == "0" or total_time == "0": if stop_time == "0" or total_time == "0":
# increment the episodes # increment the episodes
next_episode = available_episodes.index(current_episode_number) + 1 # next_episode = available_episodes.index(current_episode_number) + 1
if next_episode >= len(available_episodes): # if next_episode >= len(available_episodes):
next_episode = len(available_episodes) - 1 # next_episode = len(available_episodes) - 1
episode = available_episodes[next_episode] # episode = available_episodes[next_episode]
pass
else: else:
percentage_completion_of_episode = calculate_percentage_completion( percentage_completion_of_episode = calculate_percentage_completion(
stop_time, total_time stop_time, total_time
) )
if percentage_completion_of_episode < config.episode_complete_at: if percentage_completion_of_episode > config.episode_complete_at:
episode = current_episode_number
else:
# -- update anilist progress if user -- # -- update anilist progress if user --
remote_progress = ( remote_progress = (
fastanime_runtime_state.selected_anime_anilist["mediaListEntry"] or {} fastanime_runtime_state.selected_anime_anilist["mediaListEntry"] or {}
@@ -626,16 +630,16 @@ def provider_anime_episode_servers_menu(
) )
# increment the episodes # increment the episodes
next_episode = available_episodes.index(current_episode_number) + 1 # next_episode = available_episodes.index(current_episode_number) + 1
if next_episode >= len(available_episodes): # if next_episode >= len(available_episodes):
next_episode = len(available_episodes) - 1 # next_episode = len(available_episodes) - 1
episode = available_episodes[next_episode] # episode = available_episodes[next_episode]
stop_time = "0" # stop_time = "0"
total_time = "0" # total_time = "0"
config.media_list_track( config.media_list_track(
anime_id_anilist, anime_id_anilist,
episode_no=episode, episode_no=current_episode_number,
episode_stopped_at=stop_time, episode_stopped_at=stop_time,
episode_total_length=total_time, episode_total_length=total_time,
progress_tracking=fastanime_runtime_state.progress_tracking, progress_tracking=fastanime_runtime_state.progress_tracking,
@@ -670,7 +674,7 @@ def provider_anime_episodes_menu(
) )
# prompt for episode number # prompt for episode number
total_episodes = sorted( available_episodes = sorted(
provider_anime["availableEpisodesDetail"][translation_type], key=float provider_anime["availableEpisodesDetail"][translation_type], key=float
) )
current_episode_number = "" current_episode_number = ""
@@ -681,7 +685,7 @@ def provider_anime_episodes_menu(
# will be preferred over remote # will be preferred over remote
if ( if (
user_watch_history.get(str(anime_id_anilist), {}).get("episode_no") user_watch_history.get(str(anime_id_anilist), {}).get("episode_no")
in total_episodes in available_episodes
): ):
if ( if (
config.preferred_history == "local" config.preferred_history == "local"
@@ -690,6 +694,29 @@ def provider_anime_episodes_menu(
current_episode_number = user_watch_history[str(anime_id_anilist)][ current_episode_number = user_watch_history[str(anime_id_anilist)][
"episode_no" "episode_no"
] ]
stop_time = user_watch_history.get(str(anime_id_anilist), {}).get(
"episode_stopped_at", "0"
)
total_time = user_watch_history.get(str(anime_id_anilist), {}).get(
"episode_total_length", "0"
)
if stop_time != "0" or total_time != "0":
percentage_completion_of_episode = calculate_percentage_completion(
stop_time, total_time
)
if percentage_completion_of_episode > config.episode_complete_at:
# increment the episodes
next_episode = (
available_episodes.index(current_episode_number) + 1
)
if next_episode >= len(available_episodes):
next_episode = len(available_episodes) - 1
episode = available_episodes[next_episode]
stop_time = "0"
total_time = "0"
current_episode_number = episode
else: else:
current_episode_number = str( current_episode_number = str(
(selected_anime_anilist["mediaListEntry"] or {"progress": 0}).get( (selected_anime_anilist["mediaListEntry"] or {"progress": 0}).get(
@@ -707,7 +734,7 @@ def provider_anime_episodes_menu(
"progress" "progress"
) )
) )
if current_episode_number not in total_episodes: if current_episode_number not in available_episodes:
current_episode_number = "" current_episode_number = ""
print( print(
f"[bold cyan]Continuing from Episode:[/] [bold]{current_episode_number}[/]" f"[bold cyan]Continuing from Episode:[/] [bold]{current_episode_number}[/]"
@@ -717,8 +744,8 @@ def provider_anime_episodes_menu(
current_episode_number = "" current_episode_number = ""
# prompt for episode number if not set # prompt for episode number if not set
if not current_episode_number or current_episode_number not in total_episodes: if not current_episode_number or current_episode_number not in available_episodes:
choices = [*total_episodes, "Back"] choices = [*available_episodes, "Back"]
preview = None preview = None
if config.preview: if config.preview:
from .utils import get_fzf_episode_preview from .utils import get_fzf_episode_preview
@@ -727,7 +754,7 @@ def provider_anime_episodes_menu(
if e: if e:
eps = range(0, e + 1) eps = range(0, e + 1)
else: else:
eps = total_episodes eps = available_episodes
preview = get_fzf_episode_preview( preview = get_fzf_episode_preview(
fastanime_runtime_state.selected_anime_anilist, eps fastanime_runtime_state.selected_anime_anilist, eps
) )
@@ -756,7 +783,7 @@ def provider_anime_episodes_menu(
# ) # )
# update runtime data # update runtime data
fastanime_runtime_state.provider_available_episodes = total_episodes fastanime_runtime_state.provider_available_episodes = available_episodes
fastanime_runtime_state.provider_current_episode_number = current_episode_number fastanime_runtime_state.provider_current_episode_number = current_episode_number
# next interface # next interface
@@ -1562,6 +1589,9 @@ def fastanime_main_menu(
watch_history = list(map(int, config.watch_history.keys())) watch_history = list(map(int, config.watch_history.keys()))
return AniList.search(id_in=watch_history, sort="TRENDING_DESC") return AniList.search(id_in=watch_history, sort="TRENDING_DESC")
def _recent():
return (True, {"data": {"Page": {"media": config.user_data["recent_anime"]}}})
# WARNING: Will probably be depracated # WARNING: Will probably be depracated
def _anime_list(): def _anime_list():
anime_list = config.anime_list anime_list = config.anime_list
@@ -1580,6 +1610,8 @@ def fastanime_main_menu(
else: else:
config.load_config() config.load_config()
config.set_fastanime_config_environs()
config.anime_provider.provider = config.provider config.anime_provider.provider = config.provider
config.anime_provider.lazyload_provider(config.provider) config.anime_provider.lazyload_provider(config.provider)
@@ -1589,6 +1621,7 @@ def fastanime_main_menu(
# each option maps to anilist data that is described by the option name # each option maps to anilist data that is described by the option name
options = { options = {
f"{'🔥 ' if icons else ''}Trending": AniList.get_trending, f"{'🔥 ' if icons else ''}Trending": AniList.get_trending,
f"{'🎞️ ' if icons else ''}Recent": _recent,
f"{'📺 ' if icons else ''}Watching": lambda media_list_type="Watching": handle_animelist( f"{'📺 ' if icons else ''}Watching": lambda media_list_type="Watching": handle_animelist(
config, fastanime_runtime_state, media_list_type config, fastanime_runtime_state, media_list_type
), ),

View File

@@ -65,7 +65,7 @@ def save_image_from_url(url: str, file_name: str):
file_name: filename to use file_name: filename to use
""" """
image = requests.get(url) image = requests.get(url)
with open(f"{IMAGES_CACHE_DIR}/{file_name}.png", "wb") as f: with open(os.path.join(IMAGES_CACHE_DIR,f"{file_name}.png"), "wb") as f:
f.write(image.content) f.write(image.content)
@@ -76,7 +76,7 @@ def save_info_from_str(info: str, file_name: str):
info: the information anilist has on the anime info: the information anilist has on the anime
file_name: the filename to use file_name: the filename to use
""" """
with open(f"{ANIME_INFO_CACHE_DIR}/{file_name}", "w") as f: with open(os.path.join(ANIME_INFO_CACHE_DIR,file_name,), "w",encoding="utf-8") as f:
f.write(info) f.write(info)

View File

@@ -1,50 +1,73 @@
import re import re
import os
import shutil import shutil
import subprocess import subprocess
import logging
import time
from fastanime.constants import S_PLATFORM from ...constants import S_PLATFORM
logger = logging.getLogger(__name__)
mpv_av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)")
def stream_video(MPV, url, mpv_args, custom_args): def stream_video(MPV, url, mpv_args, custom_args):
process = subprocess.Popen(
[MPV, url, *mpv_args, *custom_args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
last_time = None
av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)")
last_time = "0" last_time = "0"
total_time = "0" total_time = "0"
if os.environ.get("FASTANIME_DISABLE_MPV_POPEN", "False") == "False":
process = subprocess.Popen(
[
MPV,
url,
*mpv_args,
*custom_args,
"--no-terminal",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
encoding="utf-8",
)
try: try:
while True: while True:
if not process.stderr: if not process.stderr:
continue time.sleep(0.1)
output = process.stderr.readline() continue
output = process.stderr.readline()
if output: if output:
# Match the timestamp in the output # Match the timestamp in the output
match = av_time_pattern.search(output.strip()) match = mpv_av_time_pattern.search(output.strip())
if match:
current_time = match.group(1)
total_time = match.group(2)
last_time = current_time
# Check if the process has terminated
retcode = process.poll()
if retcode is not None:
break
except Exception as e:
print(f"An error occurred: {e}")
logger.error(f"An error occurred: {e}")
finally:
process.terminate()
process.wait()
else:
proc = subprocess.run(
[MPV, url, *mpv_args, *custom_args], capture_output=True, text=True
)
if proc.stdout:
for line in reversed(proc.stdout.split("\n")):
match = mpv_av_time_pattern.search(line.strip())
if match: if match:
current_time = match.group(1) last_time = match.group(1)
total_time = match.group(2) total_time = match.group(2)
match.group(3) break
last_time = current_time
# print(f"Current stream time: {current_time}, Total time: {total_time}, Progress: {percentage}%")
# Check if the process has terminated
retcode = process.poll()
if retcode is not None:
print("Finshed at: ", last_time)
break
except Exception as e:
print(f"An error occurred: {e}")
finally:
process.terminate()
return last_time, total_time return last_time, total_time
@@ -184,13 +207,3 @@ def run_mpv(
mpv_args.append(f"--ytdl-format={ytdl_format}") mpv_args.append(f"--ytdl-format={ytdl_format}")
stop_time, total_time = stream_video(MPV, link, mpv_args, custom_args) stop_time, total_time = stream_video(MPV, link, mpv_args, custom_args)
return stop_time, total_time return stop_time, total_time
# Example usage
if __name__ == "__main__":
run_mpv(
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"Example Video",
"--fullscreen",
"--volume=50",
)

14
fastanime/fastanime.py Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env python3
import os
import sys
# Add the application root directory to Python path
if getattr(sys, "frozen", False):
application_path = os.path.dirname(sys.executable)
sys.path.insert(0, application_path)
# Import and run the main application
from fastanime import FastAnime
if __name__ == "__main__":
FastAnime()

View File

@@ -7,5 +7,6 @@ anime_sources = {
"animepahe": "api.AnimePaheApi", "animepahe": "api.AnimePaheApi",
"hianime": "api.HiAnimeApi", "hianime": "api.HiAnimeApi",
"nyaa": "api.NyaaApi", "nyaa": "api.NyaaApi",
"yugen": "api.YugenApi"
} }
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHE_SERVERS, *HIANIME_SERVERS] SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHE_SERVERS, *HIANIME_SERVERS]

View File

@@ -0,0 +1,216 @@
import base64
from itertools import cycle
from yt_dlp.utils import (
get_element_text_and_html_by_tag,
get_elements_text_and_html_by_attribute,
extract_attributes,
get_element_by_attribute,
)
import re
from yt_dlp.utils.traversal import get_element_html_by_attribute
from .constants import YUGEN_ENDPOINT, SEARCH_URL
from ..decorators import debug_provider
from ..base_provider import AnimeProvider
# ** Adapted from anipy-cli **
class YugenApi(AnimeProvider):
"""
Provides a fast and effective interface to YugenApi site.
"""
PROVIDER = "yugen"
api_endpoint = YUGEN_ENDPOINT
# HEADERS = {
# "Referer": ALLANIME_REFERER,
# }
@debug_provider(PROVIDER.upper())
def search_for_anime(
self,
user_query: str,
translation_type: str = "sub",
nsfw=True,
unknown=True,
**kwargs,
):
results = []
has_next = True
page = 0
while has_next:
page += 1
response = self.session.get(
SEARCH_URL, params={"q": user_query, "page": page}
)
search_results = response.json()
has_next = search_results["hasNext"]
results_html = search_results["query"]
anime = get_elements_text_and_html_by_attribute(
"class", "anime-meta", results_html, tag="a"
)
id_regex = re.compile(r"(\d+)\/([^\/]+)")
for _a in anime:
if not _a:
continue
a = extract_attributes(_a[1])
if not a:
continue
uri = a["href"]
identifier = id_regex.search(uri) # pyright:ignore
if identifier is None:
continue
if len(identifier.groups()) != 2:
continue
identifier = base64.b64encode(
f"{identifier.group(1)}/{identifier.group(2)}".encode()
).decode()
anime_title = a["title"]
languages = {"sub": 1, "dub": 0}
excl = get_element_by_attribute(
"class", "ani-exclamation", _a[1], tag="div"
)
if excl is not None:
if "dub" in excl.lower():
languages["dub"] = 1
#
results.append(
{
"id": identifier,
"title": anime_title,
"availableEpisodes": languages,
}
)
page += 1
return {
"pageInfo": {"total": len(results)},
"results": results,
}
@debug_provider(PROVIDER.upper())
def get_anime(self, anime_id: str, **kwargs):
identifier = base64.b64decode(anime_id).decode()
response = self.session.get(f"{YUGEN_ENDPOINT}/anime/{identifier}")
html_page = response.text
data_map = {
"id": anime_id,
"title": None,
"poster": None,
"genres": [],
"synopsis": None,
"release_year": None,
"status": None,
"otherTitles": [],
"availableEpisodesDetail": {},
}
sub_match = re.search(
r'<div class="ap-.+?">Episodes</div><span class="description" .+?>(\d+)</span></div>',
html_page,
)
if sub_match:
eps = int(sub_match.group(1))
data_map["availableEpisodesDetail"]["sub"] = list(map(str,range(1, eps + 1)))
dub_match = re.search(
r'<div class="ap-.+?">Episodes \(Dub\)</div><span class="description" .+?>(\d+)</span></div>',
html_page,
)
if dub_match:
eps = int(dub_match.group(1))
data_map["availableEpisodesDetail"]["dub"] = list(map(str,range(1, eps + 1)))
name = get_element_text_and_html_by_tag("h1", html_page)
if name is not None:
data_map["title"] = name[0].strip()
synopsis = get_element_by_attribute("class", "description", html_page, tag="p")
if synopsis is not None:
data_map["synopsis"] = synopsis
# FIXME: This is not working because ytdl is too strict on also getting a closing tag
try:
image = get_element_html_by_attribute(
"class", "cover", html_page, tag="img"
)
img_attrs = extract_attributes(image)
if img_attrs is not None:
data_map["image"] = img_attrs.get("src")
except Exception:
pass
data = get_elements_text_and_html_by_attribute(
"class", "data", html_page, tag="div"
)
for d in data:
title = get_element_text_and_html_by_tag("div", d[1])
desc = get_element_text_and_html_by_tag("span", d[1])
if title is None or desc is None:
continue
title = title[0]
desc = desc[0]
if title in ["Native", "Romaji"]:
data_map["alternative_names"].append(desc)
elif title == "Synonyms":
data_map["alternative_names"].extend(desc.split(","))
elif title == "Premiered":
try:
data_map["release_year"] = int(desc.split()[-1])
except (ValueError, TypeError):
pass
elif title == "Status":
data_map["status"] = title
elif title == "Genres":
data_map["genres"].extend([g.strip() for g in desc.split(",")])
return data_map
@debug_provider(PROVIDER.upper())
def get_episode_streams(
self, anime_id, episode_number: str, translation_type="sub"
):
"""get the streams of an episode
Args:
translation_type ([TODO:parameter]): [TODO:description]
anime: [TODO:description]
episode_number: [TODO:description]
Yields:
[TODO:description]
"""
identifier = base64.b64decode(anime_id).decode()
id_num, anime_title = identifier.split("/")
if translation_type == "dub":
video_query = f"{id_num}|{episode_number}|dub"
else:
video_query = f"{id_num}|{episode_number}"
#
res = self.session.post(
f"{YUGEN_ENDPOINT}/api/embed/",
data={
"id": base64.b64encode(video_query.encode()).decode(),
"ac": "0",
},
headers={"x-requested-with": "XMLHttpRequest"},
)
res = res.json()
yield {
"server": "gogoanime",
"episode_title": f"{anime_title}; Episode {episode_number}",
"headers": {},
"subtitles": [],
"links": [{"quality": quality, "link": link} for quality,link in zip(cycle(["1080","720","480","360"]),res["hls"])],
}

View File

@@ -0,0 +1,5 @@
YUGEN_ENDPOINT: str = "https://yugenanime.tv"
SEARCH_URL = YUGEN_ENDPOINT + "/api/discover/"
SERVERS_AVAILABLE = ["gogoanime"]

View File

@@ -1,5 +1,6 @@
import json import json
import logging import logging
import os
import re import re
import time import time
from datetime import datetime from datetime import datetime
@@ -49,7 +50,7 @@ class CachedRequestsSession(requests.Session):
def __init__( def __init__(
self, self,
cache_db_path: str, cache_db_path: str,
max_lifetime: int = 604800, max_lifetime: int = 259200,
max_size: int = (1024**2) * 10, max_size: int = (1024**2) * 10,
table_name: str = "fastanime_requests_cache", table_name: str = "fastanime_requests_cache",
clean_db=False, clean_db=False,
@@ -89,16 +90,10 @@ class CachedRequestsSession(requests.Session):
url, url,
params=None, params=None,
force_caching=False, force_caching=False,
fresh=0, fresh=int(os.environ.get("FASTANIME_FRESH_REQUESTS", 0)),
*args, *args,
**kwargs, **kwargs,
): ):
# TODO: improve the caching functionality and add a layer to auto delete
# expired requests
if fresh:
logger.debug("Executing fresh request")
return super().request(method, url, params=params, *args, **kwargs)
if params: if params:
url += "?" + urlencode(params) url += "?" + urlencode(params)
@@ -128,7 +123,7 @@ class CachedRequestsSession(requests.Session):
cached_request = cursor.fetchone() cached_request = cursor.fetchone()
time_after_access_db = datetime.now() time_after_access_db = datetime.now()
if cached_request: if cached_request and not fresh:
logger.debug("Found existing request in cache") logger.debug("Found existing request in cache")
( (
status_code, status_code,

View File

@@ -11,6 +11,7 @@ class RofiApi:
ROFI_EXECUTABLE = which("rofi") ROFI_EXECUTABLE = which("rofi")
rofi_theme = "" rofi_theme = ""
rofi_theme_preview = ""
rofi_theme_confirm = "" rofi_theme_confirm = ""
rofi_theme_input = "" rofi_theme_input = ""
@@ -21,8 +22,8 @@ class RofiApi:
raise Exception("Rofi not found") raise Exception("Rofi not found")
args = [self.ROFI_EXECUTABLE] args = [self.ROFI_EXECUTABLE]
if self.rofi_theme: if self.rofi_theme_preview:
args.extend(["-no-config", "-theme", self.rofi_theme]) args.extend(["-no-config", "-theme", self.rofi_theme_preview])
args.extend(["-p", f"{prompt_text.title()}", "-i", "-show-icons", "-dmenu"]) args.extend(["-p", f"{prompt_text.title()}", "-i", "-show-icons", "-dmenu"])
result = subprocess.run( result = subprocess.run(
args, args,

2039
poetry.lock generated

File diff suppressed because it is too large Load Diff

65
pyinstaller.spec Normal file
View File

@@ -0,0 +1,65 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
block_cipher = None
# Collect all required data files
datas = [
('fastanime/assets/*', 'fastanime/assets'),
]
# Collect all required hidden imports
hiddenimports = [
'click',
'rich',
'requests',
'yt_dlp',
'python_mpv',
'fuzzywuzzy',
'fastanime',
] + collect_submodules('fastanime')
a = Analysis(
['./fastanime/fastanime.py'], # Changed entry point
pathex=[],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
strip=True, # Strip debug information
optimize=2 # Optimize bytecode noarchive=False
)
pyz = PYZ(
a.pure,
a.zipped_data,
optimize=2 # Optimize bytecode cipher=block_cipher
)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='fastanime',
debug=False,
bootloader_ignore_signals=False,
strip=True,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='fastanime/assets/logo.ico'
)

View File

@@ -1,43 +1,36 @@
[tool.poetry] [project]
name = "fastanime" name = "fastanime"
version = "2.6.5" version = "2.7.4"
description = "A browser anime site experience from the terminal" description = "A browser anime site experience from the terminal"
authors = ["Benextempest <benextempest@gmail.com>"]
license = "UNLICENSE" license = "UNLICENSE"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"click>=8.1.7",
"inquirerpy>=0.3.4",
"requests>=2.32.3",
"rich>=13.9.2",
"thefuzz>=0.22.1",
"yt-dlp>=2024.10.7",
]
[tool.poetry.dependencies] [project.scripts]
python = "^3.10"
yt-dlp = "^2024.5.27"
thefuzz = "^0.22.1"
requests = "^2.32.3"
rich = { version = "^13.7.1", optional = false }
click = { version = "^8.1.7", optional = false }
inquirerpy = { version = "^0.3.4", optional = false }
mpv = { version = "^1.0.7", optional = true }
plyer = { version = "^2.1.0", optional = true }
fastapi = {extras = ["standard"], version = "^0.115.0", optional = true}
[tool.poetry.extras]
full = ["plyer", "mpv", "fastapi"]
# cli = ["rich", "click", "inquirerpy"]
mpv = ["mpv"]
notifications = ["plyer"]
api = ["fastapi"]
[tool.poetry.group.dev.dependencies]
black = "^24.4.2"
isort = "^5.13.2"
pytest = "^8.2.2"
ruff = "^0.4.10"
pre-commit = "^3.7.1"
autoflake = "^2.3.1"
tox = "^4.16.0"
pyright = "^1.1.374"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
fastanime = 'fastanime:FastAnime' fastanime = 'fastanime:FastAnime'
[project.optional-dependencies]
standard = ["fastapi[standard]>=0.115.0", "mpv>=1.0.7", "plyer>=2.1.0"]
api = ["fastapi[standard]>=0.115.0"]
notifications = ["plyer>=2.1.0"]
mpv = ["mpv>=1.0.7"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
dev-dependencies = [
"pyinstaller>=6.11.1",
"pyright>=1.1.384",
"pytest>=8.3.3",
"ruff>=0.6.9",
]

18
tox.ini
View File

@@ -5,23 +5,23 @@ env_list = lint, pyright, py{310,311}
[testenv] [testenv]
description = run unit tests description = run unit tests
deps =poetry deps =uv
commands = commands =
poetry install --all-extras uv sync --dev --all-extras
poetry run pytest uv run pytest
[testenv:lint] [testenv:lint]
description = run linters description = run linters
skip_install = true skip_install = true
deps =poetry deps =uv
commands = commands =
poetry install --all-extras uv sync --dev --all-extras
poetry run black . uv run ruff format .
[testenv:pyright] [testenv:pyright]
description = run type checking description = run type checking
skip_install = true skip_install = true
deps =poetry deps =uv
commands = commands =
poetry install --no-root --all-extras uv sync --dev --all-extras
poetry run pyright uv run pyright

1207
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff