Compare commits

...

37 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
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
26 changed files with 2464 additions and 2249 deletions

View File

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

View File

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

View File

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

View File

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

666
README.md
View File

@@ -1,9 +1,9 @@
# **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)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/Benex254/FastAnime)
![GitHub deployments](https://img.shields.io/github/deployments/Benex254/fastanime/pypi?label=PyPi%20Publish)
![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/FastAnime/FastAnime)
![GitHub deployments](https://img.shields.io/github/deployments/FastAnime/fastanime/pypi?label=PyPi%20Publish)
![PyPI - License](https://img.shields.io/pypi/l/fastanime)
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)
- [Installation](#installation)
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
- [Using uv](#using-uv)
- [Using pipx](#using-pipx)
- [Using pip](#using-pip)
- [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)
- [update subcommand](#update-subcommand)
- [completions subcommand](#completions-subcommand)
- [fastanime serve](#fastanime-serve)
- [MPV specific commands](#mpv-specific-commands)
- [Key Bindings](#key-bindings)
- [Script Messages](#script-messages)
@@ -94,14 +96,14 @@ With the following extras available:
#### Using uv
Recommended method of installation
Recommended method of installation is using [uv](https://docs.astral.sh/uv/).
```bash
# generally:
uv tool install fastanime
# if you want other functionality:
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]
@@ -132,7 +134,7 @@ pip install 'fastanime==<latest-pre-release-tag>.dev1'
### 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:
```bash
@@ -153,24 +155,17 @@ Requirements:
- [git](https://git-scm.com/)
- [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:
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`
3. Then build and Install the app:
```bash
# Normal Installation
poetry build
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 .
# build and install fastanime with uv
uv tool install .
```
4. Enjoy! Verify installation with:
@@ -181,12 +176,13 @@ fastanime --version
> [!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:
>
> - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/`
> - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc`
> - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc`
> or using the built in command `fastanime completions`
### External Dependencies
@@ -364,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.
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:
@@ -670,6 +666,632 @@ fastanime serve
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
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.
@@ -796,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 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

2
fa
View File

@@ -1,3 +1,3 @@
#!/usr/bin/env sh
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"},
"animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
"nyaa": {},
"yugen": {},
}

View File

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

View File

@@ -1,11 +1,15 @@
from typing import Literal
from fastapi import FastAPI
from requests import post
from thefuzz import fuzz
from ..AnimeProvider import AnimeProvider
from ..Utility.data import anime_normalizer
app = FastAPI()
anime_provider = AnimeProvider("allanime", "true", "true")
ANILIST_ENDPOINT = "https://graphql.anilist.co"
@app.get("/search")
@@ -23,3 +27,67 @@ def get_episode_streams(
anime_id: str, episode: str, translation_type: Literal["sub", "dub"]
):
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("--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-preview", help="Rofi theme to use for previews", type=click.Path()
)
@click.option(
"--rofi-theme-confirm",
help="Rofi theme to use for the confirm prompt",
@@ -210,6 +213,7 @@ def run_cli(
sub,
rofi,
rofi_theme,
rofi_theme_preview,
rofi_theme_confirm,
rofi_theme_input,
use_python_mpv,
@@ -325,6 +329,10 @@ def run_cli(
if 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:
ctx.obj.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}"
def update_app():
def update_app(force=False):
is_latest, release_json = check_for_updates()
if is_latest:
if is_latest and not force:
print("[green]App is up to date[/]")
return False, release_json
tag_name = release_json["tag_name"]
@@ -101,8 +101,10 @@ def update_app():
)
else:
if PIPX_EXECUTABLE := shutil.which("pipx"):
process = subprocess.run([PIPX_EXECUTABLE, "upgrade", APP_NAME])
if UV := shutil.which("uv"):
process = subprocess.run([UV, "tool", "upgrade", APP_NAME])
elif PIPX := shutil.which("pipx"):
process = subprocess.run([PIPX, "upgrade", APP_NAME])
else:
PYTHON_EXECUTABLE = sys.executable

View File

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

View File

@@ -9,6 +9,7 @@ from ..constants import (
USER_DATA_PATH,
USER_VIDEOS_DIR,
USER_WATCH_HISTORY_PATH,
S_PLATFORM,
)
from ..libs.rofi import Rofi
@@ -34,14 +35,15 @@ class Config(object):
"continue_from_history": "True",
"default_media_list_tracking": "None",
"downloads_dir": USER_VIDEOS_DIR,
"disable_mpv_popen": "True",
"episode_complete_at": "80",
"ffmpegthumbnailer_seek_time": "-1",
"force_forward_tracking": "true",
"force_window": "immediate",
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
"icons": "false",
"image_previews": "true",
"normalize_titles": "true",
"image_previews": "True" if S_PLATFORM != "win32" else "False",
"normalize_titles": "True",
"notification_duration": "2",
"player": "mpv",
"preferred_history": "local",
@@ -51,6 +53,7 @@ class Config(object):
"quality": "1080",
"recent": "50",
"rofi_theme": "",
"rofi_theme_preview": "",
"rofi_theme_confirm": "",
"rofi_theme_input": "",
"server": "top",
@@ -78,11 +81,17 @@ class Config(object):
if os.path.exists(USER_CONFIG_PATH):
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_select = self.get_auto_select()
self.cache_requests = self.get_cache_requests()
self.continue_from_history = self.get_continue_from_history()
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.episode_complete_at = self.get_episode_complete_at()
self.ffmpegthumbnailer_seek_time = self.get_ffmpegthumnailer_seek_time()
@@ -104,10 +113,12 @@ class Config(object):
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
self.rofi_theme_input = self.get_rofi_theme_input()
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_input = self.rofi_theme_input
Rofi.rofi_theme = self.rofi_theme
Rofi.rofi_theme_preview = self.rofi_theme_preview
self.server = self.get_server()
self.skip = self.get_skip()
@@ -234,6 +245,9 @@ class Config(object):
def get_rofi_theme(self):
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):
return self.configparser.get("general", "rofi_theme_input")
@@ -325,13 +339,6 @@ class Config(object):
# be sure to also give the replacement emoji
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]
# 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
@@ -386,6 +393,8 @@ use_rofi = {self.use_rofi}
# by the way i recommend getting the rofi themes from this project;
rofi_theme = {self.rofi_theme}
rofi_theme = {self.rofi_theme_preview}
rofi_theme_input = {self.rofi_theme_input}
rofi_theme_confirm = {self.rofi_theme_confirm}
@@ -433,6 +442,13 @@ recent = {self.recent}
[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]
# this will make fastanime to choose the episode that you last watched to completion
# and increment it by one
@@ -505,6 +521,15 @@ episode_complete_at = {self.episode_complete_at}
# or just switch to arch linux
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
# the default 'immediate' just makes mpv to open the window even if the video has not yet loaded
# done for asthetics

View File

@@ -593,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 update will only apply locally
# 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":
# 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]
# 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]
pass
else:
percentage_completion_of_episode = calculate_percentage_completion(
stop_time, total_time
)
if percentage_completion_of_episode < config.episode_complete_at:
episode = current_episode_number
else:
if percentage_completion_of_episode > config.episode_complete_at:
# -- update anilist progress if user --
remote_progress = (
fastanime_runtime_state.selected_anime_anilist["mediaListEntry"] or {}
@@ -634,16 +630,16 @@ def provider_anime_episode_servers_menu(
)
# 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"
# 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"
config.media_list_track(
anime_id_anilist,
episode_no=episode,
episode_no=current_episode_number,
episode_stopped_at=stop_time,
episode_total_length=total_time,
progress_tracking=fastanime_runtime_state.progress_tracking,
@@ -678,7 +674,7 @@ def provider_anime_episodes_menu(
)
# prompt for episode number
total_episodes = sorted(
available_episodes = sorted(
provider_anime["availableEpisodesDetail"][translation_type], key=float
)
current_episode_number = ""
@@ -689,7 +685,7 @@ def provider_anime_episodes_menu(
# will be preferred over remote
if (
user_watch_history.get(str(anime_id_anilist), {}).get("episode_no")
in total_episodes
in available_episodes
):
if (
config.preferred_history == "local"
@@ -698,6 +694,29 @@ def provider_anime_episodes_menu(
current_episode_number = user_watch_history[str(anime_id_anilist)][
"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:
current_episode_number = str(
(selected_anime_anilist["mediaListEntry"] or {"progress": 0}).get(
@@ -715,7 +734,7 @@ def provider_anime_episodes_menu(
"progress"
)
)
if current_episode_number not in total_episodes:
if current_episode_number not in available_episodes:
current_episode_number = ""
print(
f"[bold cyan]Continuing from Episode:[/] [bold]{current_episode_number}[/]"
@@ -725,8 +744,8 @@ def provider_anime_episodes_menu(
current_episode_number = ""
# prompt for episode number if not set
if not current_episode_number or current_episode_number not in total_episodes:
choices = [*total_episodes, "Back"]
if not current_episode_number or current_episode_number not in available_episodes:
choices = [*available_episodes, "Back"]
preview = None
if config.preview:
from .utils import get_fzf_episode_preview
@@ -735,7 +754,7 @@ def provider_anime_episodes_menu(
if e:
eps = range(0, e + 1)
else:
eps = total_episodes
eps = available_episodes
preview = get_fzf_episode_preview(
fastanime_runtime_state.selected_anime_anilist, eps
)
@@ -764,7 +783,7 @@ def provider_anime_episodes_menu(
# )
# 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
# next interface
@@ -1591,6 +1610,8 @@ def fastanime_main_menu(
else:
config.load_config()
config.set_fastanime_config_environs()
config.anime_provider.provider = config.provider
config.anime_provider.lazyload_provider(config.provider)

View File

@@ -65,7 +65,7 @@ def save_image_from_url(url: str, file_name: str):
file_name: filename to use
"""
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)
@@ -76,7 +76,7 @@ def save_info_from_str(info: str, file_name: str):
info: the information anilist has on the anime
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)

View File

@@ -1,50 +1,73 @@
import re
import os
import shutil
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):
process = subprocess.Popen(
[MPV, url, *mpv_args, *custom_args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
last_time = None
av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)")
last_time = "0"
total_time = "0"
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:
while True:
if not process.stderr:
continue
output = process.stderr.readline()
try:
while True:
if not process.stderr:
time.sleep(0.1)
continue
output = process.stderr.readline()
if output:
# Match the timestamp in the output
match = av_time_pattern.search(output.strip())
if output:
# Match the timestamp in the output
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:
current_time = match.group(1)
last_time = match.group(1)
total_time = match.group(2)
match.group(3)
last_time = current_time
# print(f"Current stream time: {current_time}, Total time: {total_time}, Progress: {percentage}%")
# Check if the process has terminated
retcode = process.poll()
if retcode is not None:
print("Finshed at: ", last_time)
break
except Exception as e:
print(f"An error occurred: {e}")
finally:
process.terminate()
break
return last_time, total_time
@@ -184,13 +207,3 @@ def run_mpv(
mpv_args.append(f"--ytdl-format={ytdl_format}")
stop_time, total_time = stream_video(MPV, link, mpv_args, custom_args)
return stop_time, total_time
# Example usage
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",
"hianime": "api.HiAnimeApi",
"nyaa": "api.NyaaApi",
"yugen": "api.YugenApi"
}
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

@@ -11,6 +11,7 @@ class RofiApi:
ROFI_EXECUTABLE = which("rofi")
rofi_theme = ""
rofi_theme_preview = ""
rofi_theme_confirm = ""
rofi_theme_input = ""
@@ -21,8 +22,8 @@ class RofiApi:
raise Exception("Rofi not found")
args = [self.ROFI_EXECUTABLE]
if self.rofi_theme:
args.extend(["-no-config", "-theme", self.rofi_theme])
if self.rofi_theme_preview:
args.extend(["-no-config", "-theme", self.rofi_theme_preview])
args.extend(["-p", f"{prompt_text.title()}", "-i", "-show-icons", "-dmenu"])
result = subprocess.run(
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"
version = "2.6.7"
version = "2.7.4"
description = "A browser anime site experience from the terminal"
authors = ["Benextempest <benextempest@gmail.com>"]
license = "UNLICENSE"
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]
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]
[project.scripts]
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]
description = run unit tests
deps =poetry
deps =uv
commands =
poetry install --all-extras
poetry run pytest
uv sync --dev --all-extras
uv run pytest
[testenv:lint]
description = run linters
skip_install = true
deps =poetry
deps =uv
commands =
poetry install --all-extras
poetry run black .
uv sync --dev --all-extras
uv run ruff format .
[testenv:pyright]
description = run type checking
skip_install = true
deps =poetry
deps =uv
commands =
poetry install --no-root --all-extras
poetry run pyright
uv sync --dev --all-extras
uv run pyright

1207
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff