mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-06 04:41:06 -08:00
Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd0e7db73c | ||
|
|
fb705b4ac2 | ||
|
|
93654be74f | ||
|
|
9b14a4c723 | ||
|
|
bce5acf7b5 | ||
|
|
228be7e1f7 | ||
|
|
2eb434e42a | ||
|
|
fbb3a00ab0 | ||
|
|
8341ffe8fd | ||
|
|
0822e2e92c | ||
|
|
3b9fbd0665 | ||
|
|
60b74bee18 | ||
|
|
d7dc63e003 | ||
|
|
d40edb6ff6 | ||
|
|
803712649f | ||
|
|
7bc0d33f69 | ||
|
|
5885d134df | ||
|
|
5500ec49c8 | ||
|
|
8c94380050 | ||
|
|
87a97dd0c6 | ||
|
|
80d9f732b1 | ||
|
|
051273dac9 | ||
|
|
036f448906 | ||
|
|
b5aeed9268 | ||
|
|
4257502b85 | ||
|
|
28a857520f | ||
|
|
4f9fff375c | ||
|
|
ce31f63788 | ||
|
|
9412c2491e | ||
|
|
8209adec62 | ||
|
|
39703d9eca | ||
|
|
57d16b3e18 | ||
|
|
73a99f8b96 | ||
|
|
309d7d5858 | ||
|
|
8d20e490ca | ||
|
|
3a6e005f3a | ||
|
|
bdf49bd7ce | ||
|
|
c4df2587d0 | ||
|
|
b38f66767f | ||
|
|
6c0e0ccf72 | ||
|
|
e39c992883 | ||
|
|
a1744fc9b3 | ||
|
|
3c5106c32c | ||
|
|
fd0d899f72 | ||
|
|
c753873f61 | ||
|
|
4c8ff2ae9b | ||
|
|
23274de367 | ||
|
|
2aec40ead0 | ||
|
|
172f2bb1de | ||
|
|
2f5684a93a | ||
|
|
1d40160abf | ||
|
|
af84d80137 | ||
|
|
e6412631ae | ||
|
|
978d8d45ba | ||
|
|
06575120d6 | ||
|
|
72cec28613 | ||
|
|
8023edcf3a | ||
|
|
0cb50cd506 | ||
|
|
9981b3dec8 | ||
|
|
50c048e158 | ||
|
|
c0a57c7814 | ||
|
|
bcdd88c725 | ||
|
|
d45d438663 | ||
|
|
3d12059e27 | ||
|
|
677f4690fa | ||
|
|
a79b59f727 | ||
|
|
5641c245e7 | ||
|
|
058fc285cd | ||
|
|
71cfe667c9 | ||
|
|
d9692201aa | ||
|
|
1fd4087b41 | ||
|
|
787eb0c9ca | ||
|
|
acd937f8ab | ||
|
|
52af68d13f | ||
|
|
1ff3074fad | ||
|
|
debaa2ffa6 | ||
|
|
5b6ccbe748 | ||
|
|
d6ca923951 | ||
|
|
0e9bf7f2de | ||
|
|
ccad2435b0 | ||
|
|
30fa9851dd | ||
|
|
000bae9bb7 | ||
|
|
8c2bb71e08 | ||
|
|
57393b085a | ||
|
|
5f721847d7 | ||
|
|
383cb62ede | ||
|
|
434ac947dd | ||
|
|
d0fb39cede | ||
|
|
f98ae77587 | ||
|
|
33e1b0fb6f | ||
|
|
7134702eb9 | ||
|
|
cac7586a86 | ||
|
|
0b9da27def | ||
|
|
ddbb4ca451 | ||
|
|
757393aa36 | ||
|
|
eb54d5e995 | ||
|
|
0d95a38321 | ||
|
|
8d2734db74 | ||
|
|
b3abcb958b | ||
|
|
0667749e4c | ||
|
|
57e73e6799 | ||
|
|
7d890b9719 | ||
|
|
8cbbcf458d | ||
|
|
67bc25a527 | ||
|
|
e668f9326a | ||
|
|
a02db6471f | ||
|
|
08b1f0c90c | ||
|
|
3ec8dbee8c | ||
|
|
473c11faca | ||
|
|
320e3799d3 | ||
|
|
a0f28ddf6d | ||
|
|
9512c3530a | ||
|
|
72602a0ec1 | ||
|
|
4daf6a2b07 | ||
|
|
8b37927f6a | ||
|
|
9d6f785a7f | ||
|
|
897c34d98c | ||
|
|
28c75215bd | ||
|
|
8697b27fe0 | ||
|
|
b6e05c877b | ||
|
|
d8c3ba6181 | ||
|
|
8b5c917038 | ||
|
|
856f62c245 | ||
|
|
02dfc9d71c | ||
|
|
cef0bae528 | ||
|
|
4867720ad2 | ||
|
|
8d85e30150 | ||
|
|
eb99b7e6ba | ||
|
|
089c049f26 | ||
|
|
a33e47d205 | ||
|
|
25dc35eaaf | ||
|
|
525586e955 | ||
|
|
5129219e23 | ||
|
|
7cd97c78b1 | ||
|
|
27b4422ef3 | ||
|
|
1c367c8aa1 | ||
|
|
7b6cc48b90 | ||
|
|
812d0110a7 | ||
|
|
60b05bf0ac | ||
|
|
d830cca3bc | ||
|
|
209e93b6d9 | ||
|
|
b10d9dc39a | ||
|
|
fe8cda094c | ||
|
|
33c06eab0a | ||
|
|
f3f4be7410 | ||
|
|
3915ef0fb6 | ||
|
|
20d26166dd | ||
|
|
ddca724bd8 | ||
|
|
b86c1a0479 | ||
|
|
1fa7830ddf | ||
|
|
59abafbe16 | ||
|
|
b6eebb9736 | ||
|
|
61db9aeea6 | ||
|
|
966301bce8 | ||
|
|
d776880306 |
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: benexl # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: benexl # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,5 +1,5 @@
|
||||
default_language_version:
|
||||
python: python3.10
|
||||
python: python3.12
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -7,7 +7,7 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: ["--profile", "black"] # Ensure compatibility with Black
|
||||
args: ["--profile", "black"]
|
||||
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.2.1
|
||||
@@ -19,17 +19,15 @@ repos:
|
||||
"--remove-unused-variables",
|
||||
"--remove-all-unused-imports",
|
||||
]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.4.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
# - repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# rev: v0.4.10
|
||||
# hooks:
|
||||
# - id: ruff
|
||||
# args: [--fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
name: black
|
||||
language_version: python3.10 # to ensure compatibilty
|
||||
#language_version: python3.10
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.analysis.autoImportCompletions": true
|
||||
}
|
||||
796
README.md
796
README.md
@@ -22,74 +22,27 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<b>My Rice</b>
|
||||
<b>Riced</b>
|
||||
</summary>
|
||||
|
||||

|
||||
|
||||
**Anilist results menu:**
|
||||

|
||||
|
||||
**Episodes menu preview:**
|
||||

|
||||
|
||||
**Without preview images enabled:**
|
||||

|
||||
|
||||
**Desktop notifications + episodes menu without image preview:**
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>fzf mode</b></summary>
|
||||
|
||||
[fastanime-fzf.webm](https://github.com/user-attachments/assets/90875a57-198b-4c78-98d5-10a459001edd)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>rofi mode</b></summary>
|
||||
|
||||
[fa_rofi_mode.webm](https://github.com/user-attachments/assets/2ce669bf-b62f-4c44-bd79-cf0dcaddf37a)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Default mode</b></summary>
|
||||
|
||||
[fa_default_mode.webm](https://github.com/user-attachments/assets/1ce3a23d-f4a0-4bc1-8518-426ec7b3b69e)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<!--toc:start-->
|
||||
|
||||
- [**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)
|
||||
- [Building from the source](#building-from-the-source)
|
||||
- [External Dependencies](#external-dependencies)
|
||||
- [Usage](#usage)
|
||||
- [The Commandline interface :fire:](#the-commandline-interface-fire)
|
||||
- [The anilist command :fire: :fire: :fire:](#the-anilist-command-fire-fire-fire)
|
||||
- [Running without any subcommand](#running-without-any-subcommand)
|
||||
- [Subcommands](#subcommands)
|
||||
- [download subcommand](#download-subcommand)
|
||||
- [search subcommand](#search-subcommand)
|
||||
- [grab subcommand](#grab-subcommand)
|
||||
- [downloads subcommand](#downloads-subcommand)
|
||||
- [config subcommand](#config-subcommand)
|
||||
- [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)
|
||||
- [styling the default interface](#styling-the-default-interface)
|
||||
- [Configuration](#configuration)
|
||||
- [Contributing](#contributing)
|
||||
- [Receiving Support](#receiving-support)
|
||||
- [Supporting the Project](#supporting-the-project)
|
||||
<!--toc:end-->
|
||||
## Installation
|
||||
|
||||

|
||||
@@ -101,12 +54,33 @@
|
||||
The app can run wherever python can run. So all you need to have is python installed on your device.
|
||||
On android you can use [termux](https://github.com/termux/termux-app).
|
||||
If you have any difficulty consult for help on the [discord channel](https://discord.gg/HBEmAwvbHV)
|
||||
|
||||
### Installation on nixos
|
||||
|
||||

|
||||
|
||||
```bash
|
||||
nix profile install github:Benexl/fastanime
|
||||
```
|
||||
|
||||
### Installation on Arch
|
||||
|
||||

|
||||
|
||||
Install from the AUR using an AUR helper such as [yay](https://github.com/Jguer/yay) or [paru](https://github.com/Morganamilo/paru), either the git version, which uses the latest commit:
|
||||
|
||||

|
||||
|
||||
```bash
|
||||
nix profile install github:Benex254/fastanime
|
||||
yay -S fastanime-git
|
||||
```
|
||||
|
||||
or the stable version, which uses a tagged release:
|
||||
|
||||

|
||||
|
||||
```bash
|
||||
yay -S fastanime
|
||||
```
|
||||
|
||||
### Installation using your favourite package manager
|
||||
@@ -118,10 +92,10 @@ With the following extras available:
|
||||
- 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/).
|
||||
Recommended method of installation is using [uv](https://docs.astral.sh/uv/).
|
||||
|
||||
```bash
|
||||
# generally:
|
||||
@@ -184,7 +158,7 @@ Requirements:
|
||||
|
||||
To build from the source, follow these steps:
|
||||
|
||||
1. Clone the repository: `git clone https://github.com/FastAnime/FastAnime.git --depth 1`
|
||||
1. Clone the repository: `git clone https://github.com/Benexl/FastAnime.git --depth 1`
|
||||
2. Navigate into the folder: `cd FastAnime`
|
||||
3. Then build and Install the app:
|
||||
|
||||
@@ -325,7 +299,7 @@ fastanime --manga search -t <manga-title>
|
||||
|
||||
#### The anilist command :fire: :fire: :fire:
|
||||
|
||||
Stream, browse, and discover anime efficiently from the terminal using the [AniList API](https://github.com/AniList/ApiV2-GraphQL-Docs).
|
||||
Uses the [AniList API](https://github.com/AniList/ApiV2-GraphQL-Docs) to create a terminal anilist client which is then intergrated with the scraping capabilities of the project.
|
||||
|
||||
##### Running without any subcommand
|
||||
|
||||
@@ -387,6 +361,33 @@ For more details visit the anilist docs or just get the completions which will i
|
||||
|
||||
Like seriously **[get the completions](https://github.com/FastAnime/FastAnime#completions-subcommand)** and the experience will be a 💯 💯 better.
|
||||
|
||||
**Fastanime anilist download:**
|
||||
Supports all the options for search except its used for downloading.
|
||||
it also supports all options for `fastanime download`
|
||||
Example:
|
||||
|
||||
```bash
|
||||
# get anime with the tag of isekai
|
||||
fastanime anilist download -T isekai
|
||||
|
||||
# get anime of 2024 and sort by popularity
|
||||
# that has already finished airing or is releasing
|
||||
# and is not in your anime lists
|
||||
fastanime anilist download -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
|
||||
|
||||
# get anime of 2024 season WINTER
|
||||
fastanime anilist download -y 2024 --season WINTER
|
||||
|
||||
# get anime genre action and tag isekai,magic
|
||||
fastanime anilist download -g Action -T Isekai -T Magic
|
||||
|
||||
# get anime of 2024 thats finished airing
|
||||
fastanime anilist download -y 2024 -S FINISHED
|
||||
|
||||
# get the most favourite anime movies
|
||||
fastanime anilist download -f MOVIE -s FAVOURITES_DESC
|
||||
```
|
||||
|
||||
The following are commands you can only run if you are signed in to your AniList account:
|
||||
|
||||
- `fastanime anilist watching`
|
||||
@@ -625,7 +626,7 @@ fastanime config --view
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` 😉.
|
||||
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` 😉.
|
||||
|
||||
#### cache subcommand
|
||||
|
||||
@@ -691,632 +692,6 @@ 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": "",
|
||||
"quality": "1080"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"server": "sharepoint",
|
||||
"headers": {},
|
||||
"subtitles": [],
|
||||
"episode_title": "Sayounara Ryuusei, Konnichiwa Jinsei; Episode 3",
|
||||
"links": [
|
||||
{
|
||||
"link": "",
|
||||
"mp4": true,
|
||||
"resolutionStr": "Mp4",
|
||||
"src": "",
|
||||
"quality": "1080"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"server": "gogoanime",
|
||||
"headers": {},
|
||||
"subtitles": [],
|
||||
"episode_title": "Sayounara Ryuusei, Konnichiwa Jinsei; Episode 3",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
</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.
|
||||
@@ -1396,7 +771,7 @@ rofi_theme_input =
|
||||
|
||||
rofi_theme_confirm =
|
||||
|
||||
notification_duration = 2
|
||||
notification_duration = 120
|
||||
|
||||
sub_lang = eng
|
||||
|
||||
@@ -1437,32 +812,31 @@ format = best[height<=1080]/bestvideo[height<=1080]+bestaudio/best
|
||||
player = mpv
|
||||
```
|
||||
|
||||
### Other Terminal Browsers I Made
|
||||
[yt-x](https://github.com/Benexl/yt-x) - browse youtube and other yt-dlp sites from the terminal
|
||||
|
||||
[lib-x](https://github.com/Benexl/lib-x) - browse your calibre library from the terminal
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome your issues and feature requests. However, due to time constraints, I currently do not plan to add another provider.
|
||||
But if you are willing to add one yourself pr's are welcome.
|
||||
pr's are highly welcome
|
||||
|
||||
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, i will ignore issues 😝.
|
||||
|
||||
## Receiving Support
|
||||
|
||||
For inquiries, join our [Discord Server](https://discord.gg/HBEmAwvbHV).
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/HBEmAwvbHV">
|
||||
<img src="https://invidget.switchblade.xyz/C4rhMA4mmK">
|
||||
</a>
|
||||
</p>
|
||||
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/Benexl/FastAnime/blob/master/fastanime/Utility/data.py) and open a pr, issues will be ignored 😝.
|
||||
|
||||
## Supporting the Project
|
||||
More pr's less issues 🙃
|
||||
Those who contribute at least five times will be able to make changes to the repo without my review.
|
||||
|
||||
Show your support by starring the GitHub repository or [buying me a coffee](https://ko-fi.com/benex254).
|
||||
More pr's less issues 🙃
|
||||
|
||||
Show your support by starring the GitHub repository.
|
||||
|
||||
[](https://ko-fi.com/Y8Y8ZAA7N)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> This project currently scrapes allanime, hianime, nyaa, yugen and animepahe.
|
||||
> The developer(s) of this application does not have any affiliation with the content providers available, and this application hosts zero content.
|
||||
> [DISCLAIMER](https://github.com/Benex254/FastAnime/blob/master/DISCLAIMER.md)
|
||||
> The developer(s) of this application does not have any affiliation with the content providers available, and this application hosts zero content.
|
||||
> [DISCLAIMER](https://github.com/Benexl/FastAnime/blob/master/DISCLAIMER.md)
|
||||
|
||||
@@ -62,11 +62,7 @@ class AnimeProvider:
|
||||
)
|
||||
|
||||
def search_for_anime(
|
||||
self,
|
||||
user_query,
|
||||
translation_type,
|
||||
nsfw=True,
|
||||
unknown=True,
|
||||
self, search_keywords, translation_type, **kwargs
|
||||
) -> "SearchResults | None":
|
||||
"""core abstraction over all providers search functionality
|
||||
|
||||
@@ -82,7 +78,7 @@ class AnimeProvider:
|
||||
"""
|
||||
anime_provider = self.anime_provider
|
||||
results = anime_provider.search_for_anime(
|
||||
user_query, translation_type, nsfw, unknown
|
||||
search_keywords, translation_type, **kwargs
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -90,6 +86,7 @@ class AnimeProvider:
|
||||
def get_anime(
|
||||
self,
|
||||
anime_id: str,
|
||||
**kwargs,
|
||||
) -> "Anime | None":
|
||||
"""core abstraction over getting info of an anime from all providers
|
||||
|
||||
@@ -101,7 +98,7 @@ class AnimeProvider:
|
||||
[TODO:return]
|
||||
"""
|
||||
anime_provider = self.anime_provider
|
||||
results = anime_provider.get_anime(anime_id)
|
||||
results = anime_provider.get_anime(anime_id, **kwargs)
|
||||
|
||||
return results
|
||||
|
||||
@@ -110,6 +107,7 @@ class AnimeProvider:
|
||||
anime_id,
|
||||
episode: str,
|
||||
translation_type: str,
|
||||
**kwargs,
|
||||
) -> "Iterator[Server] | None":
|
||||
"""core abstractions for getting juicy streams from all providers
|
||||
|
||||
@@ -124,6 +122,6 @@ class AnimeProvider:
|
||||
"""
|
||||
anime_provider = self.anime_provider
|
||||
results = anime_provider.get_episode_streams(
|
||||
anime_id, episode, translation_type
|
||||
anime_id, episode, translation_type, **kwargs
|
||||
)
|
||||
return results
|
||||
|
||||
@@ -12,7 +12,11 @@ anime_normalizer_raw = {
|
||||
"Re:Zero kara Hajimeru Isekai Seikatsu Season 3": "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season",
|
||||
},
|
||||
"hianime": {"My Star": "Oshi no Ko"},
|
||||
"animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
|
||||
"animepahe": {
|
||||
"Azumanga Daiou The Animation": "Azumanga Daioh",
|
||||
"Mairimashita! Iruma-kun 2nd Season": "Mairimashita! Iruma-kun 2",
|
||||
"Mairimashita! Iruma-kun 3rd Season": "Mairimashita! Iruma-kun 3",
|
||||
},
|
||||
"nyaa": {},
|
||||
"yugen": {},
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ class YtDLPDownloader:
|
||||
merge=False,
|
||||
clean=False,
|
||||
prompt=True,
|
||||
force_ffmpeg=False,
|
||||
hls_use_mpegts=False,
|
||||
hls_use_h264=False,
|
||||
):
|
||||
"""Helper function that downloads anime given url and path details
|
||||
|
||||
@@ -91,7 +94,35 @@ class YtDLPDownloader:
|
||||
vid_path = ""
|
||||
sub_path = ""
|
||||
for i, url in enumerate(urls):
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
options = ydl_opts
|
||||
if i == 0:
|
||||
if force_ffmpeg:
|
||||
options = options | {
|
||||
"external_downloader": {"default": "ffmpeg"},
|
||||
"external_downloader_args": {
|
||||
"ffmpeg_i1": ["-v", "error", "-stats"],
|
||||
},
|
||||
}
|
||||
if hls_use_mpegts:
|
||||
options = options | {
|
||||
"hls_use_mpegts": True,
|
||||
"outtmpl": ".".join(options["outtmpl"].split(".")[:-1]) + ".ts", # force .ts extension
|
||||
}
|
||||
elif hls_use_h264:
|
||||
options = options | {
|
||||
"external_downloader_args": options["external_downloader_args"] | {
|
||||
"ffmpeg_o1": [
|
||||
"-c:v", "copy",
|
||||
"-c:a", "aac",
|
||||
"-bsf:a", "aac_adtstoasc",
|
||||
"-q:a", "1",
|
||||
"-ac", "2",
|
||||
"-af", "loudnorm=I=-22:TP=-2.5:LRA=11,alimiter=limit=-1.5dB", # prevent clipping from HE-AAC to AAC convertion
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(options) as ydl:
|
||||
info = ydl.extract_info(url, download=True)
|
||||
if not info:
|
||||
continue
|
||||
|
||||
@@ -6,10 +6,10 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v2.8.2"
|
||||
__version__ = "v2.8.8"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
AUTHOR = "Benexl"
|
||||
GIT_REPO = "github.com"
|
||||
REPO = f"{GIT_REPO}/{AUTHOR}/{APP_NAME}"
|
||||
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
// https://github.com/Wraient/curd/blob/main/rofi/selectanime.rasi
|
||||
// Go give there project a star!
|
||||
// Was too lazy to make my own preview, so I just used theirs
|
||||
|
||||
|
||||
configuration {
|
||||
font: "Sans 12";
|
||||
line-margin: 10;
|
||||
@@ -20,12 +15,13 @@ configuration {
|
||||
|
||||
window {
|
||||
fullscreen: false;
|
||||
background-color: rgba(0, 0, 0, 1); /* Solid black background */
|
||||
background-color: rgba(0, 0, 0, 0.8); /* Solid black transparent background */
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
padding: 50px 100px;
|
||||
background-color: rgba(0, 0, 0, 1); /* Ensures black background fills entire main area */
|
||||
padding: 50px 50px;
|
||||
background-color: transparent; /* Ensures black background fills entire main area */
|
||||
children: [inputbar, listview];
|
||||
spacing: 20px;
|
||||
}
|
||||
@@ -47,7 +43,7 @@ prompt {
|
||||
|
||||
entry {
|
||||
padding: 8px;
|
||||
background-color: #444444; /* Slightly lighter gray for visibility */
|
||||
background-color: transparent; /* Slightly lighter gray for visibility */
|
||||
text-color: #FFFFFF; /* White text to make typing visible */
|
||||
placeholder: "Search...";
|
||||
placeholder-color: rgba(255, 255, 255, 0.5);
|
||||
@@ -57,19 +53,19 @@ entry {
|
||||
listview {
|
||||
layout: vertical;
|
||||
spacing: 8px;
|
||||
lines: 10;
|
||||
background-color: @background; /* Consistent black background for list items */
|
||||
lines: 9;
|
||||
background-color: transparent; /* Consistent black background for list items */
|
||||
}
|
||||
|
||||
element {
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background-color: @background; /* Uniform color for each list item */
|
||||
background-color: transparent; /* Uniform color for each list item */
|
||||
text-color: @foreground;
|
||||
}
|
||||
|
||||
element normal.normal {
|
||||
background-color: @background; /* Ensures no alternating color */
|
||||
background-color: transparent; /* Ensures no alternating color */
|
||||
}
|
||||
|
||||
element selected.normal {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// https://github.com/Wraient/curd/blob/main/rofi/userinput.rasi
|
||||
// Go give there project a star!
|
||||
// Was too lazy to make my own preview, so I just used theirs
|
||||
|
||||
configuration {
|
||||
font: "Sans 12";
|
||||
}
|
||||
@@ -14,17 +10,19 @@ configuration {
|
||||
window {
|
||||
fullscreen: true;
|
||||
transparency: "real";
|
||||
background-color: @background-color;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
children: [ message, listview, inputbar ];
|
||||
padding: 40% 30%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
message {
|
||||
border: 0;
|
||||
padding: 10px;
|
||||
border-radius:20px;
|
||||
margin: 0 0 20px 0;
|
||||
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
||||
}
|
||||
@@ -42,6 +40,7 @@ prompt {
|
||||
|
||||
entry {
|
||||
padding: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
listview {
|
||||
@@ -52,4 +51,5 @@ listview {
|
||||
textbox {
|
||||
horizontal-align: 0.5; /* Center the text */
|
||||
font: "Sans Bold 24"; /* Match message font */
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// https://github.com/Wraient/curd/blob/main/rofi/userinput.rasi
|
||||
// Go give there project a star!
|
||||
// Was too lazy to make my own preview, so I just used theirs
|
||||
|
||||
configuration {
|
||||
font: "Sans 12";
|
||||
}
|
||||
@@ -14,17 +10,19 @@ configuration {
|
||||
window {
|
||||
fullscreen: true;
|
||||
transparency: "real";
|
||||
background-color: @background-color;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
children: [ message, listview, inputbar ];
|
||||
padding: 40% 30%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
message {
|
||||
border: 0;
|
||||
padding: 10px;
|
||||
border-radius:20px;
|
||||
margin: 0 0 20px 0;
|
||||
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
||||
}
|
||||
@@ -42,6 +40,7 @@ prompt {
|
||||
|
||||
entry {
|
||||
padding: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
listview {
|
||||
@@ -52,4 +51,5 @@ listview {
|
||||
textbox {
|
||||
horizontal-align: 0.5; /* Center the text */
|
||||
font: "Sans Bold 24"; /* Match message font */
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -1,122 +1,120 @@
|
||||
// Based on https://github.com/Wraient/curd/blob/main/rofi/selectanimepreview.rasi
|
||||
// Go give there project a star!
|
||||
// Was too lazy to make my own preview, so I just used theirs
|
||||
|
||||
// Colours
|
||||
* {
|
||||
background-color: transparent;
|
||||
background: #1D2330;
|
||||
background-transparent: #1D2330A0;
|
||||
text-color: #BBBBBB;
|
||||
text-color-selected: #FFFFFF;
|
||||
primary: #BB77BB;
|
||||
important: #BF616A;
|
||||
background-color: transparent; /* Transparent background for the global UI */
|
||||
background: #000000; /* Solid black background */
|
||||
background-transparent: #1D2330A0; /* Semi-transparent background */
|
||||
text-color: #BBBBBB; /* Default text color (light gray) */
|
||||
text-color-selected: #FFFFFF; /* Text color when selected (white) */
|
||||
primary: rgba(53, 132, 228, 0.75); /* Blusish primary color */
|
||||
important: rgba(53, 132, 228, 0.75); /* Bluish primary color */
|
||||
}
|
||||
|
||||
configuration {
|
||||
font: "Roboto 17";
|
||||
show-icons: true;
|
||||
font: "Roboto 14"; /* Sets the global font to Roboto, size 14 */
|
||||
show-icons: true; /* Option to display icons in the UI */
|
||||
}
|
||||
|
||||
window {
|
||||
fullscreen: true;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transparency: "real";
|
||||
background-color: @background-transparent;
|
||||
border: 0px;
|
||||
border-color: @primary;
|
||||
fullscreen: true; /* The window will open in fullscreen */
|
||||
height: 100%; /* Full window height */
|
||||
width: 100%; /* Full window width */
|
||||
transparency: "real"; /* Real transparency effect */
|
||||
background-color: @background-transparent; /* Transparent background */
|
||||
border: 0px; /* No border around the window */
|
||||
border-color: @primary; /* Border color set to the primary color */
|
||||
}
|
||||
|
||||
mainbox {
|
||||
children: [prompt, inputbar-box, listview];
|
||||
padding: 0px;
|
||||
children: [prompt, inputbar-box, listview]; /* Main box contains prompt, input bar, and list view */
|
||||
padding: 0px; /* No padding around the main box */
|
||||
}
|
||||
|
||||
prompt {
|
||||
width: 100%;
|
||||
margin: 10px 0px 0px 30px;
|
||||
text-color: @important;
|
||||
font: "Roboto Bold 27";
|
||||
width: 100%; /* Prompt takes full width */
|
||||
margin: 10px 0px 0px 30px; /* Margin around the prompt */
|
||||
text-color: @important; /* Text color for prompt (important color) */
|
||||
font: "Roboto Bold 27"; /* Bold Roboto font, size 27 */
|
||||
}
|
||||
|
||||
listview {
|
||||
layout: vertical;
|
||||
padding: 60px;
|
||||
dynamic: true;
|
||||
columns: 7;
|
||||
spacing: 20px;
|
||||
horizontal-align: center; /* Center the list items */
|
||||
layout: vertical; /* Vertical layout for list items */
|
||||
padding: 10px; /* Padding inside the list view */
|
||||
spacing: 20px; /* Space between items in the list */
|
||||
columns: 8; /* Maximum 8 items per row */
|
||||
dynamic: true; /* Allows the list to dynamically adjust */
|
||||
orientation: horizontal; /* Horizontal orientation for list items */
|
||||
}
|
||||
|
||||
inputbar-box {
|
||||
children: [dummy, inputbar, dummy];
|
||||
orientation: horizontal;
|
||||
expand: false;
|
||||
children: [dummy, inputbar, dummy]; /* Input bar is centered with dummy placeholders */
|
||||
orientation: horizontal; /* Horizontal layout for input bar */
|
||||
expand: false; /* Does not expand to fill the space */
|
||||
}
|
||||
|
||||
inputbar {
|
||||
children: [textbox-prompt, entry];
|
||||
margin: 0px;
|
||||
background-color: @primary;
|
||||
border: 4px;
|
||||
border-color: @primary;
|
||||
border-radius: 8px;
|
||||
children: [textbox-prompt, entry]; /* Contains a prompt and an entry field */
|
||||
margin: 0px; /* No margin around the input bar */
|
||||
background-color: @primary; /* Background color set to the primary color */
|
||||
border: 4px; /* Border thickness around the input bar */
|
||||
border-color: @primary; /* Border color matches the primary color */
|
||||
border-radius: 8px; /* Rounded corners for the input bar */
|
||||
}
|
||||
|
||||
textbox-prompt {
|
||||
text-color: @background;
|
||||
horizontal-align: 0.5;
|
||||
vertical-align: 0.5;
|
||||
expand: false;
|
||||
text-color: @background; /* Text color inside prompt matches the background color */
|
||||
horizontal-align: 0.5; /* Horizontally centered */
|
||||
vertical-align: 0.5; /* Vertically centered */
|
||||
expand: false; /* Does not expand to fill available space */
|
||||
}
|
||||
|
||||
entry {
|
||||
expand: false;
|
||||
padding: 8px;
|
||||
margin: -6px;
|
||||
horizontal-align: 0;
|
||||
width: 300;
|
||||
background-color: @background;
|
||||
border: 6px;
|
||||
border-color: @primary;
|
||||
border-radius: 8px;
|
||||
cursor: text;
|
||||
expand: false; /* Entry field does not expand */
|
||||
padding: 8px; /* Padding inside the entry field */
|
||||
margin: -6px; /* Negative margin to position entry properly */
|
||||
horizontal-align: 0; /* Left-aligned text inside the entry field */
|
||||
width: 300; /* Fixed width for the entry field */
|
||||
background-color: @background; /* Entry background color matches the global background */
|
||||
border: 6px; /* Border thickness around the entry field */
|
||||
border-color: @primary; /* Border color matches the primary color */
|
||||
border-radius: 8px; /* Rounded corners for the entry field */
|
||||
cursor: text; /* Cursor changes to text input cursor inside the entry field */
|
||||
}
|
||||
|
||||
element {
|
||||
children: [dummy, element-box, dummy];
|
||||
padding: 5px;
|
||||
orientation: vertical;
|
||||
border: 0px;
|
||||
border-radius: 16px;
|
||||
background-color: transparent; /* Default background */
|
||||
children: [dummy, element-box, dummy]; /* Contains an element box with dummy placeholders */
|
||||
padding: 5px; /* Padding around the element */
|
||||
orientation: vertical; /* Vertical layout for element content */
|
||||
border: 0px; /* No border around the element */
|
||||
border-radius: 16px; /* Rounded corners for the element */
|
||||
background-color: transparent; /* Transparent background */
|
||||
width: 100px; /* Width of each element */
|
||||
height: 50px; /* Height of each element */
|
||||
}
|
||||
|
||||
element selected {
|
||||
background-color: @primary; /* Solid color for selected item */
|
||||
background-color: @primary; /* Background color of the element when selected */
|
||||
}
|
||||
|
||||
element-box {
|
||||
children: [element-icon, element-text];
|
||||
orientation: vertical;
|
||||
expand: false;
|
||||
cursor: pointer;
|
||||
children: [element-icon, element-text]; /* Element box contains an icon and text */
|
||||
orientation: vertical; /* Vertical layout for icon and text */
|
||||
expand: false; /* Does not expand to fill available space */
|
||||
cursor: pointer; /* Cursor changes to a pointer when hovering over the element */
|
||||
}
|
||||
|
||||
element-icon {
|
||||
padding: 10px;
|
||||
cursor: inherit;
|
||||
size: 33%;
|
||||
margin: 10px;
|
||||
padding: 10px; /* Padding inside the icon */
|
||||
cursor: inherit; /* Inherits cursor style from the parent */
|
||||
size: 33%; /* Icon size is set to 33% of the parent element */
|
||||
margin: 10px; /* Margin around the icon */
|
||||
}
|
||||
|
||||
element-text {
|
||||
horizontal-align: 0.5;
|
||||
cursor: inherit;
|
||||
text-color: @text-color;
|
||||
horizontal-align: 0.5; /* Horizontally center-aligns the text */
|
||||
cursor: inherit; /* Inherits cursor style from the parent */
|
||||
text-color: @text-color; /* Text color for element text */
|
||||
}
|
||||
|
||||
element-text selected {
|
||||
text-color: @text-color-selected;
|
||||
text-color: @text-color-selected; /* Text color when the element is selected */
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
fastanime --icons --default anilist
|
||||
\b
|
||||
# viewing manga
|
||||
fastanime --manga search -t <manga-title>
|
||||
fastanime --manga search -t <manga-title>
|
||||
""",
|
||||
)
|
||||
@click.version_option(__version__, "--version")
|
||||
@@ -142,7 +142,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
@click.option(
|
||||
"--normalize-titles/--no-normalize-titles",
|
||||
type=bool,
|
||||
help="whether to normalize anime and episode titls given by providers",
|
||||
help="whether to normalize anime and episode titles given by providers",
|
||||
)
|
||||
@click.option("-d", "--downloads-dir", type=click.Path(), help="Downloads location")
|
||||
@click.option("--fzf", is_flag=True, help="Use fzf for the ui")
|
||||
@@ -184,6 +184,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
@click.option(
|
||||
"--fresh-requests", is_flag=True, help="Force the requests cache to be updated"
|
||||
)
|
||||
@click.option("--no-config", is_flag=True, help="Don't load the user config")
|
||||
@click.pass_context
|
||||
def run_cli(
|
||||
ctx: click.Context,
|
||||
@@ -220,45 +221,64 @@ def run_cli(
|
||||
sync_play,
|
||||
player,
|
||||
fresh_requests,
|
||||
no_config,
|
||||
):
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import Config
|
||||
|
||||
ctx.obj = Config()
|
||||
if ctx.obj.check_for_updates and ctx.invoked_subcommand != "completions":
|
||||
from .app_updater import check_for_updates
|
||||
ctx.obj = Config(no_config)
|
||||
if (
|
||||
ctx.obj.check_for_updates
|
||||
and ctx.invoked_subcommand != "completions"
|
||||
and "notifier" not in sys.argv
|
||||
):
|
||||
import time
|
||||
|
||||
print("Checking for updates...")
|
||||
print("So you can enjoy the latest features and bug fixes")
|
||||
print(
|
||||
"You can disable this by setting check_for_updates to False in the config"
|
||||
)
|
||||
is_latest, github_release_data = check_for_updates()
|
||||
if not is_latest:
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from .app_updater import update_app
|
||||
from rich.prompt import Confirm
|
||||
last_update = ctx.obj.user_data["meta"]["last_updated"]
|
||||
now = time.time()
|
||||
# checks after every 12 hours
|
||||
if (now - last_update) > 43200:
|
||||
ctx.obj.user_data["meta"]["last_updated"] = now
|
||||
ctx.obj._update_user_data()
|
||||
|
||||
def _print_release(release_data):
|
||||
console = Console()
|
||||
body = Markdown(release_data["body"])
|
||||
tag = github_release_data["tag_name"]
|
||||
tag_title = release_data["name"]
|
||||
github_page_url = release_data["html_url"]
|
||||
console.print(f"Release Page: {github_page_url}")
|
||||
console.print(f"Tag: {tag}")
|
||||
console.print(f"Title: {tag_title}")
|
||||
console.print(body)
|
||||
from .app_updater import check_for_updates
|
||||
|
||||
if Confirm.ask(
|
||||
"A new version of fastanime is available, would you like to update?"
|
||||
):
|
||||
_, release_json = update_app()
|
||||
print("Successfully updated")
|
||||
_print_release(release_json)
|
||||
exit(0)
|
||||
print("Checking for updates...", file=sys.stderr)
|
||||
print("So you can enjoy the latest features and bug fixes", file=sys.stderr)
|
||||
print(
|
||||
"You can disable this by setting check_for_updates to False in the config",
|
||||
file=sys.stderr,
|
||||
)
|
||||
is_latest, github_release_data = check_for_updates()
|
||||
if not is_latest:
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from rich.prompt import Confirm
|
||||
|
||||
from .app_updater import update_app
|
||||
|
||||
def _print_release(release_data):
|
||||
console = Console()
|
||||
body = Markdown(release_data["body"])
|
||||
tag = github_release_data["tag_name"]
|
||||
tag_title = release_data["name"]
|
||||
github_page_url = release_data["html_url"]
|
||||
console.print(f"Release Page: {github_page_url}")
|
||||
console.print(f"Tag: {tag}")
|
||||
console.print(f"Title: {tag_title}")
|
||||
console.print(body)
|
||||
|
||||
if Confirm.ask(
|
||||
"A new version of fastanime is available, would you like to update?"
|
||||
):
|
||||
_, release_json = update_app()
|
||||
print("Successfully updated")
|
||||
_print_release(release_json)
|
||||
exit(0)
|
||||
else:
|
||||
print("You are using the latest version of fastanime", file=sys.stderr)
|
||||
|
||||
ctx.obj.manga = manga
|
||||
if log:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
import requests
|
||||
from rich import print
|
||||
@@ -128,9 +128,13 @@ def update_app(force=False):
|
||||
"install",
|
||||
APP_NAME,
|
||||
"-U",
|
||||
"--user",
|
||||
"--no-warn-script-location",
|
||||
]
|
||||
if sys.prefix == sys.base_prefix:
|
||||
# ensure NOT in a venv, where --user flag can cause an error.
|
||||
# TODO: Get value of 'include-system-site-packages' in pyenv.cfg.
|
||||
args.append('--user')
|
||||
|
||||
process = subprocess.run(args)
|
||||
if process.returncode == 0:
|
||||
return True, release_json
|
||||
|
||||
@@ -21,6 +21,8 @@ commands = {
|
||||
"planning": "planning.planning",
|
||||
"notifier": "notifier.notifier",
|
||||
"stats": "stats.stats",
|
||||
"download": "download.download",
|
||||
"downloads": "downloads.downloads",
|
||||
}
|
||||
|
||||
|
||||
@@ -78,15 +80,13 @@ commands = {
|
||||
fastanime --log-file anilist notifier
|
||||
""",
|
||||
)
|
||||
@click.option("--resume", is_flag=True, help="Resume from the last session")
|
||||
@click.pass_context
|
||||
def anilist(ctx: click.Context):
|
||||
def anilist(ctx: click.Context, resume: bool):
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ....anilist import AniList
|
||||
from ....AnimeProvider import AnimeProvider
|
||||
from ...interfaces.anilist_interfaces import (
|
||||
fastanime_main_menu as anilist_interface,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
@@ -96,4 +96,33 @@ def anilist(ctx: click.Context):
|
||||
AniList.update_login_info(user, user["token"])
|
||||
if ctx.invoked_subcommand is None:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
anilist_interface(ctx.obj, fastanime_runtime_state)
|
||||
if resume:
|
||||
from ...interfaces.anilist_interfaces import (
|
||||
anime_provider_search_results_menu,
|
||||
)
|
||||
|
||||
if not config.user_data["recent_anime"]:
|
||||
click.echo("No recent anime found", err=True, color=True)
|
||||
return
|
||||
fastanime_runtime_state.anilist_results_data = {
|
||||
"data": {"Page": {"media": config.user_data["recent_anime"]}}
|
||||
}
|
||||
|
||||
fastanime_runtime_state.selected_anime_anilist = config.user_data[
|
||||
"recent_anime"
|
||||
][0]
|
||||
fastanime_runtime_state.selected_anime_id_anilist = config.user_data[
|
||||
"recent_anime"
|
||||
][0]["id"]
|
||||
fastanime_runtime_state.selected_anime_title_anilist = (
|
||||
config.user_data["recent_anime"][0]["title"]["romaji"]
|
||||
or config.user_data["recent_anime"][0]["title"]["english"]
|
||||
)
|
||||
anime_provider_search_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
else:
|
||||
from ...interfaces.anilist_interfaces import (
|
||||
fastanime_main_menu as anilist_interface,
|
||||
)
|
||||
|
||||
anilist_interface(ctx.obj, fastanime_runtime_state)
|
||||
|
||||
@@ -42,5 +42,12 @@ def completed(config: "Config", dump_json):
|
||||
from ...interfaces import anilist_interfaces
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Completed", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
477
fastanime/cli/commands/anilist/data.py
Normal file
477
fastanime/cli/commands/anilist/data.py
Normal file
@@ -0,0 +1,477 @@
|
||||
sorts_available = [
|
||||
"ID",
|
||||
"ID_DESC",
|
||||
"TITLE_ROMAJI",
|
||||
"TITLE_ROMAJI_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"TITLE_NATIVE",
|
||||
"TITLE_NATIVE_DESC",
|
||||
"TYPE",
|
||||
"TYPE_DESC",
|
||||
"FORMAT",
|
||||
"FORMAT_DESC",
|
||||
"START_DATE",
|
||||
"START_DATE_DESC",
|
||||
"END_DATE",
|
||||
"END_DATE_DESC",
|
||||
"SCORE",
|
||||
"SCORE_DESC",
|
||||
"POPULARITY",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING",
|
||||
"TRENDING_DESC",
|
||||
"EPISODES",
|
||||
"EPISODES_DESC",
|
||||
"DURATION",
|
||||
"DURATION_DESC",
|
||||
"STATUS",
|
||||
"STATUS_DESC",
|
||||
"CHAPTERS",
|
||||
"CHAPTERS_DESC",
|
||||
"VOLUMES",
|
||||
"VOLUMES_DESC",
|
||||
"UPDATED_AT",
|
||||
"UPDATED_AT_DESC",
|
||||
"SEARCH_MATCH",
|
||||
"FAVOURITES",
|
||||
"FAVOURITES_DESC",
|
||||
]
|
||||
|
||||
media_statuses_available = [
|
||||
"FINISHED",
|
||||
"RELEASING",
|
||||
"NOT_YET_RELEASED",
|
||||
"CANCELLED",
|
||||
"HIATUS",
|
||||
]
|
||||
seasons_available = ["WINTER", "SPRING", "SUMMER", "FALL"]
|
||||
genres_available = [
|
||||
"Action",
|
||||
"Adventure",
|
||||
"Comedy",
|
||||
"Drama",
|
||||
"Ecchi",
|
||||
"Fantasy",
|
||||
"Horror",
|
||||
"Mahou Shoujo",
|
||||
"Mecha",
|
||||
"Music",
|
||||
"Mystery",
|
||||
"Psychological",
|
||||
"Romance",
|
||||
"Sci-Fi",
|
||||
"Slice of Life",
|
||||
"Sports",
|
||||
"Supernatural",
|
||||
"Thriller",
|
||||
"Hentai",
|
||||
]
|
||||
media_formats_available = [
|
||||
"TV",
|
||||
"TV_SHORT",
|
||||
"MOVIE",
|
||||
"SPECIAL",
|
||||
"OVA",
|
||||
"MUSIC",
|
||||
"NOVEL",
|
||||
"ONE_SHOT",
|
||||
]
|
||||
years_available = [
|
||||
"1900",
|
||||
"1910",
|
||||
"1920",
|
||||
"1930",
|
||||
"1940",
|
||||
"1950",
|
||||
"1960",
|
||||
"1970",
|
||||
"1980",
|
||||
"1990",
|
||||
"2000",
|
||||
"2004",
|
||||
"2005",
|
||||
"2006",
|
||||
"2007",
|
||||
"2008",
|
||||
"2009",
|
||||
"2010",
|
||||
"2011",
|
||||
"2012",
|
||||
"2013",
|
||||
"2014",
|
||||
"2015",
|
||||
"2016",
|
||||
"2017",
|
||||
"2018",
|
||||
"2019",
|
||||
"2020",
|
||||
"2021",
|
||||
"2022",
|
||||
"2023",
|
||||
"2024",
|
||||
"2025",
|
||||
]
|
||||
|
||||
tags_available = {
|
||||
"Cast": ["Polyamorous"],
|
||||
"Cast Main Cast": [
|
||||
"Anti-Hero",
|
||||
"Elderly Protagonist",
|
||||
"Ensemble Cast",
|
||||
"Estranged Family",
|
||||
"Female Protagonist",
|
||||
"Male Protagonist",
|
||||
"Primarily Adult Cast",
|
||||
"Primarily Animal Cast",
|
||||
"Primarily Child Cast",
|
||||
"Primarily Female Cast",
|
||||
"Primarily Male Cast",
|
||||
"Primarily Teen Cast",
|
||||
],
|
||||
"Cast Traits": [
|
||||
"Age Regression",
|
||||
"Agender",
|
||||
"Aliens",
|
||||
"Amnesia",
|
||||
"Angels",
|
||||
"Anthropomorphism",
|
||||
"Aromantic",
|
||||
"Arranged Marriage",
|
||||
"Artificial Intelligence",
|
||||
"Asexual",
|
||||
"Butler",
|
||||
"Centaur",
|
||||
"Chimera",
|
||||
"Chuunibyou",
|
||||
"Clone",
|
||||
"Cosplay",
|
||||
"Cowboys",
|
||||
"Crossdressing",
|
||||
"Cyborg",
|
||||
"Delinquents",
|
||||
"Demons",
|
||||
"Detective",
|
||||
"Dinosaurs",
|
||||
"Disability",
|
||||
"Dissociative Identities",
|
||||
"Dragons",
|
||||
"Dullahan",
|
||||
"Elf",
|
||||
"Fairy",
|
||||
"Femboy",
|
||||
"Ghost",
|
||||
"Goblin",
|
||||
"Gods",
|
||||
"Gyaru",
|
||||
"Hikikomori",
|
||||
"Homeless",
|
||||
"Idol",
|
||||
"Kemonomimi",
|
||||
"Kuudere",
|
||||
"Maids",
|
||||
"Mermaid",
|
||||
"Monster Boy",
|
||||
"Monster Girl",
|
||||
"Nekomimi",
|
||||
"Ninja",
|
||||
"Nudity",
|
||||
"Nun",
|
||||
"Office Lady",
|
||||
"Oiran",
|
||||
"Ojou-sama",
|
||||
"Orphan",
|
||||
"Pirates",
|
||||
"Robots",
|
||||
"Samurai",
|
||||
"Shrine Maiden",
|
||||
"Skeleton",
|
||||
"Succubus",
|
||||
"Tanned Skin",
|
||||
"Teacher",
|
||||
"Tomboy",
|
||||
"Transgender",
|
||||
"Tsundere",
|
||||
"Twins",
|
||||
"Vampire",
|
||||
"Veterinarian",
|
||||
"Vikings",
|
||||
"Villainess",
|
||||
"VTuber",
|
||||
"Werewolf",
|
||||
"Witch",
|
||||
"Yandere",
|
||||
"Zombie",
|
||||
],
|
||||
"Demographic": ["Josei", "Kids", "Seinen", "Shoujo", "Shounen"],
|
||||
"Setting": ["Matriarchy"],
|
||||
"Setting Scene": [
|
||||
"Bar",
|
||||
"Boarding School",
|
||||
"Circus",
|
||||
"Coastal",
|
||||
"College",
|
||||
"Desert",
|
||||
"Dungeon",
|
||||
"Foreign",
|
||||
"Inn",
|
||||
"Konbini",
|
||||
"Natural Disaster",
|
||||
"Office",
|
||||
"Outdoor",
|
||||
"Prison",
|
||||
"Restaurant",
|
||||
"Rural",
|
||||
"School",
|
||||
"School Club",
|
||||
"Snowscape",
|
||||
"Urban",
|
||||
"Work",
|
||||
],
|
||||
"Setting Time": [
|
||||
"Achronological Order",
|
||||
"Anachronism",
|
||||
"Ancient China",
|
||||
"Dystopian",
|
||||
"Historical",
|
||||
"Time Skip",
|
||||
],
|
||||
"Setting Universe": [
|
||||
"Afterlife",
|
||||
"Alternate Universe",
|
||||
"Augmented Reality",
|
||||
"Omegaverse",
|
||||
"Post-Apocalyptic",
|
||||
"Space",
|
||||
"Urban Fantasy",
|
||||
"Virtual World",
|
||||
],
|
||||
"Technical": [
|
||||
"4-koma",
|
||||
"Achromatic",
|
||||
"Advertisement",
|
||||
"Anthology",
|
||||
"CGI",
|
||||
"Episodic",
|
||||
"Flash",
|
||||
"Full CGI",
|
||||
"Full Color",
|
||||
"No Dialogue",
|
||||
"Non-fiction",
|
||||
"POV",
|
||||
"Puppetry",
|
||||
"Rotoscoping",
|
||||
"Stop Motion",
|
||||
],
|
||||
"Theme Action": [
|
||||
"Archery",
|
||||
"Battle Royale",
|
||||
"Espionage",
|
||||
"Fugitive",
|
||||
"Guns",
|
||||
"Martial Arts",
|
||||
"Spearplay",
|
||||
"Swordplay",
|
||||
],
|
||||
"Theme Arts": [
|
||||
"Acting",
|
||||
"Calligraphy",
|
||||
"Classic Literature",
|
||||
"Drawing",
|
||||
"Fashion",
|
||||
"Food",
|
||||
"Makeup",
|
||||
"Photography",
|
||||
"Rakugo",
|
||||
"Writing",
|
||||
],
|
||||
"Theme Arts-Music": [
|
||||
"Band",
|
||||
"Classical Music",
|
||||
"Dancing",
|
||||
"Hip-hop Music",
|
||||
"Jazz Music",
|
||||
"Metal Music",
|
||||
"Musical Theater",
|
||||
"Rock Music",
|
||||
],
|
||||
"Theme Comedy": ["Parody", "Satire", "Slapstick", "Surreal Comedy"],
|
||||
"Theme Drama": [
|
||||
"Bullying",
|
||||
"Class Struggle",
|
||||
"Coming of Age",
|
||||
"Conspiracy",
|
||||
"Eco-Horror",
|
||||
"Fake Relationship",
|
||||
"Kingdom Management",
|
||||
"Rehabilitation",
|
||||
"Revenge",
|
||||
"Suicide",
|
||||
"Tragedy",
|
||||
],
|
||||
"Theme Fantasy": [
|
||||
"Alchemy",
|
||||
"Body Swapping",
|
||||
"Cultivation",
|
||||
"Fairy Tale",
|
||||
"Henshin",
|
||||
"Isekai",
|
||||
"Kaiju",
|
||||
"Magic",
|
||||
"Mythology",
|
||||
"Necromancy",
|
||||
"Shapeshifting",
|
||||
"Steampunk",
|
||||
"Super Power",
|
||||
"Superhero",
|
||||
"Wuxia",
|
||||
"Youkai",
|
||||
],
|
||||
"Theme Game": ["Board Game", "E-Sports", "Video Games"],
|
||||
"Theme Game-Card & Board Game": [
|
||||
"Card Battle",
|
||||
"Go",
|
||||
"Karuta",
|
||||
"Mahjong",
|
||||
"Poker",
|
||||
"Shogi",
|
||||
],
|
||||
"Theme Game-Sport": [
|
||||
"Acrobatics",
|
||||
"Airsoft",
|
||||
"American Football",
|
||||
"Athletics",
|
||||
"Badminton",
|
||||
"Baseball",
|
||||
"Basketball",
|
||||
"Bowling",
|
||||
"Boxing",
|
||||
"Cheerleading",
|
||||
"Cycling",
|
||||
"Fencing",
|
||||
"Fishing",
|
||||
"Fitness",
|
||||
"Football",
|
||||
"Golf",
|
||||
"Handball",
|
||||
"Ice Skating",
|
||||
"Judo",
|
||||
"Lacrosse",
|
||||
"Parkour",
|
||||
"Rugby",
|
||||
"Scuba Diving",
|
||||
"Skateboarding",
|
||||
"Sumo",
|
||||
"Surfing",
|
||||
"Swimming",
|
||||
"Table Tennis",
|
||||
"Tennis",
|
||||
"Volleyball",
|
||||
"Wrestling",
|
||||
],
|
||||
"Theme Other": [
|
||||
"Adoption",
|
||||
"Animals",
|
||||
"Astronomy",
|
||||
"Autobiographical",
|
||||
"Biographical",
|
||||
"Body Horror",
|
||||
"Cannibalism",
|
||||
"Chibi",
|
||||
"Cosmic Horror",
|
||||
"Crime",
|
||||
"Crossover",
|
||||
"Death Game",
|
||||
"Denpa",
|
||||
"Drugs",
|
||||
"Economics",
|
||||
"Educational",
|
||||
"Environmental",
|
||||
"Ero Guro",
|
||||
"Filmmaking",
|
||||
"Found Family",
|
||||
"Gambling",
|
||||
"Gender Bending",
|
||||
"Gore",
|
||||
"Language Barrier",
|
||||
"LGBTQ+ Themes",
|
||||
"Lost Civilization",
|
||||
"Marriage",
|
||||
"Medicine",
|
||||
"Memory Manipulation",
|
||||
"Meta",
|
||||
"Mountaineering",
|
||||
"Noir",
|
||||
"Otaku Culture",
|
||||
"Pandemic",
|
||||
"Philosophy",
|
||||
"Politics",
|
||||
"Proxy Battle",
|
||||
"Psychosexual",
|
||||
"Reincarnation",
|
||||
"Religion",
|
||||
"Royal Affairs",
|
||||
"Slavery",
|
||||
"Software Development",
|
||||
"Survival",
|
||||
"Terrorism",
|
||||
"Torture",
|
||||
"Travel",
|
||||
"War",
|
||||
],
|
||||
"Theme Other-Organisations": [
|
||||
"Assassins",
|
||||
"Criminal Organization",
|
||||
"Cult",
|
||||
"Firefighters",
|
||||
"Gangs",
|
||||
"Mafia",
|
||||
"Military",
|
||||
"Police",
|
||||
"Triads",
|
||||
"Yakuza",
|
||||
],
|
||||
"Theme Other-Vehicle": [
|
||||
"Aviation",
|
||||
"Cars",
|
||||
"Mopeds",
|
||||
"Motorcycles",
|
||||
"Ships",
|
||||
"Tanks",
|
||||
"Trains",
|
||||
],
|
||||
"Theme Romance": [
|
||||
"Age Gap",
|
||||
"Bisexual",
|
||||
"Boys' Love",
|
||||
"Female Harem",
|
||||
"Heterosexual",
|
||||
"Love Triangle",
|
||||
"Male Harem",
|
||||
"Matchmaking",
|
||||
"Mixed Gender Harem",
|
||||
"Teens' Love",
|
||||
"Unrequited Love",
|
||||
"Yuri",
|
||||
],
|
||||
"Theme Sci Fi": [
|
||||
"Cyberpunk",
|
||||
"Space Opera",
|
||||
"Time Loop",
|
||||
"Time Manipulation",
|
||||
"Tokusatsu",
|
||||
],
|
||||
"Theme Sci Fi-Mecha": ["Real Robot", "Super Robot"],
|
||||
"Theme Slice of Life": [
|
||||
"Agriculture",
|
||||
"Cute Boys Doing Cute Things",
|
||||
"Cute Girls Doing Cute Things",
|
||||
"Family Life",
|
||||
"Horticulture",
|
||||
"Iyashikei",
|
||||
"Parenthood",
|
||||
],
|
||||
}
|
||||
tags_available_list = []
|
||||
for tag_category, tags_in_category in tags_available.items():
|
||||
tags_available_list.extend(tags_in_category)
|
||||
401
fastanime/cli/commands/anilist/download.py
Normal file
401
fastanime/cli/commands/anilist/download.py
Normal file
@@ -0,0 +1,401 @@
|
||||
import click
|
||||
|
||||
from ...completion_functions import anime_titles_shell_complete
|
||||
from .data import (
|
||||
genres_available,
|
||||
media_formats_available,
|
||||
media_statuses_available,
|
||||
seasons_available,
|
||||
sorts_available,
|
||||
tags_available_list,
|
||||
years_available,
|
||||
)
|
||||
|
||||
|
||||
@click.command(
|
||||
help="download anime using anilists api to get the titles",
|
||||
short_help="download anime with anilist intergration",
|
||||
)
|
||||
@click.option("--title", "-t", shell_complete=anime_titles_shell_complete)
|
||||
@click.option(
|
||||
"--season",
|
||||
help="The season the media was released",
|
||||
type=click.Choice(seasons_available),
|
||||
)
|
||||
@click.option(
|
||||
"--status",
|
||||
"-S",
|
||||
help="The media status of the anime",
|
||||
multiple=True,
|
||||
type=click.Choice(media_statuses_available),
|
||||
)
|
||||
@click.option(
|
||||
"--sort",
|
||||
"-s",
|
||||
help="What to sort the search results on",
|
||||
type=click.Choice(sorts_available),
|
||||
)
|
||||
@click.option(
|
||||
"--genres",
|
||||
"-g",
|
||||
multiple=True,
|
||||
help="the genres to filter by",
|
||||
type=click.Choice(genres_available),
|
||||
)
|
||||
@click.option(
|
||||
"--tags",
|
||||
"-T",
|
||||
multiple=True,
|
||||
help="the tags to filter by",
|
||||
type=click.Choice(tags_available_list),
|
||||
)
|
||||
@click.option(
|
||||
"--media-format",
|
||||
"-f",
|
||||
multiple=True,
|
||||
help="Media format",
|
||||
type=click.Choice(media_formats_available),
|
||||
)
|
||||
@click.option(
|
||||
"--year",
|
||||
"-y",
|
||||
type=click.Choice(years_available),
|
||||
help="the year the media was released",
|
||||
)
|
||||
@click.option(
|
||||
"--on-list/--not-on-list",
|
||||
"-L/-no-L",
|
||||
help="Whether the anime should be in your list or not",
|
||||
type=bool,
|
||||
)
|
||||
@click.option(
|
||||
"--episode-range",
|
||||
"-r",
|
||||
help="A range of episodes to download (start-end)",
|
||||
)
|
||||
@click.option(
|
||||
"--force-unknown-ext",
|
||||
"-F",
|
||||
help="This option forces yt-dlp to download extensions its not aware of",
|
||||
is_flag=True,
|
||||
)
|
||||
@click.option(
|
||||
"--silent/--no-silent",
|
||||
"-q/-V",
|
||||
type=bool,
|
||||
help="Download silently (during download)",
|
||||
default=True,
|
||||
)
|
||||
@click.option("--verbose", "-v", is_flag=True, help="Download verbosely (everywhere)")
|
||||
@click.option(
|
||||
"--merge", "-m", is_flag=True, help="Merge the subfile with video using ffmpeg"
|
||||
)
|
||||
@click.option(
|
||||
"--clean",
|
||||
"-c",
|
||||
is_flag=True,
|
||||
help="After merging delete the original files",
|
||||
)
|
||||
@click.option(
|
||||
"--wait-time",
|
||||
"-w",
|
||||
type=int,
|
||||
help="The amount of time to wait after downloading is complete before the screen is completely cleared",
|
||||
default=60,
|
||||
)
|
||||
@click.option(
|
||||
"--prompt/--no-prompt",
|
||||
help="Whether to prompt for anything instead just do the best thing",
|
||||
default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--force-ffmpeg",
|
||||
is_flag=True,
|
||||
help="Force the use of FFmpeg for downloading (supports large variety of streams but slower)",
|
||||
)
|
||||
@click.option(
|
||||
"--hls-use-mpegts",
|
||||
is_flag=True,
|
||||
help="Use mpegts for hls streams, resulted in .ts file (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.option(
|
||||
"--hls-use-h264",
|
||||
is_flag=True,
|
||||
help="Use H.264 (MP4) for hls streams, resulted in .mp4 file (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.option(
|
||||
"--max-results", "-M", type=int, help="The maximum number of results to show"
|
||||
)
|
||||
@click.pass_obj
|
||||
def download(
|
||||
config,
|
||||
title,
|
||||
season,
|
||||
status,
|
||||
sort,
|
||||
genres,
|
||||
tags,
|
||||
media_format,
|
||||
year,
|
||||
on_list,
|
||||
episode_range,
|
||||
force_unknown_ext,
|
||||
silent,
|
||||
verbose,
|
||||
merge,
|
||||
clean,
|
||||
wait_time,
|
||||
prompt,
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
hls_use_h264,
|
||||
max_results,
|
||||
):
|
||||
from rich import print
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
force_ffmpeg |= (hls_use_mpegts or hls_use_h264)
|
||||
|
||||
success, anilist_search_results = AniList.search(
|
||||
query=title,
|
||||
sort=sort,
|
||||
status_in=list(status),
|
||||
genre_in=list(genres),
|
||||
season=season,
|
||||
tag_in=list(tags),
|
||||
seasonYear=year,
|
||||
format_in=list(media_format),
|
||||
on_list=on_list,
|
||||
max_results=max_results,
|
||||
)
|
||||
if success:
|
||||
import time
|
||||
|
||||
from rich.progress import Progress
|
||||
from thefuzz import fuzz
|
||||
|
||||
from ....AnimeProvider import AnimeProvider
|
||||
from ....libs.anime_provider.types import Anime
|
||||
from ....libs.fzf import fzf
|
||||
from ....Utility.data import anime_normalizer
|
||||
from ....Utility.downloader.downloader import downloader
|
||||
from ...utils.tools import exit_app
|
||||
from ...utils.utils import (
|
||||
filter_by_quality,
|
||||
fuzzy_inquirer,
|
||||
move_preferred_subtitle_lang_to_top,
|
||||
)
|
||||
|
||||
anime_provider = AnimeProvider(config.provider)
|
||||
anilist_anime_info = None
|
||||
|
||||
translation_type = config.translation_type
|
||||
download_dir = config.downloads_dir
|
||||
anime_titles = [
|
||||
(anime["title"]["romaji"] or anime["title"]["english"])
|
||||
for anime in anilist_search_results["data"]["Page"]["media"]
|
||||
]
|
||||
print(f"[green bold]Queued:[/] {anime_titles}")
|
||||
for i, anime_title in enumerate(anime_titles):
|
||||
print(f"[green bold]Now Downloading: [/] {anime_title}")
|
||||
# ---- search for anime ----
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Search Results...", total=None)
|
||||
search_results = anime_provider.search_for_anime(
|
||||
anime_title, translation_type=translation_type
|
||||
)
|
||||
if not search_results:
|
||||
print(
|
||||
"No search results found from provider for {}".format(anime_title)
|
||||
)
|
||||
continue
|
||||
search_results = search_results["results"]
|
||||
if not search_results:
|
||||
print("Nothing muches your search term")
|
||||
continue
|
||||
search_results_ = {
|
||||
search_result["title"]: search_result
|
||||
for search_result in search_results
|
||||
}
|
||||
|
||||
if config.auto_select:
|
||||
selected_anime_title = max(
|
||||
search_results_.keys(),
|
||||
key=lambda title: fuzz.ratio(
|
||||
anime_normalizer.get(title, title), anime_title
|
||||
),
|
||||
)
|
||||
print("[cyan]Auto selecting:[/] ", selected_anime_title)
|
||||
else:
|
||||
choices = list(search_results_.keys())
|
||||
if config.use_fzf:
|
||||
selected_anime_title = fzf.run(
|
||||
choices, "Please Select title", "FastAnime"
|
||||
)
|
||||
else:
|
||||
selected_anime_title = fuzzy_inquirer(
|
||||
choices,
|
||||
"Please Select title",
|
||||
)
|
||||
|
||||
# ---- fetch anime ----
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Anime...", total=None)
|
||||
anime: Anime | None = anime_provider.get_anime(
|
||||
search_results_[selected_anime_title]["id"]
|
||||
)
|
||||
if not anime:
|
||||
print("Failed to fetch anime {}".format(selected_anime_title))
|
||||
continue
|
||||
|
||||
episodes = sorted(
|
||||
anime["availableEpisodesDetail"][config.translation_type], key=float
|
||||
)
|
||||
# where the magic happens
|
||||
if episode_range:
|
||||
if ":" in episode_range:
|
||||
ep_range_tuple = episode_range.split(":")
|
||||
if len(ep_range_tuple) == 2 and all(ep_range_tuple):
|
||||
episodes_start, episodes_end = ep_range_tuple
|
||||
episodes_range = episodes[
|
||||
int(episodes_start) : int(episodes_end)
|
||||
]
|
||||
elif len(ep_range_tuple) == 3 and all(ep_range_tuple):
|
||||
episodes_start, episodes_end, step = ep_range_tuple
|
||||
episodes_range = episodes[
|
||||
int(episodes_start) : int(episodes_end) : int(step)
|
||||
]
|
||||
else:
|
||||
episodes_start, episodes_end = ep_range_tuple
|
||||
if episodes_start.strip():
|
||||
episodes_range = episodes[int(episodes_start) :]
|
||||
elif episodes_end.strip():
|
||||
episodes_range = episodes[: int(episodes_end)]
|
||||
else:
|
||||
episodes_range = episodes
|
||||
else:
|
||||
episodes_range = episodes[int(episode_range) :]
|
||||
print(f"[green bold]Downloading: [/] {episodes_range}")
|
||||
|
||||
else:
|
||||
episodes_range = sorted(episodes, key=float)
|
||||
|
||||
if config.normalize_titles:
|
||||
anilist_anime_info = anilist_search_results["data"]["Page"]["media"][i]
|
||||
|
||||
# lets download em
|
||||
for episode in episodes_range:
|
||||
try:
|
||||
episode = str(episode)
|
||||
if episode not in episodes:
|
||||
print(
|
||||
f"[cyan]Warning[/]: Episode {episode} not found, skipping"
|
||||
)
|
||||
continue
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching Episode Streams...", total=None)
|
||||
streams = anime_provider.get_episode_streams(
|
||||
anime["id"], episode, config.translation_type
|
||||
)
|
||||
if not streams:
|
||||
print("No streams skipping")
|
||||
continue
|
||||
# ---- fetch servers ----
|
||||
if config.server == "top":
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
server_name = next(streams, None)
|
||||
if not server_name:
|
||||
print("Sth went wrong when fetching the server")
|
||||
continue
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, server_name["links"]
|
||||
)
|
||||
if not stream_link:
|
||||
print("[yellow bold]WARNING:[/] No streams found")
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
continue
|
||||
link = stream_link["link"]
|
||||
provider_headers = server_name["headers"]
|
||||
episode_title = server_name["episode_title"]
|
||||
subtitles = server_name["subtitles"]
|
||||
else:
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching servers", total=None)
|
||||
# prompt for server selection
|
||||
servers = {server["server"]: server for server in streams}
|
||||
servers_names = list(servers.keys())
|
||||
if config.server in servers_names:
|
||||
server_name = config.server
|
||||
else:
|
||||
if config.use_fzf:
|
||||
server_name = fzf.run(servers_names, "Select an link")
|
||||
else:
|
||||
server_name = fuzzy_inquirer(
|
||||
servers_names,
|
||||
"Select link",
|
||||
)
|
||||
stream_link = filter_by_quality(
|
||||
config.quality, servers[server_name]["links"]
|
||||
)
|
||||
if not stream_link:
|
||||
print("[yellow bold]WARNING:[/] No streams found")
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
continue
|
||||
link = stream_link["link"]
|
||||
provider_headers = servers[server_name]["headers"]
|
||||
|
||||
subtitles = servers[server_name]["subtitles"]
|
||||
episode_title = servers[server_name]["episode_title"]
|
||||
|
||||
if anilist_anime_info:
|
||||
selected_anime_title = (
|
||||
anilist_anime_info["title"][config.preferred_language]
|
||||
or anilist_anime_info["title"]["romaji"]
|
||||
or anilist_anime_info["title"]["english"]
|
||||
)
|
||||
import re
|
||||
|
||||
for episode_detail in anilist_anime_info["streamingEpisodes"]:
|
||||
if re.match(
|
||||
f".*Episode {episode} .*", episode_detail["title"]
|
||||
):
|
||||
episode_title = episode_detail["title"]
|
||||
break
|
||||
print(f"[purple]Now Downloading:[/] {episode_title}")
|
||||
subtitles = move_preferred_subtitle_lang_to_top(
|
||||
subtitles, config.sub_lang
|
||||
)
|
||||
downloader._download_file(
|
||||
link,
|
||||
selected_anime_title,
|
||||
episode_title,
|
||||
download_dir,
|
||||
silent,
|
||||
vid_format=config.format,
|
||||
force_unknown_ext=force_unknown_ext,
|
||||
verbose=verbose,
|
||||
headers=provider_headers,
|
||||
sub=subtitles[0]["url"] if subtitles else "",
|
||||
merge=merge,
|
||||
clean=clean,
|
||||
prompt=prompt,
|
||||
force_ffmpeg=force_ffmpeg,
|
||||
hls_use_mpegts=hls_use_mpegts,
|
||||
hls_use_h264=hls_use_h264,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
time.sleep(1)
|
||||
print("Continuing...")
|
||||
print("Done Downloading")
|
||||
time.sleep(wait_time)
|
||||
exit_app()
|
||||
else:
|
||||
from sys import exit
|
||||
|
||||
print("Failed to search for anime", anilist_search_results)
|
||||
exit(1)
|
||||
358
fastanime/cli/commands/anilist/downloads.py
Normal file
358
fastanime/cli/commands/anilist/downloads.py
Normal file
@@ -0,0 +1,358 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
from ...completion_functions import downloaded_anime_titles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
from ..config import Config
|
||||
|
||||
|
||||
@click.command(
|
||||
help="View and watch your downloads using mpv",
|
||||
short_help="Watch downloads",
|
||||
epilog="""
|
||||
\b
|
||||
\b\bExamples:
|
||||
fastanime downloads
|
||||
\b
|
||||
# view individual episodes
|
||||
fastanime downloads --view-episodes
|
||||
# --- or ---
|
||||
fastanime downloads -v
|
||||
\b
|
||||
# to set seek time when using ffmpegthumbnailer for local previews
|
||||
# -1 means random and is the default
|
||||
fastanime downloads --time-to-seek <intRange(-1,100)>
|
||||
# --- or ---
|
||||
fastanime downloads -t <intRange(-1,100)>
|
||||
\b
|
||||
# to watch a specific title
|
||||
# be sure to get the completions for the best experience
|
||||
fastanime downloads --title <title>
|
||||
\b
|
||||
# to get the path to the downloads folder set
|
||||
fastanime downloads --path
|
||||
# useful when you want to use the value for other programs
|
||||
""",
|
||||
)
|
||||
@click.option("--path", "-p", help="print the downloads folder and exit", is_flag=True)
|
||||
@click.option(
|
||||
"--title",
|
||||
"-T",
|
||||
shell_complete=downloaded_anime_titles,
|
||||
help="watch a specific title",
|
||||
)
|
||||
@click.option("--view-episodes", "-v", help="View individual episodes", is_flag=True)
|
||||
@click.option(
|
||||
"--ffmpegthumbnailer-seek-time",
|
||||
"--time-to-seek",
|
||||
"-t",
|
||||
type=click.IntRange(-1, 100),
|
||||
help="ffmpegthumbnailer seek time",
|
||||
)
|
||||
@click.pass_obj
|
||||
def downloads(
|
||||
config: "Config", path: bool, title, view_episodes, ffmpegthumbnailer_seek_time
|
||||
):
|
||||
import os
|
||||
|
||||
from ....cli.utils.mpv import run_mpv
|
||||
from ....libs.fzf import fzf
|
||||
from ....libs.rofi import Rofi
|
||||
from ....Utility.utils import sort_by_episode_number
|
||||
from ...utils.tools import exit_app
|
||||
from ...utils.utils import fuzzy_inquirer
|
||||
|
||||
if not ffmpegthumbnailer_seek_time:
|
||||
ffmpegthumbnailer_seek_time = config.ffmpegthumbnailer_seek_time
|
||||
USER_VIDEOS_DIR = config.downloads_dir
|
||||
if path:
|
||||
print(USER_VIDEOS_DIR)
|
||||
return
|
||||
if not os.path.exists(USER_VIDEOS_DIR):
|
||||
print("Downloads directory specified does not exist")
|
||||
return
|
||||
anime_downloads = sorted(
|
||||
os.listdir(USER_VIDEOS_DIR),
|
||||
)
|
||||
anime_downloads.append("Exit")
|
||||
|
||||
def create_thumbnails(video_path, anime_title, downloads_thumbnail_cache_dir):
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
FFMPEG_THUMBNAILER = shutil.which("ffmpegthumbnailer")
|
||||
if not FFMPEG_THUMBNAILER:
|
||||
return
|
||||
|
||||
out = os.path.join(downloads_thumbnail_cache_dir, anime_title)
|
||||
if ffmpegthumbnailer_seek_time == -1:
|
||||
import random
|
||||
|
||||
seektime = str(random.randrange(0, 100))
|
||||
else:
|
||||
seektime = str(ffmpegthumbnailer_seek_time)
|
||||
_ = subprocess.run(
|
||||
[
|
||||
FFMPEG_THUMBNAILER,
|
||||
"-i",
|
||||
video_path,
|
||||
"-o",
|
||||
out,
|
||||
"-s",
|
||||
"0",
|
||||
"-t",
|
||||
seektime,
|
||||
],
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
|
||||
def get_previews_anime(workers=None, bg=True):
|
||||
import concurrent.futures
|
||||
import random
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
if not shutil.which("ffmpegthumbnailer"):
|
||||
print("ffmpegthumbnailer not found")
|
||||
logger.error("ffmpegthumbnailer not found")
|
||||
return
|
||||
|
||||
from ....constants import APP_CACHE_DIR
|
||||
from ...utils.scripts import bash_functions
|
||||
|
||||
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _worker():
|
||||
# use concurrency to download the images as fast as possible
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
for anime_title in anime_downloads:
|
||||
anime_path = os.path.join(USER_VIDEOS_DIR, anime_title)
|
||||
if not os.path.isdir(anime_path):
|
||||
continue
|
||||
playlist = [
|
||||
anime
|
||||
for anime in sorted(
|
||||
os.listdir(anime_path),
|
||||
)
|
||||
if "mp4" in anime
|
||||
]
|
||||
if playlist:
|
||||
# actual link to download image from
|
||||
video_path = os.path.join(anime_path, random.choice(playlist))
|
||||
future_to_url[
|
||||
executor.submit(
|
||||
create_thumbnails,
|
||||
video_path,
|
||||
anime_title,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
] = anime_title
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
logger.error("%r generated an exception: %s" % (url, e))
|
||||
|
||||
if bg:
|
||||
from threading import Thread
|
||||
|
||||
worker = Thread(target=_worker)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
else:
|
||||
_worker()
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then
|
||||
if ! fzf-preview %s/{} 2>/dev/null; then
|
||||
echo Loading...
|
||||
fi
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
bash_functions,
|
||||
downloads_thumbnail_cache_dir,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
return preview
|
||||
|
||||
def get_previews_episodes(anime_playlist_path, workers=None, bg=True):
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from ....constants import APP_CACHE_DIR
|
||||
from ...utils.scripts import bash_functions
|
||||
|
||||
if not shutil.which("ffmpegthumbnailer"):
|
||||
print("ffmpegthumbnailer not found")
|
||||
logger.error("ffmpegthumbnailer not found")
|
||||
return
|
||||
|
||||
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _worker():
|
||||
import concurrent.futures
|
||||
|
||||
# use concurrency to download the images as fast as possible
|
||||
# anime_playlist_path = os.path.join(USER_VIDEOS_DIR, anime_playlist_path)
|
||||
if not os.path.isdir(anime_playlist_path):
|
||||
return
|
||||
anime_episodes = sorted(
|
||||
os.listdir(anime_playlist_path), key=sort_by_episode_number
|
||||
)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
# load the jobs
|
||||
future_to_url = {}
|
||||
for episode_title in anime_episodes:
|
||||
episode_path = os.path.join(anime_playlist_path, episode_title)
|
||||
|
||||
# actual link to download image from
|
||||
future_to_url[
|
||||
executor.submit(
|
||||
create_thumbnails,
|
||||
episode_path,
|
||||
episode_title,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
] = episode_title
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
url = future_to_url[future]
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
logger.error("%r generated an exception: %s" % (url, e))
|
||||
|
||||
if bg:
|
||||
from threading import Thread
|
||||
|
||||
worker = Thread(target=_worker)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
else:
|
||||
_worker()
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then
|
||||
if ! fzf-preview %s/{} 2>/dev/null; then
|
||||
echo Loading...
|
||||
fi
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
bash_functions,
|
||||
downloads_thumbnail_cache_dir,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
return preview
|
||||
|
||||
def stream_episode(
|
||||
anime_playlist_path,
|
||||
):
|
||||
if view_episodes:
|
||||
if not os.path.isdir(anime_playlist_path):
|
||||
print(anime_playlist_path, "is not dir")
|
||||
exit_app(1)
|
||||
return
|
||||
episodes = sorted(
|
||||
os.listdir(anime_playlist_path), key=sort_by_episode_number
|
||||
)
|
||||
downloaded_episodes = [*episodes, "Back"]
|
||||
|
||||
if config.use_fzf:
|
||||
if not config.preview:
|
||||
episode_title = fzf.run(
|
||||
downloaded_episodes,
|
||||
"Enter Episode ",
|
||||
)
|
||||
else:
|
||||
preview = get_previews_episodes(anime_playlist_path)
|
||||
episode_title = fzf.run(
|
||||
downloaded_episodes,
|
||||
"Enter Episode ",
|
||||
preview=preview,
|
||||
)
|
||||
elif config.use_rofi:
|
||||
episode_title = Rofi.run(downloaded_episodes, "Enter Episode")
|
||||
else:
|
||||
episode_title = fuzzy_inquirer(
|
||||
downloaded_episodes,
|
||||
"Enter Playlist Name",
|
||||
)
|
||||
if episode_title == "Back":
|
||||
stream_anime()
|
||||
return
|
||||
episode_path = os.path.join(anime_playlist_path, episode_title)
|
||||
if config.sync_play:
|
||||
from ...utils.syncplay import SyncPlayer
|
||||
|
||||
SyncPlayer(episode_path)
|
||||
else:
|
||||
run_mpv(
|
||||
episode_path,
|
||||
player=config.player,
|
||||
)
|
||||
stream_episode(anime_playlist_path)
|
||||
|
||||
def stream_anime(title=None):
|
||||
if title:
|
||||
from thefuzz import fuzz
|
||||
|
||||
playlist_name = max(anime_downloads, key=lambda t: fuzz.ratio(title, t))
|
||||
elif config.use_fzf:
|
||||
if not config.preview:
|
||||
playlist_name = fzf.run(
|
||||
anime_downloads,
|
||||
"Enter Playlist Name",
|
||||
)
|
||||
else:
|
||||
preview = get_previews_anime()
|
||||
playlist_name = fzf.run(
|
||||
anime_downloads,
|
||||
"Enter Playlist Name",
|
||||
preview=preview,
|
||||
)
|
||||
elif config.use_rofi:
|
||||
playlist_name = Rofi.run(anime_downloads, "Enter Playlist Name")
|
||||
else:
|
||||
playlist_name = fuzzy_inquirer(
|
||||
anime_downloads,
|
||||
"Enter Playlist Name",
|
||||
)
|
||||
if playlist_name == "Exit":
|
||||
exit_app()
|
||||
return
|
||||
playlist = os.path.join(USER_VIDEOS_DIR, playlist_name)
|
||||
if view_episodes:
|
||||
stream_episode(
|
||||
playlist,
|
||||
)
|
||||
else:
|
||||
if config.sync_play:
|
||||
from ...utils.syncplay import SyncPlayer
|
||||
|
||||
SyncPlayer(playlist)
|
||||
else:
|
||||
run_mpv(
|
||||
playlist,
|
||||
player=config.player,
|
||||
)
|
||||
stream_anime()
|
||||
|
||||
stream_anime(title)
|
||||
@@ -42,5 +42,12 @@ def dropped(config: "Config", dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Dropped", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -26,6 +26,9 @@ def favourites(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_most_favourite
|
||||
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -11,15 +11,20 @@ if TYPE_CHECKING:
|
||||
@click.option("--erase", "-e", help="Erase your login details", is_flag=True)
|
||||
@click.pass_obj
|
||||
def login(config: "Config", status, erase):
|
||||
from os import path
|
||||
from sys import exit
|
||||
|
||||
from rich import print
|
||||
from rich.prompt import Confirm, Prompt
|
||||
|
||||
from ....constants import S_PLATFORM
|
||||
|
||||
if status:
|
||||
is_logged_in = True if config.user else False
|
||||
message = (
|
||||
"You are logged in :smile:" if is_logged_in else "You arent logged in :cry:"
|
||||
"You are logged in :smile:"
|
||||
if is_logged_in
|
||||
else "You aren't logged in :cry:"
|
||||
)
|
||||
print(message)
|
||||
print(config.user)
|
||||
@@ -46,9 +51,19 @@ def login(config: "Config", status, erase):
|
||||
print(
|
||||
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
||||
)
|
||||
launch(config.fastanime_anilist_app_login_url, wait=True)
|
||||
print("Please paste the token provided here")
|
||||
token = Prompt.ask("Enter token")
|
||||
token = ""
|
||||
if S_PLATFORM.startswith("darwin"):
|
||||
anilist_key_file_path = path.expanduser("~") + "/Downloads/anilist_key.txt"
|
||||
launch(config.fastanime_anilist_app_login_url, wait=False)
|
||||
Prompt.ask(
|
||||
"MacOS detected.\nPress any key once the token provided has been pasted into "
|
||||
+ anilist_key_file_path
|
||||
)
|
||||
with open(anilist_key_file_path, "r") as key_file:
|
||||
token = key_file.read().strip()
|
||||
else:
|
||||
launch(config.fastanime_anilist_app_login_url, wait=False)
|
||||
token = Prompt.ask("Enter token")
|
||||
user = AniList.login_user(token)
|
||||
if not user:
|
||||
print("Sth went wrong", user)
|
||||
|
||||
@@ -30,12 +30,12 @@ def notifier(config: "Config"):
|
||||
|
||||
notified = os.path.join(APP_DATA_DIR, "last_notification.json")
|
||||
anime_image_path = os.path.join(APP_CACHE_DIR, "notification_image")
|
||||
notification_duration = config.notification_duration * 60
|
||||
notification_duration = config.notification_duration
|
||||
notification_image_path = ""
|
||||
|
||||
if not config.user:
|
||||
print("Not Authenticated")
|
||||
print("Run the following to get started: fastanime anilist loggin")
|
||||
print("Run the following to get started: fastanime anilist login")
|
||||
exit(1)
|
||||
run = True
|
||||
# WARNING: Mess around with this value at your own risk
|
||||
|
||||
@@ -41,6 +41,12 @@ def paused(config: "Config", dump_json):
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anilist_config = FastAnimeRuntimeState()
|
||||
anilist_config.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, anilist_config)
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Paused", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -42,5 +42,12 @@ def planning(config: "Config", dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Planned", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -25,6 +25,9 @@ def popular(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_most_popular
|
||||
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,7 @@ import click
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Get random anime from anilist based on a range of anilist anime ids that are seected at random",
|
||||
help="Get random anime from anilist based on a range of anilist anime ids that are selected at random",
|
||||
short_help="View random anime",
|
||||
)
|
||||
@click.option(
|
||||
|
||||
@@ -26,6 +26,11 @@ def recent(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
AniList.get_most_recently_updated
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -42,5 +42,12 @@ def rewatching(config: "Config", dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Rewatching", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -25,6 +25,9 @@ def scores(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_most_scored
|
||||
fastanime_runtime_state.anilist_results_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -1,369 +1,15 @@
|
||||
import click
|
||||
|
||||
from ...completion_functions import anime_titles_shell_complete
|
||||
|
||||
tags_available = {
|
||||
"Cast": ["Polyamorous"],
|
||||
"Cast Main Cast": [
|
||||
"Anti-Hero",
|
||||
"Elderly Protagonist",
|
||||
"Ensemble Cast",
|
||||
"Estranged Family",
|
||||
"Female Protagonist",
|
||||
"Male Protagonist",
|
||||
"Primarily Adult Cast",
|
||||
"Primarily Animal Cast",
|
||||
"Primarily Child Cast",
|
||||
"Primarily Female Cast",
|
||||
"Primarily Male Cast",
|
||||
"Primarily Teen Cast",
|
||||
],
|
||||
"Cast Traits": [
|
||||
"Age Regression",
|
||||
"Agender",
|
||||
"Aliens",
|
||||
"Amnesia",
|
||||
"Angels",
|
||||
"Anthropomorphism",
|
||||
"Aromantic",
|
||||
"Arranged Marriage",
|
||||
"Artificial Intelligence",
|
||||
"Asexual",
|
||||
"Butler",
|
||||
"Centaur",
|
||||
"Chimera",
|
||||
"Chuunibyou",
|
||||
"Clone",
|
||||
"Cosplay",
|
||||
"Cowboys",
|
||||
"Crossdressing",
|
||||
"Cyborg",
|
||||
"Delinquents",
|
||||
"Demons",
|
||||
"Detective",
|
||||
"Dinosaurs",
|
||||
"Disability",
|
||||
"Dissociative Identities",
|
||||
"Dragons",
|
||||
"Dullahan",
|
||||
"Elf",
|
||||
"Fairy",
|
||||
"Femboy",
|
||||
"Ghost",
|
||||
"Goblin",
|
||||
"Gods",
|
||||
"Gyaru",
|
||||
"Hikikomori",
|
||||
"Homeless",
|
||||
"Idol",
|
||||
"Kemonomimi",
|
||||
"Kuudere",
|
||||
"Maids",
|
||||
"Mermaid",
|
||||
"Monster Boy",
|
||||
"Monster Girl",
|
||||
"Nekomimi",
|
||||
"Ninja",
|
||||
"Nudity",
|
||||
"Nun",
|
||||
"Office Lady",
|
||||
"Oiran",
|
||||
"Ojou-sama",
|
||||
"Orphan",
|
||||
"Pirates",
|
||||
"Robots",
|
||||
"Samurai",
|
||||
"Shrine Maiden",
|
||||
"Skeleton",
|
||||
"Succubus",
|
||||
"Tanned Skin",
|
||||
"Teacher",
|
||||
"Tomboy",
|
||||
"Transgender",
|
||||
"Tsundere",
|
||||
"Twins",
|
||||
"Vampire",
|
||||
"Veterinarian",
|
||||
"Vikings",
|
||||
"Villainess",
|
||||
"VTuber",
|
||||
"Werewolf",
|
||||
"Witch",
|
||||
"Yandere",
|
||||
"Zombie",
|
||||
],
|
||||
"Demographic": ["Josei", "Kids", "Seinen", "Shoujo", "Shounen"],
|
||||
"Setting": ["Matriarchy"],
|
||||
"Setting Scene": [
|
||||
"Bar",
|
||||
"Boarding School",
|
||||
"Circus",
|
||||
"Coastal",
|
||||
"College",
|
||||
"Desert",
|
||||
"Dungeon",
|
||||
"Foreign",
|
||||
"Inn",
|
||||
"Konbini",
|
||||
"Natural Disaster",
|
||||
"Office",
|
||||
"Outdoor",
|
||||
"Prison",
|
||||
"Restaurant",
|
||||
"Rural",
|
||||
"School",
|
||||
"School Club",
|
||||
"Snowscape",
|
||||
"Urban",
|
||||
"Work",
|
||||
],
|
||||
"Setting Time": [
|
||||
"Achronological Order",
|
||||
"Anachronism",
|
||||
"Ancient China",
|
||||
"Dystopian",
|
||||
"Historical",
|
||||
"Time Skip",
|
||||
],
|
||||
"Setting Universe": [
|
||||
"Afterlife",
|
||||
"Alternate Universe",
|
||||
"Augmented Reality",
|
||||
"Omegaverse",
|
||||
"Post-Apocalyptic",
|
||||
"Space",
|
||||
"Urban Fantasy",
|
||||
"Virtual World",
|
||||
],
|
||||
"Technical": [
|
||||
"4-koma",
|
||||
"Achromatic",
|
||||
"Advertisement",
|
||||
"Anthology",
|
||||
"CGI",
|
||||
"Episodic",
|
||||
"Flash",
|
||||
"Full CGI",
|
||||
"Full Color",
|
||||
"No Dialogue",
|
||||
"Non-fiction",
|
||||
"POV",
|
||||
"Puppetry",
|
||||
"Rotoscoping",
|
||||
"Stop Motion",
|
||||
],
|
||||
"Theme Action": [
|
||||
"Archery",
|
||||
"Battle Royale",
|
||||
"Espionage",
|
||||
"Fugitive",
|
||||
"Guns",
|
||||
"Martial Arts",
|
||||
"Spearplay",
|
||||
"Swordplay",
|
||||
],
|
||||
"Theme Arts": [
|
||||
"Acting",
|
||||
"Calligraphy",
|
||||
"Classic Literature",
|
||||
"Drawing",
|
||||
"Fashion",
|
||||
"Food",
|
||||
"Makeup",
|
||||
"Photography",
|
||||
"Rakugo",
|
||||
"Writing",
|
||||
],
|
||||
"Theme Arts-Music": [
|
||||
"Band",
|
||||
"Classical Music",
|
||||
"Dancing",
|
||||
"Hip-hop Music",
|
||||
"Jazz Music",
|
||||
"Metal Music",
|
||||
"Musical Theater",
|
||||
"Rock Music",
|
||||
],
|
||||
"Theme Comedy": ["Parody", "Satire", "Slapstick", "Surreal Comedy"],
|
||||
"Theme Drama": [
|
||||
"Bullying",
|
||||
"Class Struggle",
|
||||
"Coming of Age",
|
||||
"Conspiracy",
|
||||
"Eco-Horror",
|
||||
"Fake Relationship",
|
||||
"Kingdom Management",
|
||||
"Rehabilitation",
|
||||
"Revenge",
|
||||
"Suicide",
|
||||
"Tragedy",
|
||||
],
|
||||
"Theme Fantasy": [
|
||||
"Alchemy",
|
||||
"Body Swapping",
|
||||
"Cultivation",
|
||||
"Fairy Tale",
|
||||
"Henshin",
|
||||
"Isekai",
|
||||
"Kaiju",
|
||||
"Magic",
|
||||
"Mythology",
|
||||
"Necromancy",
|
||||
"Shapeshifting",
|
||||
"Steampunk",
|
||||
"Super Power",
|
||||
"Superhero",
|
||||
"Wuxia",
|
||||
"Youkai",
|
||||
],
|
||||
"Theme Game": ["Board Game", "E-Sports", "Video Games"],
|
||||
"Theme Game-Card & Board Game": [
|
||||
"Card Battle",
|
||||
"Go",
|
||||
"Karuta",
|
||||
"Mahjong",
|
||||
"Poker",
|
||||
"Shogi",
|
||||
],
|
||||
"Theme Game-Sport": [
|
||||
"Acrobatics",
|
||||
"Airsoft",
|
||||
"American Football",
|
||||
"Athletics",
|
||||
"Badminton",
|
||||
"Baseball",
|
||||
"Basketball",
|
||||
"Bowling",
|
||||
"Boxing",
|
||||
"Cheerleading",
|
||||
"Cycling",
|
||||
"Fencing",
|
||||
"Fishing",
|
||||
"Fitness",
|
||||
"Football",
|
||||
"Golf",
|
||||
"Handball",
|
||||
"Ice Skating",
|
||||
"Judo",
|
||||
"Lacrosse",
|
||||
"Parkour",
|
||||
"Rugby",
|
||||
"Scuba Diving",
|
||||
"Skateboarding",
|
||||
"Sumo",
|
||||
"Surfing",
|
||||
"Swimming",
|
||||
"Table Tennis",
|
||||
"Tennis",
|
||||
"Volleyball",
|
||||
"Wrestling",
|
||||
],
|
||||
"Theme Other": [
|
||||
"Adoption",
|
||||
"Animals",
|
||||
"Astronomy",
|
||||
"Autobiographical",
|
||||
"Biographical",
|
||||
"Body Horror",
|
||||
"Cannibalism",
|
||||
"Chibi",
|
||||
"Cosmic Horror",
|
||||
"Crime",
|
||||
"Crossover",
|
||||
"Death Game",
|
||||
"Denpa",
|
||||
"Drugs",
|
||||
"Economics",
|
||||
"Educational",
|
||||
"Environmental",
|
||||
"Ero Guro",
|
||||
"Filmmaking",
|
||||
"Found Family",
|
||||
"Gambling",
|
||||
"Gender Bending",
|
||||
"Gore",
|
||||
"Language Barrier",
|
||||
"LGBTQ+ Themes",
|
||||
"Lost Civilization",
|
||||
"Marriage",
|
||||
"Medicine",
|
||||
"Memory Manipulation",
|
||||
"Meta",
|
||||
"Mountaineering",
|
||||
"Noir",
|
||||
"Otaku Culture",
|
||||
"Pandemic",
|
||||
"Philosophy",
|
||||
"Politics",
|
||||
"Proxy Battle",
|
||||
"Psychosexual",
|
||||
"Reincarnation",
|
||||
"Religion",
|
||||
"Royal Affairs",
|
||||
"Slavery",
|
||||
"Software Development",
|
||||
"Survival",
|
||||
"Terrorism",
|
||||
"Torture",
|
||||
"Travel",
|
||||
"War",
|
||||
],
|
||||
"Theme Other-Organisations": [
|
||||
"Assassins",
|
||||
"Criminal Organization",
|
||||
"Cult",
|
||||
"Firefighters",
|
||||
"Gangs",
|
||||
"Mafia",
|
||||
"Military",
|
||||
"Police",
|
||||
"Triads",
|
||||
"Yakuza",
|
||||
],
|
||||
"Theme Other-Vehicle": [
|
||||
"Aviation",
|
||||
"Cars",
|
||||
"Mopeds",
|
||||
"Motorcycles",
|
||||
"Ships",
|
||||
"Tanks",
|
||||
"Trains",
|
||||
],
|
||||
"Theme Romance": [
|
||||
"Age Gap",
|
||||
"Bisexual",
|
||||
"Boys' Love",
|
||||
"Female Harem",
|
||||
"Heterosexual",
|
||||
"Love Triangle",
|
||||
"Male Harem",
|
||||
"Matchmaking",
|
||||
"Mixed Gender Harem",
|
||||
"Teens' Love",
|
||||
"Unrequited Love",
|
||||
"Yuri",
|
||||
],
|
||||
"Theme Sci Fi": [
|
||||
"Cyberpunk",
|
||||
"Space Opera",
|
||||
"Time Loop",
|
||||
"Time Manipulation",
|
||||
"Tokusatsu",
|
||||
],
|
||||
"Theme Sci Fi-Mecha": ["Real Robot", "Super Robot"],
|
||||
"Theme Slice of Life": [
|
||||
"Agriculture",
|
||||
"Cute Boys Doing Cute Things",
|
||||
"Cute Girls Doing Cute Things",
|
||||
"Family Life",
|
||||
"Horticulture",
|
||||
"Iyashikei",
|
||||
"Parenthood",
|
||||
],
|
||||
}
|
||||
tags_available_list = []
|
||||
for tag_category, tags_in_category in tags_available.items():
|
||||
tags_available_list.extend(tags_in_category)
|
||||
from .data import (
|
||||
genres_available,
|
||||
media_formats_available,
|
||||
media_statuses_available,
|
||||
seasons_available,
|
||||
sorts_available,
|
||||
tags_available_list,
|
||||
years_available,
|
||||
)
|
||||
|
||||
|
||||
@click.command(
|
||||
@@ -380,91 +26,27 @@ for tag_category, tags_in_category in tags_available.items():
|
||||
@click.option(
|
||||
"--season",
|
||||
help="The season the media was released",
|
||||
type=click.Choice(["WINTER", "SPRING", "SUMMER", "FALL"]),
|
||||
type=click.Choice(seasons_available),
|
||||
)
|
||||
@click.option(
|
||||
"--status",
|
||||
"-S",
|
||||
help="The media status of the anime",
|
||||
multiple=True,
|
||||
type=click.Choice(
|
||||
["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]
|
||||
),
|
||||
type=click.Choice(media_statuses_available),
|
||||
)
|
||||
@click.option(
|
||||
"--sort",
|
||||
"-s",
|
||||
help="What to sort the search results on",
|
||||
type=click.Choice(
|
||||
[
|
||||
"ID",
|
||||
"ID_DESC",
|
||||
"TITLE_ROMAJI",
|
||||
"TITLE_ROMAJI_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"TITLE_NATIVE",
|
||||
"TITLE_NATIVE_DESC",
|
||||
"TYPE",
|
||||
"TYPE_DESC",
|
||||
"FORMAT",
|
||||
"FORMAT_DESC",
|
||||
"START_DATE",
|
||||
"START_DATE_DESC",
|
||||
"END_DATE",
|
||||
"END_DATE_DESC",
|
||||
"SCORE",
|
||||
"SCORE_DESC",
|
||||
"POPULARITY",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING",
|
||||
"TRENDING_DESC",
|
||||
"EPISODES",
|
||||
"EPISODES_DESC",
|
||||
"DURATION",
|
||||
"DURATION_DESC",
|
||||
"STATUS",
|
||||
"STATUS_DESC",
|
||||
"CHAPTERS",
|
||||
"CHAPTERS_DESC",
|
||||
"VOLUMES",
|
||||
"VOLUMES_DESC",
|
||||
"UPDATED_AT",
|
||||
"UPDATED_AT_DESC",
|
||||
"SEARCH_MATCH",
|
||||
"FAVOURITES",
|
||||
"FAVOURITES_DESC",
|
||||
]
|
||||
),
|
||||
type=click.Choice(sorts_available),
|
||||
)
|
||||
@click.option(
|
||||
"--genres",
|
||||
"-g",
|
||||
multiple=True,
|
||||
help="the genres to filter by",
|
||||
type=click.Choice(
|
||||
[
|
||||
"Action",
|
||||
"Adventure",
|
||||
"Comedy",
|
||||
"Drama",
|
||||
"Ecchi",
|
||||
"Fantasy",
|
||||
"Horror",
|
||||
"Mahou Shoujo",
|
||||
"Mecha",
|
||||
"Music",
|
||||
"Mystery",
|
||||
"Psychological",
|
||||
"Romance",
|
||||
"Sci-Fi",
|
||||
"Slice of Life",
|
||||
"Sports",
|
||||
"Supernatural",
|
||||
"Thriller",
|
||||
"Hentai",
|
||||
]
|
||||
),
|
||||
type=click.Choice(genres_available),
|
||||
)
|
||||
@click.option(
|
||||
"--tags",
|
||||
@@ -478,49 +60,12 @@ for tag_category, tags_in_category in tags_available.items():
|
||||
"-f",
|
||||
multiple=True,
|
||||
help="Media format",
|
||||
type=click.Choice(
|
||||
["TV", "TV_SHORT", "MOVIE", "SPECIAL", "OVA", "MUSIC", "NOVEL", "ONE_SHOT"]
|
||||
),
|
||||
type=click.Choice(media_formats_available),
|
||||
)
|
||||
@click.option(
|
||||
"--year",
|
||||
"-y",
|
||||
type=click.Choice(
|
||||
[
|
||||
"1900",
|
||||
"1910",
|
||||
"1920",
|
||||
"1930",
|
||||
"1940",
|
||||
"1950",
|
||||
"1960",
|
||||
"1970",
|
||||
"1980",
|
||||
"1990",
|
||||
"2000",
|
||||
"2004",
|
||||
"2005",
|
||||
"2006",
|
||||
"2007",
|
||||
"2008",
|
||||
"2009",
|
||||
"2010",
|
||||
"2011",
|
||||
"2012",
|
||||
"2013",
|
||||
"2014",
|
||||
"2015",
|
||||
"2016",
|
||||
"2017",
|
||||
"2018",
|
||||
"2019",
|
||||
"2020",
|
||||
"2021",
|
||||
"2022",
|
||||
"2023",
|
||||
"2024",
|
||||
]
|
||||
),
|
||||
type=click.Choice(years_available),
|
||||
help="the year the media was released",
|
||||
)
|
||||
@click.option(
|
||||
@@ -566,6 +111,22 @@ def search(
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda page=1, **kwargs: AniList.search(
|
||||
query=title,
|
||||
sort=sort,
|
||||
status_in=list(status),
|
||||
genre_in=list(genres),
|
||||
season=season,
|
||||
tag_in=list(tags),
|
||||
seasonYear=year,
|
||||
format_in=list(media_format),
|
||||
on_list=on_list,
|
||||
page=page,
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = search_results
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -26,6 +26,9 @@ def trending(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_trending
|
||||
fastanime_runtime_state.anilist_results_data = data
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,7 @@ import click
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Fetch the 15 most anticipited anime", short_help="View upcoming anime"
|
||||
help="Fetch the 15 most anticipated anime", short_help="View upcoming anime"
|
||||
)
|
||||
@click.option(
|
||||
"--dump-json",
|
||||
@@ -25,6 +25,9 @@ def upcoming(config, dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = AniList.get_upcoming_anime
|
||||
fastanime_runtime_state.anilist_results_data = data
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
|
||||
@@ -42,5 +42,12 @@ def watching(config: "Config", dump_json):
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
|
||||
fastanime_runtime_state.current_page = 1
|
||||
fastanime_runtime_state.current_data_loader = (
|
||||
lambda config, **kwargs: anilist_interfaces._handle_animelist(
|
||||
config, fastanime_runtime_state, "Watching", **kwargs
|
||||
)
|
||||
)
|
||||
fastanime_runtime_state.anilist_results_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
@@ -114,6 +114,21 @@ if TYPE_CHECKING:
|
||||
help="Whether to prompt for anything instead just do the best thing",
|
||||
default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--force-ffmpeg",
|
||||
is_flag=True,
|
||||
help="Force the use of FFmpeg for downloading (supports large variety of streams but slower)",
|
||||
)
|
||||
@click.option(
|
||||
"--hls-use-mpegts",
|
||||
is_flag=True,
|
||||
help="Use mpegts for hls streams, resulted in .ts file (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.option(
|
||||
"--hls-use-h264",
|
||||
is_flag=True,
|
||||
help="Use H.264 (MP4) for hls streams, resulted in .mp4 file (useful for some streams: see Docs) (this option forces --force-ffmpeg to be True)",
|
||||
)
|
||||
@click.pass_obj
|
||||
def download(
|
||||
config: "Config",
|
||||
@@ -127,6 +142,9 @@ def download(
|
||||
clean,
|
||||
wait_time,
|
||||
prompt,
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
hls_use_h264,
|
||||
):
|
||||
import time
|
||||
|
||||
@@ -146,6 +164,8 @@ def download(
|
||||
move_preferred_subtitle_lang_to_top,
|
||||
)
|
||||
|
||||
force_ffmpeg |= (hls_use_mpegts or hls_use_h264)
|
||||
|
||||
anime_provider = AnimeProvider(config.provider)
|
||||
anilist_anime_info = None
|
||||
|
||||
@@ -185,6 +205,9 @@ def download(
|
||||
clean,
|
||||
wait_time,
|
||||
prompt,
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
hls_use_h264,
|
||||
)
|
||||
return
|
||||
search_results = search_results["results"]
|
||||
@@ -236,6 +259,9 @@ def download(
|
||||
clean,
|
||||
wait_time,
|
||||
prompt,
|
||||
force_ffmpeg,
|
||||
hls_use_mpegts,
|
||||
hls_use_h264,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -369,6 +395,9 @@ def download(
|
||||
merge=merge,
|
||||
clean=clean,
|
||||
prompt=prompt,
|
||||
force_ffmpeg=force_ffmpeg,
|
||||
hls_use_mpegts=hls_use_mpegts,
|
||||
hls_use_h264=hls_use_h264,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
@@ -124,7 +124,7 @@ def downloads(
|
||||
return
|
||||
|
||||
from ...constants import APP_CACHE_DIR
|
||||
from ..utils.scripts import fzf_preview
|
||||
from ..utils.scripts import bash_functions
|
||||
|
||||
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
@@ -183,7 +183,7 @@ def downloads(
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
bash_functions,
|
||||
downloads_thumbnail_cache_dir,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
@@ -194,7 +194,7 @@ def downloads(
|
||||
from pathlib import Path
|
||||
|
||||
from ...constants import APP_CACHE_DIR
|
||||
from ..utils.scripts import fzf_preview
|
||||
from ..utils.scripts import bash_functions
|
||||
|
||||
if not shutil.which("ffmpegthumbnailer"):
|
||||
print("ffmpegthumbnailer not found")
|
||||
@@ -256,7 +256,7 @@ def downloads(
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
bash_functions,
|
||||
downloads_thumbnail_cache_dir,
|
||||
downloads_thumbnail_cache_dir,
|
||||
)
|
||||
|
||||
@@ -213,25 +213,21 @@ def grab(
|
||||
|
||||
# lets download em
|
||||
for episode in episodes_range:
|
||||
try:
|
||||
if episode not in episodes:
|
||||
continue
|
||||
streams = anime_provider.get_episode_streams(
|
||||
anime["id"], episode, config.translation_type
|
||||
)
|
||||
if not streams:
|
||||
continue
|
||||
episode_streams = {server["server"]: server for server in streams}
|
||||
if episode not in episodes:
|
||||
continue
|
||||
streams = anime_provider.get_episode_streams(
|
||||
anime["id"], episode, config.translation_type
|
||||
)
|
||||
if not streams:
|
||||
continue
|
||||
episode_streams = {server["server"]: server for server in streams}
|
||||
|
||||
if episode_streams_only:
|
||||
grabbed_anime[episode] = episode_streams
|
||||
else:
|
||||
grabbed_anime["episodes_streams"][ # pyright:ignore
|
||||
episode
|
||||
] = episode_streams
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
if episode_streams_only:
|
||||
grabbed_anime[episode] = episode_streams
|
||||
else:
|
||||
grabbed_anime["episodes_streams"][ # pyright:ignore
|
||||
episode
|
||||
] = episode_streams
|
||||
|
||||
# grab the full data for single title and appen to final result or episode streams
|
||||
grabbed_animes.append(grabbed_anime)
|
||||
|
||||
@@ -59,7 +59,7 @@ def get_anime_titles(query: str, variables: dict = {}):
|
||||
else:
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
logger.error(f"Something unexpected occurred {e}")
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@ import logging
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
from typing import TYPE_CHECKING
|
||||
from ..libs.fzf import FZF_DEFAULT_OPTS, HEADER
|
||||
|
||||
from ..constants import (
|
||||
ASSETS_DIR,
|
||||
S_PLATFORM,
|
||||
USER_CONFIG_PATH,
|
||||
USER_DATA_PATH,
|
||||
USER_VIDEOS_DIR,
|
||||
ASSETS_DIR,
|
||||
USER_WATCH_HISTORY_PATH,
|
||||
S_PLATFORM,
|
||||
)
|
||||
from ..libs.fzf import FZF_DEFAULT_OPTS, HEADER
|
||||
from ..libs.rofi import Rofi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -29,9 +29,15 @@ class Config(object):
|
||||
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
|
||||
)
|
||||
anime_provider: "AnimeProvider"
|
||||
user_data = {"recent_anime": [], "animelist": [], "user": {}}
|
||||
user_data = {
|
||||
"recent_anime": [],
|
||||
"animelist": [],
|
||||
"user": {},
|
||||
"meta": {"last_updated": 0},
|
||||
}
|
||||
default_config = {
|
||||
"auto_next": "False",
|
||||
"menu_order": "",
|
||||
"auto_select": "True",
|
||||
"cache_requests": "true",
|
||||
"check_for_updates": "True",
|
||||
@@ -39,6 +45,7 @@ class Config(object):
|
||||
"default_media_list_tracking": "None",
|
||||
"downloads_dir": USER_VIDEOS_DIR,
|
||||
"disable_mpv_popen": "True",
|
||||
"discord": "False",
|
||||
"episode_complete_at": "80",
|
||||
"ffmpegthumbnailer_seek_time": "-1",
|
||||
"force_forward_tracking": "true",
|
||||
@@ -49,8 +56,11 @@ class Config(object):
|
||||
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
||||
"icons": "false",
|
||||
"image_previews": "True" if S_PLATFORM != "win32" else "False",
|
||||
"image_renderer": "icat" if os.environ.get("KITTY_WINDOW_ID") else "chafa",
|
||||
"normalize_titles": "True",
|
||||
"notification_duration": "2",
|
||||
"notification_duration": "120",
|
||||
"max_cache_lifetime": "03:00:00",
|
||||
"per_page": "15",
|
||||
"player": "mpv",
|
||||
"preferred_history": "local",
|
||||
"preferred_language": "english",
|
||||
@@ -75,18 +85,18 @@ class Config(object):
|
||||
"use_rofi": "false",
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, no_config) -> None:
|
||||
self.initialize_user_data_and_watch_history_recent_anime()
|
||||
self.load_config()
|
||||
self.load_config(no_config)
|
||||
|
||||
def load_config(self):
|
||||
def load_config(self, no_config=False):
|
||||
self.configparser = ConfigParser(self.default_config)
|
||||
self.configparser.add_section("stream")
|
||||
self.configparser.add_section("general")
|
||||
self.configparser.add_section("anilist")
|
||||
|
||||
# --- set config values from file or using defaults ---
|
||||
if os.path.exists(USER_CONFIG_PATH):
|
||||
if os.path.exists(USER_CONFIG_PATH) and not no_config:
|
||||
self.configparser.read(USER_CONFIG_PATH, encoding="utf-8")
|
||||
|
||||
# get the configuration
|
||||
@@ -105,6 +115,7 @@ class Config(object):
|
||||
self.disable_mpv_popen = self.configparser.getboolean(
|
||||
"stream", "disable_mpv_popen"
|
||||
)
|
||||
self.discord = self.configparser.getboolean("general", "discord")
|
||||
self.downloads_dir = self.configparser.get("general", "downloads_dir")
|
||||
self.episode_complete_at = self.configparser.getint(
|
||||
"stream", "episode_complete_at"
|
||||
@@ -122,12 +133,23 @@ class Config(object):
|
||||
self.header_ascii_art = self.configparser.get("general", "header_ascii_art")
|
||||
self.icons = self.configparser.getboolean("general", "icons")
|
||||
self.image_previews = self.configparser.getboolean("general", "image_previews")
|
||||
self.image_renderer = self.configparser.get("general", "image_renderer")
|
||||
self.normalize_titles = self.configparser.getboolean(
|
||||
"general", "normalize_titles"
|
||||
)
|
||||
self.notification_duration = self.configparser.getint(
|
||||
"general", "notification_duration"
|
||||
)
|
||||
self._max_cache_lifetime = self.configparser.get(
|
||||
"general", "max_cache_lifetime"
|
||||
)
|
||||
max_cache_lifetime = list(map(int, self._max_cache_lifetime.split(":")))
|
||||
self.max_cache_lifetime = (
|
||||
max_cache_lifetime[0] * 86400
|
||||
+ max_cache_lifetime[1] * 3600
|
||||
+ max_cache_lifetime[2] * 60
|
||||
)
|
||||
self.per_page = self.configparser.get("anilist", "per_page")
|
||||
self.player = self.configparser.get("stream", "player")
|
||||
self.preferred_history = self.configparser.get("stream", "preferred_history")
|
||||
self.preferred_language = self.configparser.get("general", "preferred_language")
|
||||
@@ -148,6 +170,7 @@ class Config(object):
|
||||
self.server = self.configparser.get("stream", "server")
|
||||
self.skip = self.configparser.getboolean("stream", "skip")
|
||||
self.sort_by = self.configparser.get("anilist", "sort_by")
|
||||
self.menu_order = self.configparser.get("general", "menu_order")
|
||||
self.sub_lang = self.configparser.get("general", "sub_lang")
|
||||
self.translation_type = self.configparser.get("stream", "translation_type")
|
||||
self.use_fzf = self.configparser.getboolean("general", "use_fzf")
|
||||
@@ -258,107 +281,117 @@ class Config(object):
|
||||
#
|
||||
[general]
|
||||
# Can you rice it?
|
||||
# for the preview pane
|
||||
# For the preview pane
|
||||
preview_separator_color = {self.preview_separator_color}
|
||||
|
||||
preview_header_color = {self.preview_header_color}
|
||||
|
||||
# for the header
|
||||
# be sure to indent
|
||||
header_ascii_art = {new_line.join([tab+line for line in self.header_ascii_art.split(new_line)])}
|
||||
# For the header
|
||||
# Be sure to indent
|
||||
header_ascii_art = {new_line.join([tab + line for line in self.header_ascii_art.split(new_line)])}
|
||||
|
||||
header_color = {self.header_color}
|
||||
|
||||
# to be passed to fzf
|
||||
# be sure to indent
|
||||
fzf_opts = {new_line.join([tab+line for line in self.fzf_opts.split(new_line)])}
|
||||
# the image renderer to use [icat/chafa]
|
||||
image_renderer = {self.image_renderer}
|
||||
|
||||
# To be passed to fzf
|
||||
# Be sure to indent
|
||||
fzf_opts = {new_line.join([tab + line for line in self.fzf_opts.split(new_line)])}
|
||||
|
||||
# whether to show the icons in the tui [True/False]
|
||||
# more like emojis
|
||||
# by the way if you have any recommendations
|
||||
# to which should be used where please
|
||||
# Whether to show the icons in the TUI [True/False]
|
||||
# More like emojis
|
||||
# By the way, if you have any recommendations
|
||||
# for which should be used where, please
|
||||
# don't hesitate to share your opinion
|
||||
# cause it's a lot of work
|
||||
# because it's a lot of work
|
||||
# to look for the right one for each menu option
|
||||
# be sure to also give the replacement emoji
|
||||
# Be sure to also give the replacement emoji
|
||||
icons = {self.icons}
|
||||
|
||||
# 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
|
||||
# this also applies to episode titles
|
||||
# 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
|
||||
# This also applies to episode titles
|
||||
normalize_titles = {self.normalize_titles}
|
||||
|
||||
# whether to check for updates every time you run the script [True/False]
|
||||
# this is useful for keeping your script up to date
|
||||
# cause there are always new features being added 😄
|
||||
# Whether to check for updates every time you run the script [True/False]
|
||||
# This is useful for keeping your script up to date
|
||||
# because there are always new features being added 😄
|
||||
check_for_updates = {self.check_for_updates}
|
||||
|
||||
# can be [allanime, animepahe, hianime, nyaa, yugen]
|
||||
# allanime is the most realible
|
||||
# animepahe provides different links to streams of different quality so a quality can be selected reliably with --quality option
|
||||
# hianime usually provides subs in different languuages and its servers are generally faster
|
||||
# NOTE: currently they are encrypting the video links
|
||||
# though am working on it
|
||||
# however, you can still get the links to the subs
|
||||
# Can be [allanime, animepahe, hianime, nyaa, yugen]
|
||||
# Allanime is the most reliable
|
||||
# Animepahe provides different links to streams of different quality, so a quality can be selected reliably with the --quality option
|
||||
# Hianime usually provides subs in different languages, and its servers are generally faster
|
||||
# NOTE: Currently, they are encrypting the video links
|
||||
# though I’m working on it
|
||||
# However, you can still get the links to the subs
|
||||
# with ```fastanime grab``` command
|
||||
# yugen meh
|
||||
# nyaa those who prefer torrents, though not reliable due to auto selection of results
|
||||
# as most of the data in nyaa is not structured
|
||||
# though works relatively well for new anime
|
||||
# esp with subsplease and horriblesubs
|
||||
# oh and you should have webtorrent cli to use this
|
||||
# Yugen meh
|
||||
# Nyaa for those who prefer torrents, though not reliable due to auto-selection of results
|
||||
# as most of the data in Nyaa is not structured
|
||||
# though it works relatively well for new anime
|
||||
# especially with SubsPlease and HorribleSubs
|
||||
# Oh, and you should have webtorrent CLI to use this
|
||||
provider = {self.provider}
|
||||
|
||||
# Display language [english, romaji]
|
||||
# this is passed to anilist directly and is used to set the language which the anime titles will be in
|
||||
# when using the anilist interface
|
||||
# This is passed to Anilist directly and is used to set the language for anime titles
|
||||
# when using the Anilist interface
|
||||
preferred_language = {self.preferred_language}
|
||||
|
||||
# Download directory
|
||||
# where you will find your videos after downloading them with 'fastanime download' command
|
||||
# Where you will find your videos after downloading them with 'fastanime download' command
|
||||
downloads_dir = {self.downloads_dir}
|
||||
|
||||
# whether to show a preview window when using fzf or rofi [True/False]
|
||||
# the preview requires you have a commandline image viewer as documented in the README
|
||||
# this is only when using fzf or rofi
|
||||
# if you dont care about image and text previews it doesnt matter
|
||||
# though its awesome
|
||||
# try it and you will see
|
||||
# Whether to show a preview window when using fzf or rofi [True/False]
|
||||
# The preview requires you to have a command-line image viewer as documented in the README
|
||||
# This is only when using fzf or rofi
|
||||
# If you don't care about image and text previews, it doesn’t matter
|
||||
# though it’s awesome
|
||||
# Try it, and you will see
|
||||
preview = {self.preview}
|
||||
|
||||
# whether to show images in the preview [true/false]
|
||||
# windows users just swtich to linux 😄
|
||||
# cause even if you enable it
|
||||
# Whether to show images in the preview [True/False]
|
||||
# Windows users: just switch to Linux 😄
|
||||
# because even if you enable it
|
||||
# it won't look pretty
|
||||
# just be satisfied with the text previews
|
||||
# so forget it exists 🤣
|
||||
# Just be satisfied with the text previews
|
||||
# So forget it exists 🤣
|
||||
image_previews = {self.image_previews}
|
||||
|
||||
# the time to seek when using ffmpegthumbnailer [-1 to 100]
|
||||
# -1 means random and is the default
|
||||
# ffmpegthumbnailer is used to generate previews
|
||||
# and you can select at what time in the video to extract an image
|
||||
# random makes things quite exciting cause you never no at what time it will extract the image from
|
||||
# used by the ```fastanime downloads``` command
|
||||
# ffmpegthumbnailer is used to generate previews,
|
||||
# allowing you to select the time in the video to extract an image.
|
||||
# Random makes things quite exciting because you never know at what time it will extract the image.
|
||||
# Used by the `fastanime downloads` command.
|
||||
ffmpegthumbnailer_seek_time = {self.ffmpegthumbnailer_seek_time}
|
||||
|
||||
# specify the order of menu items in a comma-separated list.
|
||||
# Only include the base names of menu options (e.g., "Trending", "Recent").
|
||||
# The default value is 'Trending,Recent,Watching,Paused,Dropped,Planned,Completed,Rewatching,Recently Updated Anime,Search,Watch History,Random Anime,Most Popular Anime,Most Favourite Anime,Most Scored Anime,Upcoming Anime,Edit Config,Exit'.
|
||||
# Leave blank to use the default menu order.
|
||||
# You can also omit some options by not including them in the list.
|
||||
menu_order = {self.menu_order}
|
||||
|
||||
# whether to use fzf as the interface for the anilist command and others. [True/False]
|
||||
use_fzf = {self.use_fzf}
|
||||
|
||||
# whether to use rofi for the ui [True/False]
|
||||
# it's more useful if you want to create a desktop entry
|
||||
# which can be setup with 'fastanime config --desktop-entry'
|
||||
# though if you want it to be your sole interface even when fastanime is run directly from the terminal
|
||||
# whether to use rofi for the UI [True/False]
|
||||
# It's more useful if you want to create a desktop entry,
|
||||
# which can be set up with 'fastanime config --desktop-entry'.
|
||||
# If you want it to be your sole interface even when fastanime is run directly from the terminal, enable this.
|
||||
use_rofi = {self.use_rofi}
|
||||
|
||||
# rofi themes to use <path>
|
||||
# the values of this option is the path to the rofi config files to use
|
||||
# i choose to split it into 4 since it gives the best look and feel
|
||||
# you can refer to the rofi demo on github to see for your self
|
||||
# i need help designing the default rofi themes
|
||||
# if you fancy yourself a rofi ricer please contribute to making
|
||||
# the default theme better
|
||||
# The value of this option is the path to the rofi config files to use.
|
||||
# I chose to split it into 4 since it gives the best look and feel.
|
||||
# You can refer to the rofi demo on GitHub to see for yourself.
|
||||
# I need help designing the default rofi themes.
|
||||
# If you fancy yourself a rofi ricer, please contribute to improving
|
||||
# the default theme.
|
||||
rofi_theme = {self.rofi_theme}
|
||||
|
||||
rofi_theme_preview = {self.rofi_theme_preview}
|
||||
@@ -367,47 +400,54 @@ rofi_theme_input = {self.rofi_theme_input}
|
||||
|
||||
rofi_theme_confirm = {self.rofi_theme_confirm}
|
||||
|
||||
# the duration in minutes a notification will stay in the screen
|
||||
# used by notifier command
|
||||
# the duration in minutes a notification will stay on the screen.
|
||||
# Used by the notifier command.
|
||||
notification_duration = {self.notification_duration}
|
||||
|
||||
# used when the provider gives subs of different languages
|
||||
# currently its the case for:
|
||||
# hianime
|
||||
# the values for this option are the short names for languages
|
||||
# regex is used to determine what you selected
|
||||
# used when the provider offers subtitles in different languages.
|
||||
# Currently, this is the case for:
|
||||
# hianime.
|
||||
# The values for this option are the short names for languages.
|
||||
# Regex is used to determine what you selected.
|
||||
sub_lang = {self.sub_lang}
|
||||
|
||||
# what is your default media list tracking [track/disabled/prompt]
|
||||
# only affects your anilist anime list
|
||||
# track - means your progress will always be reflected in your anilist anime list
|
||||
# disabled - means progress tracking will no longer be reflected in your anime list
|
||||
# prompt - means for every anime you will be prompted whether you want your progress to be tracked or not
|
||||
# This only affects your anilist anime list.
|
||||
# track - means your progress will always be reflected in your anilist anime list.
|
||||
# disabled - means progress tracking will no longer be reflected in your anime list.
|
||||
# prompt - means you will be prompted for each anime whether you want your progress to be tracked or not.
|
||||
default_media_list_tracking = {self.default_media_list_tracking}
|
||||
|
||||
# whether media list tracking should only be updated when the next episode is greater than the previous
|
||||
# this affects only your anilist anime list
|
||||
# whether media list tracking should only be updated when the next episode is greater than the previous.
|
||||
# This only affects your anilist anime list.
|
||||
force_forward_tracking = {self.force_forward_tracking}
|
||||
|
||||
# whether to cache requests [true/false]
|
||||
# this makes the experience better and more faster
|
||||
# as data need not always be fetched from web server
|
||||
# and instead can be gotten locally
|
||||
# from the cached_requests_db
|
||||
# This improves the experience by making it faster,
|
||||
# as data doesn't always need to be fetched from the web server
|
||||
# and can instead be retrieved locally from the cached_requests_db.
|
||||
cache_requests = {self.cache_requests}
|
||||
|
||||
# whether to use a persistent store (basically a sqlitedb) for storing some data the provider requires
|
||||
# to enable a seamless experience [true/false]
|
||||
# this option exists primarily because i think it may help in the optimization
|
||||
# of fastanime as a library in a website project
|
||||
# for now i don't recommend changing it
|
||||
# leave it as is
|
||||
# the max lifetime for a cached request <days:hours:minutes>
|
||||
# Defaults to 3 days = 03:00:00.
|
||||
# This is the time after which a cached request will be deleted (technically).
|
||||
max_cache_lifetime = {self._max_cache_lifetime}
|
||||
|
||||
# whether to use a persistent store (basically an SQLite DB) for storing some data the provider requires
|
||||
# to enable a seamless experience. [true/false]
|
||||
# This option exists primarily to optimize FastAnime as a library in a website project.
|
||||
# For now, it's not recommended to change it. Leave it as is.
|
||||
use_persistent_provider_store = {self.use_persistent_provider_store}
|
||||
|
||||
# no of recent anime to keep [0-50]
|
||||
# 0 will disable recent anime tracking
|
||||
# number of recent anime to keep [0-50].
|
||||
# 0 will disable recent anime tracking.
|
||||
recent = {self.recent}
|
||||
|
||||
# enable or disable Discord activity updater.
|
||||
# If you want to enable it, please follow the link below to register the app with your Discord account:
|
||||
# https://discord.com/oauth2/authorize?client_id=1292070065583165512
|
||||
discord = {self.discord}
|
||||
|
||||
|
||||
[stream]
|
||||
# the quality of the stream [1080,720,480,360]
|
||||
@@ -440,7 +480,7 @@ translation_type = {self.translation_type}
|
||||
# what server to use for a particular provider
|
||||
# allanime: [dropbox, sharepoint, wetransfer, gogoanime, wixmp]
|
||||
# animepahe: [kwik]
|
||||
# hianime: [HD1, HD2, StreamSB, StreamTape]
|
||||
# hianime: [HD1, HD2, StreamSB, StreamTape] : only HD2 for now
|
||||
# yugen: [gogoanime]
|
||||
# 'top' can also be used as a value for this option
|
||||
# 'top' will cause fastanime to auto select the first server it sees
|
||||
@@ -460,7 +500,7 @@ auto_next = {self.auto_next}
|
||||
# But 99% of the time will be accurate
|
||||
# if this happens just turn off auto_select in the menus or from the commandline
|
||||
# and manually select the correct anime title
|
||||
# edit this file <https://github.com/Benex254/FastAnime/blob/master/fastanime/Utility/data.py>
|
||||
# edit this file <https://github.com/Benexl/FastAnime/blob/master/fastanime/Utility/data.py>
|
||||
# and to the dictionary of the provider
|
||||
# the provider title (key) and their corresponding anilist names (value)
|
||||
# and then please open a pr
|
||||
@@ -530,9 +570,12 @@ format = {self.format}
|
||||
# since you will miss out on some features if you use the others
|
||||
player = {self.player}
|
||||
|
||||
[anilist]
|
||||
per_page = {self.per_page}
|
||||
|
||||
#
|
||||
# HOPE YOU ENJOY FASTANIME AND BE SURE TO STAR THE PROJECT ON GITHUB
|
||||
# https://github.com/Benex254/FastAnime
|
||||
# https://github.com/Benexl/FastAnime
|
||||
#
|
||||
# Also join the discord server
|
||||
# where the anime tech community lives :)
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from click import clear
|
||||
@@ -14,6 +15,7 @@ from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ...anilist import AniList
|
||||
from ...constants import USER_CONFIG_PATH
|
||||
from ...libs.discord import discord
|
||||
from ...libs.fzf import fzf
|
||||
from ...libs.rofi import Rofi
|
||||
from ...Utility.data import anime_normalizer
|
||||
@@ -45,12 +47,17 @@ def calculate_percentage_completion(start_time, end_time):
|
||||
[TODO:return]
|
||||
"""
|
||||
|
||||
start = start_time.split(":")
|
||||
end = end_time.split(":")
|
||||
start_secs = int(start[0]) * 3600 + int(start[1]) * 60 + int(start[2])
|
||||
end_secs = int(end[0]) * 3600 + int(end[1]) * 60 + int(end[2])
|
||||
return start_secs / end_secs * 100
|
||||
try:
|
||||
start = start_time.split(":")
|
||||
end = end_time.split(":")
|
||||
start_secs = int(start[0]) * 3600 + int(start[1]) * 60 + int(start[2])
|
||||
end_secs = int(end[0]) * 3600 + int(end[1]) * 60 + int(end[2])
|
||||
return start_secs / end_secs * 100
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def discord_updater(show,episode,switch):
|
||||
discord.discord_connect(show,episode,switch)
|
||||
|
||||
def media_player_controls(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
@@ -507,6 +514,12 @@ def provider_anime_episode_servers_menu(
|
||||
"[bold magenta] Episode: [/]",
|
||||
current_episode_number,
|
||||
)
|
||||
# update discord activity for user
|
||||
switch = threading.Event()
|
||||
if config.discord:
|
||||
discord_proc = threading.Thread(target=discord_updater, args=(provider_anime_title,current_episode_number,switch))
|
||||
discord_proc.start()
|
||||
|
||||
# try to get the timestamp you left off from if available
|
||||
start_time = config.watch_history.get(str(anime_id_anilist), {}).get(
|
||||
"episode_stopped_at", "0"
|
||||
@@ -589,6 +602,10 @@ def provider_anime_episode_servers_menu(
|
||||
)
|
||||
print("Finished at: ", stop_time)
|
||||
|
||||
# stop discord activity updater
|
||||
if config.discord:
|
||||
switch.set()
|
||||
|
||||
# update_watch_history
|
||||
# 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
|
||||
@@ -701,7 +718,7 @@ def provider_anime_episodes_menu(
|
||||
total_time = user_watch_history.get(str(anime_id_anilist), {}).get(
|
||||
"episode_total_length", "0"
|
||||
)
|
||||
if stop_time != "0" or total_time != "0":
|
||||
if stop_time != "0" and total_time != "0":
|
||||
percentage_completion_of_episode = calculate_percentage_completion(
|
||||
stop_time, total_time
|
||||
)
|
||||
@@ -747,18 +764,22 @@ def provider_anime_episodes_menu(
|
||||
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
|
||||
|
||||
e = fastanime_runtime_state.selected_anime_anilist["episodes"]
|
||||
if e:
|
||||
eps = range(0, e + 1)
|
||||
else:
|
||||
eps = available_episodes
|
||||
preview = get_fzf_episode_preview(
|
||||
fastanime_runtime_state.selected_anime_anilist, eps
|
||||
)
|
||||
if config.use_fzf:
|
||||
if config.preview:
|
||||
from .utils import get_fzf_episode_preview
|
||||
|
||||
e = fastanime_runtime_state.selected_anime_anilist["episodes"]
|
||||
if e:
|
||||
eps = range(0, e + 1)
|
||||
else:
|
||||
eps = available_episodes
|
||||
preview = get_fzf_episode_preview(
|
||||
fastanime_runtime_state.selected_anime_anilist, eps
|
||||
)
|
||||
|
||||
if not preview:
|
||||
print("Failed to find bash executable which is necessary for preview with fzf.\nIf you are on Windows, please make sure Git is installed and available in PATH.")
|
||||
|
||||
current_episode_number = fzf.run(
|
||||
choices, prompt="Select Episode", header=anime_title, preview=preview
|
||||
)
|
||||
@@ -1340,6 +1361,58 @@ def media_actions_menu(
|
||||
set_prefered_progress_tracking(config, fastanime_runtime_state, update=True)
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
|
||||
def _relations(config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"):
|
||||
"""Helper function to get anime recommendations
|
||||
Args:
|
||||
config: [TODO:description]
|
||||
fastanime_runtime_state: [TODO:description]
|
||||
"""
|
||||
relations = AniList.get_related_anime_for(
|
||||
fastanime_runtime_state.selected_anime_id_anilist
|
||||
)
|
||||
if not relations[0]:
|
||||
print("No recommendations found", relations[1])
|
||||
input("Enter to continue...")
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
|
||||
relations = relations[1]["data"]["Page"]["relations"] # pyright:ignore
|
||||
fastanime_runtime_state.anilist_results_data = {
|
||||
"data": {"Page": {"media": relations["nodes"]}} # pyright:ignore
|
||||
}
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
def _recommendations(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
):
|
||||
"""Helper function to get anime recommendations
|
||||
Args:
|
||||
config: [TODO:description]
|
||||
fastanime_runtime_state: [TODO:description]
|
||||
"""
|
||||
recommendations = AniList.get_recommended_anime_for(
|
||||
fastanime_runtime_state.selected_anime_id_anilist
|
||||
)
|
||||
if not recommendations[0]:
|
||||
print("No recommendations found", recommendations[1])
|
||||
input("Enter to continue...")
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
|
||||
fastanime_runtime_state.anilist_results_data = {
|
||||
"data": {
|
||||
"Page": {
|
||||
"media": [
|
||||
media["media"]
|
||||
for media in recommendations[1]["data"]["Page"][
|
||||
"recommendations" # pyright:ignore
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
icons = config.icons
|
||||
options = {
|
||||
f"{'📽️ ' if icons else ''}Stream ({progress}/{episodes_total})": _stream_anime,
|
||||
@@ -1349,6 +1422,8 @@ def media_actions_menu(
|
||||
f"{'✨ ' if icons else ''}Progress Tracking": _set_progress_tracking,
|
||||
f"{'📥 ' if icons else ''}Add to List": _add_to_list,
|
||||
f"{'📤 ' if icons else ''}Remove from List": _remove_from_list,
|
||||
f"{'📖 ' if icons else ''}Recommendations": _recommendations,
|
||||
f"{'📖 ' if icons else ''}Relations": _relations,
|
||||
f"{'📖 ' if icons else ''}View Info": _view_info,
|
||||
f"{'🎧 ' if icons else ''}Change Translation Type": _change_translation_type,
|
||||
f"{'💽 ' if icons else ''}Change Provider": _change_provider,
|
||||
@@ -1421,12 +1496,15 @@ def anilist_results_menu(
|
||||
anime_data[title] = anime
|
||||
|
||||
# prompt for the anime of choice
|
||||
choices = [*anime_data.keys(), "Back"]
|
||||
choices = [*anime_data.keys(), "Next Page", "Previous Page", "Back"]
|
||||
if config.use_fzf:
|
||||
if config.preview:
|
||||
from .utils import get_fzf_anime_preview
|
||||
|
||||
preview = get_fzf_anime_preview(search_results, anime_data.keys())
|
||||
if not preview:
|
||||
print("Failed to find bash executable which is necessary for preview with fzf.\nIf you are on Windows, please make sure Git is installed and available in PATH.")
|
||||
|
||||
selected_anime_title = fzf.run(
|
||||
choices,
|
||||
prompt="Select Anime",
|
||||
@@ -1460,6 +1538,43 @@ def anilist_results_menu(
|
||||
if selected_anime_title == "Back":
|
||||
fastanime_main_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
if selected_anime_title == "Next Page":
|
||||
fastanime_runtime_state.current_page = page = (
|
||||
fastanime_runtime_state.current_page + 1
|
||||
)
|
||||
success, data = fastanime_runtime_state.current_data_loader(
|
||||
config=config, page=page
|
||||
)
|
||||
if success:
|
||||
fastanime_runtime_state.anilist_results_data = data
|
||||
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
print("Failed to get next page")
|
||||
print(data)
|
||||
input("Enter to continue...")
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
|
||||
return
|
||||
if selected_anime_title == "Previous Page":
|
||||
fastanime_runtime_state.current_page = page = (
|
||||
(fastanime_runtime_state.current_page - 1)
|
||||
if fastanime_runtime_state.current_page > 1
|
||||
else 1
|
||||
)
|
||||
success, data = fastanime_runtime_state.current_data_loader(
|
||||
config=config, page=page
|
||||
)
|
||||
if success:
|
||||
fastanime_runtime_state.anilist_results_data = data
|
||||
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
print("Failed to get previous page")
|
||||
print(data)
|
||||
input("Enter to continue...")
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
|
||||
selected_anime: "AnilistBaseMediaDataSchema" = anime_data[selected_anime_title]
|
||||
fastanime_runtime_state.selected_anime_anilist = selected_anime
|
||||
@@ -1474,8 +1589,11 @@ def anilist_results_menu(
|
||||
#
|
||||
# ---- FASTANIME MAIN MENU ----
|
||||
#
|
||||
def handle_animelist(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState", list_type: str
|
||||
def _handle_animelist(
|
||||
config: "Config",
|
||||
fastanime_runtime_state: "FastAnimeRuntimeState",
|
||||
list_type: str,
|
||||
page=1,
|
||||
):
|
||||
"""A helper function that handles user media lists
|
||||
|
||||
@@ -1508,13 +1626,13 @@ def handle_animelist(
|
||||
status = "DROPPED"
|
||||
case "Paused":
|
||||
status = "PAUSED"
|
||||
case "Repeating":
|
||||
case "Rewatching":
|
||||
status = "REPEATING"
|
||||
case _:
|
||||
return
|
||||
|
||||
# get the media list
|
||||
anime_list = AniList.get_anime_list(status)
|
||||
anime_list = AniList.get_anime_list(status, page=page)
|
||||
# handle null
|
||||
if not anime_list:
|
||||
print("Sth went wrong", anime_list)
|
||||
@@ -1545,6 +1663,56 @@ def handle_animelist(
|
||||
return anime_list
|
||||
|
||||
|
||||
def _anilist_search(config: "Config", page=1):
|
||||
"""A function that enables seaching of an anime
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
# TODO: Add filters and other search features
|
||||
if config.use_rofi:
|
||||
search_term = str(Rofi.ask("Search for"))
|
||||
else:
|
||||
search_term = Prompt.ask("[cyan]Search for[/]")
|
||||
|
||||
return AniList.search(query=search_term, page=page)
|
||||
|
||||
|
||||
def _anilist_random(config: "Config", page=1):
|
||||
"""A function that generates random anilist ids enabling random discovery of anime
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
random_anime = range(1, 15000)
|
||||
random_anime = random.sample(random_anime, k=50)
|
||||
|
||||
return AniList.search(id_in=list(random_anime))
|
||||
|
||||
|
||||
def _watch_history(config: "Config", page=1):
|
||||
"""Function that lets you see all the anime that has locally been saved to your watch history
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
watch_history = list(map(int, config.watch_history.keys()))
|
||||
return AniList.search(id_in=watch_history, sort="TRENDING_DESC", page=page)
|
||||
|
||||
|
||||
def _recent(config: "Config", page=1):
|
||||
return (
|
||||
True,
|
||||
{"data": {"Page": {"media": config.user_data["recent_anime"]}}},
|
||||
)
|
||||
|
||||
|
||||
# WARNING: Will probably be depracated
|
||||
def _anime_list(config: "Config", page=1):
|
||||
anime_list = config.anime_list
|
||||
return AniList.search(id_in=anime_list, pages=page)
|
||||
|
||||
|
||||
def fastanime_main_menu(
|
||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||
):
|
||||
@@ -1555,52 +1723,7 @@ def fastanime_main_menu(
|
||||
fastanime_runtime_state: A query dict used to store data during navigation of the ui # initially this was very messy
|
||||
"""
|
||||
|
||||
def _anilist_search():
|
||||
"""A function that enables seaching of an anime
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
# TODO: Add filters and other search features
|
||||
if config.use_rofi:
|
||||
search_term = str(Rofi.ask("Search for"))
|
||||
else:
|
||||
search_term = Prompt.ask("[cyan]Search for[/]")
|
||||
|
||||
return AniList.search(query=search_term)
|
||||
|
||||
def _anilist_random():
|
||||
"""A function that generates random anilist ids enabling random discovery of anime
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
random_anime = range(1, 15000)
|
||||
random_anime = random.sample(random_anime, k=50)
|
||||
|
||||
return AniList.search(id_in=list(random_anime))
|
||||
|
||||
def _watch_history():
|
||||
"""Function that lets you see all the anime that has locally been saved to your watch history
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
watch_history = list(map(int, config.watch_history.keys()))
|
||||
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
|
||||
def _anime_list():
|
||||
anime_list = config.anime_list
|
||||
return AniList.search(id_in=anime_list)
|
||||
|
||||
def _edit_config():
|
||||
def _edit_config(*args, **kwargs):
|
||||
"""Helper function to edit your config when the ui is still running"""
|
||||
|
||||
from click import edit
|
||||
@@ -1625,23 +1748,23 @@ def fastanime_main_menu(
|
||||
options = {
|
||||
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(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'📺 ' if icons else ''}Watching": lambda config, media_list_type="Watching", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'⏸ ' if icons else ''}Paused": lambda media_list_type="Paused": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'⏸ ' if icons else ''}Paused": lambda config, media_list_type="Paused", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'🚮 ' if icons else ''}Dropped": lambda media_list_type="Dropped": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'🚮 ' if icons else ''}Dropped": lambda config, media_list_type="Dropped", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'📑 ' if icons else ''}Planned": lambda media_list_type="Planned": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'📑 ' if icons else ''}Planned": lambda config, media_list_type="Planned", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'✅ ' if icons else ''}Completed": lambda media_list_type="Completed": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'✅ ' if icons else ''}Completed": lambda config, media_list_type="Completed", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'🔁 ' if icons else ''}Rewatching": lambda media_list_type="Repeating": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
f"{'🔁 ' if icons else ''}Rewatching": lambda config, media_list_type="Rewatching", page=1: _handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type, page=page
|
||||
),
|
||||
f"{'🔔 ' if icons else ''}Recently Updated Anime": AniList.get_most_recently_updated,
|
||||
f"{'🔎 ' if icons else ''}Search": _anilist_search,
|
||||
@@ -1655,6 +1778,18 @@ def fastanime_main_menu(
|
||||
f"{'📝 ' if icons else ''}Edit Config": _edit_config,
|
||||
f"{'❌ ' if icons else ''}Exit": exit_app,
|
||||
}
|
||||
|
||||
# Load main menu order if set in config file
|
||||
if config.menu_order:
|
||||
menu_order_list = config.menu_order.split(",")
|
||||
lookup = {key.split(" ", 1)[-1]: key for key in options}
|
||||
ordered_dict = {
|
||||
lookup[key]: options[lookup[key]]
|
||||
for key in menu_order_list
|
||||
if key in lookup
|
||||
}
|
||||
options = ordered_dict
|
||||
|
||||
# prompt user to select an action
|
||||
choices = list(options.keys())
|
||||
if config.use_fzf:
|
||||
@@ -1670,7 +1805,9 @@ def fastanime_main_menu(
|
||||
choices,
|
||||
"Select Action",
|
||||
)
|
||||
anilist_data = options[action]()
|
||||
fastanime_runtime_state.current_data_loader = options[action]
|
||||
fastanime_runtime_state.current_page = 1
|
||||
anilist_data = options[action](config=config)
|
||||
# anilist data is a (bool,data)
|
||||
# the bool indicated success
|
||||
if anilist_data[0]:
|
||||
|
||||
@@ -5,15 +5,15 @@ import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
from threading import Thread
|
||||
from hashlib import sha256
|
||||
|
||||
import requests
|
||||
from yt_dlp.utils import clean_html, sanitize_filename
|
||||
|
||||
from yt_dlp.utils import clean_html
|
||||
from ...constants import APP_CACHE_DIR, S_PLATFORM
|
||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from ...Utility import anilist_data_helper
|
||||
from ..utils.scripts import fzf_preview
|
||||
from ..utils.utils import get_true_fg
|
||||
from ..utils.scripts import bash_functions
|
||||
from ..utils.utils import get_true_fg, which_bashlike
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,7 +69,12 @@ def save_image_from_url(url: str, file_name: str):
|
||||
file_name: filename to use
|
||||
"""
|
||||
image = requests.get(url)
|
||||
with open(os.path.join(IMAGES_CACHE_DIR, f"{file_name}.png"), "wb") as f:
|
||||
with open(
|
||||
os.path.join(
|
||||
IMAGES_CACHE_DIR, f"{sha256(file_name.encode('utf-8')).hexdigest()}.png"
|
||||
),
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(image.content)
|
||||
|
||||
|
||||
@@ -83,7 +88,7 @@ def save_info_from_str(info: str, file_name: str):
|
||||
with open(
|
||||
os.path.join(
|
||||
ANIME_INFO_CACHE_DIR,
|
||||
file_name,
|
||||
sha256(file_name.encode("utf-8")).hexdigest(),
|
||||
),
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
@@ -96,7 +101,7 @@ def write_search_results(
|
||||
titles: list[str],
|
||||
workers: int | None = None,
|
||||
):
|
||||
"""A helper function used by and run in a background thread by get_fzf_preview function inorder to get the actual preview data to be displayed by fzf
|
||||
"""A helper function used by and run in a background thread by get_fzf_preview function in order to get the actual preview data to be displayed by fzf
|
||||
|
||||
Args:
|
||||
anilist_results: the anilist results from an anilist action
|
||||
@@ -108,11 +113,21 @@ def write_search_results(
|
||||
future_to_task = {}
|
||||
for anime, title in zip(anilist_results, titles):
|
||||
# actual image url
|
||||
image_url = ""
|
||||
if os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower() == "true":
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_task[
|
||||
executor.submit(save_image_from_url, image_url, title)
|
||||
] = image_url
|
||||
|
||||
if not (
|
||||
os.path.exists(
|
||||
os.path.join(
|
||||
IMAGES_CACHE_DIR,
|
||||
f"{sha256(title.encode('utf-8')).hexdigest()}.png",
|
||||
)
|
||||
)
|
||||
):
|
||||
future_to_task[
|
||||
executor.submit(save_image_from_url, image_url, title)
|
||||
] = image_url
|
||||
|
||||
mediaListName = "Not in any of your lists"
|
||||
progress = "UNKNOWN"
|
||||
@@ -121,57 +136,55 @@ def write_search_results(
|
||||
progress = anime_list["progress"]
|
||||
# handle the text data
|
||||
template = f"""
|
||||
image_url={image_url}
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
echo "{get_true_fg('Title(jp):',*HEADER_COLOR)} {(anime['title']['romaji'] or "").replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Title(eng):',*HEADER_COLOR)} {(anime['title']['english'] or "").replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg("Title(jp):", *HEADER_COLOR)} {(anime["title"]["romaji"] or "").replace('"', SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg("Title(eng):", *HEADER_COLOR)} {(anime["title"]["english"] or "").replace('"', SINGLE_QUOTE)}"
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
echo "{get_true_fg('Popularity:',*HEADER_COLOR)} {anilist_data_helper.format_number_with_commas(anime['popularity'])}"
|
||||
echo "{get_true_fg('Favourites:',*HEADER_COLOR)} {anilist_data_helper.format_number_with_commas(anime['favourites'])}"
|
||||
echo "{get_true_fg('Status:',*HEADER_COLOR)} {str(anime['status']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Next Episode:',*HEADER_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Genres:',*HEADER_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime['genres']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg("Popularity:", *HEADER_COLOR)} {anilist_data_helper.format_number_with_commas(anime["popularity"])}"
|
||||
echo "{get_true_fg("Favourites:", *HEADER_COLOR)} {anilist_data_helper.format_number_with_commas(anime["favourites"])}"
|
||||
echo "{get_true_fg("Status:", *HEADER_COLOR)} {str(anime["status"]).replace('"', SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg("Next Episode:", *HEADER_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime["nextAiringEpisode"]).replace('"', SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg("Genres:", *HEADER_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime["genres"]).replace('"', SINGLE_QUOTE)}"
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
echo "{get_true_fg('Episodes:',*HEADER_COLOR)} {(anime['episodes']) or 'UNKNOWN'}"
|
||||
echo "{get_true_fg('Start Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('End Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg("Episodes:", *HEADER_COLOR)} {(anime["episodes"]) or "UNKNOWN"}"
|
||||
echo "{get_true_fg("Start Date:", *HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime["startDate"]).replace('"', SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg("End Date:", *HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime["endDate"]).replace('"', SINGLE_QUOTE)}"
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
echo "{get_true_fg('Media List:',*HEADER_COLOR)} {mediaListName.replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Progress:',*HEADER_COLOR)} {progress}"
|
||||
echo "{get_true_fg("Media List:", *HEADER_COLOR)} {mediaListName.replace('"', SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg("Progress:", *HEADER_COLOR)} {progress}"
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
# echo "{get_true_fg('Description:',*HEADER_COLOR).replace('"',SINGLE_QUOTE)}"
|
||||
# echo "{get_true_fg("Description:", *HEADER_COLOR).replace('"', SINGLE_QUOTE)}"
|
||||
"""
|
||||
template = textwrap.dedent(template)
|
||||
template = f"""
|
||||
{template}
|
||||
echo "
|
||||
{textwrap.fill(clean_html(
|
||||
(anime['description']) or "").replace('"',SINGLE_QUOTE), width=45)}
|
||||
"
|
||||
echo "{textwrap.fill(clean_html((anime["description"]) or "").replace('"', SINGLE_QUOTE), width=45)}"
|
||||
"""
|
||||
future_to_task[executor.submit(save_info_from_str, template, title)] = title
|
||||
|
||||
@@ -202,9 +215,18 @@ def get_rofi_icons(
|
||||
for anime, title in zip(anilist_results, titles):
|
||||
# actual link to download image from
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_url[executor.submit(save_image_from_url, image_url, title)] = (
|
||||
image_url
|
||||
)
|
||||
|
||||
if not (
|
||||
os.path.exists(
|
||||
os.path.join(
|
||||
IMAGES_CACHE_DIR,
|
||||
f"{sha256(title.encode('utf-8')).hexdigest()}.png",
|
||||
)
|
||||
)
|
||||
):
|
||||
future_to_url[
|
||||
executor.submit(save_image_from_url, image_url, title)
|
||||
] = image_url
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
@@ -232,13 +254,22 @@ def get_fzf_manga_preview(manga_results, workers=None, wait=False):
|
||||
future_to_url = {}
|
||||
for manga in manga_results:
|
||||
image_url = manga["poster"]
|
||||
future_to_url[
|
||||
executor.submit(
|
||||
save_image_from_url,
|
||||
image_url,
|
||||
sanitize_filename(manga["title"]),
|
||||
|
||||
if not (
|
||||
os.path.exists(
|
||||
os.path.join(
|
||||
IMAGES_CACHE_DIR,
|
||||
f"{sha256(manga['title'].encode('utf-8')).hexdigest()}.png",
|
||||
)
|
||||
)
|
||||
] = image_url
|
||||
):
|
||||
future_to_url[
|
||||
executor.submit(
|
||||
save_image_from_url,
|
||||
image_url,
|
||||
manga["title"],
|
||||
)
|
||||
] = image_url
|
||||
|
||||
# execute the jobs
|
||||
for future in concurrent.futures.as_completed(future_to_url):
|
||||
@@ -259,11 +290,13 @@ def get_fzf_manga_preview(manga_results, workers=None, wait=False):
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||
title="$(echo -n {})"
|
||||
title="$(echo -n "$title" |generate_sha256)"
|
||||
if [ -s "%s/$title" ]; then fzf_preview "%s/title"
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
bash_functions,
|
||||
IMAGES_CACHE_DIR,
|
||||
IMAGES_CACHE_DIR,
|
||||
)
|
||||
@@ -282,6 +315,9 @@ def get_fzf_episode_preview(
|
||||
titles (list[str]): sanitized titles of the anime; NOTE: its important that they are sanitized since they are used as the filenames of the images
|
||||
workers ([TODO:parameter]): Number of threads to use to download the images; defaults to as many as possible
|
||||
anilist_results: the anilist results from an anilist action
|
||||
|
||||
Returns:
|
||||
The fzf preview script to use or None if the bash is not found
|
||||
"""
|
||||
|
||||
# HEADER_COLOR = 215, 0, 95
|
||||
@@ -303,27 +339,29 @@ def get_fzf_episode_preview(
|
||||
|
||||
if episode_title and image_url:
|
||||
future_to_url[
|
||||
executor.submit(save_image_from_url, image_url, episode)
|
||||
executor.submit(save_image_from_url, image_url, str(episode))
|
||||
] = image_url
|
||||
template = textwrap.dedent(
|
||||
f"""
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo "{get_true_fg('Anime Title(eng):',*HEADER_COLOR)} {('' or anilist_result['title']['english']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Anime Title(jp):',*HEADER_COLOR)} {(anilist_result['title']['romaji'] or '').replace('"',SINGLE_QUOTE)}"
|
||||
echo
|
||||
echo "{get_true_fg("Anime Title(eng):", *HEADER_COLOR)} {("" or anilist_result["title"]["english"]).replace('"', SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg("Anime Title(jp):", *HEADER_COLOR)} {(anilist_result["title"]["romaji"] or "").replace('"', SINGLE_QUOTE)}"
|
||||
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo "{str(episode_title).replace('"',SINGLE_QUOTE)}"
|
||||
echo
|
||||
echo "{str(episode_title).replace('"', SINGLE_QUOTE)}"
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
"""
|
||||
@@ -348,22 +386,26 @@ def get_fzf_episode_preview(
|
||||
background_worker.start()
|
||||
|
||||
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
bash_path = which_bashlike()
|
||||
if not bash_path:
|
||||
return
|
||||
|
||||
os.environ["SHELL"] = bash_path
|
||||
if S_PLATFORM == "win32":
|
||||
preview = """
|
||||
%s
|
||||
title={}
|
||||
show_image_previews="%s"
|
||||
title="$(echo -n {})"
|
||||
title="$(echo -n "$title" |generate_sha256)"
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [ $show_image_previews = "true" ];then
|
||||
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||
if [ "$FASTANIME_IMAGE_PREVIEWS" = "True" ];then
|
||||
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||
if command -v "chafa">/dev/null;then
|
||||
chafa -s $dim "%s\\\\\\${title}.png"
|
||||
else
|
||||
echo please install chafa to enjoy image previews
|
||||
fi
|
||||
echo
|
||||
else
|
||||
echo
|
||||
else
|
||||
echo Loading...
|
||||
fi
|
||||
fi
|
||||
@@ -371,8 +413,7 @@ def get_fzf_episode_preview(
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
||||
bash_functions,
|
||||
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||
@@ -380,11 +421,11 @@ def get_fzf_episode_preview(
|
||||
)
|
||||
else:
|
||||
preview = """
|
||||
title={}
|
||||
%s
|
||||
show_image_previews="%s"
|
||||
if [ $show_image_previews = "true" ];then
|
||||
if [ -s %s/${title}.png ]; then fzf-preview %s/${title}.png
|
||||
title="$(echo -n {})"
|
||||
title="$(echo -n "$title" |generate_sha256)"
|
||||
if [ "$FASTANIME_IMAGE_PREVIEWS" = "True" ];then
|
||||
if [ -s %s/${title}.png ]; then fzf_preview %s/${title}.png
|
||||
else echo Loading...
|
||||
fi
|
||||
fi
|
||||
@@ -392,8 +433,7 @@ def get_fzf_episode_preview(
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
||||
bash_functions,
|
||||
IMAGES_CACHE_DIR,
|
||||
IMAGES_CACHE_DIR,
|
||||
ANIME_INFO_CACHE_DIR,
|
||||
@@ -415,7 +455,7 @@ def get_fzf_anime_preview(
|
||||
anilist_results: the anilist results got from an anilist action
|
||||
|
||||
Returns:
|
||||
THe fzf preview script to use
|
||||
The fzf preview script to use or None if the bash is not found
|
||||
"""
|
||||
# ensure images and info exists
|
||||
|
||||
@@ -426,22 +466,27 @@ def get_fzf_anime_preview(
|
||||
background_worker.start()
|
||||
|
||||
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
bash_path = which_bashlike()
|
||||
if not bash_path:
|
||||
return
|
||||
|
||||
os.environ["SHELL"] = bash_path
|
||||
|
||||
if S_PLATFORM == "win32":
|
||||
preview = """
|
||||
%s
|
||||
title={}
|
||||
show_image_previews="%s"
|
||||
title="$(echo -n {})"
|
||||
title="$(echo -n "$title" |generate_sha256)"
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [ $show_image_previews = "true" ];then
|
||||
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||
if [ "$FASTANIME_IMAGE_PREVIEWS" = "True" ];then
|
||||
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||
if command -v "chafa">/dev/null;then
|
||||
chafa -s $dim "%s\\\\\\${title}.png"
|
||||
else
|
||||
echo please install chafa to enjoy image previews
|
||||
fi
|
||||
echo
|
||||
else
|
||||
echo
|
||||
else
|
||||
echo Loading...
|
||||
fi
|
||||
fi
|
||||
@@ -449,8 +494,7 @@ def get_fzf_anime_preview(
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
||||
bash_functions,
|
||||
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||
@@ -459,10 +503,10 @@ def get_fzf_anime_preview(
|
||||
else:
|
||||
preview = """
|
||||
%s
|
||||
title={}
|
||||
show_image_previews="%s"
|
||||
if [ $show_image_previews = "true" ];then
|
||||
if [ -s "%s/${title}.png" ]; then fzf-preview "%s/${title}.png"
|
||||
title="$(echo -n {})"
|
||||
title="$(echo -n "$title" |generate_sha256)"
|
||||
if [ "$FASTANIME_IMAGE_PREVIEWS" = "True" ];then
|
||||
if [ -s "%s/${title}.png" ]; then fzf_preview "%s/${title}.png"
|
||||
else echo Loading...
|
||||
fi
|
||||
fi
|
||||
@@ -470,8 +514,7 @@ def get_fzf_anime_preview(
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
||||
bash_functions,
|
||||
IMAGES_CACHE_DIR,
|
||||
IMAGES_CACHE_DIR,
|
||||
ANIME_INFO_CACHE_DIR,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import re
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import logging
|
||||
import time
|
||||
|
||||
from ...constants import S_PLATFORM
|
||||
|
||||
@@ -5,7 +5,7 @@ import requests
|
||||
|
||||
|
||||
def print_img(url: str):
|
||||
"""helper funtion to print an image given its url
|
||||
"""helper function to print an image given its url
|
||||
|
||||
Args:
|
||||
url: [TODO:description]
|
||||
@@ -25,7 +25,7 @@ def print_img(url: str):
|
||||
return
|
||||
img_bytes = res.content
|
||||
"""
|
||||
Change made in call to chafa. Chafa dev dropped abilty
|
||||
Change made in call to chafa. Chafa dev dropped ability
|
||||
to pull from urls. Keeping old line here just in case.
|
||||
|
||||
subprocess.run([EXECUTABLE, url, "--size=15x15"], input=img_bytes)
|
||||
|
||||
@@ -1,53 +1,68 @@
|
||||
fzf_preview = r"""
|
||||
#
|
||||
# Adapted from the preview script in the fzf repo
|
||||
#
|
||||
# Dependencies:
|
||||
# - https://github.com/hpjansson/chafa
|
||||
# - https://iterm2.com/utilities/imgcat
|
||||
#
|
||||
fzf-preview() {
|
||||
file=${1/#\~\//$HOME/}
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [[ $dim = x ]]; then
|
||||
dim=$(stty size </dev/tty | awk '{print $2 "x" $1}')
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && ((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size </dev/tty | awk '{print $1}'))); then
|
||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||
# * https://github.com/junegunn/fzf/issues/2544
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
bash_functions = r"""
|
||||
generate_sha256() {
|
||||
local input
|
||||
|
||||
# 1. Use kitty icat on kitty terminal
|
||||
if [[ $KITTY_WINDOW_ID ]]; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
# Check if input is passed as an argument or piped
|
||||
if [ -n "$1" ]; then
|
||||
input="$1"
|
||||
else
|
||||
input=$(cat)
|
||||
fi
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa >/dev/null; then
|
||||
case "$(uname -a)" in
|
||||
# termux does not support sixel graphics
|
||||
# and produces weird output
|
||||
*ndroid*) chafa -s "$dim" "$file";;
|
||||
*) chafa -f sixel -s "$dim" "$file";;
|
||||
esac
|
||||
# Add a new line character so that fzf can display multiple images in the preview window
|
||||
echo
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
echo -n "$input" | sha256sum | awk '{print $1}'
|
||||
elif command -v shasum &>/dev/null; then
|
||||
echo -n "$input" | shasum -a 256 | awk '{print $1}'
|
||||
elif command -v sha256 &>/dev/null; then
|
||||
echo -n "$input" | sha256 | awk '{print $1}'
|
||||
elif command -v openssl &>/dev/null; then
|
||||
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
|
||||
else
|
||||
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
|
||||
fi
|
||||
}
|
||||
fzf_preview() {
|
||||
file=$1
|
||||
|
||||
# 3. If chafa is not found but imgcat is available, use it on iTerm2
|
||||
elif command -v imgcat >/dev/null; then
|
||||
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
|
||||
# user is running iTerm2. But for the sake of simplicity, we just assume
|
||||
# that's the case here.
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [ "$dim" = x ]; then
|
||||
dim=$(stty size </dev/tty | awk "{print \$2 \"x\" \$1}")
|
||||
fi
|
||||
if ! [ "$FASTANIME_IMAGE_RENDERER" = "icat" ] && [ -z "$KITTY_WINDOW_ID" ] && [ "$((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES))" -eq "$(stty size </dev/tty | awk "{print \$1}")" ]; then
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 4. Cannot find any suitable method to preview the image
|
||||
else
|
||||
echo install chafa or imgcat or install kitty terminal so you can enjoy image previews
|
||||
fi
|
||||
if [ "$FASTANIME_IMAGE_RENDERER" = "icat" ] && [ -z "$GHOSTTY_BIN_DIR" ]; then
|
||||
if command -v kitten >/dev/null 2>&1; then
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
elif command -v icat >/dev/null 2>&1; then
|
||||
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
else
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
fi
|
||||
|
||||
elif [ -n "$GHOSTTY_BIN_DIR" ]; then
|
||||
if command -v kitten >/dev/null 2>&1; then
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
elif command -v icat >/dev/null 2>&1; then
|
||||
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
else
|
||||
chafa -s "$dim" "$file"
|
||||
fi
|
||||
elif command -v chafa >/dev/null 2>&1; then
|
||||
case "$PLATFORM" in
|
||||
android) chafa -s "$dim" "$file" ;;
|
||||
windows) chafa -f sixel -s "$dim" "$file" ;;
|
||||
*) chafa -s "$dim" "$file" ;;
|
||||
esac
|
||||
echo
|
||||
|
||||
elif command -v imgcat >/dev/null; then
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
|
||||
else
|
||||
echo please install a terminal image viewer
|
||||
echo either icat for kitty terminal and wezterm or imgcat or chafa
|
||||
fi
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from ...libs.anime_provider.types import Anime, EpisodeStream, SearchResult, Server
|
||||
@@ -26,9 +26,11 @@ class FastAnimeRuntimeState(object):
|
||||
selected_anime_title_anilist: str
|
||||
# current_anilist_data: "AnilistDataSchema | AnilistMediaList"
|
||||
anilist_results_data: "Any"
|
||||
current_page: int
|
||||
current_data_loader: "Callable"
|
||||
|
||||
|
||||
def exit_app(exit_code=0, *args):
|
||||
def exit_app(exit_code=0, *args, **kwargs):
|
||||
import sys
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import logging
|
||||
import shutil
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from InquirerPy import inquirer
|
||||
|
||||
from fastanime.constants import S_PLATFORM
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
from ...libs.anime_provider.types import EpisodeStream
|
||||
@@ -92,7 +95,7 @@ def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]", default
|
||||
|
||||
|
||||
def format_bytes_to_human(num_of_bytes: float, suffix: str = "B"):
|
||||
"""Helper function usedd to format bytes to human
|
||||
"""Helper function used to format bytes to human
|
||||
|
||||
Args:
|
||||
num_of_bytes: the number of bytes to format
|
||||
@@ -155,3 +158,41 @@ def fuzzy_inquirer(choices: list, prompt: str, **kwargs):
|
||||
**kwargs,
|
||||
).execute()
|
||||
return action
|
||||
|
||||
|
||||
def which_win32_gitbash():
|
||||
"""Helper function that returns absolute path to the git bash executable
|
||||
(came with Git for Windows) on Windows
|
||||
|
||||
Returns:
|
||||
the path to the git bash executable or None if not found
|
||||
"""
|
||||
from os import path
|
||||
|
||||
gb_path = shutil.which("bash")
|
||||
|
||||
# Windows came with its own bash.exe but it's just an entry point for WSL not Git Bash
|
||||
if gb_path and not path.dirname(gb_path).lower().endswith("windows\\system32"):
|
||||
return gb_path
|
||||
|
||||
git_path = shutil.which("git")
|
||||
|
||||
if git_path:
|
||||
if path.dirname(git_path).endswith("cmd"):
|
||||
gb_path = path.abspath(
|
||||
path.join(path.dirname(git_path), "..", "bin", "bash.exe")
|
||||
)
|
||||
else:
|
||||
gb_path = path.join(path.dirname(git_path), "bash.exe")
|
||||
|
||||
if path.exists(gb_path):
|
||||
return gb_path
|
||||
|
||||
|
||||
def which_bashlike():
|
||||
"""Helper function that returns absolute path to the bash executable for the current platform
|
||||
|
||||
Returns:
|
||||
the path to the bash executable or None if not found
|
||||
"""
|
||||
return (shutil.which("bash") or "bash") if S_PLATFORM != "win32" else which_win32_gitbash()
|
||||
@@ -3,6 +3,7 @@ This is the core module availing all the abstractions of the anilist api
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import requests
|
||||
@@ -63,7 +64,7 @@ class AniListApi:
|
||||
self.session = requests.session()
|
||||
|
||||
def login_user(self, token: str):
|
||||
"""methosd used to login a new user enabling authenticated requests
|
||||
"""method used to login a new user enabling authenticated requests
|
||||
|
||||
Args:
|
||||
token: anilist app token
|
||||
@@ -142,6 +143,9 @@ class AniListApi:
|
||||
self,
|
||||
status: "AnilistMediaListStatus",
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
**kwargs,
|
||||
) -> tuple[bool, "AnilistMediaLists"] | tuple[bool, None]:
|
||||
"""gets an anime list from your media list given the list status
|
||||
|
||||
@@ -151,7 +155,13 @@ class AniListApi:
|
||||
Returns:
|
||||
a media list
|
||||
"""
|
||||
variables = {"status": status, "userId": self.user_id, "type": type}
|
||||
variables = {
|
||||
"status": status,
|
||||
"userId": self.user_id,
|
||||
"type": type,
|
||||
"page": page,
|
||||
"perPage": int(perPage),
|
||||
}
|
||||
return self._make_authenticated_request(media_list_query, variables)
|
||||
|
||||
def get_medialist_entry(
|
||||
@@ -237,7 +247,7 @@ class AniListApi:
|
||||
return (False, None)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
logger.error(f"Something unexpected occurred {e}")
|
||||
return (False, None) # type: ignore
|
||||
|
||||
def get_data(
|
||||
@@ -301,11 +311,12 @@ class AniListApi:
|
||||
},
|
||||
) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
logger.error(f"Something unexpected occurred {e}")
|
||||
return (False, {"Error": f"{e}"}) # type: ignore
|
||||
|
||||
def search(
|
||||
self,
|
||||
max_results=50,
|
||||
query: str | None = None,
|
||||
sort: str | None = None,
|
||||
genre_in: list[str] | None = None,
|
||||
@@ -350,69 +361,107 @@ class AniListApi:
|
||||
variables = {"id": id}
|
||||
return self.get_data(anime_query, variables)
|
||||
|
||||
def get_trending(self, type="ANIME", *_, **kwargs):
|
||||
def get_trending(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets the currently trending anime
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
trending = self.get_data(trending_query, variables)
|
||||
return trending
|
||||
|
||||
def get_most_favourite(self, type="ANIME", *_, **kwargs):
|
||||
def get_most_favourite(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets the most favoured anime on anilist
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
most_favourite = self.get_data(most_favourite_query, variables)
|
||||
return most_favourite
|
||||
|
||||
def get_most_scored(self, type="ANIME", *_, **kwargs):
|
||||
def get_most_scored(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets most scored anime on anilist
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
most_scored = self.get_data(most_scored_query, variables)
|
||||
return most_scored
|
||||
|
||||
def get_most_recently_updated(self, type="ANIME", *_, **kwargs):
|
||||
def get_most_recently_updated(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets most recently updated anime from anilist
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
most_recently_updated = self.get_data(most_recently_updated_query, variables)
|
||||
return most_recently_updated
|
||||
|
||||
def get_most_popular(
|
||||
self,
|
||||
type="ANIME",
|
||||
page=1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets most popular anime on anilist
|
||||
"""
|
||||
variables = {"type": type}
|
||||
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||
most_popular = self.get_data(most_popular_query, variables)
|
||||
return most_popular
|
||||
|
||||
def get_upcoming_anime(self, type="ANIME", page: int = 1, *_, **kwargs):
|
||||
def get_upcoming_anime(
|
||||
self,
|
||||
type="ANIME",
|
||||
page: int = 1,
|
||||
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||
*_,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Gets upcoming anime from anilist
|
||||
"""
|
||||
variables = {"page": page, "type": type}
|
||||
variables = {"page": page, "type": type, "perPage": int(perPage)}
|
||||
upcoming_anime = self.get_data(upcoming_anime_query, variables)
|
||||
return upcoming_anime
|
||||
|
||||
# NOTE: THe following methods will probably be scraped soon
|
||||
def get_recommended_anime_for(self, id: int, type="ANIME", *_, **kwargs):
|
||||
variables = {"type": type}
|
||||
def get_recommended_anime_for(self, mediaRecommendationId, page=1, *_, **kwargs):
|
||||
variables = {"mediaRecommendationId": mediaRecommendationId, "page": page}
|
||||
recommended_anime = self.get_data(recommended_query, variables)
|
||||
return recommended_anime
|
||||
|
||||
def get_charcters_of(self, id: int, type="ANIME", *_, **kwargs):
|
||||
def get_characters_of(self, id: int, type="ANIME", *_, **kwargs):
|
||||
variables = {"id": id}
|
||||
characters = self.get_data(anime_characters_query, variables)
|
||||
return characters
|
||||
|
||||
def get_related_anime_for(self, id: int, type="ANIME", *_, **kwargs):
|
||||
def get_related_anime_for(self, id: int, *_, **kwargs):
|
||||
variables = {"id": id}
|
||||
related_anime = self.get_data(anime_relations_query, variables)
|
||||
return related_anime
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
This module contains all the preset queries for the sake of neatness and convinience
|
||||
This module contains all the preset queries for the sake of neatness and convenience
|
||||
Mostly for internal usage
|
||||
"""
|
||||
|
||||
@@ -26,7 +26,7 @@ query($id:Int){
|
||||
}
|
||||
}
|
||||
body
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ query{
|
||||
large
|
||||
medium
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -193,8 +193,8 @@ mutation (
|
||||
"""
|
||||
|
||||
media_list_query = """
|
||||
query ($userId: Int, $status: MediaListStatus, $type: MediaType) {
|
||||
Page {
|
||||
query ($userId: Int, $status: MediaListStatus, $type: MediaType, $page: Int, $perPage: Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
pageInfo {
|
||||
currentPage
|
||||
total
|
||||
@@ -281,6 +281,7 @@ query ($userId: Int, $status: MediaListStatus, $type: MediaType) {
|
||||
|
||||
|
||||
optional_variables = "\
|
||||
$max_results:Int,\
|
||||
$page:Int,\
|
||||
$sort:[MediaSort],\
|
||||
$id_in:[Int],\
|
||||
@@ -310,7 +311,7 @@ $on_list:Boolean\
|
||||
search_query = (
|
||||
"""
|
||||
query($query:String,%s){
|
||||
Page(perPage: 50, page: $page) {
|
||||
Page(perPage: $max_results, page: $page) {
|
||||
pageInfo {
|
||||
total
|
||||
currentPage
|
||||
@@ -405,8 +406,8 @@ query($query:String,%s){
|
||||
)
|
||||
|
||||
trending_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(sort: TRENDING_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
@@ -470,8 +471,8 @@ query ($type: MediaType) {
|
||||
|
||||
# mosts
|
||||
most_favourite_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(sort: FAVOURITES_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
@@ -538,8 +539,8 @@ query ($type: MediaType) {
|
||||
"""
|
||||
|
||||
most_scored_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(sort: SCORE_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
@@ -602,8 +603,8 @@ query ($type: MediaType) {
|
||||
"""
|
||||
|
||||
most_popular_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(sort: POPULARITY_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||
id
|
||||
idMal
|
||||
@@ -666,8 +667,8 @@ query ($type: MediaType) {
|
||||
"""
|
||||
|
||||
most_recently_updated_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
media(
|
||||
sort: UPDATED_AT_DESC
|
||||
type: $type
|
||||
@@ -737,63 +738,64 @@ query ($type: MediaType) {
|
||||
"""
|
||||
|
||||
recommended_query = """
|
||||
query ($type: MediaType) {
|
||||
Page(perPage: 15) {
|
||||
media(type: $type, genre_not_in: ["hentai"]) {
|
||||
recommendations(sort: RATING_DESC) {
|
||||
nodes {
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
description
|
||||
episodes
|
||||
trailer {
|
||||
site
|
||||
id
|
||||
}
|
||||
genres
|
||||
synonyms
|
||||
averageScore
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
title
|
||||
thumbnail
|
||||
}
|
||||
favourites
|
||||
tags {
|
||||
name
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
status
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
query ($mediaRecommendationId: Int, $page: Int) {
|
||||
Page(perPage: 50, page: $page) {
|
||||
recommendations(mediaRecommendationId: $mediaRecommendationId) {
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
description
|
||||
episodes
|
||||
trailer {
|
||||
site
|
||||
id
|
||||
}
|
||||
genres
|
||||
synonyms
|
||||
averageScore
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
title
|
||||
thumbnail
|
||||
}
|
||||
favourites
|
||||
tags {
|
||||
name
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
status
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -801,6 +803,7 @@ query ($type: MediaType) {
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
anime_characters_query = """
|
||||
query ($id: Int, $type: MediaType) {
|
||||
Page {
|
||||
@@ -837,66 +840,59 @@ query ($id: Int, $type: MediaType) {
|
||||
|
||||
|
||||
anime_relations_query = """
|
||||
query ($id: Int, $type: MediaType) {
|
||||
Page(perPage: 20) {
|
||||
media(
|
||||
id: $id
|
||||
sort: POPULARITY_DESC
|
||||
type: $type
|
||||
genre_not_in: ["hentai"]
|
||||
) {
|
||||
relations {
|
||||
nodes {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
description
|
||||
episodes
|
||||
trailer {
|
||||
site
|
||||
id
|
||||
}
|
||||
genres
|
||||
synonyms
|
||||
averageScore
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
title
|
||||
thumbnail
|
||||
}
|
||||
favourites
|
||||
tags {
|
||||
name
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
query ($id: Int) {
|
||||
Media(id: $id) {
|
||||
relations {
|
||||
nodes {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
mediaListEntry {
|
||||
status
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
id
|
||||
progress
|
||||
}
|
||||
description
|
||||
episodes
|
||||
trailer {
|
||||
site
|
||||
id
|
||||
}
|
||||
genres
|
||||
synonyms
|
||||
averageScore
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
title
|
||||
thumbnail
|
||||
}
|
||||
favourites
|
||||
tags {
|
||||
name
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
status
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -913,7 +909,7 @@ query ($id: Int,$type:MediaType) {
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
episode
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -922,8 +918,8 @@ query ($id: Int,$type:MediaType) {
|
||||
"""
|
||||
|
||||
upcoming_anime_query = """
|
||||
query ($page: Int, $type: MediaType) {
|
||||
Page(page: $page) {
|
||||
query ($page: Int, $type: MediaType,$perPage:Int) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
pageInfo {
|
||||
total
|
||||
perPage
|
||||
|
||||
@@ -3,10 +3,10 @@ from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHE_SERVERS
|
||||
from .hianime.constants import SERVERS_AVAILABLE as HIANIME_SERVERS
|
||||
|
||||
anime_sources = {
|
||||
"allanime": "api.AllAnimeAPI",
|
||||
"animepahe": "api.AnimePaheApi",
|
||||
"hianime": "api.HiAnimeApi",
|
||||
"nyaa": "api.NyaaApi",
|
||||
"yugen": "api.YugenApi"
|
||||
"allanime": "api.AllAnime",
|
||||
"animepahe": "api.AnimePahe",
|
||||
"hianime": "api.HiAnime",
|
||||
"nyaa": "api.Nyaa",
|
||||
"yugen": "api.Yugen",
|
||||
}
|
||||
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHE_SERVERS, *HIANIME_SERVERS]
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
"""a module that handles the scraping of allanime
|
||||
|
||||
abstraction over allanime api
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -10,207 +5,215 @@ from typing import TYPE_CHECKING
|
||||
from ...anime_provider.base_provider import AnimeProvider
|
||||
from ..decorators import debug_provider
|
||||
from ..utils import give_random_quality, one_digit_symmetric_xor
|
||||
from .constants import ALLANIME_API_ENDPOINT, ALLANIME_BASE, ALLANIME_REFERER
|
||||
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
||||
from .constants import (
|
||||
API_BASE_URL,
|
||||
API_ENDPOINT,
|
||||
API_REFERER,
|
||||
DEFAULT_COUNTRY_OF_ORIGIN,
|
||||
DEFAULT_NSFW,
|
||||
DEFAULT_PAGE,
|
||||
DEFAULT_PER_PAGE,
|
||||
DEFAULT_UNKNOWN,
|
||||
MP4_SERVER_JUICY_STREAM_REGEX,
|
||||
)
|
||||
from .gql_queries import EPISODES_GQL, SEARCH_GQL, SHOW_GQL
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import AllAnimeEpisode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: create tests for the api
|
||||
#
|
||||
# ** Based on ani-cli **
|
||||
class AllAnimeAPI(AnimeProvider):
|
||||
class AllAnime(AnimeProvider):
|
||||
"""
|
||||
Provides a fast and effective interface to AllAnime site.
|
||||
AllAnime is a provider class for fetching anime data from the AllAnime API.
|
||||
Attributes:
|
||||
HEADERS (dict): Default headers for API requests.
|
||||
Methods:
|
||||
_execute_graphql_query(query: str, variables: dict) -> dict:
|
||||
Executes a GraphQL query and returns the response data.
|
||||
search_for_anime(
|
||||
**kwargs
|
||||
) -> dict:
|
||||
Searches for anime based on the provided keywords and other parameters.
|
||||
get_anime(show_id: str) -> dict:
|
||||
Retrieves detailed information about a specific anime by its ID.
|
||||
_get_anime_episode(
|
||||
show_id: str, episode, translation_type: str = "sub"
|
||||
Retrieves information about a specific episode of an anime.
|
||||
get_episode_streams(
|
||||
) -> generator:
|
||||
Retrieves streaming links for a specific episode of an anime.
|
||||
"""
|
||||
|
||||
PROVIDER = "allanime"
|
||||
api_endpoint = ALLANIME_API_ENDPOINT
|
||||
HEADERS = {
|
||||
"Referer": ALLANIME_REFERER,
|
||||
"Referer": API_REFERER,
|
||||
}
|
||||
|
||||
def _fetch_gql(self, query: str, variables: dict):
|
||||
"""main abstraction over all requests to the allanime api
|
||||
def _execute_graphql_query(self, query: str, variables: dict):
|
||||
"""
|
||||
Executes a GraphQL query using the provided query string and variables.
|
||||
|
||||
Args:
|
||||
query: [TODO:description]
|
||||
variables: [TODO:description]
|
||||
query (str): The GraphQL query string to be executed.
|
||||
variables (dict): A dictionary of variables to be used in the query.
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
dict: The JSON response data from the GraphQL API.
|
||||
|
||||
Raises:
|
||||
requests.exceptions.HTTPError: If the HTTP request returned an unsuccessful status code.
|
||||
"""
|
||||
|
||||
response = self.session.get(
|
||||
self.api_endpoint,
|
||||
API_ENDPOINT,
|
||||
params={
|
||||
"variables": json.dumps(variables),
|
||||
"query": query,
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
if response.ok:
|
||||
return response.json()["data"]
|
||||
else:
|
||||
logger.error("[ALLANIME-ERROR]: ", response.text)
|
||||
return {}
|
||||
response.raise_for_status()
|
||||
return response.json()["data"]
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def search_for_anime(
|
||||
self,
|
||||
user_query: str,
|
||||
translation_type: str = "sub",
|
||||
nsfw=True,
|
||||
unknown=True,
|
||||
search_keywords: str,
|
||||
translation_type: str,
|
||||
*,
|
||||
nsfw=DEFAULT_NSFW,
|
||||
unknown=DEFAULT_UNKNOWN,
|
||||
limit=DEFAULT_PER_PAGE,
|
||||
page=DEFAULT_PAGE,
|
||||
country_of_origin=DEFAULT_COUNTRY_OF_ORIGIN,
|
||||
**kwargs,
|
||||
):
|
||||
"""search for an anime title using allanime provider
|
||||
|
||||
"""
|
||||
Search for anime based on given keywords and filters.
|
||||
Args:
|
||||
nsfw ([TODO:parameter]): [TODO:description]
|
||||
unknown ([TODO:parameter]): [TODO:description]
|
||||
user_query: [TODO:description]
|
||||
translation_type: [TODO:description]
|
||||
**kwargs: [TODO:args]
|
||||
|
||||
search_keywords (str): The keywords to search for.
|
||||
translation_type (str, optional): The type of translation to search for (e.g., "sub" or "dub"). Defaults to "sub".
|
||||
limit (int, optional): The maximum number of results to return. Defaults to 40.
|
||||
page (int, optional): The page number to return. Defaults to 1.
|
||||
country_of_origin (str, optional): The country of origin filter. Defaults to "all".
|
||||
nsfw (bool, optional): Whether to include adult content in the search results. Defaults to True.
|
||||
unknown (bool, optional): Whether to include unknown content in the search results. Defaults to True.
|
||||
**kwargs: Additional keyword arguments.
|
||||
Returns:
|
||||
[TODO:return]
|
||||
dict: A dictionary containing the page information and a list of search results. Each result includes:
|
||||
- id (str): The ID of the anime.
|
||||
- title (str): The title of the anime.
|
||||
- type (str): The type of the anime.
|
||||
- availableEpisodes (int): The number of available episodes.
|
||||
"""
|
||||
search = {"allowAdult": nsfw, "allowUnknown": unknown, "query": user_query}
|
||||
limit = 40
|
||||
translationtype = translation_type
|
||||
countryorigin = "all"
|
||||
page = 1
|
||||
variables = {
|
||||
"search": search,
|
||||
"limit": limit,
|
||||
"page": page,
|
||||
"translationtype": translationtype,
|
||||
"countryorigin": countryorigin,
|
||||
}
|
||||
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
|
||||
page_info = search_results["shows"]["pageInfo"]
|
||||
results = []
|
||||
for result in search_results["shows"]["edges"]:
|
||||
normalized_result = {
|
||||
"id": result["_id"],
|
||||
"title": result["name"],
|
||||
"type": result["__typename"],
|
||||
"availableEpisodes": result["availableEpisodes"],
|
||||
}
|
||||
results.append(normalized_result)
|
||||
|
||||
normalized_search_results = {
|
||||
"pageInfo": page_info,
|
||||
"results": results,
|
||||
}
|
||||
return normalized_search_results
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_anime(self, allanime_show_id: str):
|
||||
"""get an anime details given its id
|
||||
|
||||
Args:
|
||||
allanime_show_id: [TODO:description]
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
variables = {"showId": allanime_show_id}
|
||||
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
|
||||
id: str = anime["show"]["_id"]
|
||||
title: str = anime["show"]["name"]
|
||||
availableEpisodesDetail = anime["show"]["availableEpisodesDetail"]
|
||||
self.store.set(allanime_show_id, "anime_info", {"title": title})
|
||||
type = anime.get("__typename")
|
||||
normalized_anime = {
|
||||
"id": id,
|
||||
"title": title,
|
||||
"availableEpisodesDetail": availableEpisodesDetail,
|
||||
"type": type,
|
||||
}
|
||||
return normalized_anime
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def _get_anime_episode(
|
||||
self, allanime_show_id: str, episode, translation_type: str = "sub"
|
||||
) -> "AllAnimeEpisode | dict":
|
||||
"""get the episode details and sources info
|
||||
|
||||
Args:
|
||||
allanime_show_id: [TODO:description]
|
||||
episode_string: [TODO:description]
|
||||
translation_type: [TODO:description]
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
variables = {
|
||||
"showId": allanime_show_id,
|
||||
"translationType": translation_type,
|
||||
"episodeString": episode,
|
||||
}
|
||||
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
|
||||
return episode["episode"]
|
||||
|
||||
@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]
|
||||
"""
|
||||
|
||||
anime_title = (self.store.get(anime_id, "anime_info", "") or {"title": ""})[
|
||||
"title"
|
||||
]
|
||||
allanime_episode = self._get_anime_episode(
|
||||
anime_id, episode_number, translation_type
|
||||
search_results = self._execute_graphql_query(
|
||||
SEARCH_GQL,
|
||||
variables={
|
||||
"search": {
|
||||
"allowAdult": nsfw,
|
||||
"allowUnknown": unknown,
|
||||
"query": search_keywords,
|
||||
},
|
||||
"limit": limit,
|
||||
"page": page,
|
||||
"translationtype": translation_type,
|
||||
"countryorigin": country_of_origin,
|
||||
},
|
||||
)
|
||||
if not allanime_episode:
|
||||
return []
|
||||
return {
|
||||
"pageInfo": search_results["shows"]["pageInfo"],
|
||||
"results": [
|
||||
{
|
||||
"id": result["_id"],
|
||||
"title": result["name"],
|
||||
"type": result["__typename"],
|
||||
"availableEpisodes": result["availableEpisodes"],
|
||||
}
|
||||
for result in search_results["shows"]["edges"]
|
||||
],
|
||||
}
|
||||
|
||||
embeds = allanime_episode["sourceUrls"]
|
||||
@debug_provider
|
||||
def get_anime(self, id: str, **kwargs):
|
||||
"""
|
||||
Fetches anime details using the provided show ID.
|
||||
Args:
|
||||
id (str): The ID of the anime show to fetch details for.
|
||||
Returns:
|
||||
dict: A dictionary containing the anime details, including:
|
||||
- id (str): The unique identifier of the anime show.
|
||||
- title (str): The title of the anime show.
|
||||
- availableEpisodesDetail (list): A list of available episodes details.
|
||||
- type (str, optional): The type of the anime show.
|
||||
"""
|
||||
|
||||
@debug_provider(self.PROVIDER.upper())
|
||||
def _get_server(embed):
|
||||
# filter the working streams no need to get all since the others are mostly hsl
|
||||
# TODO: should i just get all the servers and handle the hsl??
|
||||
if embed.get("sourceName", "") not in (
|
||||
# priorities based on death note
|
||||
"Sak", # 7
|
||||
"S-mp4", # 7.9
|
||||
"Luf-mp4", # 7.7
|
||||
"Default", # 8.5
|
||||
"Yt-mp4", # 7.9
|
||||
"Kir", # NA
|
||||
# "Vid-mp4" # 4
|
||||
# "Ok", # 3.5
|
||||
# "Ss-Hls", # 5.5
|
||||
# "Mp4", # 4
|
||||
):
|
||||
return
|
||||
url = embed.get("sourceUrl")
|
||||
#
|
||||
if not url:
|
||||
return
|
||||
if url.startswith("--"):
|
||||
url = url[2:]
|
||||
url = one_digit_symmetric_xor(56, url)
|
||||
anime = self._execute_graphql_query(SHOW_GQL, variables={"showId": id})
|
||||
self.store.set(id, "anime_info", {"title": anime["show"]["name"]})
|
||||
return {
|
||||
"id": anime["show"]["_id"],
|
||||
"title": anime["show"]["name"],
|
||||
"availableEpisodesDetail": anime["show"]["availableEpisodesDetail"],
|
||||
"type": anime.get("__typename"),
|
||||
}
|
||||
|
||||
if "tools.fast4speed.rsvp" in url:
|
||||
@debug_provider
|
||||
def _get_anime_episode(
|
||||
self, anime_id: str, episode, translation_type: str = "sub"
|
||||
) -> "AllAnimeEpisode":
|
||||
"""
|
||||
Fetches a specific episode of an anime by its ID and episode number.
|
||||
Args:
|
||||
anime_id (str): The unique identifier of the anime.
|
||||
episode (str): The episode number or string identifier.
|
||||
translation_type (str, optional): The type of translation for the episode. Defaults to "sub".
|
||||
Returns:
|
||||
AllAnimeEpisode: The episode details retrieved from the GraphQL query.
|
||||
"""
|
||||
return self._execute_graphql_query(
|
||||
EPISODES_GQL,
|
||||
variables={
|
||||
"showId": anime_id,
|
||||
"translationType": translation_type,
|
||||
"episodeString": episode,
|
||||
},
|
||||
)["episode"]
|
||||
|
||||
@debug_provider
|
||||
def _get_server(
|
||||
self,
|
||||
embed,
|
||||
anime_title: str,
|
||||
allanime_episode: "AllAnimeEpisode",
|
||||
episode_number,
|
||||
):
|
||||
"""
|
||||
Retrieves the streaming server information for a given anime episode based on the provided embed data.
|
||||
Args:
|
||||
embed (dict): A dictionary containing the embed data, including the source URL and source name.
|
||||
anime_title (str): The title of the anime.
|
||||
allanime_episode (AllAnimeEpisode): An object representing the episode details.
|
||||
Returns:
|
||||
dict: A dictionary containing server information, headers, subtitles, episode title, and links to the stream.
|
||||
Returns None if no valid URL or stream is found.
|
||||
Raises:
|
||||
requests.exceptions.RequestException: If there is an issue with the HTTP request.
|
||||
"""
|
||||
|
||||
url = embed.get("sourceUrl")
|
||||
#
|
||||
if not url:
|
||||
return
|
||||
if url.startswith("--"):
|
||||
url = one_digit_symmetric_xor(56, url[2:])
|
||||
|
||||
# FIRST CASE
|
||||
match embed["sourceName"]:
|
||||
case "Yt-mp4":
|
||||
logger.debug("Found streams from Yt")
|
||||
return {
|
||||
"server": "Yt",
|
||||
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"links": [
|
||||
{
|
||||
@@ -219,77 +222,280 @@ class AllAnimeAPI(AnimeProvider):
|
||||
}
|
||||
],
|
||||
}
|
||||
case "Mp4":
|
||||
logger.debug("Found streams from Mp4")
|
||||
response = self.session.get(
|
||||
url,
|
||||
fresh=1, # pyright: ignore
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
vid = MP4_SERVER_JUICY_STREAM_REGEX.search(embed_html)
|
||||
if not vid:
|
||||
return
|
||||
return {
|
||||
"server": "mp4-upload",
|
||||
"headers": {"Referer": "https://www.mp4upload.com/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": [{"link": vid.group(1), "quality": "1080"}],
|
||||
}
|
||||
case "Fm-Hls":
|
||||
# TODO: requires decoding obsfucated js (filemoon)
|
||||
logger.debug("Found streams from Fm-Hls")
|
||||
response = self.session.get(
|
||||
url,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
vid = MP4_SERVER_JUICY_STREAM_REGEX.search(embed_html)
|
||||
if not vid:
|
||||
return
|
||||
return {
|
||||
"server": "filemoon",
|
||||
"headers": {"Referer": "https://www.mp4upload.com/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": [{"link": vid.group(1), "quality": "1080"}],
|
||||
}
|
||||
case "Ok":
|
||||
# TODO: requires decoding the obsfucated js (filemoon)
|
||||
response = self.session.get(
|
||||
url,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
vid = MP4_SERVER_JUICY_STREAM_REGEX.search(embed_html)
|
||||
logger.debug("Found streams from Ok")
|
||||
return {
|
||||
"server": "filemoon",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Vid-mp4":
|
||||
# TODO: requires some serious work i think : )
|
||||
response = self.session.get(
|
||||
url,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
logger.debug("Found streams from vid-mp4")
|
||||
return {
|
||||
"server": "Vid-mp4",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Ss-Hls":
|
||||
# TODO: requires some serious work i think : )
|
||||
response = self.session.get(
|
||||
url,
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
embed_html = response.text.replace(" ", "").replace("\n", "")
|
||||
logger.debug("Found streams from Ss-Hls")
|
||||
return {
|
||||
"server": "StreamSb",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
|
||||
# get the stream url for an episode of the defined source names
|
||||
embed_url = f"https://{ALLANIME_BASE}{url.replace('clock', 'clock.json')}"
|
||||
resp = self.session.get(
|
||||
embed_url,
|
||||
timeout=10,
|
||||
)
|
||||
# get the stream url for an episode of the defined source names
|
||||
response = self.session.get(
|
||||
f"https://{API_BASE_URL}{url.replace('clock', 'clock.json')}",
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if resp.ok:
|
||||
match embed["sourceName"]:
|
||||
case "Luf-mp4":
|
||||
logger.debug("allanime:Found streams from gogoanime")
|
||||
return {
|
||||
"server": "gogoanime",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
}
|
||||
case "Kir":
|
||||
logger.debug("allanime:Found streams from wetransfer")
|
||||
return {
|
||||
"server": "wetransfer",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
}
|
||||
case "S-mp4":
|
||||
logger.debug("allanime:Found streams from sharepoint")
|
||||
return {
|
||||
"server": "sharepoint",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
}
|
||||
case "Sak":
|
||||
logger.debug("allanime:Found streams from dropbox")
|
||||
return {
|
||||
"server": "dropbox",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
}
|
||||
case "Default":
|
||||
logger.debug("allanime:Found streams from wixmp")
|
||||
return {
|
||||
"server": "wixmp",
|
||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (
|
||||
allanime_episode["notes"] or f"{anime_title}"
|
||||
)
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(resp.json()["links"]),
|
||||
}
|
||||
response.raise_for_status()
|
||||
|
||||
for embed in embeds:
|
||||
if server := _get_server(embed):
|
||||
# SECOND CASE
|
||||
match embed["sourceName"]:
|
||||
case "Luf-mp4":
|
||||
logger.debug("Found streams from gogoanime")
|
||||
return {
|
||||
"server": "gogoanime",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Kir":
|
||||
logger.debug("Found streams from wetransfer")
|
||||
return {
|
||||
"server": "weTransfer",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "S-mp4":
|
||||
logger.debug("Found streams from sharepoint")
|
||||
return {
|
||||
"server": "sharepoint",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Sak":
|
||||
logger.debug("Found streams from dropbox")
|
||||
return {
|
||||
"server": "dropbox",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
case "Default":
|
||||
logger.debug("Found streams from wixmp")
|
||||
return {
|
||||
"server": "wixmp",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
|
||||
case "Ak":
|
||||
# TODO: works but needs further probing
|
||||
logger.debug("Found streams from Ak")
|
||||
return {
|
||||
"server": "Ak",
|
||||
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||
"subtitles": [],
|
||||
"episode_title": (allanime_episode["notes"] or f"{anime_title}")
|
||||
+ f"; Episode {episode_number}",
|
||||
"links": give_random_quality(response.json()["links"]),
|
||||
}
|
||||
|
||||
@debug_provider
|
||||
def get_episode_streams(
|
||||
self, anime_id, episode_number: str, translation_type="sub", **kwargs
|
||||
):
|
||||
"""
|
||||
Retrieve streaming information for a specific episode of an anime.
|
||||
Args:
|
||||
anime_id (str): The unique identifier for the anime.
|
||||
episode_number (str): The episode number to retrieve streams for.
|
||||
translation_type (str, optional): The type of translation for the episode (e.g., "sub" for subtitles). Defaults to "sub".
|
||||
Yields:
|
||||
dict: A dictionary containing streaming information for the episode, including:
|
||||
- server (str): The name of the streaming server.
|
||||
- episode_title (str): The title of the episode.
|
||||
- headers (dict): HTTP headers required for accessing the stream.
|
||||
- subtitles (list): A list of subtitles available for the episode.
|
||||
- links (list): A list of dictionaries containing streaming links and their quality.
|
||||
"""
|
||||
anime_title = (self.store.get(anime_id, "anime_info", "") or {"title": ""})[
|
||||
"title"
|
||||
]
|
||||
allanime_episode = self._get_anime_episode(
|
||||
anime_id, episode_number, translation_type
|
||||
)
|
||||
|
||||
for embed in allanime_episode["sourceUrls"]:
|
||||
if embed.get("sourceName", "") not in (
|
||||
# priorities based on death note
|
||||
"Sak", # 7
|
||||
"S-mp4", # 7.9
|
||||
"Luf-mp4", # 7.7
|
||||
"Default", # 8.5
|
||||
"Yt-mp4", # 7.9
|
||||
"Kir", # NA
|
||||
"Mp4", # 4
|
||||
# "Ak",#
|
||||
# "Vid-mp4", # 4
|
||||
# "Ok", # 3.5
|
||||
# "Ss-Hls", # 5.5
|
||||
# "Fm-Hls",#
|
||||
):
|
||||
logger.debug(f"Found {embed['sourceName']} but ignoring")
|
||||
continue
|
||||
if server := self._get_server(
|
||||
embed, anime_title, allanime_episode, episode_number
|
||||
):
|
||||
yield server
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import subprocess
|
||||
|
||||
allanime = AllAnime(cache_requests="True", use_persistent_provider_store="False")
|
||||
search_term = input("Enter the search term for the anime: ")
|
||||
translation_type = input("Enter the translation type (sub/dub): ")
|
||||
|
||||
search_results = allanime.search_for_anime(
|
||||
search_keywords=search_term, translation_type=translation_type
|
||||
)
|
||||
|
||||
if not search_results["results"]:
|
||||
print("No results found.")
|
||||
exit()
|
||||
|
||||
print("Search Results:")
|
||||
for idx, result in enumerate(search_results["results"], start=1):
|
||||
print(f"{idx}. {result['title']} (ID: {result['id']})")
|
||||
|
||||
anime_choice = int(input("Enter the number of the anime you want to watch: ")) - 1
|
||||
anime_id = search_results["results"][anime_choice]["id"]
|
||||
|
||||
anime_details = allanime.get_anime(anime_id)
|
||||
print(f"Selected Anime: {anime_details['title']}")
|
||||
|
||||
print("Available Episodes:")
|
||||
for idx, episode in enumerate(
|
||||
sorted(anime_details["availableEpisodesDetail"][translation_type], key=float),
|
||||
start=1,
|
||||
):
|
||||
print(f"{idx}. Episode {episode}")
|
||||
|
||||
episode_choice = (
|
||||
int(input("Enter the number of the episode you want to watch: ")) - 1
|
||||
)
|
||||
episode_number = anime_details["availableEpisodesDetail"][translation_type][
|
||||
episode_choice
|
||||
]
|
||||
|
||||
streams = list(
|
||||
allanime.get_episode_streams(anime_id, episode_number, translation_type)
|
||||
)
|
||||
if not streams:
|
||||
print("No streams available.")
|
||||
exit()
|
||||
|
||||
print("Available Streams:")
|
||||
for idx, stream in enumerate(streams, start=1):
|
||||
print(f"{idx}. Server: {stream['server']}")
|
||||
|
||||
server_choice = int(input("Enter the number of the server you want to use: ")) - 1
|
||||
selected_stream = streams[server_choice]
|
||||
|
||||
stream_link = selected_stream["links"][0]["link"]
|
||||
mpv_args = ["mpv", stream_link]
|
||||
headers = selected_stream["headers"]
|
||||
if headers:
|
||||
mpv_headers = "--http-header-fields="
|
||||
for header_name, header_value in headers.items():
|
||||
mpv_headers += f"{header_name}:{header_value},"
|
||||
mpv_args.append(mpv_headers)
|
||||
subprocess.run(mpv_args)
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer", "wixmp", "Yt"]
|
||||
ALLANIME_BASE = "allanime.day"
|
||||
ALLANIME_REFERER = "https://allanime.to/"
|
||||
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
|
||||
import re
|
||||
|
||||
SERVERS_AVAILABLE = [
|
||||
"sharepoint",
|
||||
"dropbox",
|
||||
"gogoanime",
|
||||
"weTransfer",
|
||||
"wixmp",
|
||||
"Yt",
|
||||
"mp4-upload",
|
||||
]
|
||||
API_BASE_URL = "allanime.day"
|
||||
API_REFERER = "https://allanime.to/"
|
||||
API_ENDPOINT = f"https://api.{API_BASE_URL}/api/"
|
||||
|
||||
# search constants
|
||||
DEFAULT_COUNTRY_OF_ORIGIN = "all"
|
||||
DEFAULT_NSFW = True
|
||||
DEFAULT_UNKNOWN = True
|
||||
DEFAULT_PER_PAGE = 40
|
||||
DEFAULT_PAGE = 1
|
||||
|
||||
# regex stuff
|
||||
|
||||
MP4_SERVER_JUICY_STREAM_REGEX = re.compile(
|
||||
r"video/mp4\",src:\"(https?://.*/video\.mp4)\""
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ALLANIME_SEARCH_GQL = """
|
||||
SEARCH_GQL = """
|
||||
query (
|
||||
$search: SearchInput
|
||||
$limit: Int
|
||||
@@ -27,7 +27,7 @@ query (
|
||||
"""
|
||||
|
||||
|
||||
ALLANIME_EPISODES_GQL = """\
|
||||
EPISODES_GQL = """\
|
||||
query (
|
||||
$showId: String!
|
||||
$translationType: VaildTranslationTypeEnumType!
|
||||
@@ -45,7 +45,7 @@ query (
|
||||
}
|
||||
"""
|
||||
|
||||
ALLANIME_SHOW_GQL = """
|
||||
SHOW_GQL = """
|
||||
query ($showId: String!) {
|
||||
show(_id: $showId) {
|
||||
_id
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -15,49 +14,33 @@ from ..decorators import debug_provider
|
||||
from .constants import (
|
||||
ANIMEPAHE_BASE,
|
||||
ANIMEPAHE_ENDPOINT,
|
||||
JUICY_STREAM_REGEX,
|
||||
REQUEST_HEADERS,
|
||||
SERVER_HEADERS,
|
||||
)
|
||||
from .utils import process_animepahe_embed_page
|
||||
from .extractors import process_animepahe_embed_page
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimePaheSearchResult
|
||||
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KWIK_RE = re.compile(r"Player\|(.+?)'")
|
||||
|
||||
|
||||
class AnimePaheApi(AnimeProvider):
|
||||
class AnimePahe(AnimeProvider):
|
||||
search_page: "AnimePaheSearchPage"
|
||||
anime: "AnimePaheAnimePage"
|
||||
HEADERS = REQUEST_HEADERS
|
||||
PROVIDER = "animepahe"
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def search_for_anime(self, user_query: str, *args):
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
||||
@debug_provider
|
||||
def search_for_anime(self, search_keywords: str, translation_type, **kwargs):
|
||||
response = self.session.get(
|
||||
url,
|
||||
ANIMEPAHE_ENDPOINT, params={"m": "search", "q": search_keywords}
|
||||
)
|
||||
if not response.ok:
|
||||
return
|
||||
response.raise_for_status()
|
||||
data: "AnimePaheSearchPage" = response.json()
|
||||
self.search_page = data
|
||||
for animepahe_search_result in data["data"]:
|
||||
self.store.set(
|
||||
str(animepahe_search_result["session"]),
|
||||
"search_result",
|
||||
animepahe_search_result,
|
||||
)
|
||||
|
||||
return {
|
||||
"pageInfo": {
|
||||
"total": data["total"],
|
||||
"perPage": data["per_page"],
|
||||
"currentPage": data["current_page"],
|
||||
},
|
||||
"results": [
|
||||
results = []
|
||||
for result in data["data"]:
|
||||
results.append(
|
||||
{
|
||||
"availableEpisodes": list(range(result["episodes"])),
|
||||
"id": result["session"],
|
||||
@@ -69,55 +52,81 @@ class AnimePaheApi(AnimeProvider):
|
||||
"season": result["season"],
|
||||
"poster": result["poster"],
|
||||
}
|
||||
for result in data["data"]
|
||||
],
|
||||
)
|
||||
self.store.set(
|
||||
str(result["session"]),
|
||||
"search_result",
|
||||
result,
|
||||
)
|
||||
|
||||
return {
|
||||
"pageInfo": {
|
||||
"total": data["total"],
|
||||
"perPage": data["per_page"],
|
||||
"currentPage": data["current_page"],
|
||||
},
|
||||
"results": results,
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_anime(self, session_id: str, *args):
|
||||
@debug_provider
|
||||
def _pages_loader(
|
||||
self,
|
||||
data,
|
||||
session_id,
|
||||
params,
|
||||
page,
|
||||
):
|
||||
response = self.session.get(ANIMEPAHE_ENDPOINT, params=params)
|
||||
response.raise_for_status()
|
||||
if not data:
|
||||
data.update(response.json())
|
||||
else:
|
||||
if ep_data := response.json().get("data"):
|
||||
data["data"].extend(ep_data)
|
||||
if response.json()["next_page_url"]:
|
||||
# TODO: Refine this
|
||||
time.sleep(
|
||||
random.choice(
|
||||
[
|
||||
0.25,
|
||||
0.1,
|
||||
0.5,
|
||||
0.75,
|
||||
1,
|
||||
]
|
||||
)
|
||||
)
|
||||
page += 1
|
||||
self._pages_loader(
|
||||
data,
|
||||
session_id,
|
||||
params={
|
||||
"m": "release",
|
||||
"page": page,
|
||||
"id": session_id,
|
||||
"sort": "episode_asc",
|
||||
},
|
||||
page=page,
|
||||
)
|
||||
return data
|
||||
|
||||
@debug_provider
|
||||
def get_anime(self, session_id: str, **kwargs):
|
||||
page = 1
|
||||
if d := self.store.get(str(session_id), "search_result"):
|
||||
anime_result: "AnimePaheSearchResult" = d
|
||||
data: "AnimePaheAnimePage" = {} # pyright:ignore
|
||||
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
|
||||
|
||||
def _pages_loader(
|
||||
url,
|
||||
page,
|
||||
):
|
||||
response = self.session.get(
|
||||
url,
|
||||
)
|
||||
if response.ok:
|
||||
if not data:
|
||||
data.update(response.json())
|
||||
else:
|
||||
if ep_data := response.json().get("data"):
|
||||
data["data"].extend(ep_data)
|
||||
if response.json()["next_page_url"]:
|
||||
# TODO: Refine this
|
||||
time.sleep(
|
||||
random.choice(
|
||||
[
|
||||
0.25,
|
||||
0.1,
|
||||
0.5,
|
||||
0.75,
|
||||
1,
|
||||
]
|
||||
)
|
||||
)
|
||||
page += 1
|
||||
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
|
||||
_pages_loader(
|
||||
url,
|
||||
page,
|
||||
)
|
||||
|
||||
_pages_loader(
|
||||
url,
|
||||
page,
|
||||
data = self._pages_loader(
|
||||
data,
|
||||
session_id,
|
||||
params={
|
||||
"m": "release",
|
||||
"id": session_id,
|
||||
"sort": "episode_asc",
|
||||
"page": page,
|
||||
},
|
||||
page=page,
|
||||
)
|
||||
|
||||
if not data:
|
||||
@@ -151,47 +160,13 @@ class AnimePaheApi(AnimeProvider):
|
||||
],
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_episode_streams(
|
||||
self, anime_id, episode_number: str, translation_type, *args
|
||||
):
|
||||
anime_title = ""
|
||||
episode = None
|
||||
# extract episode details from memory
|
||||
if d := self.store.get(str(anime_id), "anime_info"):
|
||||
anime_title = d["title"]
|
||||
episode = [
|
||||
episode
|
||||
for episode in d["data"]
|
||||
if float(episode["episode"]) == float(episode_number)
|
||||
]
|
||||
|
||||
if not episode:
|
||||
logger.error(f"[ANIMEPAHE-ERROR]: episode {episode_number} doesn't exist")
|
||||
return []
|
||||
episode = episode[0]
|
||||
|
||||
# fetch the episode page
|
||||
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
|
||||
response = self.session.get(url)
|
||||
# get the element containing links to juicy streams
|
||||
c = get_element_by_id("resolutionMenu", response.text)
|
||||
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
|
||||
# convert the elements containing embed links to a neat dict containing:
|
||||
# data-src
|
||||
# data-audio
|
||||
# data-resolution
|
||||
res_dicts = [extract_attributes(item) for item in resolutionMenuItems]
|
||||
|
||||
# get the episode title
|
||||
episode_title = (
|
||||
f"{episode['title'] or anime_title}; Episode {episode['episode']}"
|
||||
)
|
||||
@debug_provider
|
||||
def _get_server(self, episode, res_dicts, anime_title, translation_type):
|
||||
# get all links
|
||||
streams = {
|
||||
"server": "kwik",
|
||||
"links": [],
|
||||
"episode_title": episode_title,
|
||||
"episode_title": f"{episode['title'] or anime_title}; Episode {episode['episode']}",
|
||||
"subtitles": [],
|
||||
"headers": {},
|
||||
}
|
||||
@@ -207,23 +182,22 @@ class AnimePaheApi(AnimeProvider):
|
||||
logger.warning(
|
||||
"[ANIMEPAHE-WARN]: embed url not found please report to the developers"
|
||||
)
|
||||
return []
|
||||
continue
|
||||
# get embed page
|
||||
embed_response = self.session.get(
|
||||
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
|
||||
)
|
||||
if not response.ok:
|
||||
continue
|
||||
embed_response.raise_for_status()
|
||||
embed_page = embed_response.text
|
||||
|
||||
decoded_js = process_animepahe_embed_page(embed_page)
|
||||
if not decoded_js:
|
||||
logger.error("[ANIMEPAHE-ERROR]: failed to decode embed page")
|
||||
return
|
||||
continue
|
||||
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
||||
if not juicy_stream:
|
||||
logger.error("[ANIMEPAHE-ERROR]: failed to find juicy stream")
|
||||
return
|
||||
continue
|
||||
juicy_stream = juicy_stream.group(1)
|
||||
# add the link
|
||||
streams["links"].append(
|
||||
@@ -233,4 +207,119 @@ class AnimePaheApi(AnimeProvider):
|
||||
"link": juicy_stream,
|
||||
}
|
||||
)
|
||||
yield streams
|
||||
return streams
|
||||
|
||||
@debug_provider
|
||||
def get_episode_streams(
|
||||
self, anime_id, episode_number: str, translation_type, **kwargs
|
||||
):
|
||||
anime_title = ""
|
||||
# extract episode details from memory
|
||||
anime_info = self.store.get(str(anime_id), "anime_info")
|
||||
if not anime_info:
|
||||
logger.error(
|
||||
f"[ANIMEPAHE-ERROR]: Anime with ID {anime_id} not found in store"
|
||||
)
|
||||
return
|
||||
|
||||
anime_title = anime_info["title"]
|
||||
episode = next(
|
||||
(
|
||||
ep
|
||||
for ep in anime_info["data"]
|
||||
if float(ep["episode"]) == float(episode_number)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not episode:
|
||||
logger.error(
|
||||
f"[ANIMEPAHE-ERROR]: Episode {episode_number} doesn't exist for anime {anime_title}"
|
||||
)
|
||||
return
|
||||
|
||||
# fetch the episode page
|
||||
url = f"{ANIMEPAHE_BASE}/play/{anime_id}/{episode['session']}"
|
||||
response = self.session.get(url)
|
||||
|
||||
response.raise_for_status()
|
||||
# get the element containing links to juicy streams
|
||||
c = get_element_by_id("resolutionMenu", response.text)
|
||||
resolutionMenuItems = get_elements_html_by_class("dropdown-item", c)
|
||||
# convert the elements containing embed links to a neat dict containing:
|
||||
# data-src
|
||||
# data-audio
|
||||
# data-resolution
|
||||
res_dicts = [extract_attributes(item) for item in resolutionMenuItems]
|
||||
if _server := self._get_server(
|
||||
episode, res_dicts, anime_title, translation_type
|
||||
):
|
||||
yield _server
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import subprocess
|
||||
|
||||
animepahe = AnimePahe(cache_requests="True", use_persistent_provider_store="False")
|
||||
search_term = input("Enter the search term for the anime: ")
|
||||
translation_type = input("Enter the translation type (sub/dub): ")
|
||||
|
||||
search_results = animepahe.search_for_anime(
|
||||
search_keywords=search_term, translation_type=translation_type
|
||||
)
|
||||
|
||||
if not search_results or not search_results["results"]:
|
||||
print("No results found.")
|
||||
exit()
|
||||
|
||||
print("Search Results:")
|
||||
for idx, result in enumerate(search_results["results"], start=1):
|
||||
print(f"{idx}. {result['title']} (ID: {result['id']})")
|
||||
|
||||
anime_choice = int(input("Enter the number of the anime you want to watch: ")) - 1
|
||||
anime_id = search_results["results"][anime_choice]["id"]
|
||||
|
||||
anime_details = animepahe.get_anime(anime_id)
|
||||
|
||||
if anime_details is None:
|
||||
print("Failed to get anime details.")
|
||||
exit()
|
||||
print(f"Selected Anime: {anime_details['title']}")
|
||||
|
||||
print("Available Episodes:")
|
||||
for idx, episode in enumerate(
|
||||
sorted(anime_details["availableEpisodesDetail"][translation_type], key=float),
|
||||
start=1,
|
||||
):
|
||||
print(f"{idx}. Episode {episode}")
|
||||
|
||||
episode_choice = (
|
||||
int(input("Enter the number of the episode you want to watch: ")) - 1
|
||||
)
|
||||
episode_number = anime_details["availableEpisodesDetail"][translation_type][
|
||||
episode_choice
|
||||
]
|
||||
|
||||
streams = list(
|
||||
animepahe.get_episode_streams(anime_id, episode_number, translation_type)
|
||||
)
|
||||
if not streams:
|
||||
print("No streams available.")
|
||||
exit()
|
||||
|
||||
print("Available Streams:")
|
||||
for idx, stream in enumerate(streams, start=1):
|
||||
print(f"{idx}. Server: {stream['server']}")
|
||||
|
||||
server_choice = int(input("Enter the number of the server you want to use: ")) - 1
|
||||
selected_stream = streams[server_choice]
|
||||
|
||||
stream_link = selected_stream["links"][0]["link"]
|
||||
mpv_args = ["mpv", stream_link]
|
||||
headers = selected_stream["headers"]
|
||||
if headers:
|
||||
mpv_headers = "--http-header-fields="
|
||||
for header_name, header_value in headers.items():
|
||||
mpv_headers += f"{header_name}:{header_value},"
|
||||
mpv_args.append(mpv_headers)
|
||||
subprocess.run(mpv_args)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import re
|
||||
|
||||
ANIMEPAHE = "animepahe.ru"
|
||||
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
|
||||
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
|
||||
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api"
|
||||
|
||||
SERVERS_AVAILABLE = ["kwik"]
|
||||
REQUEST_HEADERS = {
|
||||
@@ -31,3 +33,5 @@ SERVER_HEADERS = {
|
||||
"Priority": "u=4",
|
||||
"TE": "trailers",
|
||||
}
|
||||
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
||||
KWIK_RE = re.compile(r"Player\|(.+?)'")
|
||||
|
||||
@@ -10,7 +10,6 @@ from .providers_store import ProviderStore
|
||||
class AnimeProvider:
|
||||
session: requests.Session
|
||||
|
||||
PROVIDER = ""
|
||||
USER_AGENT = random_user_agent()
|
||||
HEADERS = {}
|
||||
|
||||
@@ -19,7 +18,10 @@ class AnimeProvider:
|
||||
from ..common.requests_cacher import CachedRequestsSession
|
||||
|
||||
self.session = CachedRequestsSession(
|
||||
os.path.join(APP_CACHE_DIR, "cached_requests.db")
|
||||
os.path.join(APP_CACHE_DIR, "cached_requests.db"),
|
||||
max_lifetime=int(
|
||||
os.environ.get("FASTANIME_MAX_CACHE_LIFETIME", 259200)
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.session = requests.session()
|
||||
@@ -27,7 +29,7 @@ class AnimeProvider:
|
||||
if use_persistent_provider_store.lower() == "true":
|
||||
self.store = ProviderStore(
|
||||
"persistent",
|
||||
self.PROVIDER,
|
||||
self.__class__.__name__,
|
||||
os.path.join(APP_CACHE_DIR, "anime_providers_store.db"),
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -5,21 +5,19 @@ import os
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def debug_provider(provider_name: str):
|
||||
def _provider_function_decorator(provider_function):
|
||||
@functools.wraps(provider_function)
|
||||
def _provider_function_wrapper(*args, **kwargs):
|
||||
if not os.environ.get("FASTANIME_DEBUG"):
|
||||
try:
|
||||
return provider_function(*args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"[{provider_name}@{provider_function.__name__}]: {e}")
|
||||
else:
|
||||
return provider_function(*args, **kwargs)
|
||||
def debug_provider(provider_function):
|
||||
@functools.wraps(provider_function)
|
||||
def _provider_function_wrapper(self, *args, **kwargs):
|
||||
provider_name = self.__class__.__name__.upper()
|
||||
if not os.environ.get("FASTANIME_DEBUG"):
|
||||
try:
|
||||
return provider_function(self, *args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"[{provider_name}@{provider_function.__name__}]: {e}")
|
||||
else:
|
||||
return provider_function(self, *args, **kwargs)
|
||||
|
||||
return _provider_function_wrapper
|
||||
|
||||
return _provider_function_decorator
|
||||
return _provider_function_wrapper
|
||||
|
||||
|
||||
def ensure_internet_connection(provider_function):
|
||||
|
||||
@@ -17,6 +17,7 @@ from ..base_provider import AnimeProvider
|
||||
from ..decorators import debug_provider
|
||||
from ..utils import give_random_quality
|
||||
from .constants import SERVERS_AVAILABLE
|
||||
from .extractors import MegaCloud
|
||||
from .types import HiAnimeStream
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,13 +39,11 @@ class ParseAnchorAndImgTag(HTMLParser):
|
||||
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
||||
|
||||
|
||||
class HiAnimeApi(AnimeProvider):
|
||||
class HiAnime(AnimeProvider):
|
||||
# HEADERS = {"Referer": "https://hianime.to/home"}
|
||||
|
||||
PROVIDER = "hianime"
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def search_for_anime(self, anime_title: str, *args):
|
||||
@debug_provider
|
||||
def search_for_anime(self, anime_title: str, translation_type, **kwargs):
|
||||
query = quote_plus(anime_title)
|
||||
url = f"https://hianime.to/search?keyword={query}"
|
||||
response = self.session.get(url)
|
||||
@@ -91,8 +90,8 @@ class HiAnimeApi(AnimeProvider):
|
||||
self.store.set(result["id"], "search_result", result)
|
||||
return {"pageInfo": {}, "results": results}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_anime(self, hianime_id, *args):
|
||||
@debug_provider
|
||||
def get_anime(self, hianime_id, **kwargs):
|
||||
anime_result = {}
|
||||
if d := self.store.get(str(hianime_id), "search_result"):
|
||||
anime_result = d
|
||||
@@ -144,8 +143,8 @@ class HiAnimeApi(AnimeProvider):
|
||||
"episodes_info": episodes_info,
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
def get_episode_streams(self, anime_id, episode, translation_type, *args):
|
||||
@debug_provider
|
||||
def get_episode_streams(self, anime_id, episode, translation_type, **kwargs):
|
||||
if d := self.store.get(str(anime_id), "anime_info"):
|
||||
episodes_info = d
|
||||
episode_details = [
|
||||
@@ -191,53 +190,85 @@ class HiAnimeApi(AnimeProvider):
|
||||
if not servers_html:
|
||||
return
|
||||
|
||||
@debug_provider(self.PROVIDER.upper())
|
||||
@debug_provider
|
||||
def _get_server(server_name, server_html):
|
||||
# keys: [ data-type: translation_type, data-id: embed_id, data-server-id: server_id ]
|
||||
servers_info = extract_attributes(server_html)
|
||||
embed_url = f"https://hianime.to/ajax/v2/episode/sources?id={servers_info['data-id']}"
|
||||
server_id = servers_info["data-id"]
|
||||
embed_url = (
|
||||
f"https://hianime.to/ajax/v2/episode/sources?id={server_id}"
|
||||
)
|
||||
embed_response = self.session.get(embed_url)
|
||||
if embed_response.ok:
|
||||
embed_json = embed_response.json()
|
||||
raw_link_to_streams = embed_json["link"]
|
||||
match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams)
|
||||
if not match:
|
||||
return
|
||||
provider_domain = match.group(1)
|
||||
embed_type = match.group(2)
|
||||
episode_number = match.group(3)
|
||||
source_id = match.group(4)
|
||||
match server_name:
|
||||
# TODO: Finish the other servers
|
||||
case "HD2":
|
||||
data = MegaCloud(self.session).extract(
|
||||
raw_link_to_streams
|
||||
)
|
||||
return {
|
||||
"headers": {},
|
||||
"subtitles": [
|
||||
{
|
||||
"url": track["file"],
|
||||
"language": track["label"],
|
||||
}
|
||||
for track in data["tracks"]
|
||||
if track["kind"] == "captions"
|
||||
],
|
||||
"server": server_name,
|
||||
"episode_title": episode_details["title"],
|
||||
"links": give_random_quality(
|
||||
[
|
||||
{"link": link["url"]}
|
||||
for link in data["sources"]
|
||||
]
|
||||
),
|
||||
}
|
||||
case _:
|
||||
# NOTE: THIS METHOD DOES'NT WORK will get the other servers later
|
||||
match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams)
|
||||
if not match:
|
||||
return
|
||||
provider_domain = match.group(1)
|
||||
embed_type = match.group(2)
|
||||
episode_number = match.group(3)
|
||||
source_id = match.group(4)
|
||||
|
||||
link_to_streams = f"https://{provider_domain}/embed-{embed_type}/ajax/e-{episode_number}/getSources?id={source_id}"
|
||||
link_to_streams_response = self.session.get(link_to_streams)
|
||||
if link_to_streams_response.ok:
|
||||
juicy_streams_json: "HiAnimeStream" = (
|
||||
link_to_streams_response.json()
|
||||
)
|
||||
# TODO: Hianime decided to fucking encrypt shit
|
||||
# so got to fix it later
|
||||
return {
|
||||
"headers": {},
|
||||
"subtitles": [
|
||||
{
|
||||
"url": track["file"],
|
||||
"language": track["label"],
|
||||
link_to_streams = f"https://{provider_domain}/embed-{embed_type}/ajax/e-{episode_number}/getSources?id={source_id}"
|
||||
link_to_streams_response = self.session.get(
|
||||
link_to_streams
|
||||
)
|
||||
if link_to_streams_response.ok:
|
||||
juicy_streams_json: "HiAnimeStream" = (
|
||||
link_to_streams_response.json()
|
||||
)
|
||||
|
||||
return {
|
||||
"headers": {},
|
||||
"subtitles": [
|
||||
{
|
||||
"url": track["file"],
|
||||
"language": track["label"],
|
||||
}
|
||||
for track in juicy_streams_json["tracks"]
|
||||
if track["kind"] == "captions"
|
||||
],
|
||||
"server": server_name,
|
||||
"episode_title": episode_details["title"],
|
||||
"links": give_random_quality(
|
||||
[
|
||||
{"link": link["file"]}
|
||||
for link in juicy_streams_json["tracks"]
|
||||
]
|
||||
),
|
||||
}
|
||||
for track in juicy_streams_json["tracks"]
|
||||
if track["kind"] == "captions"
|
||||
],
|
||||
"server": server_name,
|
||||
"episode_title": episode_details["title"],
|
||||
"links": give_random_quality(
|
||||
[
|
||||
{"link": link["file"]}
|
||||
for link in juicy_streams_json["tracks"]
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
for server_name, server_html in zip(
|
||||
cycle(SERVERS_AVAILABLE), servers_html
|
||||
):
|
||||
if server := _get_server(server_name, server_html):
|
||||
yield server
|
||||
if server_name == "HD2":
|
||||
if server := _get_server(server_name, server_html):
|
||||
yield server
|
||||
|
||||
@@ -1 +1,26 @@
|
||||
SERVERS_AVAILABLE = ["HD1", "HD2", "StreamSB", "StreamTape"]
|
||||
""""
|
||||
| "hd-1"
|
||||
| "hd-2"
|
||||
| "megacloud"
|
||||
| "streamsb"
|
||||
| "streamtape";
|
||||
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
VidStreaming = "hd-1",
|
||||
MegaCloud = "megacloud",
|
||||
StreamSB = "streamsb",
|
||||
StreamTape = "streamtape",
|
||||
VidCloud = "hd-2",
|
||||
AsianLoad = "asianload",
|
||||
GogoCDN = "gogocdn",
|
||||
MixDrop = "mixdrop",
|
||||
UpCloud = "upcloud",
|
||||
VizCloud = "vizcloud",
|
||||
MyCloud = "mycloud",
|
||||
Filemoon = "filemoon",
|
||||
|
||||
"""
|
||||
|
||||
191
fastanime/libs/anime_provider/hianime/extractors.py
Normal file
191
fastanime/libs/anime_provider/hianime/extractors.py
Normal file
@@ -0,0 +1,191 @@
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from base64 import b64decode
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...common.requests_cacher import CachedRequestsSession
|
||||
|
||||
|
||||
# Constants
|
||||
megacloud = {
|
||||
"script": "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=",
|
||||
"sources": "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=",
|
||||
}
|
||||
|
||||
|
||||
class HiAnimeError(Exception):
|
||||
def __init__(self, message, context, status_code):
|
||||
super().__init__(f"{context}: {message} (Status: {status_code})")
|
||||
self.context = context
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
# Adapted from https://github.com/ghoshRitesh12/aniwatch
|
||||
class MegaCloud:
|
||||
def __init__(self, session):
|
||||
self.session: "CachedRequestsSession" = session
|
||||
|
||||
def extract(self, video_url: str) -> Dict:
|
||||
try:
|
||||
extracted_data = {
|
||||
"tracks": [],
|
||||
"intro": {"start": 0, "end": 0},
|
||||
"outro": {"start": 0, "end": 0},
|
||||
"sources": [],
|
||||
}
|
||||
|
||||
video_id = video_url.split("/")[-1].split("?")[0]
|
||||
response = self.session.get(
|
||||
megacloud["sources"] + video_id,
|
||||
headers={
|
||||
"Accept": "*/*",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Referer": video_url,
|
||||
},
|
||||
fresh=1, # pyright: ignore
|
||||
)
|
||||
srcs_data = response.json()
|
||||
|
||||
if not srcs_data:
|
||||
raise HiAnimeError(
|
||||
"Url may have an invalid video id", "getAnimeEpisodeSources", 400
|
||||
)
|
||||
|
||||
encrypted_string = srcs_data["sources"]
|
||||
if not srcs_data["encrypted"] and isinstance(encrypted_string, list):
|
||||
extracted_data.update(
|
||||
{
|
||||
"intro": srcs_data["intro"],
|
||||
"outro": srcs_data["outro"],
|
||||
"tracks": srcs_data["tracks"],
|
||||
"sources": [
|
||||
{"url": s["file"], "type": s["type"]}
|
||||
for s in encrypted_string
|
||||
],
|
||||
}
|
||||
)
|
||||
return extracted_data
|
||||
|
||||
# Fetch decryption script
|
||||
script_response = self.session.get(
|
||||
megacloud["script"] + str(int(time.time() * 1000)),
|
||||
fresh=1, # pyright: ignore
|
||||
)
|
||||
script_text = script_response.text
|
||||
if not script_text:
|
||||
raise HiAnimeError(
|
||||
"Couldn't fetch script to decrypt resource",
|
||||
"getAnimeEpisodeSources",
|
||||
500,
|
||||
)
|
||||
|
||||
vars_ = self.extract_variables(script_text)
|
||||
if not vars_:
|
||||
raise Exception(
|
||||
"Can't find variables. Perhaps the extractor is outdated."
|
||||
)
|
||||
|
||||
secret, encrypted_source = self.get_secret(encrypted_string, vars_)
|
||||
decrypted = self.decrypt(encrypted_source, secret)
|
||||
|
||||
try:
|
||||
sources = json.loads(decrypted)
|
||||
extracted_data.update(
|
||||
{
|
||||
"intro": srcs_data["intro"],
|
||||
"outro": srcs_data["outro"],
|
||||
"tracks": srcs_data["tracks"],
|
||||
"sources": [
|
||||
{"url": s["file"], "type": s["type"]} for s in sources
|
||||
],
|
||||
}
|
||||
)
|
||||
return extracted_data
|
||||
except Exception:
|
||||
raise HiAnimeError(
|
||||
"Failed to decrypt resource", "getAnimeEpisodeSources", 500
|
||||
)
|
||||
except Exception as err:
|
||||
raise err
|
||||
|
||||
def extract_variables(self, text: str) -> List[List[int]]:
|
||||
regex = r"case\s*0x[0-9a-f]+:(?![^;]*=partKey)\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);"
|
||||
matches = re.finditer(regex, text)
|
||||
vars_ = []
|
||||
for match in matches:
|
||||
key1 = self.matching_key(match[1], text)
|
||||
key2 = self.matching_key(match[2], text)
|
||||
try:
|
||||
vars_.append([int(key1, 16), int(key2, 16)])
|
||||
except ValueError:
|
||||
continue
|
||||
return vars_
|
||||
|
||||
def get_secret(
|
||||
self, encrypted_string: str, values: List[List[int]]
|
||||
) -> tuple[str, str]:
|
||||
secret = []
|
||||
encrypted_source_array = list(encrypted_string)
|
||||
current_index = 0
|
||||
|
||||
for start, length in values:
|
||||
start += current_index
|
||||
end = start + length
|
||||
secret.extend(encrypted_string[start:end])
|
||||
encrypted_source_array[start:end] = [""] * length
|
||||
current_index += length
|
||||
|
||||
encrypted_source = "".join(encrypted_source_array) # .replace("\x00", "")
|
||||
return ("".join(secret), encrypted_source)
|
||||
|
||||
def decrypt(self, encrypted: str, key_or_secret: str, maybe_iv: str = "") -> str:
|
||||
if maybe_iv:
|
||||
key = key_or_secret.encode()
|
||||
iv = maybe_iv.encode()
|
||||
contents = encrypted
|
||||
else:
|
||||
# Decode the Base64 string
|
||||
cypher = b64decode(encrypted)
|
||||
|
||||
# Extract the salt from the cypher text
|
||||
salt = cypher[8:16]
|
||||
|
||||
# Combine the key_or_secret with the salt
|
||||
password = key_or_secret.encode() + salt
|
||||
|
||||
# Generate MD5 hashes
|
||||
md5_hashes = []
|
||||
digest = password
|
||||
for _ in range(3):
|
||||
md5 = hashlib.md5()
|
||||
md5.update(digest)
|
||||
md5_hashes.append(md5.digest())
|
||||
digest = md5_hashes[-1] + password
|
||||
|
||||
# Derive the key and IV
|
||||
key = md5_hashes[0] + md5_hashes[1]
|
||||
iv = md5_hashes[2]
|
||||
|
||||
# Extract the encrypted contents
|
||||
contents = cypher[16:]
|
||||
|
||||
# Initialize the AES decipher
|
||||
decipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
|
||||
# Decrypt and decode
|
||||
decrypted = decipher.decrypt(contents).decode("utf-8") # pyright: ignore
|
||||
|
||||
# Remove any padding (PKCS#7)
|
||||
pad = ord(decrypted[-1])
|
||||
return decrypted[:-pad]
|
||||
|
||||
def matching_key(self, value: str, script: str) -> str:
|
||||
match = re.search(rf",{value}=((?:0x)?[0-9a-fA-F]+)", script)
|
||||
if match:
|
||||
return match.group(1).replace("0x", "")
|
||||
raise Exception("Failed to match the key")
|
||||
0
fastanime/libs/anime_provider/nyaa/__init__.py
Normal file
0
fastanime/libs/anime_provider/nyaa/__init__.py
Normal file
@@ -27,11 +27,10 @@ EXTRACT_USEFUL_INFO_PATTERN_2 = re.compile(
|
||||
)
|
||||
|
||||
|
||||
class NyaaApi(AnimeProvider):
|
||||
class Nyaa(AnimeProvider):
|
||||
search_results: SearchResults
|
||||
PROVIDER = "nyaa"
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def search_for_anime(self, user_query: str, *args, **_):
|
||||
self.search_results = search_for_anime_with_anilist(
|
||||
user_query, True
|
||||
@@ -39,7 +38,7 @@ class NyaaApi(AnimeProvider):
|
||||
self.user_query = user_query
|
||||
return self.search_results
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def get_anime(self, anilist_id: str, *_):
|
||||
for anime in self.search_results["results"]:
|
||||
if anime["id"] == anilist_id:
|
||||
@@ -55,7 +54,7 @@ class NyaaApi(AnimeProvider):
|
||||
},
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def get_episode_streams(
|
||||
self,
|
||||
anime_id: str,
|
||||
|
||||
0
fastanime/libs/anime_provider/yugen/__init__.py
Normal file
0
fastanime/libs/anime_provider/yugen/__init__.py
Normal file
@@ -1,32 +1,32 @@
|
||||
import base64
|
||||
import re
|
||||
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,
|
||||
get_element_text_and_html_by_tag,
|
||||
get_elements_text_and_html_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
|
||||
from ..decorators import debug_provider
|
||||
from .constants import SEARCH_URL, YUGEN_ENDPOINT
|
||||
|
||||
|
||||
# ** Adapted from anipy-cli **
|
||||
class YugenApi(AnimeProvider):
|
||||
class Yugen(AnimeProvider):
|
||||
"""
|
||||
Provides a fast and effective interface to YugenApi site.
|
||||
"""
|
||||
|
||||
PROVIDER = "yugen"
|
||||
api_endpoint = YUGEN_ENDPOINT
|
||||
# HEADERS = {
|
||||
# "Referer": ALLANIME_REFERER,
|
||||
# }
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def search_for_anime(
|
||||
self,
|
||||
user_query: str,
|
||||
@@ -94,7 +94,7 @@ class YugenApi(AnimeProvider):
|
||||
"results": results,
|
||||
}
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def get_anime(self, anime_id: str, **kwargs):
|
||||
identifier = base64.b64decode(anime_id).decode()
|
||||
response = self.session.get(f"{YUGEN_ENDPOINT}/anime/{identifier}")
|
||||
@@ -118,7 +118,9 @@ class YugenApi(AnimeProvider):
|
||||
|
||||
if sub_match:
|
||||
eps = int(sub_match.group(1))
|
||||
data_map["availableEpisodesDetail"]["sub"] = list(map(str,range(1, eps + 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>',
|
||||
@@ -127,7 +129,9 @@ class YugenApi(AnimeProvider):
|
||||
|
||||
if dub_match:
|
||||
eps = int(dub_match.group(1))
|
||||
data_map["availableEpisodesDetail"]["dub"] = list(map(str,range(1, eps + 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:
|
||||
@@ -174,7 +178,7 @@ class YugenApi(AnimeProvider):
|
||||
|
||||
return data_map
|
||||
|
||||
@debug_provider(PROVIDER.upper())
|
||||
@debug_provider
|
||||
def get_episode_streams(
|
||||
self, anime_id, episode_number: str, translation_type="sub"
|
||||
):
|
||||
@@ -212,5 +216,10 @@ class YugenApi(AnimeProvider):
|
||||
"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"])],
|
||||
"links": [
|
||||
{"quality": quality, "link": link}
|
||||
for quality, link in zip(
|
||||
cycle(["1080", "720", "480", "360"]), res["hls"]
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
YUGEN_ENDPOINT: str = "https://yugenanime.tv"
|
||||
|
||||
SEARCH_URL = YUGEN_ENDPOINT + "/api/discover/"
|
||||
|
||||
@@ -246,7 +246,7 @@ def get_mal_id_and_anilist_id(anime_title: str) -> "dict[str,int] | None":
|
||||
)
|
||||
return {"id_anilist": anime["id"], "id_mal": anime["idMal"]}
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
logger.error(f"Something unexpected occurred {e}")
|
||||
|
||||
|
||||
def get_basic_anime_info_by_title(anime_title: str):
|
||||
@@ -320,4 +320,4 @@ def get_basic_anime_info_by_title(anime_title: str):
|
||||
],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Something unexpected occured {e}")
|
||||
logger.error(f"Something unexpected occurred {e}")
|
||||
|
||||
@@ -80,7 +80,8 @@ class CachedRequestsSession(requests.Session):
|
||||
response_headers TEXT,
|
||||
data BLOB,
|
||||
redirection_policy INT,
|
||||
cache_expiry INTEGER
|
||||
cache_expiry INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)"""
|
||||
)
|
||||
|
||||
@@ -117,6 +118,8 @@ class CachedRequestsSession(requests.Session):
|
||||
url = ?
|
||||
AND redirection_policy = ?
|
||||
AND cache_expiry > ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(url, redirection_policy, int(time.time())),
|
||||
)
|
||||
@@ -162,8 +165,15 @@ class CachedRequestsSession(requests.Session):
|
||||
logger.debug("Caching the current request")
|
||||
cursor.execute(
|
||||
f"""
|
||||
INSERT INTO {self.table_name}
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO {self.table_name} (
|
||||
url,
|
||||
status_code,
|
||||
request_headers,
|
||||
response_headers,
|
||||
data,
|
||||
redirection_policy,
|
||||
cache_expiry
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
url,
|
||||
|
||||
11
fastanime/libs/discord/discord.py
Normal file
11
fastanime/libs/discord/discord.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from pypresence import Presence
|
||||
import time
|
||||
|
||||
def discord_connect(show, episode, switch):
|
||||
presence = Presence(client_id = '1292070065583165512')
|
||||
presence.connect()
|
||||
if not switch.is_set():
|
||||
presence.update(details = show, state = "Watching episode "+episode)
|
||||
time.sleep(10)
|
||||
else:
|
||||
presence.close()
|
||||
@@ -8,10 +8,12 @@ from typing import Callable, List
|
||||
from click import clear
|
||||
from rich import print
|
||||
|
||||
from ...cli.utils.tools import exit_app
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
FZF_DEFAULT_OPTS = """
|
||||
FZF_DEFAULT_OPTS = """
|
||||
--color=fg:#d0d0d0,fg+:#d0d0d0,bg:#121212,bg+:#262626
|
||||
--color=hl:#5f87af,hl+:#5fd7ff,info:#afaf87,marker:#87ff00
|
||||
--color=prompt:#d7005f,spinner:#af5fff,pointer:#af5fff,header:#87afaf
|
||||
@@ -127,7 +129,10 @@ class FZF:
|
||||
encoding="utf-8",
|
||||
)
|
||||
if not result or result.returncode != 0 or not result.stdout:
|
||||
print("sth went wrong:confused:")
|
||||
if result.returncode == 130: # fzf terminated by ctrl-c
|
||||
exit_app()
|
||||
|
||||
print("sth went wrong :confused:")
|
||||
input("press enter to try again...")
|
||||
clear()
|
||||
return self._run_fzf(commands, _fzf_input)
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
pythonPackages = python.pkgs;
|
||||
fastanimeEnv = pythonPackages.buildPythonApplication {
|
||||
pname = "fastanime";
|
||||
version = "2.8.2";
|
||||
version = "2.8.8";
|
||||
|
||||
src = ./.;
|
||||
|
||||
preBuild = ''
|
||||
sed -i 's/rich>=13.9.2/rich>=13.8.1/' pyproject.toml
|
||||
sed -i 's/pycryptodome>=3.21.0/pycryptodome>=3.20.0/' pyproject.toml
|
||||
'';
|
||||
|
||||
# Add runtime dependencies
|
||||
@@ -35,6 +36,8 @@
|
||||
plyer
|
||||
mpv
|
||||
fastapi
|
||||
pycryptodome
|
||||
pypresence
|
||||
];
|
||||
|
||||
# Ensure compatibility with the pyproject.toml
|
||||
|
||||
@@ -8,7 +8,7 @@ sed -i "s/^version.*/version = \"$VERSION\"/" "$CLI_DIR/pyproject.toml" &&
|
||||
sed -i "s/version = .*/version = \"$VERSION\";/" "$CLI_DIR/flake.nix" &&
|
||||
git stage "$CLI_DIR/pyproject.toml" "$CLI_DIR/fastanime/__init__.py" "$CLI_DIR/flake.nix" &&
|
||||
git commit -m "chore: bump version (v$VERSION)" &&
|
||||
nix flake lock &&
|
||||
# nix flake lock &&
|
||||
uv lock &&
|
||||
git stage "$CLI_DIR/flake.lock" "$CLI_DIR/uv.lock" &&
|
||||
git commit -m "chore: update lock files" &&
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "fastanime"
|
||||
version = "2.8.2"
|
||||
version = "2.8.8"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
license = "UNLICENSE"
|
||||
readme = "README.md"
|
||||
@@ -8,10 +8,12 @@ requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"click>=8.1.7",
|
||||
"inquirerpy>=0.3.4",
|
||||
"pycryptodome>=3.21.0",
|
||||
"pypresence>=4.3.0",
|
||||
"requests>=2.32.3",
|
||||
"rich>=13.9.2",
|
||||
"thefuzz>=0.22.1",
|
||||
"yt-dlp>=2024.10.7",
|
||||
"yt-dlp[default]>=2024.10.7",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -29,6 +31,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"pre-commit>=4.0.1",
|
||||
"pyinstaller>=6.11.1",
|
||||
"pyright>=1.1.384",
|
||||
"pytest>=8.3.3",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastanime.cli import run_cli
|
||||
|
||||
@@ -151,7 +152,7 @@ def test_anilist_watching_help(runner: CliRunner):
|
||||
|
||||
|
||||
def test_check_for_updates_not_called_on_completions(runner):
|
||||
with patch('fastanime.cli.app_updater.check_for_updates') as mock_check_for_updates:
|
||||
with patch("fastanime.cli.app_updater.check_for_updates") as mock_check_for_updates:
|
||||
result = runner.invoke(run_cli, ["completions"])
|
||||
assert result.exit_code == 0
|
||||
mock_check_for_updates.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user