mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-13 00:00:01 -08:00
Compare commits
102 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 |
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:
|
default_language_version:
|
||||||
python: python3.10
|
python: python3.12
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
@@ -7,7 +7,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
args: ["--profile", "black"] # Ensure compatibility with Black
|
args: ["--profile", "black"]
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/autoflake
|
- repo: https://github.com/PyCQA/autoflake
|
||||||
rev: v2.2.1
|
rev: v2.2.1
|
||||||
@@ -19,17 +19,15 @@ repos:
|
|||||||
"--remove-unused-variables",
|
"--remove-unused-variables",
|
||||||
"--remove-all-unused-imports",
|
"--remove-all-unused-imports",
|
||||||
]
|
]
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
# - repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# rev: v0.4.10
|
||||||
rev: v0.4.10
|
# hooks:
|
||||||
hooks:
|
# - id: ruff
|
||||||
# Run the linter.
|
# args: [--fix]
|
||||||
- id: ruff
|
|
||||||
args: [--fix]
|
|
||||||
|
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
rev: 24.4.2
|
rev: 24.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
name: 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
|
||||||
|
}
|
||||||
109
README.md
109
README.md
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
<b>My Rice</b>
|
<b>Riced</b>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
**Anilist results menu:**
|
**Anilist results menu:**
|
||||||
@@ -43,62 +43,6 @@
|
|||||||
|
|
||||||
</details>
|
</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
|
## Installation
|
||||||
|
|
||||||

|

|
||||||
@@ -119,6 +63,26 @@ If you have any difficulty consult for help on the [discord channel](https://dis
|
|||||||
nix profile install github:Benexl/fastanime
|
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
|
||||||
|
yay -S fastanime-git
|
||||||
|
```
|
||||||
|
|
||||||
|
or the stable version, which uses a tagged release:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay -S fastanime
|
||||||
|
```
|
||||||
|
|
||||||
### Installation using your favourite package manager
|
### Installation using your favourite package manager
|
||||||
|
|
||||||
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
|
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
|
||||||
@@ -128,7 +92,7 @@ With the following extras available:
|
|||||||
- api - which installs dependencies required to use `fastanime serve`
|
- api - which installs dependencies required to use `fastanime serve`
|
||||||
- mpv - which installs python mpv
|
- mpv - which installs python mpv
|
||||||
- notifications - which installs plyer required for desktop notifications
|
- notifications - which installs plyer required for desktop notifications
|
||||||
|
|
||||||
#### Using uv
|
#### 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/).
|
||||||
@@ -807,7 +771,7 @@ rofi_theme_input =
|
|||||||
|
|
||||||
rofi_theme_confirm =
|
rofi_theme_confirm =
|
||||||
|
|
||||||
notification_duration = 2
|
notification_duration = 120
|
||||||
|
|
||||||
sub_lang = eng
|
sub_lang = eng
|
||||||
|
|
||||||
@@ -848,29 +812,26 @@ format = best[height<=1080]/bestvideo[height<=1080]+bestaudio/best
|
|||||||
player = mpv
|
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
|
## Contributing
|
||||||
|
|
||||||
We welcome your issues and feature requests. However, due to time constraints, I currently do not plan to add another provider.
|
pr's are highly welcome
|
||||||
But if you are willing to add one yourself pr's are 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/Benexl/FastAnime/blob/master/fastanime/Utility/data.py) and open a pr, i will ignore issues 😝.
|
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 😝.
|
||||||
|
|
||||||
## 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>
|
|
||||||
|
|
||||||
## Supporting the Project
|
## Supporting the Project
|
||||||
|
|
||||||
More pr's less issues 🙃
|
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/benexl).
|
Show your support by starring the GitHub repository.
|
||||||
|
|
||||||
|
[](https://ko-fi.com/Y8Y8ZAA7N)
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
|
|||||||
@@ -62,11 +62,7 @@ class AnimeProvider:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def search_for_anime(
|
def search_for_anime(
|
||||||
self,
|
self, search_keywords, translation_type, **kwargs
|
||||||
user_query,
|
|
||||||
translation_type,
|
|
||||||
nsfw=True,
|
|
||||||
unknown=True,
|
|
||||||
) -> "SearchResults | None":
|
) -> "SearchResults | None":
|
||||||
"""core abstraction over all providers search functionality
|
"""core abstraction over all providers search functionality
|
||||||
|
|
||||||
@@ -82,7 +78,7 @@ class AnimeProvider:
|
|||||||
"""
|
"""
|
||||||
anime_provider = self.anime_provider
|
anime_provider = self.anime_provider
|
||||||
results = anime_provider.search_for_anime(
|
results = anime_provider.search_for_anime(
|
||||||
user_query, translation_type, nsfw, unknown
|
search_keywords, translation_type, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -90,6 +86,7 @@ class AnimeProvider:
|
|||||||
def get_anime(
|
def get_anime(
|
||||||
self,
|
self,
|
||||||
anime_id: str,
|
anime_id: str,
|
||||||
|
**kwargs,
|
||||||
) -> "Anime | None":
|
) -> "Anime | None":
|
||||||
"""core abstraction over getting info of an anime from all providers
|
"""core abstraction over getting info of an anime from all providers
|
||||||
|
|
||||||
@@ -101,7 +98,7 @@ class AnimeProvider:
|
|||||||
[TODO:return]
|
[TODO:return]
|
||||||
"""
|
"""
|
||||||
anime_provider = self.anime_provider
|
anime_provider = self.anime_provider
|
||||||
results = anime_provider.get_anime(anime_id)
|
results = anime_provider.get_anime(anime_id, **kwargs)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -110,6 +107,7 @@ class AnimeProvider:
|
|||||||
anime_id,
|
anime_id,
|
||||||
episode: str,
|
episode: str,
|
||||||
translation_type: str,
|
translation_type: str,
|
||||||
|
**kwargs,
|
||||||
) -> "Iterator[Server] | None":
|
) -> "Iterator[Server] | None":
|
||||||
"""core abstractions for getting juicy streams from all providers
|
"""core abstractions for getting juicy streams from all providers
|
||||||
|
|
||||||
@@ -124,6 +122,6 @@ class AnimeProvider:
|
|||||||
"""
|
"""
|
||||||
anime_provider = self.anime_provider
|
anime_provider = self.anime_provider
|
||||||
results = anime_provider.get_episode_streams(
|
results = anime_provider.get_episode_streams(
|
||||||
anime_id, episode, translation_type
|
anime_id, episode, translation_type, **kwargs
|
||||||
)
|
)
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ anime_normalizer_raw = {
|
|||||||
"Re:Zero kara Hajimeru Isekai Seikatsu Season 3": "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season",
|
"Re:Zero kara Hajimeru Isekai Seikatsu Season 3": "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season",
|
||||||
},
|
},
|
||||||
"hianime": {"My Star": "Oshi no Ko"},
|
"hianime": {"My Star": "Oshi no Ko"},
|
||||||
"animepahe": {
|
"animepahe": {
|
||||||
"Azumanga Daiou The Animation": "Azumanga Daioh",
|
"Azumanga Daiou The Animation": "Azumanga Daioh",
|
||||||
"Mairimashita! Iruma-kun 2nd Season": "Mairimashita! Iruma-kun 2",
|
"Mairimashita! Iruma-kun 2nd Season": "Mairimashita! Iruma-kun 2",
|
||||||
"Mairimashita! Iruma-kun 3rd Season": "Mairimashita! Iruma-kun 3"
|
"Mairimashita! Iruma-kun 3rd Season": "Mairimashita! Iruma-kun 3",
|
||||||
},
|
},
|
||||||
"nyaa": {},
|
"nyaa": {},
|
||||||
"yugen": {},
|
"yugen": {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class YtDLPDownloader:
|
|||||||
prompt=True,
|
prompt=True,
|
||||||
force_ffmpeg=False,
|
force_ffmpeg=False,
|
||||||
hls_use_mpegts=False,
|
hls_use_mpegts=False,
|
||||||
|
hls_use_h264=False,
|
||||||
):
|
):
|
||||||
"""Helper function that downloads anime given url and path details
|
"""Helper function that downloads anime given url and path details
|
||||||
|
|
||||||
@@ -97,16 +98,28 @@ class YtDLPDownloader:
|
|||||||
if i == 0:
|
if i == 0:
|
||||||
if force_ffmpeg:
|
if force_ffmpeg:
|
||||||
options = options | {
|
options = options | {
|
||||||
"external_downloader": {
|
"external_downloader": {"default": "ffmpeg"},
|
||||||
'default': 'ffmpeg'
|
|
||||||
},
|
|
||||||
"external_downloader_args": {
|
"external_downloader_args": {
|
||||||
"ffmpeg_i1": ["-v", "error", "-stats"],
|
"ffmpeg_i1": ["-v", "error", "-stats"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if hls_use_mpegts:
|
if hls_use_mpegts:
|
||||||
options = options | {
|
options = options | {
|
||||||
"hls_use_mpegts": hls_use_mpegts,
|
"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:
|
with yt_dlp.YoutubeDL(options) as ydl:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
|||||||
) # noqa: F541
|
) # noqa: F541
|
||||||
|
|
||||||
|
|
||||||
__version__ = "v2.8.4"
|
__version__ = "v2.8.8"
|
||||||
|
|
||||||
APP_NAME = "FastAnime"
|
APP_NAME = "FastAnime"
|
||||||
AUTHOR = "Benexl"
|
AUTHOR = "Benexl"
|
||||||
|
|||||||
@@ -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 {
|
configuration {
|
||||||
font: "Sans 12";
|
font: "Sans 12";
|
||||||
line-margin: 10;
|
line-margin: 10;
|
||||||
@@ -20,12 +15,13 @@ configuration {
|
|||||||
|
|
||||||
window {
|
window {
|
||||||
fullscreen: false;
|
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 {
|
mainbox {
|
||||||
padding: 50px 100px;
|
padding: 50px 50px;
|
||||||
background-color: rgba(0, 0, 0, 1); /* Ensures black background fills entire main area */
|
background-color: transparent; /* Ensures black background fills entire main area */
|
||||||
children: [inputbar, listview];
|
children: [inputbar, listview];
|
||||||
spacing: 20px;
|
spacing: 20px;
|
||||||
}
|
}
|
||||||
@@ -47,7 +43,7 @@ prompt {
|
|||||||
|
|
||||||
entry {
|
entry {
|
||||||
padding: 8px;
|
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 */
|
text-color: #FFFFFF; /* White text to make typing visible */
|
||||||
placeholder: "Search...";
|
placeholder: "Search...";
|
||||||
placeholder-color: rgba(255, 255, 255, 0.5);
|
placeholder-color: rgba(255, 255, 255, 0.5);
|
||||||
@@ -57,19 +53,19 @@ entry {
|
|||||||
listview {
|
listview {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
spacing: 8px;
|
spacing: 8px;
|
||||||
lines: 10;
|
lines: 9;
|
||||||
background-color: @background; /* Consistent black background for list items */
|
background-color: transparent; /* Consistent black background for list items */
|
||||||
}
|
}
|
||||||
|
|
||||||
element {
|
element {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: @background; /* Uniform color for each list item */
|
background-color: transparent; /* Uniform color for each list item */
|
||||||
text-color: @foreground;
|
text-color: @foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
element normal.normal {
|
element normal.normal {
|
||||||
background-color: @background; /* Ensures no alternating color */
|
background-color: transparent; /* Ensures no alternating color */
|
||||||
}
|
}
|
||||||
|
|
||||||
element selected.normal {
|
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 {
|
configuration {
|
||||||
font: "Sans 12";
|
font: "Sans 12";
|
||||||
}
|
}
|
||||||
@@ -14,17 +10,19 @@ configuration {
|
|||||||
window {
|
window {
|
||||||
fullscreen: true;
|
fullscreen: true;
|
||||||
transparency: "real";
|
transparency: "real";
|
||||||
background-color: @background-color;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
mainbox {
|
mainbox {
|
||||||
children: [ message, listview, inputbar ];
|
children: [ message, listview, inputbar ];
|
||||||
padding: 40% 30%;
|
padding: 40% 30%;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
message {
|
message {
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
border-radius:20px;
|
||||||
margin: 0 0 20px 0;
|
margin: 0 0 20px 0;
|
||||||
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
||||||
}
|
}
|
||||||
@@ -42,6 +40,7 @@ prompt {
|
|||||||
|
|
||||||
entry {
|
entry {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
listview {
|
listview {
|
||||||
@@ -52,4 +51,5 @@ listview {
|
|||||||
textbox {
|
textbox {
|
||||||
horizontal-align: 0.5; /* Center the text */
|
horizontal-align: 0.5; /* Center the text */
|
||||||
font: "Sans Bold 24"; /* Match message font */
|
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 {
|
configuration {
|
||||||
font: "Sans 12";
|
font: "Sans 12";
|
||||||
}
|
}
|
||||||
@@ -14,17 +10,19 @@ configuration {
|
|||||||
window {
|
window {
|
||||||
fullscreen: true;
|
fullscreen: true;
|
||||||
transparency: "real";
|
transparency: "real";
|
||||||
background-color: @background-color;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
mainbox {
|
mainbox {
|
||||||
children: [ message, listview, inputbar ];
|
children: [ message, listview, inputbar ];
|
||||||
padding: 40% 30%;
|
padding: 40% 30%;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
message {
|
message {
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
border-radius:20px;
|
||||||
margin: 0 0 20px 0;
|
margin: 0 0 20px 0;
|
||||||
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
||||||
}
|
}
|
||||||
@@ -42,6 +40,7 @@ prompt {
|
|||||||
|
|
||||||
entry {
|
entry {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
listview {
|
listview {
|
||||||
@@ -52,4 +51,5 @@ listview {
|
|||||||
textbox {
|
textbox {
|
||||||
horizontal-align: 0.5; /* Center the text */
|
horizontal-align: 0.5; /* Center the text */
|
||||||
font: "Sans Bold 24"; /* Match message font */
|
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
|
// Colours
|
||||||
* {
|
* {
|
||||||
background-color: transparent;
|
background-color: transparent; /* Transparent background for the global UI */
|
||||||
background: #1D2330;
|
background: #000000; /* Solid black background */
|
||||||
background-transparent: #1D2330A0;
|
background-transparent: #1D2330A0; /* Semi-transparent background */
|
||||||
text-color: #BBBBBB;
|
text-color: #BBBBBB; /* Default text color (light gray) */
|
||||||
text-color-selected: #FFFFFF;
|
text-color-selected: #FFFFFF; /* Text color when selected (white) */
|
||||||
primary: #BB77BB;
|
primary: rgba(53, 132, 228, 0.75); /* Blusish primary color */
|
||||||
important: #BF616A;
|
important: rgba(53, 132, 228, 0.75); /* Bluish primary color */
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration {
|
configuration {
|
||||||
font: "Roboto 17";
|
font: "Roboto 14"; /* Sets the global font to Roboto, size 14 */
|
||||||
show-icons: true;
|
show-icons: true; /* Option to display icons in the UI */
|
||||||
}
|
}
|
||||||
|
|
||||||
window {
|
window {
|
||||||
fullscreen: true;
|
fullscreen: true; /* The window will open in fullscreen */
|
||||||
height: 100%;
|
height: 100%; /* Full window height */
|
||||||
width: 100%;
|
width: 100%; /* Full window width */
|
||||||
transparency: "real";
|
transparency: "real"; /* Real transparency effect */
|
||||||
background-color: @background-transparent;
|
background-color: @background-transparent; /* Transparent background */
|
||||||
border: 0px;
|
border: 0px; /* No border around the window */
|
||||||
border-color: @primary;
|
border-color: @primary; /* Border color set to the primary color */
|
||||||
}
|
}
|
||||||
|
|
||||||
mainbox {
|
mainbox {
|
||||||
children: [prompt, inputbar-box, listview];
|
children: [prompt, inputbar-box, listview]; /* Main box contains prompt, input bar, and list view */
|
||||||
padding: 0px;
|
padding: 0px; /* No padding around the main box */
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt {
|
prompt {
|
||||||
width: 100%;
|
width: 100%; /* Prompt takes full width */
|
||||||
margin: 10px 0px 0px 30px;
|
margin: 10px 0px 0px 30px; /* Margin around the prompt */
|
||||||
text-color: @important;
|
text-color: @important; /* Text color for prompt (important color) */
|
||||||
font: "Roboto Bold 27";
|
font: "Roboto Bold 27"; /* Bold Roboto font, size 27 */
|
||||||
}
|
}
|
||||||
|
|
||||||
listview {
|
listview {
|
||||||
layout: vertical;
|
layout: vertical; /* Vertical layout for list items */
|
||||||
padding: 60px;
|
padding: 10px; /* Padding inside the list view */
|
||||||
dynamic: true;
|
spacing: 20px; /* Space between items in the list */
|
||||||
columns: 7;
|
columns: 8; /* Maximum 8 items per row */
|
||||||
spacing: 20px;
|
dynamic: true; /* Allows the list to dynamically adjust */
|
||||||
horizontal-align: center; /* Center the list items */
|
orientation: horizontal; /* Horizontal orientation for list items */
|
||||||
}
|
}
|
||||||
|
|
||||||
inputbar-box {
|
inputbar-box {
|
||||||
children: [dummy, inputbar, dummy];
|
children: [dummy, inputbar, dummy]; /* Input bar is centered with dummy placeholders */
|
||||||
orientation: horizontal;
|
orientation: horizontal; /* Horizontal layout for input bar */
|
||||||
expand: false;
|
expand: false; /* Does not expand to fill the space */
|
||||||
}
|
}
|
||||||
|
|
||||||
inputbar {
|
inputbar {
|
||||||
children: [textbox-prompt, entry];
|
children: [textbox-prompt, entry]; /* Contains a prompt and an entry field */
|
||||||
margin: 0px;
|
margin: 0px; /* No margin around the input bar */
|
||||||
background-color: @primary;
|
background-color: @primary; /* Background color set to the primary color */
|
||||||
border: 4px;
|
border: 4px; /* Border thickness around the input bar */
|
||||||
border-color: @primary;
|
border-color: @primary; /* Border color matches the primary color */
|
||||||
border-radius: 8px;
|
border-radius: 8px; /* Rounded corners for the input bar */
|
||||||
}
|
}
|
||||||
|
|
||||||
textbox-prompt {
|
textbox-prompt {
|
||||||
text-color: @background;
|
text-color: @background; /* Text color inside prompt matches the background color */
|
||||||
horizontal-align: 0.5;
|
horizontal-align: 0.5; /* Horizontally centered */
|
||||||
vertical-align: 0.5;
|
vertical-align: 0.5; /* Vertically centered */
|
||||||
expand: false;
|
expand: false; /* Does not expand to fill available space */
|
||||||
}
|
}
|
||||||
|
|
||||||
entry {
|
entry {
|
||||||
expand: false;
|
expand: false; /* Entry field does not expand */
|
||||||
padding: 8px;
|
padding: 8px; /* Padding inside the entry field */
|
||||||
margin: -6px;
|
margin: -6px; /* Negative margin to position entry properly */
|
||||||
horizontal-align: 0;
|
horizontal-align: 0; /* Left-aligned text inside the entry field */
|
||||||
width: 300;
|
width: 300; /* Fixed width for the entry field */
|
||||||
background-color: @background;
|
background-color: @background; /* Entry background color matches the global background */
|
||||||
border: 6px;
|
border: 6px; /* Border thickness around the entry field */
|
||||||
border-color: @primary;
|
border-color: @primary; /* Border color matches the primary color */
|
||||||
border-radius: 8px;
|
border-radius: 8px; /* Rounded corners for the entry field */
|
||||||
cursor: text;
|
cursor: text; /* Cursor changes to text input cursor inside the entry field */
|
||||||
}
|
}
|
||||||
|
|
||||||
element {
|
element {
|
||||||
children: [dummy, element-box, dummy];
|
children: [dummy, element-box, dummy]; /* Contains an element box with dummy placeholders */
|
||||||
padding: 5px;
|
padding: 5px; /* Padding around the element */
|
||||||
orientation: vertical;
|
orientation: vertical; /* Vertical layout for element content */
|
||||||
border: 0px;
|
border: 0px; /* No border around the element */
|
||||||
border-radius: 16px;
|
border-radius: 16px; /* Rounded corners for the element */
|
||||||
background-color: transparent; /* Default background */
|
background-color: transparent; /* Transparent background */
|
||||||
|
width: 100px; /* Width of each element */
|
||||||
|
height: 50px; /* Height of each element */
|
||||||
}
|
}
|
||||||
|
|
||||||
element selected {
|
element selected {
|
||||||
background-color: @primary; /* Solid color for selected item */
|
background-color: @primary; /* Background color of the element when selected */
|
||||||
}
|
}
|
||||||
|
|
||||||
element-box {
|
element-box {
|
||||||
children: [element-icon, element-text];
|
children: [element-icon, element-text]; /* Element box contains an icon and text */
|
||||||
orientation: vertical;
|
orientation: vertical; /* Vertical layout for icon and text */
|
||||||
expand: false;
|
expand: false; /* Does not expand to fill available space */
|
||||||
cursor: pointer;
|
cursor: pointer; /* Cursor changes to a pointer when hovering over the element */
|
||||||
}
|
}
|
||||||
|
|
||||||
element-icon {
|
element-icon {
|
||||||
padding: 10px;
|
padding: 10px; /* Padding inside the icon */
|
||||||
cursor: inherit;
|
cursor: inherit; /* Inherits cursor style from the parent */
|
||||||
size: 33%;
|
size: 33%; /* Icon size is set to 33% of the parent element */
|
||||||
margin: 10px;
|
margin: 10px; /* Margin around the icon */
|
||||||
}
|
}
|
||||||
|
|
||||||
element-text {
|
element-text {
|
||||||
horizontal-align: 0.5;
|
horizontal-align: 0.5; /* Horizontally center-aligns the text */
|
||||||
cursor: inherit;
|
cursor: inherit; /* Inherits cursor style from the parent */
|
||||||
text-color: @text-color;
|
text-color: @text-color; /* Text color for element text */
|
||||||
}
|
}
|
||||||
|
|
||||||
element-text selected {
|
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
|
fastanime --icons --default anilist
|
||||||
\b
|
\b
|
||||||
# viewing manga
|
# viewing manga
|
||||||
fastanime --manga search -t <manga-title>
|
fastanime --manga search -t <manga-title>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
@click.version_option(__version__, "--version")
|
@click.version_option(__version__, "--version")
|
||||||
@@ -142,7 +142,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--normalize-titles/--no-normalize-titles",
|
"--normalize-titles/--no-normalize-titles",
|
||||||
type=bool,
|
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("-d", "--downloads-dir", type=click.Path(), help="Downloads location")
|
||||||
@click.option("--fzf", is_flag=True, help="Use fzf for the ui")
|
@click.option("--fzf", is_flag=True, help="Use fzf for the ui")
|
||||||
@@ -184,6 +184,7 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--fresh-requests", is_flag=True, help="Force the requests cache to be updated"
|
"--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
|
@click.pass_context
|
||||||
def run_cli(
|
def run_cli(
|
||||||
ctx: click.Context,
|
ctx: click.Context,
|
||||||
@@ -220,13 +221,14 @@ def run_cli(
|
|||||||
sync_play,
|
sync_play,
|
||||||
player,
|
player,
|
||||||
fresh_requests,
|
fresh_requests,
|
||||||
|
no_config,
|
||||||
):
|
):
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
ctx.obj = Config()
|
ctx.obj = Config(no_config)
|
||||||
if (
|
if (
|
||||||
ctx.obj.check_for_updates
|
ctx.obj.check_for_updates
|
||||||
and ctx.invoked_subcommand != "completions"
|
and ctx.invoked_subcommand != "completions"
|
||||||
@@ -253,9 +255,10 @@ def run_cli(
|
|||||||
if not is_latest:
|
if not is_latest:
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from .app_updater import update_app
|
|
||||||
from rich.prompt import Confirm
|
from rich.prompt import Confirm
|
||||||
|
|
||||||
|
from .app_updater import update_app
|
||||||
|
|
||||||
def _print_release(release_data):
|
def _print_release(release_data):
|
||||||
console = Console()
|
console = Console()
|
||||||
body = Markdown(release_data["body"])
|
body = Markdown(release_data["body"])
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import os
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from rich import print
|
from rich import print
|
||||||
@@ -128,9 +128,13 @@ def update_app(force=False):
|
|||||||
"install",
|
"install",
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
"-U",
|
"-U",
|
||||||
"--user",
|
|
||||||
"--no-warn-script-location",
|
"--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)
|
process = subprocess.run(args)
|
||||||
if process.returncode == 0:
|
if process.returncode == 0:
|
||||||
return True, release_json
|
return True, release_json
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ years_available = [
|
|||||||
"2022",
|
"2022",
|
||||||
"2023",
|
"2023",
|
||||||
"2024",
|
"2024",
|
||||||
|
"2025",
|
||||||
]
|
]
|
||||||
|
|
||||||
tags_available = {
|
tags_available = {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
from ...completion_functions import anime_titles_shell_complete
|
from ...completion_functions import anime_titles_shell_complete
|
||||||
from .data import (
|
from .data import (
|
||||||
tags_available_list,
|
|
||||||
sorts_available,
|
|
||||||
media_statuses_available,
|
|
||||||
seasons_available,
|
|
||||||
genres_available,
|
genres_available,
|
||||||
media_formats_available,
|
media_formats_available,
|
||||||
|
media_statuses_available,
|
||||||
|
seasons_available,
|
||||||
|
sorts_available,
|
||||||
|
tags_available_list,
|
||||||
years_available,
|
years_available,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,7 +116,12 @@ from .data import (
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--hls-use-mpegts",
|
"--hls-use-mpegts",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Use mpegts for hls streams (useful for some streams: see Docs) (this option forces --force-ffmpeg to be 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(
|
@click.option(
|
||||||
"--max-results", "-M", type=int, help="The maximum number of results to show"
|
"--max-results", "-M", type=int, help="The maximum number of results to show"
|
||||||
@@ -144,12 +148,14 @@ def download(
|
|||||||
prompt,
|
prompt,
|
||||||
force_ffmpeg,
|
force_ffmpeg,
|
||||||
hls_use_mpegts,
|
hls_use_mpegts,
|
||||||
|
hls_use_h264,
|
||||||
max_results,
|
max_results,
|
||||||
):
|
):
|
||||||
from ....anilist import AniList
|
|
||||||
from rich import print
|
from rich import print
|
||||||
|
|
||||||
force_ffmpeg |= hls_use_mpegts
|
from ....anilist import AniList
|
||||||
|
|
||||||
|
force_ffmpeg |= (hls_use_mpegts or hls_use_h264)
|
||||||
|
|
||||||
success, anilist_search_results = AniList.search(
|
success, anilist_search_results = AniList.search(
|
||||||
query=title,
|
query=title,
|
||||||
@@ -379,6 +385,7 @@ def download(
|
|||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
force_ffmpeg=force_ffmpeg,
|
force_ffmpeg=force_ffmpeg,
|
||||||
hls_use_mpegts=hls_use_mpegts,
|
hls_use_mpegts=hls_use_mpegts,
|
||||||
|
hls_use_h264=hls_use_h264,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ def downloads(
|
|||||||
return
|
return
|
||||||
|
|
||||||
from ....constants import APP_CACHE_DIR
|
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")
|
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||||
@@ -183,7 +183,7 @@ def downloads(
|
|||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
bash_functions,
|
||||||
downloads_thumbnail_cache_dir,
|
downloads_thumbnail_cache_dir,
|
||||||
downloads_thumbnail_cache_dir,
|
downloads_thumbnail_cache_dir,
|
||||||
)
|
)
|
||||||
@@ -194,7 +194,7 @@ def downloads(
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ....constants import APP_CACHE_DIR
|
from ....constants import APP_CACHE_DIR
|
||||||
from ...utils.scripts import fzf_preview
|
from ...utils.scripts import bash_functions
|
||||||
|
|
||||||
if not shutil.which("ffmpegthumbnailer"):
|
if not shutil.which("ffmpegthumbnailer"):
|
||||||
print("ffmpegthumbnailer not found")
|
print("ffmpegthumbnailer not found")
|
||||||
@@ -256,7 +256,7 @@ def downloads(
|
|||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
bash_functions,
|
||||||
downloads_thumbnail_cache_dir,
|
downloads_thumbnail_cache_dir,
|
||||||
downloads_thumbnail_cache_dir,
|
downloads_thumbnail_cache_dir,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,15 +11,20 @@ if TYPE_CHECKING:
|
|||||||
@click.option("--erase", "-e", help="Erase your login details", is_flag=True)
|
@click.option("--erase", "-e", help="Erase your login details", is_flag=True)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def login(config: "Config", status, erase):
|
def login(config: "Config", status, erase):
|
||||||
|
from os import path
|
||||||
from sys import exit
|
from sys import exit
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
from rich.prompt import Confirm, Prompt
|
from rich.prompt import Confirm, Prompt
|
||||||
|
|
||||||
|
from ....constants import S_PLATFORM
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
is_logged_in = True if config.user else False
|
is_logged_in = True if config.user else False
|
||||||
message = (
|
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(message)
|
||||||
print(config.user)
|
print(config.user)
|
||||||
@@ -46,9 +51,19 @@ def login(config: "Config", status, erase):
|
|||||||
print(
|
print(
|
||||||
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
||||||
)
|
)
|
||||||
launch(config.fastanime_anilist_app_login_url, wait=True)
|
token = ""
|
||||||
print("Please paste the token provided here")
|
if S_PLATFORM.startswith("darwin"):
|
||||||
token = Prompt.ask("Enter token")
|
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)
|
user = AniList.login_user(token)
|
||||||
if not user:
|
if not user:
|
||||||
print("Sth went wrong", user)
|
print("Sth went wrong", user)
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ def notifier(config: "Config"):
|
|||||||
|
|
||||||
notified = os.path.join(APP_DATA_DIR, "last_notification.json")
|
notified = os.path.join(APP_DATA_DIR, "last_notification.json")
|
||||||
anime_image_path = os.path.join(APP_CACHE_DIR, "notification_image")
|
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 = ""
|
notification_image_path = ""
|
||||||
|
|
||||||
if not config.user:
|
if not config.user:
|
||||||
print("Not Authenticated")
|
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)
|
exit(1)
|
||||||
run = True
|
run = True
|
||||||
# WARNING: Mess around with this value at your own risk
|
# WARNING: Mess around with this value at your own risk
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import click
|
|||||||
|
|
||||||
|
|
||||||
@click.command(
|
@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",
|
short_help="View random anime",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import click
|
|||||||
|
|
||||||
from ...completion_functions import anime_titles_shell_complete
|
from ...completion_functions import anime_titles_shell_complete
|
||||||
from .data import (
|
from .data import (
|
||||||
tags_available_list,
|
|
||||||
sorts_available,
|
|
||||||
media_statuses_available,
|
|
||||||
seasons_available,
|
|
||||||
genres_available,
|
genres_available,
|
||||||
media_formats_available,
|
media_formats_available,
|
||||||
|
media_statuses_available,
|
||||||
|
seasons_available,
|
||||||
|
sorts_available,
|
||||||
|
tags_available_list,
|
||||||
years_available,
|
years_available,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import click
|
|||||||
|
|
||||||
|
|
||||||
@click.command(
|
@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(
|
@click.option(
|
||||||
"--dump-json",
|
"--dump-json",
|
||||||
|
|||||||
@@ -122,7 +122,12 @@ if TYPE_CHECKING:
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--hls-use-mpegts",
|
"--hls-use-mpegts",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Use mpegts for hls streams (useful for some streams: see Docs) (this option forces --force-ffmpeg to be 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
|
@click.pass_obj
|
||||||
def download(
|
def download(
|
||||||
@@ -139,6 +144,7 @@ def download(
|
|||||||
prompt,
|
prompt,
|
||||||
force_ffmpeg,
|
force_ffmpeg,
|
||||||
hls_use_mpegts,
|
hls_use_mpegts,
|
||||||
|
hls_use_h264,
|
||||||
):
|
):
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -158,7 +164,7 @@ def download(
|
|||||||
move_preferred_subtitle_lang_to_top,
|
move_preferred_subtitle_lang_to_top,
|
||||||
)
|
)
|
||||||
|
|
||||||
force_ffmpeg |= hls_use_mpegts
|
force_ffmpeg |= (hls_use_mpegts or hls_use_h264)
|
||||||
|
|
||||||
anime_provider = AnimeProvider(config.provider)
|
anime_provider = AnimeProvider(config.provider)
|
||||||
anilist_anime_info = None
|
anilist_anime_info = None
|
||||||
@@ -201,6 +207,7 @@ def download(
|
|||||||
prompt,
|
prompt,
|
||||||
force_ffmpeg,
|
force_ffmpeg,
|
||||||
hls_use_mpegts,
|
hls_use_mpegts,
|
||||||
|
hls_use_h264,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
search_results = search_results["results"]
|
search_results = search_results["results"]
|
||||||
@@ -254,6 +261,7 @@ def download(
|
|||||||
prompt,
|
prompt,
|
||||||
force_ffmpeg,
|
force_ffmpeg,
|
||||||
hls_use_mpegts,
|
hls_use_mpegts,
|
||||||
|
hls_use_h264,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -389,6 +397,7 @@ def download(
|
|||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
force_ffmpeg=force_ffmpeg,
|
force_ffmpeg=force_ffmpeg,
|
||||||
hls_use_mpegts=hls_use_mpegts,
|
hls_use_mpegts=hls_use_mpegts,
|
||||||
|
hls_use_h264=hls_use_h264,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ def downloads(
|
|||||||
return
|
return
|
||||||
|
|
||||||
from ...constants import APP_CACHE_DIR
|
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")
|
downloads_thumbnail_cache_dir = os.path.join(APP_CACHE_DIR, "video_thumbnails")
|
||||||
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
Path(downloads_thumbnail_cache_dir).mkdir(parents=True, exist_ok=True)
|
||||||
@@ -183,7 +183,7 @@ def downloads(
|
|||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
bash_functions,
|
||||||
downloads_thumbnail_cache_dir,
|
downloads_thumbnail_cache_dir,
|
||||||
downloads_thumbnail_cache_dir,
|
downloads_thumbnail_cache_dir,
|
||||||
)
|
)
|
||||||
@@ -194,7 +194,7 @@ def downloads(
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ...constants import APP_CACHE_DIR
|
from ...constants import APP_CACHE_DIR
|
||||||
from ..utils.scripts import fzf_preview
|
from ..utils.scripts import bash_functions
|
||||||
|
|
||||||
if not shutil.which("ffmpegthumbnailer"):
|
if not shutil.which("ffmpegthumbnailer"):
|
||||||
print("ffmpegthumbnailer not found")
|
print("ffmpegthumbnailer not found")
|
||||||
@@ -256,7 +256,7 @@ def downloads(
|
|||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
bash_functions,
|
||||||
downloads_thumbnail_cache_dir,
|
downloads_thumbnail_cache_dir,
|
||||||
downloads_thumbnail_cache_dir,
|
downloads_thumbnail_cache_dir,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ def get_anime_titles(query: str, variables: dict = {}):
|
|||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Something unexpected occured {e}")
|
logger.error(f"Something unexpected occurred {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from ..libs.fzf import FZF_DEFAULT_OPTS, HEADER
|
|
||||||
|
|
||||||
from ..constants import (
|
from ..constants import (
|
||||||
|
ASSETS_DIR,
|
||||||
|
S_PLATFORM,
|
||||||
USER_CONFIG_PATH,
|
USER_CONFIG_PATH,
|
||||||
USER_DATA_PATH,
|
USER_DATA_PATH,
|
||||||
USER_VIDEOS_DIR,
|
USER_VIDEOS_DIR,
|
||||||
ASSETS_DIR,
|
|
||||||
USER_WATCH_HISTORY_PATH,
|
USER_WATCH_HISTORY_PATH,
|
||||||
S_PLATFORM,
|
|
||||||
)
|
)
|
||||||
|
from ..libs.fzf import FZF_DEFAULT_OPTS, HEADER
|
||||||
from ..libs.rofi import Rofi
|
from ..libs.rofi import Rofi
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -45,6 +45,7 @@ class Config(object):
|
|||||||
"default_media_list_tracking": "None",
|
"default_media_list_tracking": "None",
|
||||||
"downloads_dir": USER_VIDEOS_DIR,
|
"downloads_dir": USER_VIDEOS_DIR,
|
||||||
"disable_mpv_popen": "True",
|
"disable_mpv_popen": "True",
|
||||||
|
"discord": "False",
|
||||||
"episode_complete_at": "80",
|
"episode_complete_at": "80",
|
||||||
"ffmpegthumbnailer_seek_time": "-1",
|
"ffmpegthumbnailer_seek_time": "-1",
|
||||||
"force_forward_tracking": "true",
|
"force_forward_tracking": "true",
|
||||||
@@ -55,9 +56,11 @@ class Config(object):
|
|||||||
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
||||||
"icons": "false",
|
"icons": "false",
|
||||||
"image_previews": "True" if S_PLATFORM != "win32" else "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",
|
"normalize_titles": "True",
|
||||||
"notification_duration": "2",
|
"notification_duration": "120",
|
||||||
"max_cache_lifetime": "03:00:00",
|
"max_cache_lifetime": "03:00:00",
|
||||||
|
"per_page": "15",
|
||||||
"player": "mpv",
|
"player": "mpv",
|
||||||
"preferred_history": "local",
|
"preferred_history": "local",
|
||||||
"preferred_language": "english",
|
"preferred_language": "english",
|
||||||
@@ -82,18 +85,18 @@ class Config(object):
|
|||||||
"use_rofi": "false",
|
"use_rofi": "false",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, no_config) -> None:
|
||||||
self.initialize_user_data_and_watch_history_recent_anime()
|
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 = ConfigParser(self.default_config)
|
||||||
self.configparser.add_section("stream")
|
self.configparser.add_section("stream")
|
||||||
self.configparser.add_section("general")
|
self.configparser.add_section("general")
|
||||||
self.configparser.add_section("anilist")
|
self.configparser.add_section("anilist")
|
||||||
|
|
||||||
# --- set config values from file or using defaults ---
|
# --- 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")
|
self.configparser.read(USER_CONFIG_PATH, encoding="utf-8")
|
||||||
|
|
||||||
# get the configuration
|
# get the configuration
|
||||||
@@ -112,6 +115,7 @@ class Config(object):
|
|||||||
self.disable_mpv_popen = self.configparser.getboolean(
|
self.disable_mpv_popen = self.configparser.getboolean(
|
||||||
"stream", "disable_mpv_popen"
|
"stream", "disable_mpv_popen"
|
||||||
)
|
)
|
||||||
|
self.discord = self.configparser.getboolean("general", "discord")
|
||||||
self.downloads_dir = self.configparser.get("general", "downloads_dir")
|
self.downloads_dir = self.configparser.get("general", "downloads_dir")
|
||||||
self.episode_complete_at = self.configparser.getint(
|
self.episode_complete_at = self.configparser.getint(
|
||||||
"stream", "episode_complete_at"
|
"stream", "episode_complete_at"
|
||||||
@@ -129,6 +133,7 @@ class Config(object):
|
|||||||
self.header_ascii_art = self.configparser.get("general", "header_ascii_art")
|
self.header_ascii_art = self.configparser.get("general", "header_ascii_art")
|
||||||
self.icons = self.configparser.getboolean("general", "icons")
|
self.icons = self.configparser.getboolean("general", "icons")
|
||||||
self.image_previews = self.configparser.getboolean("general", "image_previews")
|
self.image_previews = self.configparser.getboolean("general", "image_previews")
|
||||||
|
self.image_renderer = self.configparser.get("general", "image_renderer")
|
||||||
self.normalize_titles = self.configparser.getboolean(
|
self.normalize_titles = self.configparser.getboolean(
|
||||||
"general", "normalize_titles"
|
"general", "normalize_titles"
|
||||||
)
|
)
|
||||||
@@ -144,6 +149,7 @@ class Config(object):
|
|||||||
+ max_cache_lifetime[1] * 3600
|
+ max_cache_lifetime[1] * 3600
|
||||||
+ max_cache_lifetime[2] * 60
|
+ max_cache_lifetime[2] * 60
|
||||||
)
|
)
|
||||||
|
self.per_page = self.configparser.get("anilist", "per_page")
|
||||||
self.player = self.configparser.get("stream", "player")
|
self.player = self.configparser.get("stream", "player")
|
||||||
self.preferred_history = self.configparser.get("stream", "preferred_history")
|
self.preferred_history = self.configparser.get("stream", "preferred_history")
|
||||||
self.preferred_language = self.configparser.get("general", "preferred_language")
|
self.preferred_language = self.configparser.get("general", "preferred_language")
|
||||||
@@ -275,114 +281,117 @@ class Config(object):
|
|||||||
#
|
#
|
||||||
[general]
|
[general]
|
||||||
# Can you rice it?
|
# Can you rice it?
|
||||||
# for the preview pane
|
# For the preview pane
|
||||||
preview_separator_color = {self.preview_separator_color}
|
preview_separator_color = {self.preview_separator_color}
|
||||||
|
|
||||||
preview_header_color = {self.preview_header_color}
|
preview_header_color = {self.preview_header_color}
|
||||||
|
|
||||||
# for the header
|
# For the header
|
||||||
# be sure to indent
|
# Be sure to indent
|
||||||
header_ascii_art = {new_line.join([tab+line for line in self.header_ascii_art.split(new_line)])}
|
header_ascii_art = {new_line.join([tab + line for line in self.header_ascii_art.split(new_line)])}
|
||||||
|
|
||||||
header_color = {self.header_color}
|
header_color = {self.header_color}
|
||||||
|
|
||||||
# to be passed to fzf
|
# the image renderer to use [icat/chafa]
|
||||||
# be sure to indent
|
image_renderer = {self.image_renderer}
|
||||||
fzf_opts = {new_line.join([tab+line for line in self.fzf_opts.split(new_line)])}
|
|
||||||
|
# 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]
|
# Whether to show the icons in the TUI [True/False]
|
||||||
# more like emojis
|
# More like emojis
|
||||||
# by the way if you have any recommendations
|
# By the way, if you have any recommendations
|
||||||
# to which should be used where please
|
# for which should be used where, please
|
||||||
# don't hesitate to share your opinion
|
# 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
|
# 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}
|
icons = {self.icons}
|
||||||
|
|
||||||
# whether to normalize provider titles [True/False]
|
# Whether to normalize provider titles [True/False]
|
||||||
# basically takes the provider titles and finds the corresponding anilist title then changes the title to that
|
# Basically takes the provider titles and finds the corresponding Anilist title, then changes the title to that
|
||||||
# useful for uniformity especially when downloading from different providers
|
# Useful for uniformity, especially when downloading from different providers
|
||||||
# this also applies to episode titles
|
# This also applies to episode titles
|
||||||
normalize_titles = {self.normalize_titles}
|
normalize_titles = {self.normalize_titles}
|
||||||
|
|
||||||
# whether to check for updates every time you run the script [True/False]
|
# Whether to check for updates every time you run the script [True/False]
|
||||||
# this is useful for keeping your script up to date
|
# This is useful for keeping your script up to date
|
||||||
# cause there are always new features being added 😄
|
# because there are always new features being added 😄
|
||||||
check_for_updates = {self.check_for_updates}
|
check_for_updates = {self.check_for_updates}
|
||||||
|
|
||||||
# can be [allanime, animepahe, hianime, nyaa, yugen]
|
# Can be [allanime, animepahe, hianime, nyaa, yugen]
|
||||||
# allanime is the most realible
|
# Allanime is the most reliable
|
||||||
# animepahe provides different links to streams of different quality so a quality can be selected reliably with --quality option
|
# 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 languuages and its servers are generally faster
|
# Hianime usually provides subs in different languages, and its servers are generally faster
|
||||||
# NOTE: currently they are encrypting the video links
|
# NOTE: Currently, they are encrypting the video links
|
||||||
# though am working on it
|
# though I’m working on it
|
||||||
# however, you can still get the links to the subs
|
# However, you can still get the links to the subs
|
||||||
# with ```fastanime grab``` command
|
# with ```fastanime grab``` command
|
||||||
# yugen meh
|
# Yugen meh
|
||||||
# nyaa those who prefer torrents, though not reliable due to auto selection of results
|
# 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
|
# as most of the data in Nyaa is not structured
|
||||||
# though works relatively well for new anime
|
# though it works relatively well for new anime
|
||||||
# esp with subsplease and horriblesubs
|
# especially with SubsPlease and HorribleSubs
|
||||||
# oh and you should have webtorrent cli to use this
|
# Oh, and you should have webtorrent CLI to use this
|
||||||
provider = {self.provider}
|
provider = {self.provider}
|
||||||
|
|
||||||
# Display language [english, romaji]
|
# Display language [english, romaji]
|
||||||
# this is passed to anilist directly and is used to set the language which the anime titles will be in
|
# This is passed to Anilist directly and is used to set the language for anime titles
|
||||||
# when using the anilist interface
|
# when using the Anilist interface
|
||||||
preferred_language = {self.preferred_language}
|
preferred_language = {self.preferred_language}
|
||||||
|
|
||||||
# Download directory
|
# 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}
|
downloads_dir = {self.downloads_dir}
|
||||||
|
|
||||||
# whether to show a preview window when using fzf or rofi [True/False]
|
# 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
|
# The preview requires you to have a command-line image viewer as documented in the README
|
||||||
# this is only when using fzf or rofi
|
# This is only when using fzf or rofi
|
||||||
# if you dont care about image and text previews it doesnt matter
|
# If you don't care about image and text previews, it doesn’t matter
|
||||||
# though its awesome
|
# though it’s awesome
|
||||||
# try it and you will see
|
# Try it, and you will see
|
||||||
preview = {self.preview}
|
preview = {self.preview}
|
||||||
|
|
||||||
# whether to show images in the preview [true/false]
|
# Whether to show images in the preview [True/False]
|
||||||
# windows users just swtich to linux 😄
|
# Windows users: just switch to Linux 😄
|
||||||
# cause even if you enable it
|
# because even if you enable it
|
||||||
# it won't look pretty
|
# it won't look pretty
|
||||||
# just be satisfied with the text previews
|
# Just be satisfied with the text previews
|
||||||
# so forget it exists 🤣
|
# So forget it exists 🤣
|
||||||
image_previews = {self.image_previews}
|
image_previews = {self.image_previews}
|
||||||
|
|
||||||
# the time to seek when using ffmpegthumbnailer [-1 to 100]
|
# the time to seek when using ffmpegthumbnailer [-1 to 100]
|
||||||
# -1 means random and is the default
|
# -1 means random and is the default
|
||||||
# ffmpegthumbnailer is used to generate previews
|
# ffmpegthumbnailer is used to generate previews,
|
||||||
# and you can select at what time in the video to extract an image
|
# allowing you to select the 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
|
# Random makes things quite exciting because you never know at what time it will extract the image.
|
||||||
# used by the ```fastanime downloads``` command
|
# Used by the `fastanime downloads` command.
|
||||||
ffmpegthumbnailer_seek_time = {self.ffmpegthumbnailer_seek_time}
|
ffmpegthumbnailer_seek_time = {self.ffmpegthumbnailer_seek_time}
|
||||||
|
|
||||||
# specify the order of menu items in a comma-separated list.
|
# specify the order of menu items in a comma-separated list.
|
||||||
# only include the base names of menu options (e.g., "Trending", "Recent").
|
# Only include the base names of menu options (e.g., "Trending", "Recent").
|
||||||
# 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'
|
# 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.
|
# Leave blank to use the default menu order.
|
||||||
# you can also omit some options by not including them in the list
|
# You can also omit some options by not including them in the list.
|
||||||
menu_order = {self.menu_order}
|
menu_order = {self.menu_order}
|
||||||
|
|
||||||
# whether to use fzf as the interface for the anilist command and others. [True/False]
|
# whether to use fzf as the interface for the anilist command and others. [True/False]
|
||||||
use_fzf = {self.use_fzf}
|
use_fzf = {self.use_fzf}
|
||||||
|
|
||||||
# whether to use rofi for the ui [True/False]
|
# whether to use rofi for the UI [True/False]
|
||||||
# it's more useful if you want to create a desktop entry
|
# It's more useful if you want to create a desktop entry,
|
||||||
# which can be setup with 'fastanime config --desktop-entry'
|
# which can be set up with 'fastanime config --desktop-entry'.
|
||||||
# though if you want it to be your sole interface even when fastanime is run directly from the terminal
|
# 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}
|
use_rofi = {self.use_rofi}
|
||||||
|
|
||||||
# rofi themes to use <path>
|
# rofi themes to use <path>
|
||||||
# the values of this option is the path to the rofi config files to use
|
# The value 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
|
# 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 your self
|
# You can refer to the rofi demo on GitHub to see for yourself.
|
||||||
# i need help designing the default rofi themes
|
# I need help designing the default rofi themes.
|
||||||
# if you fancy yourself a rofi ricer please contribute to making
|
# If you fancy yourself a rofi ricer, please contribute to improving
|
||||||
# the default theme better
|
# the default theme.
|
||||||
rofi_theme = {self.rofi_theme}
|
rofi_theme = {self.rofi_theme}
|
||||||
|
|
||||||
rofi_theme_preview = {self.rofi_theme_preview}
|
rofi_theme_preview = {self.rofi_theme_preview}
|
||||||
@@ -391,52 +400,54 @@ rofi_theme_input = {self.rofi_theme_input}
|
|||||||
|
|
||||||
rofi_theme_confirm = {self.rofi_theme_confirm}
|
rofi_theme_confirm = {self.rofi_theme_confirm}
|
||||||
|
|
||||||
# the duration in minutes a notification will stay in the screen
|
# the duration in minutes a notification will stay on the screen.
|
||||||
# used by notifier command
|
# Used by the notifier command.
|
||||||
notification_duration = {self.notification_duration}
|
notification_duration = {self.notification_duration}
|
||||||
|
|
||||||
# used when the provider gives subs of different languages
|
# used when the provider offers subtitles in different languages.
|
||||||
# currently its the case for:
|
# Currently, this is the case for:
|
||||||
# hianime
|
# hianime.
|
||||||
# the values for this option are the short names for languages
|
# The values for this option are the short names for languages.
|
||||||
# regex is used to determine what you selected
|
# Regex is used to determine what you selected.
|
||||||
sub_lang = {self.sub_lang}
|
sub_lang = {self.sub_lang}
|
||||||
|
|
||||||
# what is your default media list tracking [track/disabled/prompt]
|
# what is your default media list tracking [track/disabled/prompt]
|
||||||
# only affects your anilist anime list
|
# This only affects your anilist anime list.
|
||||||
# track - means your progress will always be reflected in 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
|
# 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
|
# 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}
|
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
|
# whether media list tracking should only be updated when the next episode is greater than the previous.
|
||||||
# this affects only your anilist anime list
|
# This only affects your anilist anime list.
|
||||||
force_forward_tracking = {self.force_forward_tracking}
|
force_forward_tracking = {self.force_forward_tracking}
|
||||||
|
|
||||||
# whether to cache requests [true/false]
|
# whether to cache requests [true/false]
|
||||||
# this makes the experience better and more faster
|
# This improves the experience by making it faster,
|
||||||
# as data need not always be fetched from web server
|
# as data doesn't always need to be fetched from the web server
|
||||||
# and instead can be gotten locally
|
# and can instead be retrieved locally from the cached_requests_db.
|
||||||
# from the cached_requests_db
|
|
||||||
cache_requests = {self.cache_requests}
|
cache_requests = {self.cache_requests}
|
||||||
|
|
||||||
# the max lifetime for a cached request <days:hours:minutes>
|
# the max lifetime for a cached request <days:hours:minutes>
|
||||||
# defaults to 3days = 03:00:00
|
# Defaults to 3 days = 03:00:00.
|
||||||
# this is the time after which a cached request will be deleted (technically : )
|
# This is the time after which a cached request will be deleted (technically).
|
||||||
max_cache_lifetime = {self._max_cache_lifetime}
|
max_cache_lifetime = {self._max_cache_lifetime}
|
||||||
|
|
||||||
# whether to use a persistent store (basically a sqlitedb) for storing some data the provider requires
|
# whether to use a persistent store (basically an SQLite DB) for storing some data the provider requires
|
||||||
# to enable a seamless experience [true/false]
|
# to enable a seamless experience. [true/false]
|
||||||
# this option exists primarily because i think it may help in the optimization
|
# This option exists primarily to optimize FastAnime as a library in a website project.
|
||||||
# of fastanime as a library in a website project
|
# For now, it's not recommended to change it. Leave it as is.
|
||||||
# for now i don't recommend changing it
|
|
||||||
# leave it as is
|
|
||||||
use_persistent_provider_store = {self.use_persistent_provider_store}
|
use_persistent_provider_store = {self.use_persistent_provider_store}
|
||||||
|
|
||||||
# no of recent anime to keep [0-50]
|
# number of recent anime to keep [0-50].
|
||||||
# 0 will disable recent anime tracking
|
# 0 will disable recent anime tracking.
|
||||||
recent = {self.recent}
|
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]
|
[stream]
|
||||||
# the quality of the stream [1080,720,480,360]
|
# the quality of the stream [1080,720,480,360]
|
||||||
@@ -559,6 +570,9 @@ format = {self.format}
|
|||||||
# since you will miss out on some features if you use the others
|
# since you will miss out on some features if you use the others
|
||||||
player = {self.player}
|
player = {self.player}
|
||||||
|
|
||||||
|
[anilist]
|
||||||
|
per_page = {self.per_page}
|
||||||
|
|
||||||
#
|
#
|
||||||
# HOPE YOU ENJOY FASTANIME AND BE SURE TO STAR THE PROJECT ON GITHUB
|
# HOPE YOU ENJOY FASTANIME AND BE SURE TO STAR THE PROJECT ON GITHUB
|
||||||
# https://github.com/Benexl/FastAnime
|
# https://github.com/Benexl/FastAnime
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import threading
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from click import clear
|
from click import clear
|
||||||
@@ -14,6 +15,7 @@ from yt_dlp.utils import sanitize_filename
|
|||||||
|
|
||||||
from ...anilist import AniList
|
from ...anilist import AniList
|
||||||
from ...constants import USER_CONFIG_PATH
|
from ...constants import USER_CONFIG_PATH
|
||||||
|
from ...libs.discord import discord
|
||||||
from ...libs.fzf import fzf
|
from ...libs.fzf import fzf
|
||||||
from ...libs.rofi import Rofi
|
from ...libs.rofi import Rofi
|
||||||
from ...Utility.data import anime_normalizer
|
from ...Utility.data import anime_normalizer
|
||||||
@@ -54,6 +56,8 @@ def calculate_percentage_completion(start_time, end_time):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def discord_updater(show,episode,switch):
|
||||||
|
discord.discord_connect(show,episode,switch)
|
||||||
|
|
||||||
def media_player_controls(
|
def media_player_controls(
|
||||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||||
@@ -510,6 +514,12 @@ def provider_anime_episode_servers_menu(
|
|||||||
"[bold magenta] Episode: [/]",
|
"[bold magenta] Episode: [/]",
|
||||||
current_episode_number,
|
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
|
# try to get the timestamp you left off from if available
|
||||||
start_time = config.watch_history.get(str(anime_id_anilist), {}).get(
|
start_time = config.watch_history.get(str(anime_id_anilist), {}).get(
|
||||||
"episode_stopped_at", "0"
|
"episode_stopped_at", "0"
|
||||||
@@ -592,6 +602,10 @@ def provider_anime_episode_servers_menu(
|
|||||||
)
|
)
|
||||||
print("Finished at: ", stop_time)
|
print("Finished at: ", stop_time)
|
||||||
|
|
||||||
|
# stop discord activity updater
|
||||||
|
if config.discord:
|
||||||
|
switch.set()
|
||||||
|
|
||||||
# update_watch_history
|
# update_watch_history
|
||||||
# this will try to update the episode to be the next episode if delta has reached a specific threshhold
|
# this will try to update the episode to be the next episode if delta has reached a specific threshhold
|
||||||
# this update will only apply locally
|
# this update will only apply locally
|
||||||
@@ -750,18 +764,22 @@ def provider_anime_episodes_menu(
|
|||||||
if not current_episode_number or current_episode_number not in available_episodes:
|
if not current_episode_number or current_episode_number not in available_episodes:
|
||||||
choices = [*available_episodes, "Back"]
|
choices = [*available_episodes, "Back"]
|
||||||
preview = None
|
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.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(
|
current_episode_number = fzf.run(
|
||||||
choices, prompt="Select Episode", header=anime_title, preview=preview
|
choices, prompt="Select Episode", header=anime_title, preview=preview
|
||||||
)
|
)
|
||||||
@@ -1358,10 +1376,9 @@ def media_actions_menu(
|
|||||||
media_actions_menu(config, fastanime_runtime_state)
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
relations = relations[1]["data"]["Page"]["relations"] # pyright:ignore
|
||||||
fastanime_runtime_state.anilist_results_data = {
|
fastanime_runtime_state.anilist_results_data = {
|
||||||
"data": {
|
"data": {"Page": {"media": relations["nodes"]}} # pyright:ignore
|
||||||
"Page": {"media": relations[1]["data"]["Media"]["relations"]["nodes"]} # pyright:ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
anilist_results_menu(config, fastanime_runtime_state)
|
anilist_results_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
@@ -1485,6 +1502,9 @@ def anilist_results_menu(
|
|||||||
from .utils import get_fzf_anime_preview
|
from .utils import get_fzf_anime_preview
|
||||||
|
|
||||||
preview = get_fzf_anime_preview(search_results, anime_data.keys())
|
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(
|
selected_anime_title = fzf.run(
|
||||||
choices,
|
choices,
|
||||||
prompt="Select Anime",
|
prompt="Select Anime",
|
||||||
@@ -1728,34 +1748,22 @@ def fastanime_main_menu(
|
|||||||
options = {
|
options = {
|
||||||
f"{'🔥 ' if icons else ''}Trending": AniList.get_trending,
|
f"{'🔥 ' if icons else ''}Trending": AniList.get_trending,
|
||||||
f"{'🎞️ ' if icons else ''}Recent": _recent,
|
f"{'🎞️ ' if icons else ''}Recent": _recent,
|
||||||
f"{'📺 ' if icons else ''}Watching": lambda config,
|
f"{'📺 ' if icons else ''}Watching": lambda config, media_list_type="Watching", page=1: _handle_animelist(
|
||||||
media_list_type="Watching",
|
|
||||||
page=1: _handle_animelist(
|
|
||||||
config, fastanime_runtime_state, media_list_type, page=page
|
config, fastanime_runtime_state, media_list_type, page=page
|
||||||
),
|
),
|
||||||
f"{'⏸ ' if icons else ''}Paused": lambda config,
|
f"{'⏸ ' if icons else ''}Paused": lambda config, media_list_type="Paused", page=1: _handle_animelist(
|
||||||
media_list_type="Paused",
|
|
||||||
page=1: _handle_animelist(
|
|
||||||
config, fastanime_runtime_state, media_list_type, page=page
|
config, fastanime_runtime_state, media_list_type, page=page
|
||||||
),
|
),
|
||||||
f"{'🚮 ' if icons else ''}Dropped": lambda config,
|
f"{'🚮 ' if icons else ''}Dropped": lambda config, media_list_type="Dropped", page=1: _handle_animelist(
|
||||||
media_list_type="Dropped",
|
|
||||||
page=1: _handle_animelist(
|
|
||||||
config, fastanime_runtime_state, media_list_type, page=page
|
config, fastanime_runtime_state, media_list_type, page=page
|
||||||
),
|
),
|
||||||
f"{'📑 ' if icons else ''}Planned": lambda config,
|
f"{'📑 ' if icons else ''}Planned": lambda config, media_list_type="Planned", page=1: _handle_animelist(
|
||||||
media_list_type="Planned",
|
|
||||||
page=1: _handle_animelist(
|
|
||||||
config, fastanime_runtime_state, media_list_type, page=page
|
config, fastanime_runtime_state, media_list_type, page=page
|
||||||
),
|
),
|
||||||
f"{'✅ ' if icons else ''}Completed": lambda config,
|
f"{'✅ ' if icons else ''}Completed": lambda config, media_list_type="Completed", page=1: _handle_animelist(
|
||||||
media_list_type="Completed",
|
|
||||||
page=1: _handle_animelist(
|
|
||||||
config, fastanime_runtime_state, media_list_type, page=page
|
config, fastanime_runtime_state, media_list_type, page=page
|
||||||
),
|
),
|
||||||
f"{'🔁 ' if icons else ''}Rewatching": lambda config,
|
f"{'🔁 ' if icons else ''}Rewatching": lambda config, media_list_type="Rewatching", page=1: _handle_animelist(
|
||||||
media_list_type="Rewatching",
|
|
||||||
page=1: _handle_animelist(
|
|
||||||
config, fastanime_runtime_state, media_list_type, page=page
|
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 ''}Recently Updated Anime": AniList.get_most_recently_updated,
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import textwrap
|
import textwrap
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
from hashlib import sha256
|
||||||
|
|
||||||
import requests
|
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 ...constants import APP_CACHE_DIR, S_PLATFORM
|
||||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||||
from ...Utility import anilist_data_helper
|
from ...Utility import anilist_data_helper
|
||||||
from ..utils.scripts import fzf_preview
|
from ..utils.scripts import bash_functions
|
||||||
from ..utils.utils import get_true_fg
|
from ..utils.utils import get_true_fg, which_bashlike
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -69,7 +69,12 @@ def save_image_from_url(url: str, file_name: str):
|
|||||||
file_name: filename to use
|
file_name: filename to use
|
||||||
"""
|
"""
|
||||||
image = requests.get(url)
|
image = requests.get(url)
|
||||||
with open(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)
|
f.write(image.content)
|
||||||
|
|
||||||
|
|
||||||
@@ -83,7 +88,7 @@ def save_info_from_str(info: str, file_name: str):
|
|||||||
with open(
|
with open(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
ANIME_INFO_CACHE_DIR,
|
ANIME_INFO_CACHE_DIR,
|
||||||
file_name,
|
sha256(file_name.encode("utf-8")).hexdigest(),
|
||||||
),
|
),
|
||||||
"w",
|
"w",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -96,7 +101,7 @@ def write_search_results(
|
|||||||
titles: list[str],
|
titles: list[str],
|
||||||
workers: int | None = None,
|
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:
|
Args:
|
||||||
anilist_results: the anilist results from an anilist action
|
anilist_results: the anilist results from an anilist action
|
||||||
@@ -108,11 +113,21 @@ def write_search_results(
|
|||||||
future_to_task = {}
|
future_to_task = {}
|
||||||
for anime, title in zip(anilist_results, titles):
|
for anime, title in zip(anilist_results, titles):
|
||||||
# actual image url
|
# actual image url
|
||||||
|
image_url = ""
|
||||||
if os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower() == "true":
|
if os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower() == "true":
|
||||||
image_url = anime["coverImage"]["large"]
|
image_url = anime["coverImage"]["large"]
|
||||||
future_to_task[
|
|
||||||
executor.submit(save_image_from_url, image_url, title)
|
if not (
|
||||||
] = image_url
|
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"
|
mediaListName = "Not in any of your lists"
|
||||||
progress = "UNKNOWN"
|
progress = "UNKNOWN"
|
||||||
@@ -121,57 +136,55 @@ def write_search_results(
|
|||||||
progress = anime_list["progress"]
|
progress = anime_list["progress"]
|
||||||
# handle the text data
|
# handle the text data
|
||||||
template = f"""
|
template = f"""
|
||||||
|
image_url={image_url}
|
||||||
ll=2
|
ll=2
|
||||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||||
((ll++))
|
((ll++))
|
||||||
done
|
done
|
||||||
echo
|
echo
|
||||||
echo "{get_true_fg('Title(jp):',*HEADER_COLOR)} {(anime['title']['romaji'] 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)}"
|
echo "{get_true_fg("Title(eng):", *HEADER_COLOR)} {(anime["title"]["english"] or "").replace('"', SINGLE_QUOTE)}"
|
||||||
ll=2
|
ll=2
|
||||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||||
((ll++))
|
((ll++))
|
||||||
done
|
done
|
||||||
echo
|
echo
|
||||||
echo "{get_true_fg('Popularity:',*HEADER_COLOR)} {anilist_data_helper.format_number_with_commas(anime['popularity'])}"
|
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("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("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("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("Genres:", *HEADER_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime["genres"]).replace('"', SINGLE_QUOTE)}"
|
||||||
ll=2
|
ll=2
|
||||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||||
((ll++))
|
((ll++))
|
||||||
done
|
done
|
||||||
echo
|
echo
|
||||||
echo "{get_true_fg('Episodes:',*HEADER_COLOR)} {(anime['episodes']) or 'UNKNOWN'}"
|
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("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("End Date:", *HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime["endDate"]).replace('"', SINGLE_QUOTE)}"
|
||||||
ll=2
|
ll=2
|
||||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||||
((ll++))
|
((ll++))
|
||||||
done
|
done
|
||||||
echo
|
echo
|
||||||
echo "{get_true_fg('Media List:',*HEADER_COLOR)} {mediaListName.replace('"',SINGLE_QUOTE)}"
|
echo "{get_true_fg("Media List:", *HEADER_COLOR)} {mediaListName.replace('"', SINGLE_QUOTE)}"
|
||||||
echo "{get_true_fg('Progress:',*HEADER_COLOR)} {progress}"
|
echo "{get_true_fg("Progress:", *HEADER_COLOR)} {progress}"
|
||||||
ll=2
|
ll=2
|
||||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||||
((ll++))
|
((ll++))
|
||||||
done
|
done
|
||||||
echo
|
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 = textwrap.dedent(template)
|
||||||
template = f"""
|
template = f"""
|
||||||
{template}
|
{template}
|
||||||
echo "
|
echo "{textwrap.fill(clean_html((anime["description"]) or "").replace('"', SINGLE_QUOTE), width=45)}"
|
||||||
{textwrap.fill(clean_html(
|
|
||||||
(anime['description']) or "").replace('"',SINGLE_QUOTE), width=45)}
|
|
||||||
"
|
|
||||||
"""
|
"""
|
||||||
future_to_task[executor.submit(save_info_from_str, template, title)] = title
|
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):
|
for anime, title in zip(anilist_results, titles):
|
||||||
# actual link to download image from
|
# actual link to download image from
|
||||||
image_url = anime["coverImage"]["large"]
|
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
|
# execute the jobs
|
||||||
for future in concurrent.futures.as_completed(future_to_url):
|
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 = {}
|
future_to_url = {}
|
||||||
for manga in manga_results:
|
for manga in manga_results:
|
||||||
image_url = manga["poster"]
|
image_url = manga["poster"]
|
||||||
future_to_url[
|
|
||||||
executor.submit(
|
if not (
|
||||||
save_image_from_url,
|
os.path.exists(
|
||||||
image_url,
|
os.path.join(
|
||||||
sanitize_filename(manga["title"]),
|
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
|
# execute the jobs
|
||||||
for future in concurrent.futures.as_completed(future_to_url):
|
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"
|
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||||
preview = """
|
preview = """
|
||||||
%s
|
%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...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
bash_functions,
|
||||||
IMAGES_CACHE_DIR,
|
IMAGES_CACHE_DIR,
|
||||||
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
|
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
|
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
|
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
|
# HEADER_COLOR = 215, 0, 95
|
||||||
@@ -303,27 +339,29 @@ def get_fzf_episode_preview(
|
|||||||
|
|
||||||
if episode_title and image_url:
|
if episode_title and image_url:
|
||||||
future_to_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
|
] = image_url
|
||||||
template = textwrap.dedent(
|
template = textwrap.dedent(
|
||||||
f"""
|
f"""
|
||||||
ll=2
|
ll=2
|
||||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||||
((ll++))
|
((ll++))
|
||||||
done
|
done
|
||||||
echo "{get_true_fg('Anime Title(eng):',*HEADER_COLOR)} {('' or anilist_result['title']['english']).replace('"',SINGLE_QUOTE)}"
|
echo
|
||||||
echo "{get_true_fg('Anime Title(jp):',*HEADER_COLOR)} {(anilist_result['title']['romaji'] or '').replace('"',SINGLE_QUOTE)}"
|
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
|
ll=2
|
||||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||||
((ll++))
|
((ll++))
|
||||||
done
|
done
|
||||||
echo "{str(episode_title).replace('"',SINGLE_QUOTE)}"
|
echo
|
||||||
|
echo "{str(episode_title).replace('"', SINGLE_QUOTE)}"
|
||||||
ll=2
|
ll=2
|
||||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
echo -n -e "{get_true_fg("─", *SEPARATOR_COLOR, bold=False)}"
|
||||||
((ll++))
|
((ll++))
|
||||||
done
|
done
|
||||||
"""
|
"""
|
||||||
@@ -348,22 +386,26 @@ def get_fzf_episode_preview(
|
|||||||
background_worker.start()
|
background_worker.start()
|
||||||
|
|
||||||
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
|
# 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":
|
if S_PLATFORM == "win32":
|
||||||
preview = """
|
preview = """
|
||||||
%s
|
%s
|
||||||
title={}
|
title="$(echo -n {})"
|
||||||
show_image_previews="%s"
|
title="$(echo -n "$title" |generate_sha256)"
|
||||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||||
if [ $show_image_previews = "true" ];then
|
if [ "$FASTANIME_IMAGE_PREVIEWS" = "True" ];then
|
||||||
if [ -s "%s\\\\\\${title}.png" ]; then
|
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||||
if command -v "chafa">/dev/null;then
|
if command -v "chafa">/dev/null;then
|
||||||
chafa -s $dim "%s\\\\\\${title}.png"
|
chafa -s $dim "%s\\\\\\${title}.png"
|
||||||
else
|
else
|
||||||
echo please install chafa to enjoy image previews
|
echo please install chafa to enjoy image previews
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
else
|
else
|
||||||
echo Loading...
|
echo Loading...
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -371,8 +413,7 @@ def get_fzf_episode_preview(
|
|||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
bash_functions,
|
||||||
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
|
||||||
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
@@ -380,11 +421,11 @@ def get_fzf_episode_preview(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
preview = """
|
preview = """
|
||||||
title={}
|
|
||||||
%s
|
%s
|
||||||
show_image_previews="%s"
|
title="$(echo -n {})"
|
||||||
if [ $show_image_previews = "true" ];then
|
title="$(echo -n "$title" |generate_sha256)"
|
||||||
if [ -s %s/${title}.png ]; then fzf-preview %s/${title}.png
|
if [ "$FASTANIME_IMAGE_PREVIEWS" = "True" ];then
|
||||||
|
if [ -s %s/${title}.png ]; then fzf_preview %s/${title}.png
|
||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -392,8 +433,7 @@ def get_fzf_episode_preview(
|
|||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
bash_functions,
|
||||||
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
|
||||||
IMAGES_CACHE_DIR,
|
IMAGES_CACHE_DIR,
|
||||||
IMAGES_CACHE_DIR,
|
IMAGES_CACHE_DIR,
|
||||||
ANIME_INFO_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
|
anilist_results: the anilist results got from an anilist action
|
||||||
|
|
||||||
Returns:
|
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
|
# ensure images and info exists
|
||||||
|
|
||||||
@@ -426,22 +466,27 @@ def get_fzf_anime_preview(
|
|||||||
background_worker.start()
|
background_worker.start()
|
||||||
|
|
||||||
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
|
# 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":
|
if S_PLATFORM == "win32":
|
||||||
preview = """
|
preview = """
|
||||||
%s
|
%s
|
||||||
title={}
|
title="$(echo -n {})"
|
||||||
show_image_previews="%s"
|
title="$(echo -n "$title" |generate_sha256)"
|
||||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||||
if [ $show_image_previews = "true" ];then
|
if [ "$FASTANIME_IMAGE_PREVIEWS" = "True" ];then
|
||||||
if [ -s "%s\\\\\\${title}.png" ]; then
|
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||||
if command -v "chafa">/dev/null;then
|
if command -v "chafa">/dev/null;then
|
||||||
chafa -s $dim "%s\\\\\\${title}.png"
|
chafa -s $dim "%s\\\\\\${title}.png"
|
||||||
else
|
else
|
||||||
echo please install chafa to enjoy image previews
|
echo please install chafa to enjoy image previews
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
else
|
else
|
||||||
echo Loading...
|
echo Loading...
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -449,8 +494,7 @@ def get_fzf_anime_preview(
|
|||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
bash_functions,
|
||||||
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
|
||||||
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
@@ -459,10 +503,10 @@ def get_fzf_anime_preview(
|
|||||||
else:
|
else:
|
||||||
preview = """
|
preview = """
|
||||||
%s
|
%s
|
||||||
title={}
|
title="$(echo -n {})"
|
||||||
show_image_previews="%s"
|
title="$(echo -n "$title" |generate_sha256)"
|
||||||
if [ $show_image_previews = "true" ];then
|
if [ "$FASTANIME_IMAGE_PREVIEWS" = "True" ];then
|
||||||
if [ -s "%s/${title}.png" ]; then fzf-preview "%s/${title}.png"
|
if [ -s "%s/${title}.png" ]; then fzf_preview "%s/${title}.png"
|
||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -470,8 +514,7 @@ def get_fzf_anime_preview(
|
|||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
bash_functions,
|
||||||
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
|
||||||
IMAGES_CACHE_DIR,
|
IMAGES_CACHE_DIR,
|
||||||
IMAGES_CACHE_DIR,
|
IMAGES_CACHE_DIR,
|
||||||
ANIME_INFO_CACHE_DIR,
|
ANIME_INFO_CACHE_DIR,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import re
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ...constants import S_PLATFORM
|
from ...constants import S_PLATFORM
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import requests
|
|||||||
|
|
||||||
|
|
||||||
def print_img(url: str):
|
def print_img(url: str):
|
||||||
"""helper funtion to print an image given its url
|
"""helper function to print an image given its url
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: [TODO:description]
|
url: [TODO:description]
|
||||||
@@ -25,7 +25,7 @@ def print_img(url: str):
|
|||||||
return
|
return
|
||||||
img_bytes = res.content
|
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.
|
to pull from urls. Keeping old line here just in case.
|
||||||
|
|
||||||
subprocess.run([EXECUTABLE, url, "--size=15x15"], input=img_bytes)
|
subprocess.run([EXECUTABLE, url, "--size=15x15"], input=img_bytes)
|
||||||
|
|||||||
@@ -1,53 +1,68 @@
|
|||||||
fzf_preview = r"""
|
bash_functions = r"""
|
||||||
#
|
generate_sha256() {
|
||||||
# Adapted from the preview script in the fzf repo
|
local input
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 1. Use kitty icat on kitty terminal
|
# Check if input is passed as an argument or piped
|
||||||
if [[ $KITTY_WINDOW_ID ]]; then
|
if [ -n "$1" ]; then
|
||||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
input="$1"
|
||||||
# you have to use 'stream'.
|
else
|
||||||
#
|
input=$(cat)
|
||||||
# 2. The last line of the output is the ANSI reset code without newline.
|
fi
|
||||||
# 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/'
|
|
||||||
|
|
||||||
# 2. Use chafa with Sixel output
|
if command -v sha256sum &>/dev/null; then
|
||||||
elif command -v chafa >/dev/null; then
|
echo -n "$input" | sha256sum | awk '{print $1}'
|
||||||
case "$(uname -a)" in
|
elif command -v shasum &>/dev/null; then
|
||||||
# termux does not support sixel graphics
|
echo -n "$input" | shasum -a 256 | awk '{print $1}'
|
||||||
# and produces weird output
|
elif command -v sha256 &>/dev/null; then
|
||||||
*ndroid*) chafa -s "$dim" "$file";;
|
echo -n "$input" | sha256 | awk '{print $1}'
|
||||||
*) chafa -f sixel -s "$dim" "$file";;
|
elif command -v openssl &>/dev/null; then
|
||||||
esac
|
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
|
||||||
# Add a new line character so that fzf can display multiple images in the preview window
|
else
|
||||||
echo
|
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
|
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||||
elif command -v imgcat >/dev/null; then
|
if [ "$dim" = x ]; then
|
||||||
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
|
dim=$(stty size </dev/tty | awk "{print \$2 \"x\" \$1}")
|
||||||
# user is running iTerm2. But for the sake of simplicity, we just assume
|
fi
|
||||||
# that's the case here.
|
if ! [ "$FASTANIME_IMAGE_RENDERER" = "icat" ] && [ -z "$KITTY_WINDOW_ID" ] && [ "$((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES))" -eq "$(stty size </dev/tty | awk "{print \$1}")" ]; then
|
||||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||||
|
fi
|
||||||
|
|
||||||
# 4. Cannot find any suitable method to preview the image
|
if [ "$FASTANIME_IMAGE_RENDERER" = "icat" ] && [ -z "$GHOSTTY_BIN_DIR" ]; then
|
||||||
else
|
if command -v kitten >/dev/null 2>&1; then
|
||||||
echo install chafa or imgcat or install kitty terminal so you can enjoy image previews
|
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||||
fi
|
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,11 +1,10 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any
|
from typing import Any, Callable
|
||||||
|
|
||||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||||
from ...libs.anime_provider.types import Anime, EpisodeStream, SearchResult, Server
|
from ...libs.anime_provider.types import Anime, EpisodeStream, SearchResult, Server
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
|
|
||||||
class FastAnimeRuntimeState(object):
|
class FastAnimeRuntimeState(object):
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import shutil
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from InquirerPy import inquirer
|
from InquirerPy import inquirer
|
||||||
|
|
||||||
|
from fastanime.constants import S_PLATFORM
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ...libs.anime_provider.types import EpisodeStream
|
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"):
|
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:
|
Args:
|
||||||
num_of_bytes: the number of bytes to format
|
num_of_bytes: the number of bytes to format
|
||||||
@@ -155,3 +158,41 @@ def fuzzy_inquirer(choices: list, prompt: str, **kwargs):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
).execute()
|
).execute()
|
||||||
return action
|
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 logging
|
||||||
|
import os
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -63,7 +64,7 @@ class AniListApi:
|
|||||||
self.session = requests.session()
|
self.session = requests.session()
|
||||||
|
|
||||||
def login_user(self, token: str):
|
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:
|
Args:
|
||||||
token: anilist app token
|
token: anilist app token
|
||||||
@@ -139,7 +140,12 @@ class AniListApi:
|
|||||||
return self._make_authenticated_request(media_list_mutation, variables)
|
return self._make_authenticated_request(media_list_mutation, variables)
|
||||||
|
|
||||||
def get_anime_list(
|
def get_anime_list(
|
||||||
self, status: "AnilistMediaListStatus", type="ANIME", page=1, **kwargs
|
self,
|
||||||
|
status: "AnilistMediaListStatus",
|
||||||
|
type="ANIME",
|
||||||
|
page=1,
|
||||||
|
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||||
|
**kwargs,
|
||||||
) -> tuple[bool, "AnilistMediaLists"] | tuple[bool, None]:
|
) -> tuple[bool, "AnilistMediaLists"] | tuple[bool, None]:
|
||||||
"""gets an anime list from your media list given the list status
|
"""gets an anime list from your media list given the list status
|
||||||
|
|
||||||
@@ -154,6 +160,7 @@ class AniListApi:
|
|||||||
"userId": self.user_id,
|
"userId": self.user_id,
|
||||||
"type": type,
|
"type": type,
|
||||||
"page": page,
|
"page": page,
|
||||||
|
"perPage": int(perPage),
|
||||||
}
|
}
|
||||||
return self._make_authenticated_request(media_list_query, variables)
|
return self._make_authenticated_request(media_list_query, variables)
|
||||||
|
|
||||||
@@ -240,7 +247,7 @@ class AniListApi:
|
|||||||
return (False, None)
|
return (False, None)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Something unexpected occured {e}")
|
logger.error(f"Something unexpected occurred {e}")
|
||||||
return (False, None) # type: ignore
|
return (False, None) # type: ignore
|
||||||
|
|
||||||
def get_data(
|
def get_data(
|
||||||
@@ -304,7 +311,7 @@ class AniListApi:
|
|||||||
},
|
},
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
except Exception as e:
|
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
|
return (False, {"Error": f"{e}"}) # type: ignore
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
@@ -354,51 +361,92 @@ class AniListApi:
|
|||||||
variables = {"id": id}
|
variables = {"id": id}
|
||||||
return self.get_data(anime_query, variables)
|
return self.get_data(anime_query, variables)
|
||||||
|
|
||||||
def get_trending(self, type="ANIME", page=1, *_, **kwargs):
|
def get_trending(
|
||||||
|
self,
|
||||||
|
type="ANIME",
|
||||||
|
page=1,
|
||||||
|
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||||
|
*_,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Gets the currently trending anime
|
Gets the currently trending anime
|
||||||
"""
|
"""
|
||||||
variables = {"type": type, "page": page}
|
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||||
trending = self.get_data(trending_query, variables)
|
trending = self.get_data(trending_query, variables)
|
||||||
return trending
|
return trending
|
||||||
|
|
||||||
def get_most_favourite(self, type="ANIME", page=1, *_, **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
|
Gets the most favoured anime on anilist
|
||||||
"""
|
"""
|
||||||
variables = {"type": type, "page": page}
|
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||||
most_favourite = self.get_data(most_favourite_query, variables)
|
most_favourite = self.get_data(most_favourite_query, variables)
|
||||||
return most_favourite
|
return most_favourite
|
||||||
|
|
||||||
def get_most_scored(self, type="ANIME", page=1, *_, **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
|
Gets most scored anime on anilist
|
||||||
"""
|
"""
|
||||||
variables = {"type": type, "page": page}
|
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||||
most_scored = self.get_data(most_scored_query, variables)
|
most_scored = self.get_data(most_scored_query, variables)
|
||||||
return most_scored
|
return most_scored
|
||||||
|
|
||||||
def get_most_recently_updated(self, type="ANIME", page=1, *_, **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
|
Gets most recently updated anime from anilist
|
||||||
"""
|
"""
|
||||||
variables = {"type": type, "page": page}
|
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||||
most_recently_updated = self.get_data(most_recently_updated_query, variables)
|
most_recently_updated = self.get_data(most_recently_updated_query, variables)
|
||||||
return most_recently_updated
|
return most_recently_updated
|
||||||
|
|
||||||
def get_most_popular(self, type="ANIME", page=1, **kwargs):
|
def get_most_popular(
|
||||||
|
self,
|
||||||
|
type="ANIME",
|
||||||
|
page=1,
|
||||||
|
perPage=os.environ.get("FASTANIME_PER_PAGE", 15),
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Gets most popular anime on anilist
|
Gets most popular anime on anilist
|
||||||
"""
|
"""
|
||||||
variables = {"type": type, "page": page}
|
variables = {"type": type, "page": page, "perPage": int(perPage)}
|
||||||
most_popular = self.get_data(most_popular_query, variables)
|
most_popular = self.get_data(most_popular_query, variables)
|
||||||
return most_popular
|
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
|
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)
|
upcoming_anime = self.get_data(upcoming_anime_query, variables)
|
||||||
return upcoming_anime
|
return upcoming_anime
|
||||||
|
|
||||||
@@ -408,7 +456,7 @@ class AniListApi:
|
|||||||
recommended_anime = self.get_data(recommended_query, variables)
|
recommended_anime = self.get_data(recommended_query, variables)
|
||||||
return recommended_anime
|
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}
|
variables = {"id": id}
|
||||||
characters = self.get_data(anime_characters_query, variables)
|
characters = self.get_data(anime_characters_query, variables)
|
||||||
return characters
|
return characters
|
||||||
|
|||||||
@@ -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
|
Mostly for internal usage
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ query($id:Int){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
body
|
body
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ query{
|
|||||||
large
|
large
|
||||||
medium
|
medium
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -193,8 +193,8 @@ mutation (
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
media_list_query = """
|
media_list_query = """
|
||||||
query ($userId: Int, $status: MediaListStatus, $type: MediaType, $page: Int) {
|
query ($userId: Int, $status: MediaListStatus, $type: MediaType, $page: Int, $perPage: Int) {
|
||||||
Page(perPage: 15, page: $page) {
|
Page(perPage: $perPage, page: $page) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
currentPage
|
currentPage
|
||||||
total
|
total
|
||||||
@@ -406,8 +406,8 @@ query($query:String,%s){
|
|||||||
)
|
)
|
||||||
|
|
||||||
trending_query = """
|
trending_query = """
|
||||||
query ($type: MediaType, $page: Int) {
|
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||||
Page(perPage: 15, page: $page) {
|
Page(perPage: $perPage, page: $page) {
|
||||||
media(sort: TRENDING_DESC, type: $type, genre_not_in: ["hentai"]) {
|
media(sort: TRENDING_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
@@ -471,8 +471,8 @@ query ($type: MediaType, $page: Int) {
|
|||||||
|
|
||||||
# mosts
|
# mosts
|
||||||
most_favourite_query = """
|
most_favourite_query = """
|
||||||
query ($type: MediaType, $page: Int) {
|
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||||
Page(perPage: 15, page: $page) {
|
Page(perPage: $perPage, page: $page) {
|
||||||
media(sort: FAVOURITES_DESC, type: $type, genre_not_in: ["hentai"]) {
|
media(sort: FAVOURITES_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
@@ -539,8 +539,8 @@ query ($type: MediaType, $page: Int) {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
most_scored_query = """
|
most_scored_query = """
|
||||||
query ($type: MediaType, $page: Int) {
|
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||||
Page(perPage: 15, page: $page) {
|
Page(perPage: $perPage, page: $page) {
|
||||||
media(sort: SCORE_DESC, type: $type, genre_not_in: ["hentai"]) {
|
media(sort: SCORE_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
@@ -603,8 +603,8 @@ query ($type: MediaType, $page: Int) {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
most_popular_query = """
|
most_popular_query = """
|
||||||
query ($type: MediaType, $page: Int) {
|
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||||
Page(perPage: 15, page: $page) {
|
Page(perPage: $perPage, page: $page) {
|
||||||
media(sort: POPULARITY_DESC, type: $type, genre_not_in: ["hentai"]) {
|
media(sort: POPULARITY_DESC, type: $type, genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
@@ -667,8 +667,8 @@ query ($type: MediaType, $page: Int) {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
most_recently_updated_query = """
|
most_recently_updated_query = """
|
||||||
query ($type: MediaType, $page: Int) {
|
query ($type: MediaType, $page: Int,$perPage:Int) {
|
||||||
Page(perPage: 15, page: $page) {
|
Page(perPage: $perPage, page: $page) {
|
||||||
media(
|
media(
|
||||||
sort: UPDATED_AT_DESC
|
sort: UPDATED_AT_DESC
|
||||||
type: $type
|
type: $type
|
||||||
@@ -909,7 +909,7 @@ query ($id: Int,$type:MediaType) {
|
|||||||
airingAt
|
airingAt
|
||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
episode
|
episode
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -918,8 +918,8 @@ query ($id: Int,$type:MediaType) {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
upcoming_anime_query = """
|
upcoming_anime_query = """
|
||||||
query ($page: Int, $type: MediaType) {
|
query ($page: Int, $type: MediaType,$perPage:Int) {
|
||||||
Page(perPage: 15, page: $page) {
|
Page(perPage: $perPage, page: $page) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
total
|
total
|
||||||
perPage
|
perPage
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHE_SERVERS
|
|||||||
from .hianime.constants import SERVERS_AVAILABLE as HIANIME_SERVERS
|
from .hianime.constants import SERVERS_AVAILABLE as HIANIME_SERVERS
|
||||||
|
|
||||||
anime_sources = {
|
anime_sources = {
|
||||||
"allanime": "api.AllAnimeAPI",
|
"allanime": "api.AllAnime",
|
||||||
"animepahe": "api.AnimePaheApi",
|
"animepahe": "api.AnimePahe",
|
||||||
"hianime": "api.HiAnimeApi",
|
"hianime": "api.HiAnime",
|
||||||
"nyaa": "api.NyaaApi",
|
"nyaa": "api.Nyaa",
|
||||||
"yugen": "api.YugenApi"
|
"yugen": "api.Yugen",
|
||||||
}
|
}
|
||||||
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHE_SERVERS, *HIANIME_SERVERS]
|
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 json
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -10,207 +5,215 @@ from typing import TYPE_CHECKING
|
|||||||
from ...anime_provider.base_provider import AnimeProvider
|
from ...anime_provider.base_provider import AnimeProvider
|
||||||
from ..decorators import debug_provider
|
from ..decorators import debug_provider
|
||||||
from ..utils import give_random_quality, one_digit_symmetric_xor
|
from ..utils import give_random_quality, one_digit_symmetric_xor
|
||||||
from .constants import ALLANIME_API_ENDPOINT, ALLANIME_BASE, ALLANIME_REFERER
|
from .constants import (
|
||||||
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
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:
|
if TYPE_CHECKING:
|
||||||
from .types import AllAnimeEpisode
|
from .types import AllAnimeEpisode
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# TODO: create tests for the api
|
class AllAnime(AnimeProvider):
|
||||||
#
|
|
||||||
# ** Based on ani-cli **
|
|
||||||
class AllAnimeAPI(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 = {
|
HEADERS = {
|
||||||
"Referer": ALLANIME_REFERER,
|
"Referer": API_REFERER,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _fetch_gql(self, query: str, variables: dict):
|
def _execute_graphql_query(self, query: str, variables: dict):
|
||||||
"""main abstraction over all requests to the allanime api
|
"""
|
||||||
|
Executes a GraphQL query using the provided query string and variables.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: [TODO:description]
|
query (str): The GraphQL query string to be executed.
|
||||||
variables: [TODO:description]
|
variables (dict): A dictionary of variables to be used in the query.
|
||||||
|
|
||||||
Returns:
|
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(
|
response = self.session.get(
|
||||||
self.api_endpoint,
|
API_ENDPOINT,
|
||||||
params={
|
params={
|
||||||
"variables": json.dumps(variables),
|
"variables": json.dumps(variables),
|
||||||
"query": query,
|
"query": query,
|
||||||
},
|
},
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if response.ok:
|
response.raise_for_status()
|
||||||
return response.json()["data"]
|
return response.json()["data"]
|
||||||
else:
|
|
||||||
logger.error("[ALLANIME-ERROR]: ", response.text)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def search_for_anime(
|
def search_for_anime(
|
||||||
self,
|
self,
|
||||||
user_query: str,
|
search_keywords: str,
|
||||||
translation_type: str = "sub",
|
translation_type: str,
|
||||||
nsfw=True,
|
*,
|
||||||
unknown=True,
|
nsfw=DEFAULT_NSFW,
|
||||||
|
unknown=DEFAULT_UNKNOWN,
|
||||||
|
limit=DEFAULT_PER_PAGE,
|
||||||
|
page=DEFAULT_PAGE,
|
||||||
|
country_of_origin=DEFAULT_COUNTRY_OF_ORIGIN,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""search for an anime title using allanime provider
|
"""
|
||||||
|
Search for anime based on given keywords and filters.
|
||||||
Args:
|
Args:
|
||||||
nsfw ([TODO:parameter]): [TODO:description]
|
search_keywords (str): The keywords to search for.
|
||||||
unknown ([TODO:parameter]): [TODO:description]
|
translation_type (str, optional): The type of translation to search for (e.g., "sub" or "dub"). Defaults to "sub".
|
||||||
user_query: [TODO:description]
|
limit (int, optional): The maximum number of results to return. Defaults to 40.
|
||||||
translation_type: [TODO:description]
|
page (int, optional): The page number to return. Defaults to 1.
|
||||||
**kwargs: [TODO:args]
|
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:
|
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}
|
search_results = self._execute_graphql_query(
|
||||||
limit = 40
|
SEARCH_GQL,
|
||||||
translationtype = translation_type
|
variables={
|
||||||
countryorigin = "all"
|
"search": {
|
||||||
page = 1
|
"allowAdult": nsfw,
|
||||||
variables = {
|
"allowUnknown": unknown,
|
||||||
"search": search,
|
"query": search_keywords,
|
||||||
"limit": limit,
|
},
|
||||||
"page": page,
|
"limit": limit,
|
||||||
"translationtype": translationtype,
|
"page": page,
|
||||||
"countryorigin": countryorigin,
|
"translationtype": translation_type,
|
||||||
}
|
"countryorigin": country_of_origin,
|
||||||
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
|
|
||||||
)
|
)
|
||||||
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())
|
anime = self._execute_graphql_query(SHOW_GQL, variables={"showId": id})
|
||||||
def _get_server(embed):
|
self.store.set(id, "anime_info", {"title": anime["show"]["name"]})
|
||||||
# filter the working streams no need to get all since the others are mostly hsl
|
return {
|
||||||
# TODO: should i just get all the servers and handle the hsl??
|
"id": anime["show"]["_id"],
|
||||||
if embed.get("sourceName", "") not in (
|
"title": anime["show"]["name"],
|
||||||
# priorities based on death note
|
"availableEpisodesDetail": anime["show"]["availableEpisodesDetail"],
|
||||||
"Sak", # 7
|
"type": anime.get("__typename"),
|
||||||
"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)
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
"server": "Yt",
|
"server": "Yt",
|
||||||
"episode_title": f"{anime_title}; Episode {episode_number}",
|
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
"headers": {"Referer": f"https://{API_BASE_URL}/"},
|
||||||
"subtitles": [],
|
"subtitles": [],
|
||||||
"links": [
|
"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
|
# get the stream url for an episode of the defined source names
|
||||||
embed_url = f"https://{ALLANIME_BASE}{url.replace('clock', 'clock.json')}"
|
response = self.session.get(
|
||||||
resp = self.session.get(
|
f"https://{API_BASE_URL}{url.replace('clock', 'clock.json')}",
|
||||||
embed_url,
|
timeout=10,
|
||||||
timeout=10,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if resp.ok:
|
response.raise_for_status()
|
||||||
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"]),
|
|
||||||
}
|
|
||||||
|
|
||||||
for embed in embeds:
|
# SECOND CASE
|
||||||
if server := _get_server(embed):
|
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
|
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"]
|
import re
|
||||||
ALLANIME_BASE = "allanime.day"
|
|
||||||
ALLANIME_REFERER = "https://allanime.to/"
|
SERVERS_AVAILABLE = [
|
||||||
ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE)
|
"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 (
|
query (
|
||||||
$search: SearchInput
|
$search: SearchInput
|
||||||
$limit: Int
|
$limit: Int
|
||||||
@@ -27,7 +27,7 @@ query (
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
ALLANIME_EPISODES_GQL = """\
|
EPISODES_GQL = """\
|
||||||
query (
|
query (
|
||||||
$showId: String!
|
$showId: String!
|
||||||
$translationType: VaildTranslationTypeEnumType!
|
$translationType: VaildTranslationTypeEnumType!
|
||||||
@@ -45,7 +45,7 @@ query (
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ALLANIME_SHOW_GQL = """
|
SHOW_GQL = """
|
||||||
query ($showId: String!) {
|
query ($showId: String!) {
|
||||||
show(_id: $showId) {
|
show(_id: $showId) {
|
||||||
_id
|
_id
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -15,49 +14,33 @@ from ..decorators import debug_provider
|
|||||||
from .constants import (
|
from .constants import (
|
||||||
ANIMEPAHE_BASE,
|
ANIMEPAHE_BASE,
|
||||||
ANIMEPAHE_ENDPOINT,
|
ANIMEPAHE_ENDPOINT,
|
||||||
|
JUICY_STREAM_REGEX,
|
||||||
REQUEST_HEADERS,
|
REQUEST_HEADERS,
|
||||||
SERVER_HEADERS,
|
SERVER_HEADERS,
|
||||||
)
|
)
|
||||||
from .utils import process_animepahe_embed_page
|
from .extractors import process_animepahe_embed_page
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimePaheSearchResult
|
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimePaheSearchResult
|
||||||
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
KWIK_RE = re.compile(r"Player\|(.+?)'")
|
|
||||||
|
|
||||||
|
class AnimePahe(AnimeProvider):
|
||||||
class AnimePaheApi(AnimeProvider):
|
|
||||||
search_page: "AnimePaheSearchPage"
|
search_page: "AnimePaheSearchPage"
|
||||||
anime: "AnimePaheAnimePage"
|
anime: "AnimePaheAnimePage"
|
||||||
HEADERS = REQUEST_HEADERS
|
HEADERS = REQUEST_HEADERS
|
||||||
PROVIDER = "animepahe"
|
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def search_for_anime(self, user_query: str, *args):
|
def search_for_anime(self, search_keywords: str, translation_type, **kwargs):
|
||||||
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
|
||||||
response = self.session.get(
|
response = self.session.get(
|
||||||
url,
|
ANIMEPAHE_ENDPOINT, params={"m": "search", "q": search_keywords}
|
||||||
)
|
)
|
||||||
if not response.ok:
|
response.raise_for_status()
|
||||||
return
|
|
||||||
data: "AnimePaheSearchPage" = response.json()
|
data: "AnimePaheSearchPage" = response.json()
|
||||||
self.search_page = data
|
results = []
|
||||||
for animepahe_search_result in data["data"]:
|
for result in data["data"]:
|
||||||
self.store.set(
|
results.append(
|
||||||
str(animepahe_search_result["session"]),
|
|
||||||
"search_result",
|
|
||||||
animepahe_search_result,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"pageInfo": {
|
|
||||||
"total": data["total"],
|
|
||||||
"perPage": data["per_page"],
|
|
||||||
"currentPage": data["current_page"],
|
|
||||||
},
|
|
||||||
"results": [
|
|
||||||
{
|
{
|
||||||
"availableEpisodes": list(range(result["episodes"])),
|
"availableEpisodes": list(range(result["episodes"])),
|
||||||
"id": result["session"],
|
"id": result["session"],
|
||||||
@@ -69,55 +52,81 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
"season": result["season"],
|
"season": result["season"],
|
||||||
"poster": result["poster"],
|
"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())
|
@debug_provider
|
||||||
def get_anime(self, session_id: str, *args):
|
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
|
page = 1
|
||||||
if d := self.store.get(str(session_id), "search_result"):
|
if d := self.store.get(str(session_id), "search_result"):
|
||||||
anime_result: "AnimePaheSearchResult" = d
|
anime_result: "AnimePaheSearchResult" = d
|
||||||
data: "AnimePaheAnimePage" = {} # pyright:ignore
|
data: "AnimePaheAnimePage" = {} # pyright:ignore
|
||||||
|
|
||||||
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
|
data = self._pages_loader(
|
||||||
|
data,
|
||||||
def _pages_loader(
|
session_id,
|
||||||
url,
|
params={
|
||||||
page,
|
"m": "release",
|
||||||
):
|
"id": session_id,
|
||||||
response = self.session.get(
|
"sort": "episode_asc",
|
||||||
url,
|
"page": page,
|
||||||
)
|
},
|
||||||
if response.ok:
|
page=page,
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
@@ -151,47 +160,13 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def get_episode_streams(
|
def _get_server(self, episode, res_dicts, anime_title, translation_type):
|
||||||
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']}"
|
|
||||||
)
|
|
||||||
# get all links
|
# get all links
|
||||||
streams = {
|
streams = {
|
||||||
"server": "kwik",
|
"server": "kwik",
|
||||||
"links": [],
|
"links": [],
|
||||||
"episode_title": episode_title,
|
"episode_title": f"{episode['title'] or anime_title}; Episode {episode['episode']}",
|
||||||
"subtitles": [],
|
"subtitles": [],
|
||||||
"headers": {},
|
"headers": {},
|
||||||
}
|
}
|
||||||
@@ -207,23 +182,22 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
"[ANIMEPAHE-WARN]: embed url not found please report to the developers"
|
"[ANIMEPAHE-WARN]: embed url not found please report to the developers"
|
||||||
)
|
)
|
||||||
return []
|
continue
|
||||||
# get embed page
|
# get embed page
|
||||||
embed_response = self.session.get(
|
embed_response = self.session.get(
|
||||||
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
|
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
|
||||||
)
|
)
|
||||||
if not response.ok:
|
embed_response.raise_for_status()
|
||||||
continue
|
|
||||||
embed_page = embed_response.text
|
embed_page = embed_response.text
|
||||||
|
|
||||||
decoded_js = process_animepahe_embed_page(embed_page)
|
decoded_js = process_animepahe_embed_page(embed_page)
|
||||||
if not decoded_js:
|
if not decoded_js:
|
||||||
logger.error("[ANIMEPAHE-ERROR]: failed to decode embed page")
|
logger.error("[ANIMEPAHE-ERROR]: failed to decode embed page")
|
||||||
return
|
continue
|
||||||
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
||||||
if not juicy_stream:
|
if not juicy_stream:
|
||||||
logger.error("[ANIMEPAHE-ERROR]: failed to find juicy stream")
|
logger.error("[ANIMEPAHE-ERROR]: failed to find juicy stream")
|
||||||
return
|
continue
|
||||||
juicy_stream = juicy_stream.group(1)
|
juicy_stream = juicy_stream.group(1)
|
||||||
# add the link
|
# add the link
|
||||||
streams["links"].append(
|
streams["links"].append(
|
||||||
@@ -233,4 +207,119 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
"link": juicy_stream,
|
"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 = "animepahe.ru"
|
||||||
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
|
ANIMEPAHE_BASE = f"https://{ANIMEPAHE}"
|
||||||
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api?"
|
ANIMEPAHE_ENDPOINT = f"{ANIMEPAHE_BASE}/api"
|
||||||
|
|
||||||
SERVERS_AVAILABLE = ["kwik"]
|
SERVERS_AVAILABLE = ["kwik"]
|
||||||
REQUEST_HEADERS = {
|
REQUEST_HEADERS = {
|
||||||
@@ -31,3 +33,5 @@ SERVER_HEADERS = {
|
|||||||
"Priority": "u=4",
|
"Priority": "u=4",
|
||||||
"TE": "trailers",
|
"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:
|
class AnimeProvider:
|
||||||
session: requests.Session
|
session: requests.Session
|
||||||
|
|
||||||
PROVIDER = ""
|
|
||||||
USER_AGENT = random_user_agent()
|
USER_AGENT = random_user_agent()
|
||||||
HEADERS = {}
|
HEADERS = {}
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ class AnimeProvider:
|
|||||||
if use_persistent_provider_store.lower() == "true":
|
if use_persistent_provider_store.lower() == "true":
|
||||||
self.store = ProviderStore(
|
self.store = ProviderStore(
|
||||||
"persistent",
|
"persistent",
|
||||||
self.PROVIDER,
|
self.__class__.__name__,
|
||||||
os.path.join(APP_CACHE_DIR, "anime_providers_store.db"),
|
os.path.join(APP_CACHE_DIR, "anime_providers_store.db"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -5,21 +5,19 @@ import os
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def debug_provider(provider_name: str):
|
def debug_provider(provider_function):
|
||||||
def _provider_function_decorator(provider_function):
|
@functools.wraps(provider_function)
|
||||||
@functools.wraps(provider_function)
|
def _provider_function_wrapper(self, *args, **kwargs):
|
||||||
def _provider_function_wrapper(*args, **kwargs):
|
provider_name = self.__class__.__name__.upper()
|
||||||
if not os.environ.get("FASTANIME_DEBUG"):
|
if not os.environ.get("FASTANIME_DEBUG"):
|
||||||
try:
|
try:
|
||||||
return provider_function(*args, **kwargs)
|
return provider_function(self, *args, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[{provider_name}@{provider_function.__name__}]: {e}")
|
logger.error(f"[{provider_name}@{provider_function.__name__}]: {e}")
|
||||||
else:
|
else:
|
||||||
return provider_function(*args, **kwargs)
|
return provider_function(self, *args, **kwargs)
|
||||||
|
|
||||||
return _provider_function_wrapper
|
return _provider_function_wrapper
|
||||||
|
|
||||||
return _provider_function_decorator
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_internet_connection(provider_function):
|
def ensure_internet_connection(provider_function):
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import re
|
|||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
from .extractors import MegaCloud
|
|
||||||
|
|
||||||
from yt_dlp.utils import (
|
from yt_dlp.utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
@@ -18,6 +17,7 @@ from ..base_provider import AnimeProvider
|
|||||||
from ..decorators import debug_provider
|
from ..decorators import debug_provider
|
||||||
from ..utils import give_random_quality
|
from ..utils import give_random_quality
|
||||||
from .constants import SERVERS_AVAILABLE
|
from .constants import SERVERS_AVAILABLE
|
||||||
|
from .extractors import MegaCloud
|
||||||
from .types import HiAnimeStream
|
from .types import HiAnimeStream
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -39,13 +39,11 @@ class ParseAnchorAndImgTag(HTMLParser):
|
|||||||
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
|
||||||
|
|
||||||
class HiAnimeApi(AnimeProvider):
|
class HiAnime(AnimeProvider):
|
||||||
# HEADERS = {"Referer": "https://hianime.to/home"}
|
# HEADERS = {"Referer": "https://hianime.to/home"}
|
||||||
|
|
||||||
PROVIDER = "hianime"
|
@debug_provider
|
||||||
|
def search_for_anime(self, anime_title: str, translation_type, **kwargs):
|
||||||
@debug_provider(PROVIDER.upper())
|
|
||||||
def search_for_anime(self, anime_title: str, *args):
|
|
||||||
query = quote_plus(anime_title)
|
query = quote_plus(anime_title)
|
||||||
url = f"https://hianime.to/search?keyword={query}"
|
url = f"https://hianime.to/search?keyword={query}"
|
||||||
response = self.session.get(url)
|
response = self.session.get(url)
|
||||||
@@ -92,8 +90,8 @@ class HiAnimeApi(AnimeProvider):
|
|||||||
self.store.set(result["id"], "search_result", result)
|
self.store.set(result["id"], "search_result", result)
|
||||||
return {"pageInfo": {}, "results": results}
|
return {"pageInfo": {}, "results": results}
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def get_anime(self, hianime_id, *args):
|
def get_anime(self, hianime_id, **kwargs):
|
||||||
anime_result = {}
|
anime_result = {}
|
||||||
if d := self.store.get(str(hianime_id), "search_result"):
|
if d := self.store.get(str(hianime_id), "search_result"):
|
||||||
anime_result = d
|
anime_result = d
|
||||||
@@ -145,8 +143,8 @@ class HiAnimeApi(AnimeProvider):
|
|||||||
"episodes_info": episodes_info,
|
"episodes_info": episodes_info,
|
||||||
}
|
}
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def get_episode_streams(self, anime_id, episode, translation_type, *args):
|
def get_episode_streams(self, anime_id, episode, translation_type, **kwargs):
|
||||||
if d := self.store.get(str(anime_id), "anime_info"):
|
if d := self.store.get(str(anime_id), "anime_info"):
|
||||||
episodes_info = d
|
episodes_info = d
|
||||||
episode_details = [
|
episode_details = [
|
||||||
@@ -192,7 +190,7 @@ class HiAnimeApi(AnimeProvider):
|
|||||||
if not servers_html:
|
if not servers_html:
|
||||||
return
|
return
|
||||||
|
|
||||||
@debug_provider(self.PROVIDER.upper())
|
@debug_provider
|
||||||
def _get_server(server_name, server_html):
|
def _get_server(server_name, server_html):
|
||||||
# keys: [ data-type: translation_type, data-id: embed_id, data-server-id: server_id ]
|
# keys: [ data-type: translation_type, data-id: embed_id, data-server-id: server_id ]
|
||||||
servers_info = extract_attributes(server_html)
|
servers_info = extract_attributes(server_html)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import time
|
|
||||||
import re
|
|
||||||
import json
|
import json
|
||||||
from typing import List, Dict
|
import re
|
||||||
from Crypto.Cipher import AES
|
import time
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Dict, List
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ...common.requests_cacher import CachedRequestsSession
|
from ...common.requests_cacher import CachedRequestsSession
|
||||||
|
|||||||
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
|
search_results: SearchResults
|
||||||
PROVIDER = "nyaa"
|
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def search_for_anime(self, user_query: str, *args, **_):
|
def search_for_anime(self, user_query: str, *args, **_):
|
||||||
self.search_results = search_for_anime_with_anilist(
|
self.search_results = search_for_anime_with_anilist(
|
||||||
user_query, True
|
user_query, True
|
||||||
@@ -39,7 +38,7 @@ class NyaaApi(AnimeProvider):
|
|||||||
self.user_query = user_query
|
self.user_query = user_query
|
||||||
return self.search_results
|
return self.search_results
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def get_anime(self, anilist_id: str, *_):
|
def get_anime(self, anilist_id: str, *_):
|
||||||
for anime in self.search_results["results"]:
|
for anime in self.search_results["results"]:
|
||||||
if anime["id"] == anilist_id:
|
if anime["id"] == anilist_id:
|
||||||
@@ -55,7 +54,7 @@ class NyaaApi(AnimeProvider):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def get_episode_streams(
|
def get_episode_streams(
|
||||||
self,
|
self,
|
||||||
anime_id: str,
|
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 base64
|
||||||
|
import re
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
|
|
||||||
from yt_dlp.utils import (
|
from yt_dlp.utils import (
|
||||||
get_element_text_and_html_by_tag,
|
|
||||||
get_elements_text_and_html_by_attribute,
|
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
get_element_by_attribute,
|
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 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 ..base_provider import AnimeProvider
|
||||||
|
from ..decorators import debug_provider
|
||||||
|
from .constants import SEARCH_URL, YUGEN_ENDPOINT
|
||||||
|
|
||||||
|
|
||||||
# ** Adapted from anipy-cli **
|
# ** Adapted from anipy-cli **
|
||||||
class YugenApi(AnimeProvider):
|
class Yugen(AnimeProvider):
|
||||||
"""
|
"""
|
||||||
Provides a fast and effective interface to YugenApi site.
|
Provides a fast and effective interface to YugenApi site.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PROVIDER = "yugen"
|
|
||||||
api_endpoint = YUGEN_ENDPOINT
|
api_endpoint = YUGEN_ENDPOINT
|
||||||
# HEADERS = {
|
# HEADERS = {
|
||||||
# "Referer": ALLANIME_REFERER,
|
# "Referer": ALLANIME_REFERER,
|
||||||
# }
|
# }
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def search_for_anime(
|
def search_for_anime(
|
||||||
self,
|
self,
|
||||||
user_query: str,
|
user_query: str,
|
||||||
@@ -94,7 +94,7 @@ class YugenApi(AnimeProvider):
|
|||||||
"results": results,
|
"results": results,
|
||||||
}
|
}
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def get_anime(self, anime_id: str, **kwargs):
|
def get_anime(self, anime_id: str, **kwargs):
|
||||||
identifier = base64.b64decode(anime_id).decode()
|
identifier = base64.b64decode(anime_id).decode()
|
||||||
response = self.session.get(f"{YUGEN_ENDPOINT}/anime/{identifier}")
|
response = self.session.get(f"{YUGEN_ENDPOINT}/anime/{identifier}")
|
||||||
@@ -118,7 +118,9 @@ class YugenApi(AnimeProvider):
|
|||||||
|
|
||||||
if sub_match:
|
if sub_match:
|
||||||
eps = int(sub_match.group(1))
|
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(
|
dub_match = re.search(
|
||||||
r'<div class="ap-.+?">Episodes \(Dub\)</div><span class="description" .+?>(\d+)</span></div>',
|
r'<div class="ap-.+?">Episodes \(Dub\)</div><span class="description" .+?>(\d+)</span></div>',
|
||||||
@@ -127,7 +129,9 @@ class YugenApi(AnimeProvider):
|
|||||||
|
|
||||||
if dub_match:
|
if dub_match:
|
||||||
eps = int(dub_match.group(1))
|
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)
|
name = get_element_text_and_html_by_tag("h1", html_page)
|
||||||
if name is not None:
|
if name is not None:
|
||||||
@@ -174,7 +178,7 @@ class YugenApi(AnimeProvider):
|
|||||||
|
|
||||||
return data_map
|
return data_map
|
||||||
|
|
||||||
@debug_provider(PROVIDER.upper())
|
@debug_provider
|
||||||
def get_episode_streams(
|
def get_episode_streams(
|
||||||
self, anime_id, episode_number: str, translation_type="sub"
|
self, anime_id, episode_number: str, translation_type="sub"
|
||||||
):
|
):
|
||||||
@@ -212,5 +216,10 @@ class YugenApi(AnimeProvider):
|
|||||||
"episode_title": f"{anime_title}; Episode {episode_number}",
|
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"subtitles": [],
|
"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"
|
YUGEN_ENDPOINT: str = "https://yugenanime.tv"
|
||||||
|
|
||||||
SEARCH_URL = YUGEN_ENDPOINT + "/api/discover/"
|
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"]}
|
return {"id_anilist": anime["id"], "id_mal": anime["idMal"]}
|
||||||
except Exception as e:
|
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):
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Something unexpected occured {e}")
|
logger.error(f"Something unexpected occurred {e}")
|
||||||
|
|||||||
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 click import clear
|
||||||
from rich import print
|
from rich import print
|
||||||
|
|
||||||
|
from ...cli.utils.tools import exit_app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
FZF_DEFAULT_OPTS = """
|
FZF_DEFAULT_OPTS = """
|
||||||
--color=fg:#d0d0d0,fg+:#d0d0d0,bg:#121212,bg+:#262626
|
--color=fg:#d0d0d0,fg+:#d0d0d0,bg:#121212,bg+:#262626
|
||||||
--color=hl:#5f87af,hl+:#5fd7ff,info:#afaf87,marker:#87ff00
|
--color=hl:#5f87af,hl+:#5fd7ff,info:#afaf87,marker:#87ff00
|
||||||
--color=prompt:#d7005f,spinner:#af5fff,pointer:#af5fff,header:#87afaf
|
--color=prompt:#d7005f,spinner:#af5fff,pointer:#af5fff,header:#87afaf
|
||||||
@@ -127,7 +129,10 @@ class FZF:
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
if not result or result.returncode != 0 or not result.stdout:
|
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...")
|
input("press enter to try again...")
|
||||||
clear()
|
clear()
|
||||||
return self._run_fzf(commands, _fzf_input)
|
return self._run_fzf(commands, _fzf_input)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
pythonPackages = python.pkgs;
|
pythonPackages = python.pkgs;
|
||||||
fastanimeEnv = pythonPackages.buildPythonApplication {
|
fastanimeEnv = pythonPackages.buildPythonApplication {
|
||||||
pname = "fastanime";
|
pname = "fastanime";
|
||||||
version = "2.8.4";
|
version = "2.8.8";
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
mpv
|
mpv
|
||||||
fastapi
|
fastapi
|
||||||
pycryptodome
|
pycryptodome
|
||||||
|
pypresence
|
||||||
];
|
];
|
||||||
|
|
||||||
# Ensure compatibility with the pyproject.toml
|
# 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" &&
|
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 stage "$CLI_DIR/pyproject.toml" "$CLI_DIR/fastanime/__init__.py" "$CLI_DIR/flake.nix" &&
|
||||||
git commit -m "chore: bump version (v$VERSION)" &&
|
git commit -m "chore: bump version (v$VERSION)" &&
|
||||||
nix flake lock &&
|
# nix flake lock &&
|
||||||
uv lock &&
|
uv lock &&
|
||||||
git stage "$CLI_DIR/flake.lock" "$CLI_DIR/uv.lock" &&
|
git stage "$CLI_DIR/flake.lock" "$CLI_DIR/uv.lock" &&
|
||||||
git commit -m "chore: update lock files" &&
|
git commit -m "chore: update lock files" &&
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "fastanime"
|
name = "fastanime"
|
||||||
version = "2.8.4"
|
version = "2.8.8"
|
||||||
description = "A browser anime site experience from the terminal"
|
description = "A browser anime site experience from the terminal"
|
||||||
license = "UNLICENSE"
|
license = "UNLICENSE"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -9,6 +9,7 @@ dependencies = [
|
|||||||
"click>=8.1.7",
|
"click>=8.1.7",
|
||||||
"inquirerpy>=0.3.4",
|
"inquirerpy>=0.3.4",
|
||||||
"pycryptodome>=3.21.0",
|
"pycryptodome>=3.21.0",
|
||||||
|
"pypresence>=4.3.0",
|
||||||
"requests>=2.32.3",
|
"requests>=2.32.3",
|
||||||
"rich>=13.9.2",
|
"rich>=13.9.2",
|
||||||
"thefuzz>=0.22.1",
|
"thefuzz>=0.22.1",
|
||||||
@@ -30,6 +31,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
dev-dependencies = [
|
dev-dependencies = [
|
||||||
|
"pre-commit>=4.0.1",
|
||||||
"pyinstaller>=6.11.1",
|
"pyinstaller>=6.11.1",
|
||||||
"pyright>=1.1.384",
|
"pyright>=1.1.384",
|
||||||
"pytest>=8.3.3",
|
"pytest>=8.3.3",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from fastanime.cli import run_cli
|
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):
|
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"])
|
result = runner.invoke(run_cli, ["completions"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_check_for_updates.assert_not_called()
|
mock_check_for_updates.assert_not_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user