mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-13 08:00:47 -08:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32b7e917f | ||
|
|
3b35e80199 | ||
|
|
c65a1a2815 | ||
|
|
0b3615c9f5 | ||
|
|
3ac4e1ac71 | ||
|
|
d62f580d7a | ||
|
|
02e35b66cb | ||
|
|
7b11e0a301 | ||
|
|
aa8b91aed3 | ||
|
|
fe0fa97576 | ||
|
|
92059cd5ed | ||
|
|
ed3064e3b1 | ||
|
|
441d1e5e6c | ||
|
|
653b2cf4eb | ||
|
|
8d4b71e0c8 | ||
|
|
29cc6cad09 | ||
|
|
8119eef263 | ||
|
|
912c8674cf | ||
|
|
6b3ca236dd | ||
|
|
f1c352d4ff | ||
|
|
714533d845 | ||
|
|
56dd25df8d | ||
|
|
8248dc53df | ||
|
|
1a8a187de6 | ||
|
|
bc86be8c93 | ||
|
|
75026d4fc5 | ||
|
|
f8a5ccb8d2 | ||
|
|
719d1bd187 | ||
|
|
0dd83463c6 | ||
|
|
1ee50e8a55 | ||
|
|
ae95c5ea3d | ||
|
|
d64ad5e11d | ||
|
|
d1a47c6d44 | ||
|
|
51a834a62f | ||
|
|
3a030bf6f7 | ||
|
|
eb6a6fc82c | ||
|
|
437ccd94e4 | ||
|
|
d65868cc30 | ||
|
|
8678aa6544 | ||
|
|
00e5141152 | ||
|
|
90e757dfe1 | ||
|
|
8b471b08e8 | ||
|
|
158bc5710f | ||
|
|
a0b946a13d | ||
|
|
b547b75f03 | ||
|
|
58c7427a47 | ||
|
|
6220b9c55d | ||
|
|
6b9b5c131c | ||
|
|
212f2af39c | ||
|
|
f7b2b4e0c9 | ||
|
|
a747529279 | ||
|
|
1dfdcc27ce | ||
|
|
3c03289453 | ||
|
|
06fd446a72 | ||
|
|
172d912d8b | ||
|
|
2396018607 | ||
|
|
a9be9779c5 | ||
|
|
2f76b26a99 | ||
|
|
2fe5edf810 | ||
|
|
d67ee6a779 | ||
|
|
e06ec5dbd4 | ||
|
|
c1b24ba2aa | ||
|
|
59e9cf9fd0 | ||
|
|
58761f5b96 | ||
|
|
ac959da229 | ||
|
|
bacc8c48ec | ||
|
|
905a159428 | ||
|
|
20f734cab2 | ||
|
|
7c2c644aef | ||
|
|
0efc92081a | ||
|
|
fafeee2367 | ||
|
|
e03063cd76 | ||
|
|
93b38b055f | ||
|
|
045635fb55 | ||
|
|
de7f773e9e | ||
|
|
ef6a465bd2 | ||
|
|
0c623af8a4 | ||
|
|
0589f83998 | ||
|
|
e17608afd5 | ||
|
|
b915654685 | ||
|
|
2ce9bf6c47 | ||
|
|
3c22232432 | ||
|
|
3474e9520c | ||
|
|
e9bacf4f9c | ||
|
|
ef422ed6fd | ||
|
|
d0f5366908 | ||
|
|
3557205feb | ||
|
|
ba4c41d888 | ||
|
|
1427a3193c | ||
|
|
b5cee20e56 | ||
|
|
be7f464073 | ||
|
|
c7f8f168f5 | ||
|
|
ba59fbdcb0 | ||
|
|
9f54fa4998 | ||
|
|
3c9688b32c | ||
|
|
1f046447bb | ||
|
|
87e3a275bb | ||
|
|
037b5c36a4 | ||
|
|
7d8b60fb14 | ||
|
|
0ad16fee53 | ||
|
|
249243aeb4 | ||
|
|
c208dc3579 | ||
|
|
ea93f2ba23 | ||
|
|
d910a0bb6a | ||
|
|
550fcfeddc | ||
|
|
c6910e5a1c | ||
|
|
8555edb521 | ||
|
|
139193ce29 | ||
|
|
1a87375ccd | ||
|
|
83cbef40f6 | ||
|
|
85b4fc75a1 | ||
|
|
f2e2da378f | ||
|
|
7c34bc9120 | ||
|
|
6f153f2acb | ||
|
|
8171083978 | ||
|
|
db5b9a59b4 | ||
|
|
6fa656ba11 | ||
|
|
de0682c1bb |
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -8,31 +8,24 @@ jobs:
|
|||||||
debug_build:
|
debug_build:
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Python
|
|
||||||
|
- name: "Set up Python"
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
- name: Install poetry
|
|
||||||
uses: abatilo/actions-poetry@v2
|
- name: Install uv
|
||||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
uses: astral-sh/setup-uv@v3
|
||||||
run: |
|
|
||||||
poetry config virtualenvs.create true --local
|
|
||||||
poetry config virtualenvs.in-project true --local
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
|
||||||
with:
|
with:
|
||||||
path: ./.venv
|
enable-cache: true
|
||||||
key: venv-${{ hashFiles('poetry.lock') }}
|
|
||||||
- name: Install the project dependencies
|
- name: Build fastanime
|
||||||
run: poetry install
|
run: uv build
|
||||||
- name: build app
|
|
||||||
run: poetry build
|
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: fastanime_debug_build
|
name: fastanime_debug_build
|
||||||
path: |
|
path: |
|
||||||
dist
|
dist
|
||||||
!dist/*.whl
|
|
||||||
# - name: Run the automated tests (for example)
|
|
||||||
# run: poetry run pytest -v
|
|
||||||
|
|||||||
12
.github/workflows/publish.yml
vendored
12
.github/workflows/publish.yml
vendored
@@ -27,11 +27,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
- name: Build release distributions
|
- name: Install uv
|
||||||
run: |
|
uses: astral-sh/setup-uv@v3
|
||||||
# NOTE: put your own distribution build steps here.
|
with:
|
||||||
python -m pip install build
|
enable-cache: true
|
||||||
python -m build
|
|
||||||
|
- name: Build fastanime
|
||||||
|
run: uv build
|
||||||
|
|
||||||
- name: Upload distributions
|
- name: Upload distributions
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
40
.github/workflows/test.yml
vendored
40
.github/workflows/test.yml
vendored
@@ -6,37 +6,35 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11"] # List the Python versions you want to test
|
python-version: ["3.10", "3.11"] # List the Python versions you want to test
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Python ${{ matrix.python-version }}
|
- name: Install Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install poetry
|
|
||||||
uses: abatilo/actions-poetry@v2
|
- name: Install uv
|
||||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
uses: astral-sh/setup-uv@v3
|
||||||
run: |
|
|
||||||
poetry config virtualenvs.create true --local
|
|
||||||
poetry config virtualenvs.in-project true --local
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
|
||||||
with:
|
with:
|
||||||
path: ./.venv
|
enable-cache: true
|
||||||
key: venv-${{ hashFiles('poetry.lock') }}
|
|
||||||
- name: Install the project dependencies
|
- name: Install the project
|
||||||
run: poetry install
|
run: uv sync --all-extras --dev
|
||||||
- name: run linter, formatters and sort imports
|
|
||||||
run: |
|
- name: Run linter and formater
|
||||||
poetry run black .
|
run: uv run ruff check --output-format=github
|
||||||
poetry run ruff check --output-format=github . --fix
|
|
||||||
poetry run isort . --profile black
|
- name: Run type checking
|
||||||
- name: run type checking
|
run: uv run pyright
|
||||||
run: poetry run pyright
|
|
||||||
- name: run tests
|
- name: Run tests
|
||||||
run: poetry run pytest
|
run: uv run pytest tests
|
||||||
|
|||||||
11
Dockerfile
11
Dockerfile
@@ -1,10 +1,7 @@
|
|||||||
FROM ubuntu
|
FROM python:3.12-slim-bookworm
|
||||||
RUN apt-get update
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||||
RUN apt-get -y install python3
|
|
||||||
RUN apt-get update
|
|
||||||
RUN apt-get -y install pipx
|
|
||||||
RUN pipx ensurepath
|
|
||||||
COPY . /fastanime
|
COPY . /fastanime
|
||||||
|
ENV PATH=/root/.local/bin:$PATH
|
||||||
WORKDIR /fastanime
|
WORKDIR /fastanime
|
||||||
RUN pipx install .
|
RUN uv tool install .
|
||||||
CMD ["bash"]
|
CMD ["bash"]
|
||||||
|
|||||||
5
fa
5
fa
@@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# exec "${PYTHON:-python3}" -Werror -Xdev -m "$(dirname "$(realpath "$0")")/fastanime" "$@"
|
CLI_DIR="$(dirname "$(realpath "$0")")"
|
||||||
cd "$(dirname "$(realpath "$0")")" || exit 1
|
exec uv run --directory "$CLI_DIR/../" fastanime "$@"
|
||||||
exec python -m fastanime "$@"
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"""An abstraction over all providers offering added features with a simple and well typed api
|
"""An abstraction over all providers offering added features with a simple and well typed api"""
|
||||||
|
|
||||||
[TODO:description]
|
|
||||||
"""
|
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .libs.anime_provider import anime_sources
|
from .libs.anime_provider import anime_sources
|
||||||
@@ -32,19 +30,36 @@ class AnimeProvider:
|
|||||||
PROVIDERS = list(anime_sources.keys())
|
PROVIDERS = list(anime_sources.keys())
|
||||||
provider = PROVIDERS[0]
|
provider = PROVIDERS[0]
|
||||||
|
|
||||||
def __init__(self, provider, dynamic=False, retries=0) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
provider,
|
||||||
|
cache_requests=os.environ.get("FASTANIME_CACHE_REQUESTS", "false"),
|
||||||
|
use_persistent_provider_store=os.environ.get(
|
||||||
|
"FASTANIME_USE_PERSISTENT_PROVIDER_STORE", "false"
|
||||||
|
),
|
||||||
|
dynamic=False,
|
||||||
|
retries=0,
|
||||||
|
) -> None:
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
self.dynamic = dynamic
|
self.dynamic = dynamic
|
||||||
self.retries = retries
|
self.retries = retries
|
||||||
|
self.cache_requests = cache_requests
|
||||||
|
self.use_persistent_provider_store = use_persistent_provider_store
|
||||||
self.lazyload_provider(self.provider)
|
self.lazyload_provider(self.provider)
|
||||||
|
|
||||||
def lazyload_provider(self, provider):
|
def lazyload_provider(self, provider):
|
||||||
"""updates the current provider being used"""
|
"""updates the current provider being used"""
|
||||||
|
try:
|
||||||
|
self.anime_provider.session.kill_connection_to_db()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
_, anime_provider_cls_name = anime_sources[provider].split(".", 1)
|
_, anime_provider_cls_name = anime_sources[provider].split(".", 1)
|
||||||
package = f"fastanime.libs.anime_provider.{provider}"
|
package = f"fastanime.libs.anime_provider.{provider}"
|
||||||
provider_api = importlib.import_module(".api", package)
|
provider_api = importlib.import_module(".api", package)
|
||||||
anime_provider = getattr(provider_api, anime_provider_cls_name)
|
anime_provider = getattr(provider_api, anime_provider_cls_name)
|
||||||
self.anime_provider = anime_provider()
|
self.anime_provider = anime_provider(
|
||||||
|
self.cache_requests, self.use_persistent_provider_store
|
||||||
|
)
|
||||||
|
|
||||||
def search_for_anime(
|
def search_for_anime(
|
||||||
self,
|
self,
|
||||||
@@ -66,13 +81,9 @@ class AnimeProvider:
|
|||||||
[TODO:return]
|
[TODO:return]
|
||||||
"""
|
"""
|
||||||
anime_provider = self.anime_provider
|
anime_provider = self.anime_provider
|
||||||
try:
|
results = anime_provider.search_for_anime(
|
||||||
results = anime_provider.search_for_anime(
|
user_query, translation_type, nsfw, unknown
|
||||||
user_query, translation_type, nsfw, unknown
|
)
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
|
|
||||||
results = None
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -90,17 +101,13 @@ class AnimeProvider:
|
|||||||
[TODO:return]
|
[TODO:return]
|
||||||
"""
|
"""
|
||||||
anime_provider = self.anime_provider
|
anime_provider = self.anime_provider
|
||||||
try:
|
results = anime_provider.get_anime(anime_id)
|
||||||
results = anime_provider.get_anime(anime_id)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
|
|
||||||
|
|
||||||
results = None
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_episode_streams(
|
def get_episode_streams(
|
||||||
self,
|
self,
|
||||||
anime,
|
anime_id,
|
||||||
episode: str,
|
episode: str,
|
||||||
translation_type: str,
|
translation_type: str,
|
||||||
) -> "Iterator[Server] | None":
|
) -> "Iterator[Server] | None":
|
||||||
@@ -116,12 +123,7 @@ class AnimeProvider:
|
|||||||
[TODO:return]
|
[TODO:return]
|
||||||
"""
|
"""
|
||||||
anime_provider = self.anime_provider
|
anime_provider = self.anime_provider
|
||||||
try:
|
results = anime_provider.get_episode_streams(
|
||||||
results = anime_provider.get_episode_streams(
|
anime_id, episode, translation_type
|
||||||
anime, episode, translation_type
|
)
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIMEPROVIDER-ERROR]: {e}")
|
|
||||||
|
|
||||||
results = None
|
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
|
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
|
||||||
|
|
||||||
|
COMMA_REGEX = re.compile(r"([0-9]{3})(?=\d)")
|
||||||
|
|
||||||
|
|
||||||
# TODO: Add formating options for the final date
|
# TODO: Add formating options for the final date
|
||||||
def format_anilist_date_object(anilist_date_object: "AnilistDateObject"):
|
def format_anilist_date_object(anilist_date_object: "AnilistDateObject"):
|
||||||
if anilist_date_object:
|
if anilist_date_object and anilist_date_object["day"]:
|
||||||
return f"{anilist_date_object['day']}/{anilist_date_object['month']}/{anilist_date_object['year']}"
|
return f"{anilist_date_object['day']}/{anilist_date_object['month']}/{anilist_date_object['year']}"
|
||||||
else:
|
else:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
@@ -27,6 +30,12 @@ def format_list_data_with_comma(data: list | None):
|
|||||||
return "None"
|
return "None"
|
||||||
|
|
||||||
|
|
||||||
|
def format_number_with_commas(number: int | None):
|
||||||
|
if not number:
|
||||||
|
return "0"
|
||||||
|
return COMMA_REGEX.sub(lambda match: f"{match.group(1)},", str(number)[::-1])[::-1]
|
||||||
|
|
||||||
|
|
||||||
def extract_next_airing_episode(airing_episode: "AnilistMediaNextAiringEpisode"):
|
def extract_next_airing_episode(airing_episode: "AnilistMediaNextAiringEpisode"):
|
||||||
if airing_episode:
|
if airing_episode:
|
||||||
return f"{airing_episode['episode']} on {format_anilist_timestamp(airing_episode['airingAt'])}"
|
return f"{airing_episode['episode']} on {format_anilist_timestamp(airing_episode['airingAt'])}"
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ anime_normalizer_raw = {
|
|||||||
"Magia Record: Mahou Shoujo Madoka☆Magica Gaiden (TV)": "Mahou Shoujo Madoka☆Magica",
|
"Magia Record: Mahou Shoujo Madoka☆Magica Gaiden (TV)": "Mahou Shoujo Madoka☆Magica",
|
||||||
"Dungeon ni Deai o Motomeru no wa Machigatte Iru Darouka": "Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka",
|
"Dungeon ni Deai o Motomeru no wa Machigatte Iru Darouka": "Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka",
|
||||||
'Hazurewaku no "Joutai Ijou Skill" de Saikyou ni Natta Ore ga Subete wo Juurin suru made': "Hazure Waku no [Joutai Ijou Skill] de Saikyou ni Natta Ore ga Subete wo Juurin Suru made",
|
'Hazurewaku no "Joutai Ijou Skill" de Saikyou ni Natta Ore ga Subete wo Juurin suru made': "Hazure Waku no [Joutai Ijou Skill] de Saikyou ni Natta Ore ga Subete wo Juurin Suru made",
|
||||||
|
"Re:Zero kara Hajimeru Isekai Seikatsu Season 3": "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season",
|
||||||
},
|
},
|
||||||
"aniwatch": {"My Star": "Oshi no Ko"},
|
"hianime": {"My Star": "Oshi no Ko"},
|
||||||
"animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
|
"animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
|
||||||
|
"nyaa": {},
|
||||||
|
"yugen": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -19,7 +22,7 @@ def get_anime_normalizer():
|
|||||||
"""Used because there are different providers"""
|
"""Used because there are different providers"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
current_provider = os.environ["CURRENT_FASTANIME_PROVIDER"]
|
current_provider = os.environ.get("FASTANIME_PROVIDER", "allanime")
|
||||||
return anime_normalizer_raw[current_provider]
|
return anime_normalizer_raw[current_provider]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
6
fastanime/Utility/downloader/_yt_dlp.py
Normal file
6
fastanime/Utility/downloader/_yt_dlp.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from yt_dlp import YoutubeDL
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: create a class that makes yt-dlp's YoutubeDL fit in more with fastanime
|
||||||
|
class YtDlp(YoutubeDL):
|
||||||
|
pass
|
||||||
@@ -16,6 +16,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class YtDLPDownloader:
|
class YtDLPDownloader:
|
||||||
downloads_queue = Queue()
|
downloads_queue = Queue()
|
||||||
|
_thread = None
|
||||||
|
|
||||||
def _worker(self):
|
def _worker(self):
|
||||||
while True:
|
while True:
|
||||||
@@ -26,11 +27,6 @@ class YtDLPDownloader:
|
|||||||
logger.error(f"Something went wrong {e}")
|
logger.error(f"Something went wrong {e}")
|
||||||
self.downloads_queue.task_done()
|
self.downloads_queue.task_done()
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._thread = Thread(target=self._worker)
|
|
||||||
self._thread.daemon = True
|
|
||||||
self._thread.start()
|
|
||||||
|
|
||||||
def _download_file(
|
def _download_file(
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
@@ -38,6 +34,7 @@ class YtDLPDownloader:
|
|||||||
episode_title: str,
|
episode_title: str,
|
||||||
download_dir: str,
|
download_dir: str,
|
||||||
silent: bool,
|
silent: bool,
|
||||||
|
progress_hooks=[],
|
||||||
vid_format: str = "best",
|
vid_format: str = "best",
|
||||||
force_unknown_ext=False,
|
force_unknown_ext=False,
|
||||||
verbose=False,
|
verbose=False,
|
||||||
@@ -59,6 +56,25 @@ class YtDLPDownloader:
|
|||||||
"""
|
"""
|
||||||
anime_title = sanitize_filename(anime_title)
|
anime_title = sanitize_filename(anime_title)
|
||||||
episode_title = sanitize_filename(episode_title)
|
episode_title = sanitize_filename(episode_title)
|
||||||
|
if url.endswith(".torrent"):
|
||||||
|
WEBTORRENT_CLI = shutil.which("webtorrent")
|
||||||
|
if not WEBTORRENT_CLI:
|
||||||
|
import time
|
||||||
|
|
||||||
|
print(
|
||||||
|
"webtorrent cli is not installed which is required for downloading and streaming from nyaa\nplease install it or use another provider"
|
||||||
|
)
|
||||||
|
time.sleep(120)
|
||||||
|
return
|
||||||
|
cmd = [
|
||||||
|
WEBTORRENT_CLI,
|
||||||
|
"download",
|
||||||
|
url,
|
||||||
|
"--out",
|
||||||
|
os.path.join(download_dir, anime_title, episode_title),
|
||||||
|
]
|
||||||
|
subprocess.run(cmd)
|
||||||
|
return
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
# Specify the output path and template
|
# Specify the output path and template
|
||||||
"http_headers": headers,
|
"http_headers": headers,
|
||||||
@@ -67,6 +83,7 @@ class YtDLPDownloader:
|
|||||||
"verbose": verbose,
|
"verbose": verbose,
|
||||||
"format": vid_format,
|
"format": vid_format,
|
||||||
"compat_opts": ("allow-unsafe-ext",) if force_unknown_ext else tuple(),
|
"compat_opts": ("allow-unsafe-ext",) if force_unknown_ext else tuple(),
|
||||||
|
"progress_hooks": progress_hooks,
|
||||||
}
|
}
|
||||||
urls = [url]
|
urls = [url]
|
||||||
if sub:
|
if sub:
|
||||||
@@ -79,7 +96,14 @@ class YtDLPDownloader:
|
|||||||
if not info:
|
if not info:
|
||||||
continue
|
continue
|
||||||
if i == 0:
|
if i == 0:
|
||||||
vid_path = info["requested_downloads"][0]["filepath"]
|
vid_path: str = info["requested_downloads"][0]["filepath"]
|
||||||
|
if vid_path.endswith(".unknown_video"):
|
||||||
|
print("Normalizing path...")
|
||||||
|
_vid_path = vid_path.replace(".unknown_video", ".mp4")
|
||||||
|
shutil.move(vid_path, _vid_path)
|
||||||
|
vid_path = _vid_path
|
||||||
|
print("successfully normalized path")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
sub_path = info["requested_downloads"][0]["filepath"]
|
sub_path = info["requested_downloads"][0]["filepath"]
|
||||||
if sub_path and vid_path and merge:
|
if sub_path and vid_path and merge:
|
||||||
@@ -148,8 +172,15 @@ class YtDLPDownloader:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[red bold]An error[/] occurred: {e}")
|
print(f"[red bold]An error[/] occurred: {e}")
|
||||||
|
|
||||||
# WARN: May remove this legacy functionality
|
def download_file(
|
||||||
def download_file(self, url: str, title, silent=True):
|
self,
|
||||||
|
url: str,
|
||||||
|
anime_title: str,
|
||||||
|
episode_title: str,
|
||||||
|
download_dir: str,
|
||||||
|
silent: bool = True,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""A helper that just does things in the background
|
"""A helper that just does things in the background
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -157,7 +188,17 @@ class YtDLPDownloader:
|
|||||||
silent ([TODO:parameter]): [TODO:description]
|
silent ([TODO:parameter]): [TODO:description]
|
||||||
url: [TODO:description]
|
url: [TODO:description]
|
||||||
"""
|
"""
|
||||||
self.downloads_queue.put((self._download_file, (url, title, silent)))
|
if not self._thread:
|
||||||
|
self._thread = Thread(target=self._worker)
|
||||||
|
self._thread.daemon = True
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
self.downloads_queue.put(
|
||||||
|
(
|
||||||
|
self._download_file,
|
||||||
|
(url, anime_title, episode_title, download_dir, silent),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
downloader = YtDLPDownloader()
|
downloader = YtDLPDownloader()
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ def anime_title_percentage_match(
|
|||||||
title_a = str(anime["title"]["romaji"])
|
title_a = str(anime["title"]["romaji"])
|
||||||
title_b = str(anime["title"]["english"])
|
title_b = str(anime["title"]["english"])
|
||||||
percentage_ratio = max(
|
percentage_ratio = max(
|
||||||
|
*[
|
||||||
|
fuzz.ratio(title.lower(), possible_user_requested_anime_title.lower())
|
||||||
|
for title in anime["synonyms"]
|
||||||
|
],
|
||||||
fuzz.ratio(title_a.lower(), possible_user_requested_anime_title.lower()),
|
fuzz.ratio(title_a.lower(), possible_user_requested_anime_title.lower()),
|
||||||
fuzz.ratio(title_b.lower(), possible_user_requested_anime_title.lower()),
|
fuzz.ratio(title_b.lower(), possible_user_requested_anime_title.lower()),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import sys
|
|||||||
|
|
||||||
if sys.version_info < (3, 10):
|
if sys.version_info < (3, 10):
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"You are using an unsupported version of Python. Only Python versions 3.8 and above are supported by yt-dlp"
|
"You are using an unsupported version of Python. Only Python versions 3.10 and above are supported by FastAnime"
|
||||||
) # noqa: F541
|
) # noqa: F541
|
||||||
|
|
||||||
|
|
||||||
__version__ = "v2.5.2"
|
__version__ = "v2.7.5"
|
||||||
|
|
||||||
APP_NAME = "FastAnime"
|
APP_NAME = "FastAnime"
|
||||||
AUTHOR = "Benex254"
|
AUTHOR = "Benex254"
|
||||||
|
|||||||
93
fastanime/api/__init__.py
Normal file
93
fastanime/api/__init__.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from requests import post
|
||||||
|
from thefuzz import fuzz
|
||||||
|
|
||||||
|
from ..AnimeProvider import AnimeProvider
|
||||||
|
from ..Utility.data import anime_normalizer
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
anime_provider = AnimeProvider("allanime", "true", "true")
|
||||||
|
ANILIST_ENDPOINT = "https://graphql.anilist.co"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/search")
|
||||||
|
def search_for_anime(title: str, translation_type: Literal["dub", "sub"] = "sub"):
|
||||||
|
return anime_provider.search_for_anime(title, translation_type)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/anime/{anime_id}")
|
||||||
|
def get_anime(anime_id: str):
|
||||||
|
return anime_provider.get_anime(anime_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/anime/{anime_id}/watch")
|
||||||
|
def get_episode_streams(
|
||||||
|
anime_id: str, episode: str, translation_type: Literal["sub", "dub"]
|
||||||
|
):
|
||||||
|
return anime_provider.get_episode_streams(anime_id, episode, translation_type)
|
||||||
|
|
||||||
|
|
||||||
|
def get_anime_by_anilist_id(anilist_id: int):
|
||||||
|
query = f"""
|
||||||
|
query {{
|
||||||
|
Media(id: {anilist_id}) {{
|
||||||
|
id
|
||||||
|
title {{
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
native
|
||||||
|
}}
|
||||||
|
synonyms
|
||||||
|
episodes
|
||||||
|
duration
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
response = post(ANILIST_ENDPOINT, json={"query": query}).json()
|
||||||
|
return response["data"]["Media"]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/watch/{anilist_id}")
|
||||||
|
def get_episode_streams_by_anilist_id(
|
||||||
|
anilist_id: int, episode: str, translation_type: Literal["sub", "dub"]
|
||||||
|
):
|
||||||
|
anime = get_anime_by_anilist_id(anilist_id)
|
||||||
|
if not anime:
|
||||||
|
return
|
||||||
|
if search_results := anime_provider.search_for_anime(
|
||||||
|
str(anime["title"]["romaji"] or anime["title"]["english"]), translation_type
|
||||||
|
):
|
||||||
|
if not search_results["results"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
def match_title(possible_user_requested_anime_title):
|
||||||
|
possible_user_requested_anime_title = anime_normalizer.get(
|
||||||
|
possible_user_requested_anime_title, possible_user_requested_anime_title
|
||||||
|
)
|
||||||
|
title_a = str(anime["title"]["romaji"])
|
||||||
|
title_b = str(anime["title"]["english"])
|
||||||
|
percentage_ratio = max(
|
||||||
|
*[
|
||||||
|
fuzz.ratio(
|
||||||
|
title.lower(), possible_user_requested_anime_title.lower()
|
||||||
|
)
|
||||||
|
for title in anime["synonyms"]
|
||||||
|
],
|
||||||
|
fuzz.ratio(
|
||||||
|
title_a.lower(), possible_user_requested_anime_title.lower()
|
||||||
|
),
|
||||||
|
fuzz.ratio(
|
||||||
|
title_b.lower(), possible_user_requested_anime_title.lower()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return percentage_ratio
|
||||||
|
|
||||||
|
provider_anime = max(
|
||||||
|
search_results["results"], key=lambda x: match_title(x["title"])
|
||||||
|
)
|
||||||
|
anime_provider.get_anime(provider_anime["id"])
|
||||||
|
return anime_provider.get_episode_streams(
|
||||||
|
provider_anime["id"], episode, translation_type
|
||||||
|
)
|
||||||
84
fastanime/assets/rofi_theme.rasi
Normal file
84
fastanime/assets/rofi_theme.rasi
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// https://github.com/Wraient/curd/blob/main/rofi/selectanime.rasi
|
||||||
|
// Go give there project a star!
|
||||||
|
// Was too lazy to make my own preview, so I just used theirs
|
||||||
|
|
||||||
|
|
||||||
|
configuration {
|
||||||
|
font: "Sans 12";
|
||||||
|
line-margin: 10;
|
||||||
|
display-drun: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
background: #000000; /* Black background for everything */
|
||||||
|
background-alt: #000000; /* Ensures no alternation */
|
||||||
|
foreground: #CCCCCC;
|
||||||
|
selected: #3584E4;
|
||||||
|
active: #2E7D32;
|
||||||
|
urgent: #C62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
window {
|
||||||
|
fullscreen: false;
|
||||||
|
background-color: rgba(0, 0, 0, 1); /* Solid black background */
|
||||||
|
}
|
||||||
|
|
||||||
|
mainbox {
|
||||||
|
padding: 50px 100px;
|
||||||
|
background-color: rgba(0, 0, 0, 1); /* Ensures black background fills entire main area */
|
||||||
|
children: [inputbar, listview];
|
||||||
|
spacing: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputbar {
|
||||||
|
background-color: #333333; /* Dark gray background for input bar */
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
children: [prompt, entry];
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt {
|
||||||
|
enabled: true;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: @selected;
|
||||||
|
text-color: #000000;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #444444; /* Slightly lighter gray for visibility */
|
||||||
|
text-color: #FFFFFF; /* White text to make typing visible */
|
||||||
|
placeholder: "Search...";
|
||||||
|
placeholder-color: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
listview {
|
||||||
|
layout: vertical;
|
||||||
|
spacing: 8px;
|
||||||
|
lines: 10;
|
||||||
|
background-color: @background; /* Consistent black background for list items */
|
||||||
|
}
|
||||||
|
|
||||||
|
element {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: @background; /* Uniform color for each list item */
|
||||||
|
text-color: @foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
element normal.normal {
|
||||||
|
background-color: @background; /* Ensures no alternating color */
|
||||||
|
}
|
||||||
|
|
||||||
|
element selected.normal {
|
||||||
|
background-color: @selected;
|
||||||
|
text-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
element-text {
|
||||||
|
background-color: transparent;
|
||||||
|
text-color: inherit;
|
||||||
|
vertical-align: 0.5;
|
||||||
|
}
|
||||||
55
fastanime/assets/rofi_theme_confirm.rasi
Normal file
55
fastanime/assets/rofi_theme_confirm.rasi
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// https://github.com/Wraient/curd/blob/main/rofi/userinput.rasi
|
||||||
|
// Go give there project a star!
|
||||||
|
// Was too lazy to make my own preview, so I just used theirs
|
||||||
|
|
||||||
|
configuration {
|
||||||
|
font: "Sans 12";
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
text-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
window {
|
||||||
|
fullscreen: true;
|
||||||
|
transparency: "real";
|
||||||
|
background-color: @background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainbox {
|
||||||
|
children: [ message, listview, inputbar ];
|
||||||
|
padding: 40% 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
message {
|
||||||
|
border: 0;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
||||||
|
}
|
||||||
|
|
||||||
|
inputbar {
|
||||||
|
children: [ prompt, entry ];
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
listview {
|
||||||
|
lines: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the message text specifically */
|
||||||
|
textbox {
|
||||||
|
horizontal-align: 0.5; /* Center the text */
|
||||||
|
font: "Sans Bold 24"; /* Match message font */
|
||||||
|
}
|
||||||
55
fastanime/assets/rofi_theme_input.rasi
Normal file
55
fastanime/assets/rofi_theme_input.rasi
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// https://github.com/Wraient/curd/blob/main/rofi/userinput.rasi
|
||||||
|
// Go give there project a star!
|
||||||
|
// Was too lazy to make my own preview, so I just used theirs
|
||||||
|
|
||||||
|
configuration {
|
||||||
|
font: "Sans 12";
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
text-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
window {
|
||||||
|
fullscreen: true;
|
||||||
|
transparency: "real";
|
||||||
|
background-color: @background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainbox {
|
||||||
|
children: [ message, listview, inputbar ];
|
||||||
|
padding: 40% 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
message {
|
||||||
|
border: 0;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
||||||
|
}
|
||||||
|
|
||||||
|
inputbar {
|
||||||
|
children: [ prompt, entry ];
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
listview {
|
||||||
|
lines: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the message text specifically */
|
||||||
|
textbox {
|
||||||
|
horizontal-align: 0.5; /* Center the text */
|
||||||
|
font: "Sans Bold 24"; /* Match message font */
|
||||||
|
}
|
||||||
122
fastanime/assets/rofi_theme_preview.rasi
Normal file
122
fastanime/assets/rofi_theme_preview.rasi
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// Based on https://github.com/Wraient/curd/blob/main/rofi/selectanimepreview.rasi
|
||||||
|
// Go give there project a star!
|
||||||
|
// Was too lazy to make my own preview, so I just used theirs
|
||||||
|
|
||||||
|
// Colours
|
||||||
|
* {
|
||||||
|
background-color: transparent;
|
||||||
|
background: #1D2330;
|
||||||
|
background-transparent: #1D2330A0;
|
||||||
|
text-color: #BBBBBB;
|
||||||
|
text-color-selected: #FFFFFF;
|
||||||
|
primary: #BB77BB;
|
||||||
|
important: #BF616A;
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration {
|
||||||
|
font: "Roboto 17";
|
||||||
|
show-icons: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
window {
|
||||||
|
fullscreen: true;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
transparency: "real";
|
||||||
|
background-color: @background-transparent;
|
||||||
|
border: 0px;
|
||||||
|
border-color: @primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainbox {
|
||||||
|
children: [prompt, inputbar-box, listview];
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0px 0px 30px;
|
||||||
|
text-color: @important;
|
||||||
|
font: "Roboto Bold 27";
|
||||||
|
}
|
||||||
|
|
||||||
|
listview {
|
||||||
|
layout: vertical;
|
||||||
|
padding: 60px;
|
||||||
|
dynamic: true;
|
||||||
|
columns: 7;
|
||||||
|
spacing: 20px;
|
||||||
|
horizontal-align: center; /* Center the list items */
|
||||||
|
}
|
||||||
|
|
||||||
|
inputbar-box {
|
||||||
|
children: [dummy, inputbar, dummy];
|
||||||
|
orientation: horizontal;
|
||||||
|
expand: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputbar {
|
||||||
|
children: [textbox-prompt, entry];
|
||||||
|
margin: 0px;
|
||||||
|
background-color: @primary;
|
||||||
|
border: 4px;
|
||||||
|
border-color: @primary;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textbox-prompt {
|
||||||
|
text-color: @background;
|
||||||
|
horizontal-align: 0.5;
|
||||||
|
vertical-align: 0.5;
|
||||||
|
expand: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry {
|
||||||
|
expand: false;
|
||||||
|
padding: 8px;
|
||||||
|
margin: -6px;
|
||||||
|
horizontal-align: 0;
|
||||||
|
width: 300;
|
||||||
|
background-color: @background;
|
||||||
|
border: 6px;
|
||||||
|
border-color: @primary;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
element {
|
||||||
|
children: [dummy, element-box, dummy];
|
||||||
|
padding: 5px;
|
||||||
|
orientation: vertical;
|
||||||
|
border: 0px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: transparent; /* Default background */
|
||||||
|
}
|
||||||
|
|
||||||
|
element selected {
|
||||||
|
background-color: @primary; /* Solid color for selected item */
|
||||||
|
}
|
||||||
|
|
||||||
|
element-box {
|
||||||
|
children: [element-icon, element-text];
|
||||||
|
orientation: vertical;
|
||||||
|
expand: false;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
element-icon {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: inherit;
|
||||||
|
size: 33%;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
element-text {
|
||||||
|
horizontal-align: 0.5;
|
||||||
|
cursor: inherit;
|
||||||
|
text-color: @text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
element-text selected {
|
||||||
|
text-color: @text-color-selected;
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ commands = {
|
|||||||
"completions": "completions.completions",
|
"completions": "completions.completions",
|
||||||
"update": "update.update",
|
"update": "update.update",
|
||||||
"grab": "grab.grab",
|
"grab": "grab.grab",
|
||||||
|
"serve": "serve.serve",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -38,6 +39,29 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
cls=LazyGroup,
|
cls=LazyGroup,
|
||||||
help="A command line application for streaming anime that provides a complete and featureful interface",
|
help="A command line application for streaming anime that provides a complete and featureful interface",
|
||||||
short_help="Stream Anime",
|
short_help="Stream Anime",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# example of syncplay intergration
|
||||||
|
fastanime --sync-play --server sharepoint search -t <anime-title>
|
||||||
|
\b
|
||||||
|
# --- or ---
|
||||||
|
\b
|
||||||
|
# to watch with anilist intergration
|
||||||
|
fastanime --sync-play --server sharepoint anilist
|
||||||
|
\b
|
||||||
|
# downloading dubbed anime
|
||||||
|
fastanime --dub download -t <anime>
|
||||||
|
\b
|
||||||
|
# use icons and fzf for a more elegant ui with preview
|
||||||
|
fastanime --icons --preview --fzf anilist
|
||||||
|
\b
|
||||||
|
# use icons with default ui
|
||||||
|
fastanime --icons --default anilist
|
||||||
|
\b
|
||||||
|
# viewing manga
|
||||||
|
fastanime --manga search -t <manga-title>
|
||||||
|
""",
|
||||||
)
|
)
|
||||||
@click.version_option(__version__, "--version")
|
@click.version_option(__version__, "--version")
|
||||||
@click.option("--manga", "-m", help="Enable manga mode", is_flag=True)
|
@click.option("--manga", "-m", help="Enable manga mode", is_flag=True)
|
||||||
@@ -134,6 +158,9 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
@click.option("--sub", help="Set the translation type to sub", is_flag=True)
|
@click.option("--sub", help="Set the translation type to sub", is_flag=True)
|
||||||
@click.option("--rofi", help="Use rofi for the ui", is_flag=True)
|
@click.option("--rofi", help="Use rofi for the ui", is_flag=True)
|
||||||
@click.option("--rofi-theme", help="Rofi theme to use", type=click.Path())
|
@click.option("--rofi-theme", help="Rofi theme to use", type=click.Path())
|
||||||
|
@click.option(
|
||||||
|
"--rofi-theme-preview", help="Rofi theme to use for previews", type=click.Path()
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--rofi-theme-confirm",
|
"--rofi-theme-confirm",
|
||||||
help="Rofi theme to use for the confirm prompt",
|
help="Rofi theme to use for the confirm prompt",
|
||||||
@@ -154,6 +181,9 @@ signal.signal(signal.SIGINT, handle_exit)
|
|||||||
help="the player to use when streaming",
|
help="the player to use when streaming",
|
||||||
type=click.Choice(["mpv", "vlc"]),
|
type=click.Choice(["mpv", "vlc"]),
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--fresh-requests", is_flag=True, help="Force the requests cache to be updated"
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def run_cli(
|
def run_cli(
|
||||||
ctx: click.Context,
|
ctx: click.Context,
|
||||||
@@ -183,15 +213,30 @@ def run_cli(
|
|||||||
sub,
|
sub,
|
||||||
rofi,
|
rofi,
|
||||||
rofi_theme,
|
rofi_theme,
|
||||||
|
rofi_theme_preview,
|
||||||
rofi_theme_confirm,
|
rofi_theme_confirm,
|
||||||
rofi_theme_input,
|
rofi_theme_input,
|
||||||
use_python_mpv,
|
use_python_mpv,
|
||||||
sync_play,
|
sync_play,
|
||||||
player,
|
player,
|
||||||
|
fresh_requests,
|
||||||
):
|
):
|
||||||
|
import os
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
ctx.obj = Config()
|
ctx.obj = Config()
|
||||||
|
if ctx.obj.check_for_updates:
|
||||||
|
from .app_updater import check_for_updates
|
||||||
|
import time
|
||||||
|
|
||||||
|
is_latest = check_for_updates()
|
||||||
|
if not is_latest:
|
||||||
|
print(
|
||||||
|
"You are running an older version of fastanime please update to get the latest features"
|
||||||
|
)
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
ctx.obj.manga = manga
|
ctx.obj.manga = manga
|
||||||
if log:
|
if log:
|
||||||
import logging
|
import logging
|
||||||
@@ -227,13 +272,12 @@ def run_cli(
|
|||||||
|
|
||||||
install()
|
install()
|
||||||
|
|
||||||
|
if fresh_requests:
|
||||||
|
os.environ["FASTANIME_FRESH_REQUESTS"] = "1"
|
||||||
if sync_play:
|
if sync_play:
|
||||||
ctx.obj.sync_play = sync_play
|
ctx.obj.sync_play = sync_play
|
||||||
if provider:
|
if provider:
|
||||||
import os
|
|
||||||
|
|
||||||
ctx.obj.provider = provider
|
ctx.obj.provider = provider
|
||||||
os.environ["CURRENT_FASTANIME_PROVIDER"] = provider
|
|
||||||
if server:
|
if server:
|
||||||
ctx.obj.server = server
|
ctx.obj.server = server
|
||||||
if format:
|
if format:
|
||||||
@@ -296,6 +340,10 @@ def run_cli(
|
|||||||
if rofi:
|
if rofi:
|
||||||
from ..libs.rofi import Rofi
|
from ..libs.rofi import Rofi
|
||||||
|
|
||||||
|
if rofi_theme_preview:
|
||||||
|
ctx.obj.rofi_theme_preview = rofi_theme_preview
|
||||||
|
Rofi.rofi_theme_preview = rofi_theme_preview
|
||||||
|
|
||||||
if rofi_theme:
|
if rofi_theme:
|
||||||
ctx.obj.rofi_theme = rofi_theme
|
ctx.obj.rofi_theme = rofi_theme
|
||||||
Rofi.rofi_theme = rofi_theme
|
Rofi.rofi_theme = rofi_theme
|
||||||
@@ -307,3 +355,4 @@ def run_cli(
|
|||||||
if rofi_theme_confirm:
|
if rofi_theme_confirm:
|
||||||
ctx.obj.rofi_theme_confirm = rofi_theme_confirm
|
ctx.obj.rofi_theme_confirm = rofi_theme_confirm
|
||||||
Rofi.rofi_theme_confirm = rofi_theme_confirm
|
Rofi.rofi_theme_confirm = rofi_theme_confirm
|
||||||
|
ctx.obj.set_fastanime_config_environs()
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ def check_for_updates():
|
|||||||
|
|
||||||
return (is_latest, release_json)
|
return (is_latest, release_json)
|
||||||
else:
|
else:
|
||||||
|
print("Failed to check for updates")
|
||||||
print(request.text)
|
print(request.text)
|
||||||
return (False, {})
|
return (True, {})
|
||||||
|
|
||||||
|
|
||||||
def is_git_repo(author, repository):
|
def is_git_repo(author, repository):
|
||||||
@@ -75,9 +76,9 @@ def is_git_repo(author, repository):
|
|||||||
return bool(match) and match.group(1) == f"{author}/{repository}"
|
return bool(match) and match.group(1) == f"{author}/{repository}"
|
||||||
|
|
||||||
|
|
||||||
def update_app():
|
def update_app(force=False):
|
||||||
is_latest, release_json = check_for_updates()
|
is_latest, release_json = check_for_updates()
|
||||||
if is_latest:
|
if is_latest and not force:
|
||||||
print("[green]App is up to date[/]")
|
print("[green]App is up to date[/]")
|
||||||
return False, release_json
|
return False, release_json
|
||||||
tag_name = release_json["tag_name"]
|
tag_name = release_json["tag_name"]
|
||||||
@@ -101,8 +102,10 @@ def update_app():
|
|||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if PIPX_EXECUTABLE := shutil.which("pipx"):
|
if UV := shutil.which("uv"):
|
||||||
process = subprocess.run([PIPX_EXECUTABLE, "upgrade", APP_NAME])
|
process = subprocess.run([UV, "tool", "upgrade", APP_NAME])
|
||||||
|
elif PIPX := shutil.which("pipx"):
|
||||||
|
process = subprocess.run([PIPX, "upgrade", APP_NAME])
|
||||||
else:
|
else:
|
||||||
PYTHON_EXECUTABLE = sys.executable
|
PYTHON_EXECUTABLE = sys.executable
|
||||||
|
|
||||||
@@ -112,6 +115,7 @@ def update_app():
|
|||||||
"pip",
|
"pip",
|
||||||
"install",
|
"install",
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
|
"-U",
|
||||||
"--user",
|
"--user",
|
||||||
"--no-warn-script-location",
|
"--no-warn-script-location",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -30,6 +30,53 @@ commands = {
|
|||||||
invoke_without_command=True,
|
invoke_without_command=True,
|
||||||
help="A beautiful interface that gives you access to a commplete streaming experience",
|
help="A beautiful interface that gives you access to a commplete streaming experience",
|
||||||
short_help="Access all streaming options",
|
short_help="Access all streaming options",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# ---- search ----
|
||||||
|
\b
|
||||||
|
# get anime with the tag of isekai
|
||||||
|
fastanime anilist search -T isekai
|
||||||
|
\b
|
||||||
|
# get anime of 2024 and sort by popularity
|
||||||
|
# that has already finished airing or is releasing
|
||||||
|
# and is not in your anime lists
|
||||||
|
fastanime anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
|
||||||
|
\b
|
||||||
|
# get anime of 2024 season WINTER
|
||||||
|
fastanime anilist search -y 2024 --season WINTER
|
||||||
|
\b
|
||||||
|
# get anime genre action and tag isekai,magic
|
||||||
|
fastanime anilist search -g Action -T Isekai -T Magic
|
||||||
|
\b
|
||||||
|
# get anime of 2024 thats finished airing
|
||||||
|
fastanime anilist search -y 2024 -S FINISHED
|
||||||
|
\b
|
||||||
|
# get the most favourite anime movies
|
||||||
|
fastanime anilist search -f MOVIE -s FAVOURITES_DESC
|
||||||
|
\b
|
||||||
|
# ---- login ----
|
||||||
|
\b
|
||||||
|
# To sign in just run
|
||||||
|
fastanime anilist login
|
||||||
|
\b
|
||||||
|
# To view your login status
|
||||||
|
fastanime anilist login --status
|
||||||
|
\b
|
||||||
|
# To erase login data
|
||||||
|
fastanime anilist login --erase
|
||||||
|
\b
|
||||||
|
# ---- notifier ----
|
||||||
|
\b
|
||||||
|
# basic form
|
||||||
|
fastanime anilist notifier
|
||||||
|
\b
|
||||||
|
# with logging to stdout
|
||||||
|
fastanime --log anilist notifier
|
||||||
|
\b
|
||||||
|
# with logging to a file. stored in the same place as your config
|
||||||
|
fastanime --log-file anilist notifier
|
||||||
|
""",
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def anilist(ctx: click.Context):
|
def anilist(ctx: click.Context):
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ def notifier(config: "Config"):
|
|||||||
from sys import exit
|
from sys import exit
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from plyer import notification
|
|
||||||
|
try:
|
||||||
|
from plyer import notification
|
||||||
|
except ImportError:
|
||||||
|
print("Please install plyer to use this command")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
from ....anilist import AniList
|
from ....anilist import AniList
|
||||||
from ....constants import APP_CACHE_DIR, APP_DATA_DIR, APP_NAME, ICON_PATH, PLATFORM
|
from ....constants import APP_CACHE_DIR, APP_DATA_DIR, APP_NAME, ICON_PATH, PLATFORM
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
@click.command(help="Helper command to manage cache")
|
@click.command(
|
||||||
|
help="Helper command to manage cache",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# delete everything in the cache dir
|
||||||
|
fastanime cache --clean
|
||||||
|
\b
|
||||||
|
# print the path to the cache dir and exit
|
||||||
|
fastanime cache --path
|
||||||
|
\b
|
||||||
|
# print the current size of the cache dir and exit
|
||||||
|
fastanime cache --size
|
||||||
|
\b
|
||||||
|
# open the cache dir and exit
|
||||||
|
fastanime cache
|
||||||
|
""",
|
||||||
|
)
|
||||||
@click.option("--clean", help="Clean the cache dir", is_flag=True)
|
@click.option("--clean", help="Clean the cache dir", is_flag=True)
|
||||||
@click.option("--path", help="The path to the cache dir", is_flag=True)
|
@click.option("--path", help="The path to the cache dir", is_flag=True)
|
||||||
@click.option("--size", help="The size of the cache dir", is_flag=True)
|
@click.option("--size", help="The size of the cache dir", is_flag=True)
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
@click.command(help="Helper command to get shell completions")
|
@click.command(
|
||||||
|
help="Helper command to get shell completions",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# try to detect your shell and print completions
|
||||||
|
fastanime completions
|
||||||
|
\b
|
||||||
|
# print fish completions
|
||||||
|
fastanime completions --fish
|
||||||
|
\b
|
||||||
|
# print bash completions
|
||||||
|
fastanime completions --bash
|
||||||
|
\b
|
||||||
|
# print zsh completions
|
||||||
|
fastanime completions --zsh
|
||||||
|
""",
|
||||||
|
)
|
||||||
@click.option("--fish", is_flag=True, help="print fish completions")
|
@click.option("--fish", is_flag=True, help="print fish completions")
|
||||||
@click.option("--zsh", is_flag=True, help="print zsh completions")
|
@click.option("--zsh", is_flag=True, help="print zsh completions")
|
||||||
@click.option("--bash", is_flag=True, help="print bash completions")
|
@click.option("--bash", is_flag=True, help="print bash completions")
|
||||||
|
|||||||
@@ -9,6 +9,25 @@ if TYPE_CHECKING:
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="Manage your config with ease",
|
help="Manage your config with ease",
|
||||||
short_help="Edit your config",
|
short_help="Edit your config",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# Edit your config in your default editor
|
||||||
|
# NB: If it opens vim or vi exit with `:q`
|
||||||
|
fastanime config
|
||||||
|
\b
|
||||||
|
# get the path of the config file
|
||||||
|
fastanime config --path
|
||||||
|
\b
|
||||||
|
# print desktop entry info
|
||||||
|
fastanime config --desktop-entry
|
||||||
|
\b
|
||||||
|
# update your config without opening an editor
|
||||||
|
fastanime --icons --fzf --preview config --update
|
||||||
|
\b
|
||||||
|
# view the current contents of your config
|
||||||
|
fastanime config --view
|
||||||
|
""",
|
||||||
)
|
)
|
||||||
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
|
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
|
||||||
@click.option(
|
@click.option(
|
||||||
@@ -94,7 +113,7 @@ def config(user_config: "Config", path, view, desktop_entry, update):
|
|||||||
print(f"Successfully wrote \n{f.read()}")
|
print(f"Successfully wrote \n{f.read()}")
|
||||||
exit_app(0)
|
exit_app(0)
|
||||||
elif update:
|
elif update:
|
||||||
with open(USER_CONFIG_PATH, "w",encoding="utf-8") as file:
|
with open(USER_CONFIG_PATH, "w", encoding="utf-8") as file:
|
||||||
file.write(user_config.__str__())
|
file.write(user_config.__str__())
|
||||||
print("update successfull")
|
print("update successfull")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -11,6 +11,53 @@ if TYPE_CHECKING:
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="Download anime using the anime provider for a specified range",
|
help="Download anime using the anime provider for a specified range",
|
||||||
short_help="Download anime",
|
short_help="Download anime",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# Download all available episodes
|
||||||
|
# multiple titles can be specified with -t option
|
||||||
|
fastanime download -t <anime-title> -t <anime-title>
|
||||||
|
# -- or --
|
||||||
|
fastanime download -t <anime-title> -t <anime-title> -r ':'
|
||||||
|
\b
|
||||||
|
# download latest episode for the two anime titles
|
||||||
|
# the number can be any no of latest episodes but a minus sign
|
||||||
|
# must be present
|
||||||
|
fastanime download -t <anime-title> -t <anime-title> -r '-1'
|
||||||
|
\b
|
||||||
|
# latest 5
|
||||||
|
fastanime download -t <anime-title> -t <anime-title> -r '-5'
|
||||||
|
\b
|
||||||
|
# Download specific episode range
|
||||||
|
# be sure to observe the range Syntax
|
||||||
|
fastanime download -t <anime-title> -r '<episodes-start>:<episodes-end>:<step>'
|
||||||
|
\b
|
||||||
|
fastanime download -t <anime-title> -r '<episodes-start>:<episodes-end>'
|
||||||
|
\b
|
||||||
|
fastanime download -t <anime-title> -r '<episodes-start>:'
|
||||||
|
\b
|
||||||
|
fastanime download -t <anime-title> -r ':<episodes-end>'
|
||||||
|
\b
|
||||||
|
# download specific episode
|
||||||
|
# remember python indexing starts at 0
|
||||||
|
fastanime download -t <anime-title> -r '<episode-1>:<episode>'
|
||||||
|
\b
|
||||||
|
# merge subtitles with ffmpeg to mkv format; hianime tends to give subs as separate files
|
||||||
|
# and dont prompt for anything
|
||||||
|
# eg existing file in destination instead remove
|
||||||
|
# and clean
|
||||||
|
# ie remove original files (sub file and vid file)
|
||||||
|
# only keep merged files
|
||||||
|
fastanime download -t <anime-title> --merge --clean --no-prompt
|
||||||
|
\b
|
||||||
|
# EOF is used since -t always expects a title
|
||||||
|
# you can supply anime titles from file or -t at the same time
|
||||||
|
# from stdin
|
||||||
|
echo -e "<anime-title>\\n<anime-title>\\n<anime-title>" | fastanime download -t "EOF" -r <range> -f -
|
||||||
|
\b
|
||||||
|
# from file
|
||||||
|
fastanime download -t "EOF" -r <range> -f <file-path>
|
||||||
|
""",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--anime-titles",
|
"--anime-titles",
|
||||||
@@ -160,7 +207,7 @@ def download(
|
|||||||
choices = list(search_results_.keys())
|
choices = list(search_results_.keys())
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
selected_anime_title = fzf.run(
|
selected_anime_title = fzf.run(
|
||||||
choices, "Please Select title: ", "FastAnime"
|
choices, "Please Select title", "FastAnime"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
selected_anime_title = fuzzy_inquirer(
|
selected_anime_title = fuzzy_inquirer(
|
||||||
@@ -237,7 +284,7 @@ def download(
|
|||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Episode Streams...", total=None)
|
progress.add_task("Fetching Episode Streams...", total=None)
|
||||||
streams = anime_provider.get_episode_streams(
|
streams = anime_provider.get_episode_streams(
|
||||||
anime, episode, config.translation_type
|
anime["id"], episode, config.translation_type
|
||||||
)
|
)
|
||||||
if not streams:
|
if not streams:
|
||||||
print("No streams skipping")
|
print("No streams skipping")
|
||||||
@@ -272,7 +319,7 @@ def download(
|
|||||||
server_name = config.server
|
server_name = config.server
|
||||||
else:
|
else:
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
server_name = fzf.run(servers_names, "Select an link: ")
|
server_name = fzf.run(servers_names, "Select an link")
|
||||||
else:
|
else:
|
||||||
server_name = fuzzy_inquirer(
|
server_name = fuzzy_inquirer(
|
||||||
servers_names,
|
servers_names,
|
||||||
@@ -314,9 +361,9 @@ def download(
|
|||||||
episode_title,
|
episode_title,
|
||||||
download_dir,
|
download_dir,
|
||||||
silent,
|
silent,
|
||||||
config.format,
|
vid_format=config.format,
|
||||||
force_unknown_ext,
|
force_unknown_ext=force_unknown_ext,
|
||||||
verbose,
|
verbose=verbose,
|
||||||
headers=provider_headers,
|
headers=provider_headers,
|
||||||
sub=subtitles[0]["url"] if subtitles else "",
|
sub=subtitles[0]["url"] if subtitles else "",
|
||||||
merge=merge,
|
merge=merge,
|
||||||
|
|||||||
@@ -11,7 +11,32 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@click.command(
|
@click.command(
|
||||||
help="View and watch your downloads using mpv", short_help="Watch downloads"
|
help="View and watch your downloads using mpv",
|
||||||
|
short_help="Watch downloads",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
fastanime downloads
|
||||||
|
\b
|
||||||
|
# view individual episodes
|
||||||
|
fastanime downloads --view-episodes
|
||||||
|
# --- or ---
|
||||||
|
fastanime downloads -v
|
||||||
|
\b
|
||||||
|
# to set seek time when using ffmpegthumbnailer for local previews
|
||||||
|
# -1 means random and is the default
|
||||||
|
fastanime downloads --time-to-seek <intRange(-1,100)>
|
||||||
|
# --- or ---
|
||||||
|
fastanime downloads -t <intRange(-1,100)>
|
||||||
|
\b
|
||||||
|
# to watch a specific title
|
||||||
|
# be sure to get the completions for the best experience
|
||||||
|
fastanime downloads --title <title>
|
||||||
|
\b
|
||||||
|
# to get the path to the downloads folder set
|
||||||
|
fastanime downloads --path
|
||||||
|
# useful when you want to use the value for other programs
|
||||||
|
""",
|
||||||
)
|
)
|
||||||
@click.option("--path", "-p", help="print the downloads folder and exit", is_flag=True)
|
@click.option("--path", "-p", help="print the downloads folder and exit", is_flag=True)
|
||||||
@click.option(
|
@click.option(
|
||||||
@@ -268,7 +293,7 @@ def downloads(
|
|||||||
else:
|
else:
|
||||||
episode_title = fuzzy_inquirer(
|
episode_title = fuzzy_inquirer(
|
||||||
downloaded_episodes,
|
downloaded_episodes,
|
||||||
"Enter Playlist Name: ",
|
"Enter Playlist Name",
|
||||||
)
|
)
|
||||||
if episode_title == "Back":
|
if episode_title == "Back":
|
||||||
stream_anime()
|
stream_anime()
|
||||||
@@ -308,7 +333,7 @@ def downloads(
|
|||||||
else:
|
else:
|
||||||
playlist_name = fuzzy_inquirer(
|
playlist_name = fuzzy_inquirer(
|
||||||
anime_downloads,
|
anime_downloads,
|
||||||
"Enter Playlist Name: ",
|
"Enter Playlist Name",
|
||||||
)
|
)
|
||||||
if playlist_name == "Exit":
|
if playlist_name == "Exit":
|
||||||
exit_app()
|
exit_app()
|
||||||
|
|||||||
@@ -11,6 +11,41 @@ if TYPE_CHECKING:
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="Helper command to get streams for anime to use externally in a non-python application",
|
help="Helper command to get streams for anime to use externally in a non-python application",
|
||||||
short_help="Print anime streams to standard out",
|
short_help="Print anime streams to standard out",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# --- print anime info + episode streams ---
|
||||||
|
\b
|
||||||
|
# multiple titles can be specified with the -t option
|
||||||
|
fastanime grab -t <anime-title> -t <anime-title>
|
||||||
|
# -- or --
|
||||||
|
# print all available episodes
|
||||||
|
fastanime grab -t <anime-title> -r ':'
|
||||||
|
\b
|
||||||
|
# print the latest episode
|
||||||
|
fastanime grab -t <anime-title> -r '-1'
|
||||||
|
\b
|
||||||
|
# print a specific episode range
|
||||||
|
# be sure to observe the range Syntax
|
||||||
|
fastanime grab -t <anime-title> -r '<start>:<stop>'
|
||||||
|
\b
|
||||||
|
fastanime grab -t <anime-title> -r '<start>:<stop>:<step>'
|
||||||
|
\b
|
||||||
|
fastanime grab -t <anime-title> -r '<start>:'
|
||||||
|
\b
|
||||||
|
fastanime grab -t <anime-title> -r ':<end>'
|
||||||
|
\b
|
||||||
|
# --- grab options ---
|
||||||
|
\b
|
||||||
|
# print search results only
|
||||||
|
fastanime grab -t <anime-title> -r <range> --search-results-only
|
||||||
|
\b
|
||||||
|
# print anime info only
|
||||||
|
fastanime grab -t <anime-title> -r <range> --anime-info-only
|
||||||
|
\b
|
||||||
|
# print episode streams only
|
||||||
|
fastanime grab -t <anime-title> -r <range> --episode-streams-only
|
||||||
|
""",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--anime-titles",
|
"--anime-titles",
|
||||||
@@ -182,7 +217,7 @@ def grab(
|
|||||||
if episode not in episodes:
|
if episode not in episodes:
|
||||||
continue
|
continue
|
||||||
streams = anime_provider.get_episode_streams(
|
streams = anime_provider.get_episode_streams(
|
||||||
anime, episode, config.translation_type
|
anime["id"], episode, config.translation_type
|
||||||
)
|
)
|
||||||
if not streams:
|
if not streams:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -11,6 +11,29 @@ if TYPE_CHECKING:
|
|||||||
@click.command(
|
@click.command(
|
||||||
help="This subcommand directly interacts with the provider to enable basic streaming. Useful for binging anime.",
|
help="This subcommand directly interacts with the provider to enable basic streaming. Useful for binging anime.",
|
||||||
short_help="Binge anime",
|
short_help="Binge anime",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# basic form where you will still be prompted for the episode number
|
||||||
|
# multiple titles can be specified with the -t option
|
||||||
|
fastanime search -t <anime-title> -t <anime-title>
|
||||||
|
\b
|
||||||
|
# binge all episodes with this command
|
||||||
|
fastanime search -t <anime-title> -r ':'
|
||||||
|
\b
|
||||||
|
# watch latest episode
|
||||||
|
fastanime search -t <anime-title> -r '-1'
|
||||||
|
\b
|
||||||
|
# binge a specific episode range with this command
|
||||||
|
# be sure to observe the range Syntax
|
||||||
|
fastanime search -t <anime-title> -r '<start>:<stop>'
|
||||||
|
\b
|
||||||
|
fastanime search -t <anime-title> -r '<start>:<stop>:<step>'
|
||||||
|
\b
|
||||||
|
fastanime search -t <anime-title> -r '<start>:'
|
||||||
|
\b
|
||||||
|
fastanime search -t <anime-title> -r ':<end>'
|
||||||
|
""",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--anime-titles",
|
"--anime-titles",
|
||||||
@@ -76,7 +99,7 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
|||||||
preview = get_fzf_manga_preview(search_results)
|
preview = get_fzf_manga_preview(search_results)
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
search_result_manga_title = fzf.run(
|
search_result_manga_title = fzf.run(
|
||||||
choices, "Please Select title: ", preview=preview
|
choices, "Please Select title", preview=preview
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
search_result_manga_title = Rofi.run(choices, "Please Select Title")
|
search_result_manga_title = Rofi.run(choices, "Please Select Title")
|
||||||
@@ -166,7 +189,7 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
|||||||
choices = list(search_results_.keys())
|
choices = list(search_results_.keys())
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
search_result_manga_title = fzf.run(
|
search_result_manga_title = fzf.run(
|
||||||
choices, "Please Select title: ", "FastAnime"
|
choices, "Please Select title", "FastAnime"
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
search_result_manga_title = Rofi.run(choices, "Please Select Title")
|
search_result_manga_title = Rofi.run(choices, "Please Select Title")
|
||||||
@@ -224,7 +247,7 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
|||||||
|
|
||||||
anilist_anime_info = get_basic_anime_info_by_title(anime["title"])
|
anilist_anime_info = get_basic_anime_info_by_title(anime["title"])
|
||||||
|
|
||||||
def stream_anime():
|
def stream_anime(anime: "Anime"):
|
||||||
clear()
|
clear()
|
||||||
episode = None
|
episode = None
|
||||||
|
|
||||||
@@ -243,7 +266,7 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
|||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
episode = fzf.run(
|
episode = fzf.run(
|
||||||
choices,
|
choices,
|
||||||
"Select an episode: ",
|
"Select an episode",
|
||||||
header=search_result_manga_title,
|
header=search_result_manga_title,
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
@@ -260,7 +283,7 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
|||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Episode Streams...", total=None)
|
progress.add_task("Fetching Episode Streams...", total=None)
|
||||||
streams = anime_provider.get_episode_streams(
|
streams = anime_provider.get_episode_streams(
|
||||||
anime, episode, config.translation_type
|
anime["id"], episode, config.translation_type
|
||||||
)
|
)
|
||||||
if not streams:
|
if not streams:
|
||||||
print("Failed to get streams")
|
print("Failed to get streams")
|
||||||
@@ -275,13 +298,13 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
|||||||
if not server:
|
if not server:
|
||||||
print("Sth went wrong when fetching the episode")
|
print("Sth went wrong when fetching the episode")
|
||||||
input("Enter to continue")
|
input("Enter to continue")
|
||||||
stream_anime()
|
stream_anime(anime)
|
||||||
return
|
return
|
||||||
stream_link = filter_by_quality(config.quality, server["links"])
|
stream_link = filter_by_quality(config.quality, server["links"])
|
||||||
if not stream_link:
|
if not stream_link:
|
||||||
print("Quality not found")
|
print("Quality not found")
|
||||||
input("Enter to continue")
|
input("Enter to continue")
|
||||||
stream_anime()
|
stream_anime(anime)
|
||||||
return
|
return
|
||||||
link = stream_link["link"]
|
link = stream_link["link"]
|
||||||
subtitles = server["subtitles"]
|
subtitles = server["subtitles"]
|
||||||
@@ -297,7 +320,7 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
|||||||
server = config.server
|
server = config.server
|
||||||
else:
|
else:
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
server = fzf.run(servers_names, "Select an link: ")
|
server = fzf.run(servers_names, "Select an link")
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
server = Rofi.run(servers_names, "Select an link")
|
server = Rofi.run(servers_names, "Select an link")
|
||||||
else:
|
else:
|
||||||
@@ -311,7 +334,7 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
|||||||
if not stream_link:
|
if not stream_link:
|
||||||
print("Quality not found")
|
print("Quality not found")
|
||||||
input("Enter to continue")
|
input("Enter to continue")
|
||||||
stream_anime()
|
stream_anime(anime)
|
||||||
return
|
return
|
||||||
link = stream_link["link"]
|
link = stream_link["link"]
|
||||||
stream_headers = servers[server]["headers"]
|
stream_headers = servers[server]["headers"]
|
||||||
@@ -357,6 +380,6 @@ def search(config: "Config", anime_titles: str, episode_range: str):
|
|||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
print(e)
|
print(e)
|
||||||
input("Enter to continue")
|
input("Enter to continue")
|
||||||
stream_anime()
|
stream_anime(anime)
|
||||||
|
|
||||||
stream_anime()
|
stream_anime(anime)
|
||||||
|
|||||||
31
fastanime/cli/commands/serve.py
Normal file
31
fastanime/cli/commands/serve.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(
|
||||||
|
help="Command that automates the starting of the builtin fastanime server",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# default
|
||||||
|
fastanime serve
|
||||||
|
|
||||||
|
# specify host and port
|
||||||
|
fastanime serve --host 127.0.0.1 --port 8080
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
@click.option("--host", "-H", help="Specify the host to run the server on")
|
||||||
|
@click.option("--port", "-p", help="Specify the port to run the server on")
|
||||||
|
def serve(host, port):
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ...constants import APP_DIR
|
||||||
|
|
||||||
|
args = [sys.executable, "-m", "fastapi", "run"]
|
||||||
|
if host:
|
||||||
|
args.extend(["--host", host])
|
||||||
|
|
||||||
|
if port:
|
||||||
|
args.extend(["--port", port])
|
||||||
|
args.append(os.path.join(APP_DIR, "api"))
|
||||||
|
os.execv(sys.executable, args)
|
||||||
@@ -1,11 +1,24 @@
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
@click.command(help="Helper command to update fastanime to latest")
|
@click.command(
|
||||||
|
help="Helper command to update fastanime to latest",
|
||||||
|
epilog="""
|
||||||
|
\b
|
||||||
|
\b\bExamples:
|
||||||
|
# update fastanime to latest
|
||||||
|
fastanime update
|
||||||
|
\b
|
||||||
|
# check for latest release
|
||||||
|
fastanime update --check
|
||||||
|
|
||||||
|
# Force an update regardless of the current version
|
||||||
|
fastanime update --force
|
||||||
|
""",
|
||||||
|
)
|
||||||
@click.option("--check", "-c", help="Check for the latest release", is_flag=True)
|
@click.option("--check", "-c", help="Check for the latest release", is_flag=True)
|
||||||
def update(
|
@click.option("--force", "-c", help="Force update", is_flag=True)
|
||||||
check,
|
def update(check, force):
|
||||||
):
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
|
||||||
@@ -34,7 +47,7 @@ def update(
|
|||||||
print(f"You are running the latest version ({__version__}) of fastanime")
|
print(f"You are running the latest version ({__version__}) of fastanime")
|
||||||
_print_release(github_release_data)
|
_print_release(github_release_data)
|
||||||
else:
|
else:
|
||||||
success, github_release_data = update_app()
|
success, github_release_data = update_app(force)
|
||||||
_print_release(github_release_data)
|
_print_release(github_release_data)
|
||||||
if success:
|
if success:
|
||||||
print("Successfully updated")
|
print("Successfully updated")
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ import os
|
|||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ..constants import USER_CONFIG_PATH, USER_DATA_PATH, USER_VIDEOS_DIR
|
from ..constants import (
|
||||||
|
USER_CONFIG_PATH,
|
||||||
|
USER_DATA_PATH,
|
||||||
|
USER_VIDEOS_DIR,
|
||||||
|
ASSETS_DIR,
|
||||||
|
USER_WATCH_HISTORY_PATH,
|
||||||
|
S_PLATFORM,
|
||||||
|
)
|
||||||
from ..libs.rofi import Rofi
|
from ..libs.rofi import Rofi
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -16,49 +23,58 @@ class Config(object):
|
|||||||
manga = False
|
manga = False
|
||||||
sync_play = False
|
sync_play = False
|
||||||
anime_list: list
|
anime_list: list
|
||||||
watch_history: dict
|
watch_history: dict = {}
|
||||||
fastanime_anilist_app_login_url = (
|
fastanime_anilist_app_login_url = (
|
||||||
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
|
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
|
||||||
)
|
)
|
||||||
anime_provider: "AnimeProvider"
|
anime_provider: "AnimeProvider"
|
||||||
user_data = {"watch_history": {}, "animelist": [], "user": {}}
|
user_data = {"recent_anime": [], "animelist": [], "user": {}}
|
||||||
default_options = {
|
default_config = {
|
||||||
"quality": "1080",
|
|
||||||
"auto_next": "False",
|
"auto_next": "False",
|
||||||
"auto_select": "True",
|
"auto_select": "True",
|
||||||
"sort_by": "search match",
|
"cache_requests": "true",
|
||||||
"downloads_dir": USER_VIDEOS_DIR,
|
"check_for_updates": "True",
|
||||||
"translation_type": "sub",
|
|
||||||
"server": "top",
|
|
||||||
"continue_from_history": "True",
|
"continue_from_history": "True",
|
||||||
"preferred_history": "local",
|
"default_media_list_tracking": "None",
|
||||||
"use_python_mpv": "false",
|
"downloads_dir": USER_VIDEOS_DIR,
|
||||||
|
"disable_mpv_popen": "True",
|
||||||
|
"episode_complete_at": "80",
|
||||||
|
"ffmpegthumbnailer_seek_time": "-1",
|
||||||
|
"force_forward_tracking": "true",
|
||||||
"force_window": "immediate",
|
"force_window": "immediate",
|
||||||
"preferred_language": "english",
|
|
||||||
"use_fzf": "False",
|
|
||||||
"preview": "False",
|
|
||||||
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
||||||
"provider": "allanime",
|
|
||||||
"error": "3",
|
|
||||||
"icons": "false",
|
"icons": "false",
|
||||||
|
"image_previews": "True" if S_PLATFORM != "win32" else "False",
|
||||||
|
"normalize_titles": "True",
|
||||||
"notification_duration": "2",
|
"notification_duration": "2",
|
||||||
"skip": "false",
|
|
||||||
"use_rofi": "false",
|
|
||||||
"rofi_theme": "",
|
|
||||||
"rofi_theme_input": "",
|
|
||||||
"rofi_theme_confirm": "",
|
|
||||||
"ffmpegthumnailer_seek_time": "-1",
|
|
||||||
"sub_lang": "eng",
|
|
||||||
"normalize_titles": "true",
|
|
||||||
"player": "mpv",
|
"player": "mpv",
|
||||||
|
"preferred_history": "local",
|
||||||
|
"preferred_language": "english",
|
||||||
|
"preview": "False",
|
||||||
|
"provider": "allanime",
|
||||||
|
"quality": "1080",
|
||||||
|
"recent": "50",
|
||||||
|
"rofi_theme": os.path.join(ASSETS_DIR, "rofi_theme.rasi"),
|
||||||
|
"rofi_theme_preview": os.path.join(ASSETS_DIR, "rofi_theme_preview.rasi"),
|
||||||
|
"rofi_theme_confirm": os.path.join(ASSETS_DIR, "rofi_theme_confirm.rasi"),
|
||||||
|
"rofi_theme_input": os.path.join(ASSETS_DIR, "rofi_theme_input.rasi"),
|
||||||
|
"server": "top",
|
||||||
|
"skip": "false",
|
||||||
|
"sort_by": "search match",
|
||||||
|
"sub_lang": "eng",
|
||||||
|
"translation_type": "sub",
|
||||||
|
"use_fzf": "False",
|
||||||
|
"use_persistent_provider_store": "false",
|
||||||
|
"use_python_mpv": "false",
|
||||||
|
"use_rofi": "false",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.initialize_user_data()
|
self.initialize_user_data_and_watch_history_recent_anime()
|
||||||
self.load_config()
|
self.load_config()
|
||||||
|
|
||||||
def load_config(self):
|
def load_config(self):
|
||||||
self.configparser = ConfigParser(self.default_options)
|
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")
|
||||||
@@ -67,68 +83,111 @@ class Config(object):
|
|||||||
if os.path.exists(USER_CONFIG_PATH):
|
if os.path.exists(USER_CONFIG_PATH):
|
||||||
self.configparser.read(USER_CONFIG_PATH, encoding="utf-8")
|
self.configparser.read(USER_CONFIG_PATH, encoding="utf-8")
|
||||||
|
|
||||||
self.downloads_dir = self.get_downloads_dir()
|
# TODO: rewrite all this removing the useless functions
|
||||||
self.sub_lang = self.get_sub_lang()
|
# hate technical debt
|
||||||
self.provider = self.get_provider()
|
# why did i do this lol
|
||||||
self.use_fzf = self.get_use_fzf()
|
|
||||||
self.use_rofi = self.get_use_rofi()
|
|
||||||
self.skip = self.get_skip()
|
|
||||||
self.icons = self.get_icons()
|
|
||||||
self.preview = self.get_preview()
|
|
||||||
self.translation_type = self.get_translation_type()
|
|
||||||
self.sort_by = self.get_sort_by()
|
|
||||||
self.continue_from_history = self.get_continue_from_history()
|
|
||||||
self.auto_next = self.get_auto_next()
|
self.auto_next = self.get_auto_next()
|
||||||
self.normalize_titles = self.get_normalize_titles()
|
|
||||||
self.auto_select = self.get_auto_select()
|
self.auto_select = self.get_auto_select()
|
||||||
self.use_python_mpv = self.get_use_mpv_mod()
|
self.cache_requests = self.get_cache_requests()
|
||||||
self.quality = self.get_quality()
|
self.check_for_updates = self.configparser.get("general", "check_for_updates")
|
||||||
self.notification_duration = self.get_notification_duration()
|
self.continue_from_history = self.get_continue_from_history()
|
||||||
self.error = self.get_error()
|
self.default_media_list_tracking = self.get_default_media_list_tracking()
|
||||||
self.server = self.get_server()
|
self.disable_mpv_popen = self.configparser.getboolean(
|
||||||
self.format = self.get_format()
|
"stream", "disable_mpv_popen"
|
||||||
self.player = self.get_player()
|
)
|
||||||
self.force_window = self.get_force_window()
|
self.downloads_dir = self.get_downloads_dir()
|
||||||
self.preferred_language = self.get_preferred_language()
|
self.episode_complete_at = self.get_episode_complete_at()
|
||||||
self.preferred_history = self.get_preferred_history()
|
|
||||||
self.rofi_theme = self.get_rofi_theme()
|
|
||||||
Rofi.rofi_theme = self.rofi_theme
|
|
||||||
self.rofi_theme_input = self.get_rofi_theme_input()
|
|
||||||
Rofi.rofi_theme_input = self.rofi_theme_input
|
|
||||||
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
|
|
||||||
Rofi.rofi_theme_confirm = self.rofi_theme_confirm
|
|
||||||
self.ffmpegthumbnailer_seek_time = self.get_ffmpegthumnailer_seek_time()
|
self.ffmpegthumbnailer_seek_time = self.get_ffmpegthumnailer_seek_time()
|
||||||
|
self.force_forward_tracking = self.get_force_forward_tracking()
|
||||||
|
self.force_window = self.get_force_window()
|
||||||
|
self.format = self.get_format()
|
||||||
|
self.icons = self.get_icons()
|
||||||
|
self.image_previews = self.get_image_previews()
|
||||||
|
self.normalize_titles = self.get_normalize_titles()
|
||||||
|
self.notification_duration = self.get_notification_duration()
|
||||||
|
self.player = self.get_player()
|
||||||
|
self.preferred_history = self.get_preferred_history()
|
||||||
|
self.preferred_language = self.get_preferred_language()
|
||||||
|
self.preview = self.get_preview()
|
||||||
|
self.provider = self.get_provider()
|
||||||
|
self.quality = self.get_quality()
|
||||||
|
|
||||||
|
self.recent = self.get_recent()
|
||||||
|
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
|
||||||
|
self.rofi_theme_input = self.get_rofi_theme_input()
|
||||||
|
self.rofi_theme = self.get_rofi_theme()
|
||||||
|
self.rofi_theme_preview = self.get_rofi_theme_preview()
|
||||||
|
|
||||||
|
Rofi.rofi_theme_confirm = self.rofi_theme_confirm
|
||||||
|
Rofi.rofi_theme_input = self.rofi_theme_input
|
||||||
|
Rofi.rofi_theme = self.rofi_theme
|
||||||
|
Rofi.rofi_theme_preview = self.rofi_theme_preview
|
||||||
|
|
||||||
|
self.server = self.get_server()
|
||||||
|
self.skip = self.get_skip()
|
||||||
|
self.sort_by = self.get_sort_by()
|
||||||
|
self.sub_lang = self.get_sub_lang()
|
||||||
|
self.translation_type = self.get_translation_type()
|
||||||
|
self.use_fzf = self.get_use_fzf()
|
||||||
|
self.use_python_mpv = self.get_use_mpv_mod()
|
||||||
|
self.use_rofi = self.get_use_rofi()
|
||||||
|
self.use_persistent_provider_store = self.get_use_persistent_provider_store()
|
||||||
|
|
||||||
# ---- setup user data ------
|
# ---- setup user data ------
|
||||||
self.watch_history: dict = self.user_data.get("watch_history", {})
|
|
||||||
self.anime_list: list = self.user_data.get("animelist", [])
|
self.anime_list: list = self.user_data.get("animelist", [])
|
||||||
self.user: dict = self.user_data.get("user", {})
|
self.user: dict = self.user_data.get("user", {})
|
||||||
|
|
||||||
os.environ["CURRENT_FASTANIME_PROVIDER"] = self.provider
|
|
||||||
if not os.path.exists(USER_CONFIG_PATH):
|
if not os.path.exists(USER_CONFIG_PATH):
|
||||||
with open(USER_CONFIG_PATH, "w", encoding="utf-8") as config:
|
with open(USER_CONFIG_PATH, "w", encoding="utf-8") as config:
|
||||||
config.write(self.__repr__())
|
config.write(self.__repr__())
|
||||||
|
|
||||||
|
def set_fastanime_config_environs(self):
|
||||||
|
current_config = []
|
||||||
|
for key in self.default_config:
|
||||||
|
current_config.append((f"FASTANIME_{key.upper()}", str(getattr(self, key))))
|
||||||
|
os.environ.update(current_config)
|
||||||
|
|
||||||
def update_user(self, user):
|
def update_user(self, user):
|
||||||
self.user = user
|
self.user = user
|
||||||
self.user_data["user"] = user
|
self.user_data["user"] = user
|
||||||
self._update_user_data()
|
self._update_user_data()
|
||||||
|
|
||||||
def update_watch_history(
|
def update_recent(self, recent_anime: list):
|
||||||
self, anime_id: int, episode: str, start_time="0", total_time="0"
|
recent_anime_ids = []
|
||||||
|
_recent_anime = []
|
||||||
|
for anime in recent_anime[::-1]:
|
||||||
|
if (
|
||||||
|
anime["id"] not in recent_anime_ids
|
||||||
|
and len(recent_anime_ids) <= self.recent
|
||||||
|
):
|
||||||
|
_recent_anime.append(anime)
|
||||||
|
recent_anime_ids.append(anime["id"])
|
||||||
|
|
||||||
|
self.user_data["recent_anime"] = _recent_anime
|
||||||
|
self._update_user_data()
|
||||||
|
|
||||||
|
def media_list_track(
|
||||||
|
self,
|
||||||
|
anime_id: int,
|
||||||
|
episode_no: str,
|
||||||
|
episode_stopped_at="0",
|
||||||
|
episode_total_length="0",
|
||||||
|
progress_tracking="prompt",
|
||||||
):
|
):
|
||||||
self.watch_history.update(
|
self.watch_history.update(
|
||||||
{
|
{
|
||||||
str(anime_id): {
|
str(anime_id): {
|
||||||
"episode": episode,
|
"episode_no": episode_no,
|
||||||
"start_time": start_time,
|
"episode_stopped_at": episode_stopped_at,
|
||||||
"total_time": total_time,
|
"episode_total_length": episode_total_length,
|
||||||
|
"progress_tracking": progress_tracking,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.user_data["watch_history"] = self.watch_history
|
with open(USER_WATCH_HISTORY_PATH, "w") as f:
|
||||||
self._update_user_data()
|
json.dump(self.watch_history, f)
|
||||||
|
|
||||||
def initialize_user_data(self):
|
def initialize_user_data_and_watch_history_recent_anime(self):
|
||||||
try:
|
try:
|
||||||
if os.path.isfile(USER_DATA_PATH):
|
if os.path.isfile(USER_DATA_PATH):
|
||||||
with open(USER_DATA_PATH, "r") as f:
|
with open(USER_DATA_PATH, "r") as f:
|
||||||
@@ -136,6 +195,13 @@ class Config(object):
|
|||||||
self.user_data.update(user_data)
|
self.user_data.update(user_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
try:
|
||||||
|
if os.path.isfile(USER_WATCH_HISTORY_PATH):
|
||||||
|
with open(USER_WATCH_HISTORY_PATH, "r") as f:
|
||||||
|
watch_history = json.load(f)
|
||||||
|
self.watch_history.update(watch_history)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
def _update_user_data(self):
|
def _update_user_data(self):
|
||||||
"""method that updates the actual user data file"""
|
"""method that updates the actual user data file"""
|
||||||
@@ -149,7 +215,7 @@ class Config(object):
|
|||||||
return self.configparser.get("general", "provider")
|
return self.configparser.get("general", "provider")
|
||||||
|
|
||||||
def get_ffmpegthumnailer_seek_time(self):
|
def get_ffmpegthumnailer_seek_time(self):
|
||||||
return self.configparser.getint("general", "ffmpegthumnailer_seek_time")
|
return self.configparser.getint("general", "ffmpegthumbnailer_seek_time")
|
||||||
|
|
||||||
def get_preferred_language(self):
|
def get_preferred_language(self):
|
||||||
return self.configparser.get("general", "preferred_language")
|
return self.configparser.get("general", "preferred_language")
|
||||||
@@ -163,12 +229,18 @@ class Config(object):
|
|||||||
def get_icons(self):
|
def get_icons(self):
|
||||||
return self.configparser.getboolean("general", "icons")
|
return self.configparser.getboolean("general", "icons")
|
||||||
|
|
||||||
|
def get_image_previews(self):
|
||||||
|
return self.configparser.getboolean("general", "image_previews")
|
||||||
|
|
||||||
def get_preview(self):
|
def get_preview(self):
|
||||||
return self.configparser.getboolean("general", "preview")
|
return self.configparser.getboolean("general", "preview")
|
||||||
|
|
||||||
def get_use_fzf(self):
|
def get_use_fzf(self):
|
||||||
return self.configparser.getboolean("general", "use_fzf")
|
return self.configparser.getboolean("general", "use_fzf")
|
||||||
|
|
||||||
|
def get_use_persistent_provider_store(self):
|
||||||
|
return self.configparser.getboolean("general", "use_persistent_provider_store")
|
||||||
|
|
||||||
# rofi conifiguration
|
# rofi conifiguration
|
||||||
def get_use_rofi(self):
|
def get_use_rofi(self):
|
||||||
return self.configparser.getboolean("general", "use_rofi")
|
return self.configparser.getboolean("general", "use_rofi")
|
||||||
@@ -176,15 +248,30 @@ class Config(object):
|
|||||||
def get_rofi_theme(self):
|
def get_rofi_theme(self):
|
||||||
return self.configparser.get("general", "rofi_theme")
|
return self.configparser.get("general", "rofi_theme")
|
||||||
|
|
||||||
|
def get_rofi_theme_preview(self):
|
||||||
|
return self.configparser.get("general", "rofi_theme_preview")
|
||||||
|
|
||||||
def get_rofi_theme_input(self):
|
def get_rofi_theme_input(self):
|
||||||
return self.configparser.get("general", "rofi_theme_input")
|
return self.configparser.get("general", "rofi_theme_input")
|
||||||
|
|
||||||
def get_rofi_theme_confirm(self):
|
def get_rofi_theme_confirm(self):
|
||||||
return self.configparser.get("general", "rofi_theme_confirm")
|
return self.configparser.get("general", "rofi_theme_confirm")
|
||||||
|
|
||||||
|
def get_force_forward_tracking(self):
|
||||||
|
return self.configparser.getboolean("general", "force_forward_tracking")
|
||||||
|
|
||||||
|
def get_cache_requests(self):
|
||||||
|
return self.configparser.getboolean("general", "cache_requests")
|
||||||
|
|
||||||
|
def get_default_media_list_tracking(self):
|
||||||
|
return self.configparser.get("general", "default_media_list_tracking")
|
||||||
|
|
||||||
def get_normalize_titles(self):
|
def get_normalize_titles(self):
|
||||||
return self.configparser.getboolean("general", "normalize_titles")
|
return self.configparser.getboolean("general", "normalize_titles")
|
||||||
|
|
||||||
|
def get_recent(self):
|
||||||
|
return self.configparser.getint("general", "recent")
|
||||||
|
|
||||||
# --- stream section ---
|
# --- stream section ---
|
||||||
def get_skip(self):
|
def get_skip(self):
|
||||||
return self.configparser.getboolean("stream", "skip")
|
return self.configparser.getboolean("stream", "skip")
|
||||||
@@ -204,8 +291,8 @@ class Config(object):
|
|||||||
def get_notification_duration(self):
|
def get_notification_duration(self):
|
||||||
return self.configparser.getint("general", "notification_duration")
|
return self.configparser.getint("general", "notification_duration")
|
||||||
|
|
||||||
def get_error(self):
|
def get_episode_complete_at(self):
|
||||||
return self.configparser.getint("stream", "error")
|
return self.configparser.getint("stream", "episode_complete_at")
|
||||||
|
|
||||||
def get_force_window(self):
|
def get_force_window(self):
|
||||||
return self.configparser.get("stream", "force_window")
|
return self.configparser.get("stream", "force_window")
|
||||||
@@ -255,23 +342,21 @@ class Config(object):
|
|||||||
# be sure to also give the replacement emoji
|
# be sure to also give the replacement emoji
|
||||||
icons = {self.icons}
|
icons = {self.icons}
|
||||||
|
|
||||||
# the quality of the stream [1080,720,480,360]
|
|
||||||
# this option is usually only reliable when:
|
|
||||||
# provider=animepahe
|
|
||||||
# since it provides links that actually point to streams of different qualities
|
|
||||||
# while the rest just point to another link that can provide the anime from the same server
|
|
||||||
quality = {self.quality}
|
|
||||||
|
|
||||||
# whether to normalize provider titles [True/False]
|
# whether to normalize provider titles [True/False]
|
||||||
# basically takes the provider titles and finds the corresponding anilist title then changes the title to that
|
# basically takes the provider titles and finds the corresponding anilist title then changes the title to that
|
||||||
# useful for uniformity especially when downloading from different providers
|
# useful for uniformity especially when downloading from different providers
|
||||||
# this also applies to episode titles
|
# this also applies to episode titles
|
||||||
normalize_titles = {self.normalize_titles}
|
normalize_titles = {self.normalize_titles}
|
||||||
|
|
||||||
# can be [allanime, animepahe, aniwatch]
|
# whether to check for updates every time you run the script [True/False]
|
||||||
|
# this is useful for keeping your script up to date
|
||||||
|
# cause there are always new features being added 😄
|
||||||
|
check_for_updates = {self.check_for_updates}
|
||||||
|
|
||||||
|
# can be [allanime, animepahe, hianime]
|
||||||
# allanime is the most realible
|
# allanime is the most realible
|
||||||
# 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 --quality option
|
||||||
# aniwatch which is now hianime usually provides subs in different languuages and its servers are generally faster
|
# hianime which is now hianime usually provides subs in different languuages and its servers are generally faster
|
||||||
provider = {self.provider}
|
provider = {self.provider}
|
||||||
|
|
||||||
# Display language [english, romaji]
|
# Display language [english, romaji]
|
||||||
@@ -291,6 +376,9 @@ downloads_dir = {self.downloads_dir}
|
|||||||
# 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]
|
||||||
|
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 and you can select at what time in the video to extract an image
|
# ffmpegthumbnailer is used to generate previews and you can select at what time in the video to extract an image
|
||||||
@@ -313,6 +401,8 @@ use_rofi = {self.use_rofi}
|
|||||||
# by the way i recommend getting the rofi themes from this project;
|
# by the way i recommend getting the rofi themes from this project;
|
||||||
rofi_theme = {self.rofi_theme}
|
rofi_theme = {self.rofi_theme}
|
||||||
|
|
||||||
|
rofi_theme_preview = {self.rofi_theme_preview}
|
||||||
|
|
||||||
rofi_theme_input = {self.rofi_theme_input}
|
rofi_theme_input = {self.rofi_theme_input}
|
||||||
|
|
||||||
rofi_theme_confirm = {self.rofi_theme_confirm}
|
rofi_theme_confirm = {self.rofi_theme_confirm}
|
||||||
@@ -323,13 +413,50 @@ notification_duration = {self.notification_duration}
|
|||||||
|
|
||||||
# used when the provider gives subs of different languages
|
# used when the provider gives subs of different languages
|
||||||
# currently its the case for:
|
# currently its the case for:
|
||||||
# aniwatch
|
# hianime
|
||||||
# the values for this option are the short names for countries
|
# the values for this option are the short names for countries
|
||||||
# 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]
|
||||||
|
# only affects your anilist anime list
|
||||||
|
# track - means your progress will always be reflected in your anilist anime list
|
||||||
|
# disabled - means progress tracking will no longer be reflected in your anime list
|
||||||
|
# prompt - means for every anime you will be prompted whether you want your progress to be tracked or not
|
||||||
|
default_media_list_tracking = {self.default_media_list_tracking}
|
||||||
|
|
||||||
|
# whether media list tracking should only be updated when the next episode is greater than the previous
|
||||||
|
# this affects only your anilist anime list
|
||||||
|
force_forward_tracking = {self.force_forward_tracking}
|
||||||
|
|
||||||
|
# whether to cache requests [true/false]
|
||||||
|
# this makes the experience better and more faster
|
||||||
|
# as data need not always be fetched from web server
|
||||||
|
# and instead can be gotten locally
|
||||||
|
# from the cached_requests_db
|
||||||
|
cache_requests = {self.cache_requests}
|
||||||
|
|
||||||
|
# whether to use a persistent store (basically a sqlitedb) for storing some data the provider requires
|
||||||
|
# to enable a seamless experience [true/false]
|
||||||
|
# this option exists primarily because i think it may help in the optimization
|
||||||
|
# of fastanime as a library in a website project
|
||||||
|
# for now i don't recommend changing it
|
||||||
|
# leave it as is
|
||||||
|
use_persistent_provider_store = {self.use_persistent_provider_store}
|
||||||
|
|
||||||
|
# no of recent anime to keep [0-50]
|
||||||
|
# 0 will disable recent anime tracking
|
||||||
|
recent = {self.recent}
|
||||||
|
|
||||||
|
|
||||||
[stream]
|
[stream]
|
||||||
|
# the quality of the stream [1080,720,480,360]
|
||||||
|
# this option is usually only reliable when:
|
||||||
|
# provider=animepahe
|
||||||
|
# since it provides links that actually point to streams of different qualities
|
||||||
|
# while the rest just point to another link that can provide the anime from the same server
|
||||||
|
quality = {self.quality}
|
||||||
|
|
||||||
# Auto continue from watch history [True/False]
|
# Auto continue from watch history [True/False]
|
||||||
# this will make fastanime to choose the episode that you last watched to completion
|
# this will make fastanime to choose the episode that you last watched to completion
|
||||||
# and increment it by one
|
# and increment it by one
|
||||||
@@ -350,7 +477,7 @@ translation_type = {self.translation_type}
|
|||||||
# what server to use for a particular provider
|
# what server to use for a particular provider
|
||||||
# allanime: [dropbox, sharepoint, wetransfer, gogoanime, wixmp]
|
# allanime: [dropbox, sharepoint, wetransfer, gogoanime, wixmp]
|
||||||
# animepahe: [kwik]
|
# animepahe: [kwik]
|
||||||
# aniwatch: [HD1, HD2, StreamSB, StreamTape]
|
# hianime: [HD1, HD2, StreamSB, StreamTape]
|
||||||
# 'top' can also be used as a value for this option
|
# 'top' can also be used as a value for this option
|
||||||
# 'top' will cause fastanime to auto select the first server it sees
|
# 'top' will cause fastanime to auto select the first server it sees
|
||||||
# this saves on resources and is faster since not all servers are being fetched
|
# this saves on resources and is faster since not all servers are being fetched
|
||||||
@@ -378,15 +505,19 @@ auto_select = {self.auto_select}
|
|||||||
# so its disabled for now
|
# so its disabled for now
|
||||||
skip = {self.skip}
|
skip = {self.skip}
|
||||||
|
|
||||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
# at what percentage progress should the episode be considered as completed [0-100]
|
||||||
# used in the continue from time stamp
|
# this value is used to determine whether to increment the current episode number and save it to your local list
|
||||||
error = {self.error}
|
# so you can continue immediately to the next episode without select it the next time you decide to watch the anime
|
||||||
|
# it is also used to determine whether your anilist anime list should be updated or not
|
||||||
|
episode_complete_at = {self.episode_complete_at}
|
||||||
|
|
||||||
# whether to use python-mpv [True/False]
|
# whether to use python-mpv [True/False]
|
||||||
# to enable superior control over the player
|
# to enable superior control over the player
|
||||||
# adding more options to it
|
# adding more options to it
|
||||||
# Enable this one and you will be wonder why you did not discover fastanime sooner
|
# Enable this one and you will be wonder why you did not discover fastanime sooner
|
||||||
# Since you basically don't have to close the player window to go to the next or previous episode, switch servers, change translation type or change to a given episode x
|
# Since you basically don't have to close the player window
|
||||||
|
# to go to the next or previous episode, switch servers,
|
||||||
|
# change translation type or change to a given episode x
|
||||||
# so try it if you haven't already
|
# so try it if you haven't already
|
||||||
# if you have any issues setting it up
|
# if you have any issues setting it up
|
||||||
# don't be afraid to ask
|
# don't be afraid to ask
|
||||||
@@ -398,6 +529,15 @@ error = {self.error}
|
|||||||
# or just switch to arch linux
|
# or just switch to arch linux
|
||||||
use_python_mpv = {self.use_python_mpv}
|
use_python_mpv = {self.use_python_mpv}
|
||||||
|
|
||||||
|
|
||||||
|
# whether to use popen to get the timestamps for continue_from_history
|
||||||
|
# implemented because popen does not work for some reason in nixos
|
||||||
|
# if you are on nixos and you have a solution to this problem please share
|
||||||
|
# i will be glad to hear it 😄
|
||||||
|
# So for now ignore this option
|
||||||
|
# and anyways the new method of getting timestamps is better
|
||||||
|
disable_mpv_popen = {self.disable_mpv_popen}
|
||||||
|
|
||||||
# force mpv window
|
# force mpv window
|
||||||
# the default 'immediate' just makes mpv to open the window even if the video has not yet loaded
|
# the default 'immediate' just makes mpv to open the window even if the video has not yet loaded
|
||||||
# done for asthetics
|
# done for asthetics
|
||||||
@@ -410,7 +550,7 @@ force_window = immediate
|
|||||||
# only works for downloaded anime if:
|
# only works for downloaded anime if:
|
||||||
# provider=allanime, server=gogoanime
|
# provider=allanime, server=gogoanime
|
||||||
# provider=allanime, server=wixmp
|
# provider=allanime, server=wixmp
|
||||||
# provider=aniwatch
|
# provider=hianime
|
||||||
# this is because they provider a m3u8 file that contans multiple quality streams
|
# this is because they provider a m3u8 file that contans multiple quality streams
|
||||||
format = {self.format}
|
format = {self.format}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
from datetime import datetime
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from click import clear
|
from click import clear
|
||||||
@@ -35,8 +34,7 @@ if TYPE_CHECKING:
|
|||||||
from ..utils.tools import FastAnimeRuntimeState
|
from ..utils.tools import FastAnimeRuntimeState
|
||||||
|
|
||||||
|
|
||||||
# TODO: make the error handling more sane
|
def calculate_percentage_completion(start_time, end_time):
|
||||||
def calculate_time_delta(start_time, end_time):
|
|
||||||
"""helper function used to calculate the difference between two timestamps in seconds
|
"""helper function used to calculate the difference between two timestamps in seconds
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -46,16 +44,12 @@ def calculate_time_delta(start_time, end_time):
|
|||||||
Returns:
|
Returns:
|
||||||
[TODO:return]
|
[TODO:return]
|
||||||
"""
|
"""
|
||||||
time_format = "%H:%M:%S"
|
|
||||||
|
|
||||||
# Convert string times to datetime objects
|
start = start_time.split(":")
|
||||||
start = datetime.strptime(start_time, time_format)
|
end = end_time.split(":")
|
||||||
end = datetime.strptime(end_time, time_format)
|
start_secs = int(start[0]) * 3600 + int(start[1]) * 60 + int(start[2])
|
||||||
|
end_secs = int(end[0]) * 3600 + int(end[1]) * 60 + int(end[2])
|
||||||
# Calculate the difference
|
return start_secs / end_secs * 100
|
||||||
delta = end - start
|
|
||||||
|
|
||||||
return delta
|
|
||||||
|
|
||||||
|
|
||||||
def media_player_controls(
|
def media_player_controls(
|
||||||
@@ -103,10 +97,12 @@ def media_player_controls(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
config.watch_history[str(anime_id_anilist)]["episode"]
|
config.watch_history[str(anime_id_anilist)]["episode_no"]
|
||||||
== current_episode_number
|
== current_episode_number
|
||||||
):
|
):
|
||||||
start_time = config.watch_history[str(anime_id_anilist)]["start_time"]
|
start_time = config.watch_history[str(anime_id_anilist)][
|
||||||
|
"episode_stopped_at"
|
||||||
|
]
|
||||||
print("[green]Continuing from:[/] ", start_time)
|
print("[green]Continuing from:[/] ", start_time)
|
||||||
else:
|
else:
|
||||||
start_time = "0"
|
start_time = "0"
|
||||||
@@ -171,9 +167,10 @@ def media_player_controls(
|
|||||||
if stop_time == "0" or total_time == "0":
|
if stop_time == "0" or total_time == "0":
|
||||||
episode = str(int(current_episode_number) + 1)
|
episode = str(int(current_episode_number) + 1)
|
||||||
else:
|
else:
|
||||||
error = 5 * 60
|
percentage_completion_of_episode = calculate_percentage_completion(
|
||||||
delta = calculate_time_delta(stop_time, total_time)
|
stop_time, total_time
|
||||||
if delta.total_seconds() > error:
|
)
|
||||||
|
if percentage_completion_of_episode < config.episode_complete_at:
|
||||||
episode = current_episode_number
|
episode = current_episode_number
|
||||||
else:
|
else:
|
||||||
episode = str(int(current_episode_number) + 1)
|
episode = str(int(current_episode_number) + 1)
|
||||||
@@ -181,28 +178,34 @@ def media_player_controls(
|
|||||||
total_time = "0"
|
total_time = "0"
|
||||||
|
|
||||||
clear()
|
clear()
|
||||||
config.update_watch_history(anime_id_anilist, episode, stop_time, total_time)
|
config.media_list_track(
|
||||||
|
anime_id_anilist,
|
||||||
|
episode_no=episode,
|
||||||
|
episode_stopped_at=stop_time,
|
||||||
|
episode_total_length=total_time,
|
||||||
|
progress_tracking=fastanime_runtime_state.progress_tracking,
|
||||||
|
)
|
||||||
media_player_controls(config, fastanime_runtime_state)
|
media_player_controls(config, fastanime_runtime_state)
|
||||||
|
|
||||||
def _next_episode():
|
def _next_episode():
|
||||||
"""watch the next episode"""
|
"""watch the next episode"""
|
||||||
# ensures you dont accidentally erase your progress for an in complete episode
|
# ensures you dont accidentally erase your progress for an in complete episode
|
||||||
stop_time = config.watch_history.get(str(anime_id_anilist), {}).get(
|
stop_time = config.watch_history.get(str(anime_id_anilist), {}).get(
|
||||||
"start_time", "0"
|
"episode_stopped_at", "0"
|
||||||
)
|
)
|
||||||
|
|
||||||
total_time = config.watch_history.get(str(anime_id_anilist), {}).get(
|
total_time = config.watch_history.get(str(anime_id_anilist), {}).get(
|
||||||
"total_time", "0"
|
"episode_total_length", "0"
|
||||||
)
|
)
|
||||||
|
|
||||||
# compute if the episode is actually completed
|
# compute if the episode is actually completed
|
||||||
error = config.error * 60
|
|
||||||
if stop_time == "0" or total_time == "0":
|
if stop_time == "0" or total_time == "0":
|
||||||
dt = 0
|
percentage_completion_of_episode = 0
|
||||||
else:
|
else:
|
||||||
delta = calculate_time_delta(stop_time, total_time)
|
percentage_completion_of_episode = calculate_percentage_completion(
|
||||||
dt = delta.total_seconds()
|
stop_time, total_time
|
||||||
if dt > error:
|
)
|
||||||
|
if percentage_completion_of_episode < config.episode_complete_at:
|
||||||
if config.auto_next:
|
if config.auto_next:
|
||||||
if config.use_rofi:
|
if config.use_rofi:
|
||||||
if not Rofi.confirm(
|
if not Rofi.confirm(
|
||||||
@@ -236,7 +239,11 @@ def media_player_controls(
|
|||||||
]
|
]
|
||||||
|
|
||||||
# update user config
|
# update user config
|
||||||
config.update_watch_history(anime_id_anilist, available_episodes[next_episode])
|
config.media_list_track(
|
||||||
|
anime_id_anilist,
|
||||||
|
episode_no=available_episodes[next_episode],
|
||||||
|
progress_tracking=fastanime_runtime_state.progress_tracking,
|
||||||
|
)
|
||||||
|
|
||||||
# call interface
|
# call interface
|
||||||
provider_anime_episode_servers_menu(config, fastanime_runtime_state)
|
provider_anime_episode_servers_menu(config, fastanime_runtime_state)
|
||||||
@@ -260,7 +267,11 @@ def media_player_controls(
|
|||||||
]
|
]
|
||||||
|
|
||||||
# update user config
|
# update user config
|
||||||
config.update_watch_history(anime_id_anilist, available_episodes[prev_episode])
|
config.media_list_track(
|
||||||
|
anime_id_anilist,
|
||||||
|
episode_no=available_episodes[prev_episode],
|
||||||
|
progress_tracking=fastanime_runtime_state.progress_tracking,
|
||||||
|
)
|
||||||
|
|
||||||
# call interface
|
# call interface
|
||||||
provider_anime_episode_servers_menu(config, fastanime_runtime_state)
|
provider_anime_episode_servers_menu(config, fastanime_runtime_state)
|
||||||
@@ -273,7 +284,7 @@ def media_player_controls(
|
|||||||
# prompt for new quality
|
# prompt for new quality
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
quality = fzf.run(
|
quality = fzf.run(
|
||||||
options, prompt="Select Quality:", header="Quality Options"
|
options, prompt="Select Quality", header="Quality Options"
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
quality = Rofi.run(options, "Select Quality")
|
quality = Rofi.run(options, "Select Quality")
|
||||||
@@ -291,7 +302,7 @@ def media_player_controls(
|
|||||||
options = ["sub", "dub"]
|
options = ["sub", "dub"]
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
translation_type = fzf.run(
|
translation_type = fzf.run(
|
||||||
options, prompt="Select Translation Type: ", header="Lang Options"
|
options, prompt="Select Translation Type", header="Lang Options"
|
||||||
).lower()
|
).lower()
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
translation_type = Rofi.run(options, "Select Translation Type")
|
translation_type = Rofi.run(options, "Select Translation Type")
|
||||||
@@ -337,7 +348,7 @@ def media_player_controls(
|
|||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
action = fzf.run(
|
action = fzf.run(
|
||||||
choices,
|
choices,
|
||||||
prompt="Select Action:",
|
prompt="Select Action",
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
action = Rofi.run(choices, "Select Action")
|
action = Rofi.run(choices, "Select Action")
|
||||||
@@ -376,7 +387,7 @@ def provider_anime_episode_servers_menu(
|
|||||||
with Progress() as progress:
|
with Progress() as progress:
|
||||||
progress.add_task("Fetching Episode Streams...", total=None)
|
progress.add_task("Fetching Episode Streams...", total=None)
|
||||||
episode_streams_generator = anime_provider.get_episode_streams(
|
episode_streams_generator = anime_provider.get_episode_streams(
|
||||||
provider_anime,
|
provider_anime["id"],
|
||||||
current_episode_number,
|
current_episode_number,
|
||||||
translation_type,
|
translation_type,
|
||||||
)
|
)
|
||||||
@@ -435,7 +446,7 @@ def provider_anime_episode_servers_menu(
|
|||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
server_name = fzf.run(
|
server_name = fzf.run(
|
||||||
choices,
|
choices,
|
||||||
prompt="Select Server: ",
|
prompt="Select Server",
|
||||||
header="Servers",
|
header="Servers",
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
@@ -496,21 +507,12 @@ def provider_anime_episode_servers_menu(
|
|||||||
"[bold magenta] Episode: [/]",
|
"[bold magenta] Episode: [/]",
|
||||||
current_episode_number,
|
current_episode_number,
|
||||||
)
|
)
|
||||||
# -- update anilist progress if user --
|
|
||||||
if config.user and current_episode_number:
|
|
||||||
AniList.update_anime_list(
|
|
||||||
{
|
|
||||||
"mediaId": anime_id_anilist,
|
|
||||||
"progress": int(float(current_episode_number)),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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(
|
||||||
"start_time", "0"
|
"episode_stopped_at", "0"
|
||||||
)
|
)
|
||||||
episode_in_history = config.watch_history.get(str(anime_id_anilist), {}).get(
|
episode_in_history = config.watch_history.get(str(anime_id_anilist), {}).get(
|
||||||
"episode", ""
|
"episode_no", ""
|
||||||
)
|
)
|
||||||
if start_time != "0" and episode_in_history == current_episode_number:
|
if start_time != "0" and episode_in_history == current_episode_number:
|
||||||
print("[green]Continuing from:[/] ", start_time)
|
print("[green]Continuing from:[/] ", start_time)
|
||||||
@@ -537,6 +539,14 @@ def provider_anime_episode_servers_menu(
|
|||||||
episode_title = episode_detail["title"]
|
episode_title = episode_detail["title"]
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if config.recent:
|
||||||
|
config.update_recent(
|
||||||
|
[
|
||||||
|
*config.user_data["recent_anime"],
|
||||||
|
fastanime_runtime_state.selected_anime_anilist,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
print("Updating recent anime...")
|
||||||
if config.sync_play:
|
if config.sync_play:
|
||||||
from ..utils.syncplay import SyncPlayer
|
from ..utils.syncplay import SyncPlayer
|
||||||
|
|
||||||
@@ -583,34 +593,56 @@ def provider_anime_episode_servers_menu(
|
|||||||
# this will try to update the episode to be the next episode if delta has reached a specific threshhold
|
# this will try to update the episode to be the next episode if delta has reached a specific threshhold
|
||||||
# this update will only apply locally
|
# this update will only apply locally
|
||||||
# the remote(anilist) is only updated when its certain you are going to open the player
|
# the remote(anilist) is only updated when its certain you are going to open the player
|
||||||
available_episodes: list[str] = sorted(
|
|
||||||
fastanime_runtime_state.provider_available_episodes, key=float
|
|
||||||
)
|
|
||||||
if stop_time == "0" or total_time == "0":
|
if stop_time == "0" or total_time == "0":
|
||||||
# increment the episodes
|
# increment the episodes
|
||||||
next_episode = available_episodes.index(current_episode_number) + 1
|
# next_episode = available_episodes.index(current_episode_number) + 1
|
||||||
if next_episode >= len(available_episodes):
|
# if next_episode >= len(available_episodes):
|
||||||
next_episode = len(available_episodes) - 1
|
# next_episode = len(available_episodes) - 1
|
||||||
episode = available_episodes[next_episode]
|
# episode = available_episodes[next_episode]
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
error = config.error * 60
|
percentage_completion_of_episode = calculate_percentage_completion(
|
||||||
delta = calculate_time_delta(stop_time, total_time)
|
stop_time, total_time
|
||||||
if delta.total_seconds() > error:
|
)
|
||||||
episode = current_episode_number
|
if percentage_completion_of_episode > config.episode_complete_at:
|
||||||
else:
|
# -- update anilist progress if user --
|
||||||
# increment the episodes
|
remote_progress = (
|
||||||
next_episode = available_episodes.index(current_episode_number) + 1
|
fastanime_runtime_state.selected_anime_anilist["mediaListEntry"] or {}
|
||||||
if next_episode >= len(available_episodes):
|
).get("progress")
|
||||||
next_episode = len(available_episodes) - 1
|
disable_anilist_update = False
|
||||||
episode = available_episodes[next_episode]
|
if remote_progress:
|
||||||
stop_time = "0"
|
if (
|
||||||
total_time = "0"
|
float(remote_progress) > float(current_episode_number)
|
||||||
|
and config.force_forward_tracking
|
||||||
|
):
|
||||||
|
disable_anilist_update = True
|
||||||
|
if (
|
||||||
|
fastanime_runtime_state.progress_tracking == "track"
|
||||||
|
and config.user
|
||||||
|
and not disable_anilist_update
|
||||||
|
and current_episode_number
|
||||||
|
):
|
||||||
|
AniList.update_anime_list(
|
||||||
|
{
|
||||||
|
"mediaId": anime_id_anilist,
|
||||||
|
"progress": int(float(current_episode_number)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
config.update_watch_history(
|
# increment the episodes
|
||||||
|
# next_episode = available_episodes.index(current_episode_number) + 1
|
||||||
|
# if next_episode >= len(available_episodes):
|
||||||
|
# next_episode = len(available_episodes) - 1
|
||||||
|
# episode = available_episodes[next_episode]
|
||||||
|
# stop_time = "0"
|
||||||
|
# total_time = "0"
|
||||||
|
|
||||||
|
config.media_list_track(
|
||||||
anime_id_anilist,
|
anime_id_anilist,
|
||||||
episode,
|
episode_no=current_episode_number,
|
||||||
start_time=stop_time,
|
episode_stopped_at=stop_time,
|
||||||
total_time=total_time,
|
episode_total_length=total_time,
|
||||||
|
progress_tracking=fastanime_runtime_state.progress_tracking,
|
||||||
)
|
)
|
||||||
|
|
||||||
# switch to controls
|
# switch to controls
|
||||||
@@ -642,7 +674,7 @@ def provider_anime_episodes_menu(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# prompt for episode number
|
# prompt for episode number
|
||||||
total_episodes = sorted(
|
available_episodes = sorted(
|
||||||
provider_anime["availableEpisodesDetail"][translation_type], key=float
|
provider_anime["availableEpisodesDetail"][translation_type], key=float
|
||||||
)
|
)
|
||||||
current_episode_number = ""
|
current_episode_number = ""
|
||||||
@@ -652,16 +684,39 @@ def provider_anime_episodes_menu(
|
|||||||
# the user watch history thats locally available
|
# the user watch history thats locally available
|
||||||
# will be preferred over remote
|
# will be preferred over remote
|
||||||
if (
|
if (
|
||||||
user_watch_history.get(str(anime_id_anilist), {}).get("episode")
|
user_watch_history.get(str(anime_id_anilist), {}).get("episode_no")
|
||||||
in total_episodes
|
in available_episodes
|
||||||
):
|
):
|
||||||
if (
|
if (
|
||||||
config.preferred_history == "local"
|
config.preferred_history == "local"
|
||||||
or not selected_anime_anilist["mediaListEntry"]
|
or not selected_anime_anilist["mediaListEntry"]
|
||||||
):
|
):
|
||||||
current_episode_number = user_watch_history[str(anime_id_anilist)][
|
current_episode_number = user_watch_history[str(anime_id_anilist)][
|
||||||
"episode"
|
"episode_no"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
stop_time = user_watch_history.get(str(anime_id_anilist), {}).get(
|
||||||
|
"episode_stopped_at", "0"
|
||||||
|
)
|
||||||
|
total_time = user_watch_history.get(str(anime_id_anilist), {}).get(
|
||||||
|
"episode_total_length", "0"
|
||||||
|
)
|
||||||
|
if stop_time != "0" or total_time != "0":
|
||||||
|
percentage_completion_of_episode = calculate_percentage_completion(
|
||||||
|
stop_time, total_time
|
||||||
|
)
|
||||||
|
if percentage_completion_of_episode > config.episode_complete_at:
|
||||||
|
# increment the episodes
|
||||||
|
next_episode = (
|
||||||
|
available_episodes.index(current_episode_number) + 1
|
||||||
|
)
|
||||||
|
if next_episode >= len(available_episodes):
|
||||||
|
next_episode = len(available_episodes) - 1
|
||||||
|
episode = available_episodes[next_episode]
|
||||||
|
stop_time = "0"
|
||||||
|
total_time = "0"
|
||||||
|
current_episode_number = episode
|
||||||
|
|
||||||
else:
|
else:
|
||||||
current_episode_number = str(
|
current_episode_number = str(
|
||||||
(selected_anime_anilist["mediaListEntry"] or {"progress": 0}).get(
|
(selected_anime_anilist["mediaListEntry"] or {"progress": 0}).get(
|
||||||
@@ -679,7 +734,7 @@ def provider_anime_episodes_menu(
|
|||||||
"progress"
|
"progress"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if current_episode_number not in total_episodes:
|
if current_episode_number not in available_episodes:
|
||||||
current_episode_number = ""
|
current_episode_number = ""
|
||||||
print(
|
print(
|
||||||
f"[bold cyan]Continuing from Episode:[/] [bold]{current_episode_number}[/]"
|
f"[bold cyan]Continuing from Episode:[/] [bold]{current_episode_number}[/]"
|
||||||
@@ -689,8 +744,8 @@ def provider_anime_episodes_menu(
|
|||||||
current_episode_number = ""
|
current_episode_number = ""
|
||||||
|
|
||||||
# prompt for episode number if not set
|
# prompt for episode number if not set
|
||||||
if not current_episode_number or current_episode_number not in total_episodes:
|
if not current_episode_number or current_episode_number not in available_episodes:
|
||||||
choices = [*total_episodes, "Back"]
|
choices = [*available_episodes, "Back"]
|
||||||
preview = None
|
preview = None
|
||||||
if config.preview:
|
if config.preview:
|
||||||
from .utils import get_fzf_episode_preview
|
from .utils import get_fzf_episode_preview
|
||||||
@@ -699,13 +754,13 @@ def provider_anime_episodes_menu(
|
|||||||
if e:
|
if e:
|
||||||
eps = range(0, e + 1)
|
eps = range(0, e + 1)
|
||||||
else:
|
else:
|
||||||
eps = total_episodes
|
eps = available_episodes
|
||||||
preview = get_fzf_episode_preview(
|
preview = get_fzf_episode_preview(
|
||||||
fastanime_runtime_state.selected_anime_anilist, eps
|
fastanime_runtime_state.selected_anime_anilist, eps
|
||||||
)
|
)
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
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
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
current_episode_number = Rofi.run(choices, "Select Episode")
|
current_episode_number = Rofi.run(choices, "Select Episode")
|
||||||
@@ -728,7 +783,7 @@ def provider_anime_episodes_menu(
|
|||||||
# )
|
# )
|
||||||
|
|
||||||
# update runtime data
|
# update runtime data
|
||||||
fastanime_runtime_state.provider_available_episodes = total_episodes
|
fastanime_runtime_state.provider_available_episodes = available_episodes
|
||||||
fastanime_runtime_state.provider_current_episode_number = current_episode_number
|
fastanime_runtime_state.provider_current_episode_number = current_episode_number
|
||||||
|
|
||||||
# next interface
|
# next interface
|
||||||
@@ -765,6 +820,39 @@ def fetch_anime_episode(
|
|||||||
#
|
#
|
||||||
# ---- ANIME PROVIDER SEARCH RESULTS MENU ----
|
# ---- ANIME PROVIDER SEARCH RESULTS MENU ----
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
def set_prefered_progress_tracking(
|
||||||
|
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState", update=False
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
fastanime_runtime_state.progress_tracking == ""
|
||||||
|
or update
|
||||||
|
or fastanime_runtime_state.progress_tracking == "prompt"
|
||||||
|
):
|
||||||
|
if config.default_media_list_tracking == "track":
|
||||||
|
fastanime_runtime_state.progress_tracking = "track"
|
||||||
|
elif config.default_media_list_tracking == "disabled":
|
||||||
|
fastanime_runtime_state.progress_tracking = "disabled"
|
||||||
|
else:
|
||||||
|
options = ["disabled", "track"]
|
||||||
|
if config.use_fzf:
|
||||||
|
fastanime_runtime_state.progress_tracking = fzf.run(
|
||||||
|
options,
|
||||||
|
"Enter your preferred progress tracking for the current anime",
|
||||||
|
)
|
||||||
|
elif config.use_rofi:
|
||||||
|
fastanime_runtime_state.progress_tracking = Rofi.run(
|
||||||
|
options,
|
||||||
|
"Enter your preferred progress tracking for the current anime",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fastanime_runtime_state.progress_tracking = fuzzy_inquirer(
|
||||||
|
options,
|
||||||
|
"Enter your preferred progress tracking for the current anime",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def anime_provider_search_results_menu(
|
def anime_provider_search_results_menu(
|
||||||
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||||
):
|
):
|
||||||
@@ -830,7 +918,7 @@ def anime_provider_search_results_menu(
|
|||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
provider_anime_title = fzf.run(
|
provider_anime_title = fzf.run(
|
||||||
choices,
|
choices,
|
||||||
prompt="Select Search Result:",
|
prompt="Select Search Result",
|
||||||
header="Anime Search Results",
|
header="Anime Search Results",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -852,6 +940,11 @@ def anime_provider_search_results_menu(
|
|||||||
fastanime_runtime_state.provider_anime_search_result = provider_search_results[
|
fastanime_runtime_state.provider_anime_search_result = provider_search_results[
|
||||||
provider_anime_title
|
provider_anime_title
|
||||||
]
|
]
|
||||||
|
|
||||||
|
fastanime_runtime_state.progress_tracking = config.watch_history.get(
|
||||||
|
str(fastanime_runtime_state.selected_anime_id_anilist), {}
|
||||||
|
).get("progress_tracking", "prompt")
|
||||||
|
set_prefered_progress_tracking(config, fastanime_runtime_state)
|
||||||
fetch_anime_episode(config, fastanime_runtime_state)
|
fetch_anime_episode(config, fastanime_runtime_state)
|
||||||
|
|
||||||
|
|
||||||
@@ -900,7 +993,7 @@ def media_actions_menu(
|
|||||||
media_actions_menu(config, fastanime_runtime_state)
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
else:
|
else:
|
||||||
if not config.use_rofi:
|
if not config.use_rofi:
|
||||||
print("no trailer available :confused:")
|
print("no trailer available :confused")
|
||||||
input("Enter to continue...")
|
input("Enter to continue...")
|
||||||
else:
|
else:
|
||||||
if not Rofi.confirm("No trailler found!!Enter to continue"):
|
if not Rofi.confirm("No trailler found!!Enter to continue"):
|
||||||
@@ -1033,7 +1126,7 @@ def media_actions_menu(
|
|||||||
options = ["Sub", "Dub"]
|
options = ["Sub", "Dub"]
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
translation_type = fzf.run(
|
translation_type = fzf.run(
|
||||||
options, prompt="Select Translation Type:", header="Language Options"
|
options, prompt="Select Translation Type", header="Language Options"
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
translation_type = Rofi.run(options, "Select Translation Type")
|
translation_type = Rofi.run(options, "Select Translation Type")
|
||||||
@@ -1062,10 +1155,10 @@ def media_actions_menu(
|
|||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
player = fzf.run(
|
player = fzf.run(
|
||||||
options,
|
options,
|
||||||
prompt="Select Player:",
|
prompt="Select Player",
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
player = Rofi.run(options, "Select Player: ")
|
player = Rofi.run(options, "Select Player")
|
||||||
else:
|
else:
|
||||||
player = fuzzy_inquirer(
|
player = fuzzy_inquirer(
|
||||||
options,
|
options,
|
||||||
@@ -1202,7 +1295,7 @@ def media_actions_menu(
|
|||||||
options = list(anime_sources.keys())
|
options = list(anime_sources.keys())
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
provider = fzf.run(
|
provider = fzf.run(
|
||||||
options, prompt="Select Translation Type:", header="Language Options"
|
options, prompt="Select Translation Type", header="Language Options"
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
provider = Rofi.run(options, "Select Translation Type")
|
provider = Rofi.run(options, "Select Translation Type")
|
||||||
@@ -1241,12 +1334,19 @@ def media_actions_menu(
|
|||||||
config.continue_from_history = False
|
config.continue_from_history = False
|
||||||
anime_provider_search_results_menu(config, fastanime_runtime_state)
|
anime_provider_search_results_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
|
def _set_progress_tracking(
|
||||||
|
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
|
||||||
|
):
|
||||||
|
set_prefered_progress_tracking(config, fastanime_runtime_state, update=True)
|
||||||
|
media_actions_menu(config, fastanime_runtime_state)
|
||||||
|
|
||||||
icons = config.icons
|
icons = config.icons
|
||||||
options = {
|
options = {
|
||||||
f"{'📽️ ' if icons else ''}Stream ({progress}/{episodes_total})": _stream_anime,
|
f"{'📽️ ' if icons else ''}Stream ({progress}/{episodes_total})": _stream_anime,
|
||||||
f"{'📽️ ' if icons else ''}Episodes": _select_episode_to_stream,
|
f"{'📽️ ' if icons else ''}Episodes": _select_episode_to_stream,
|
||||||
f"{'📼 ' if icons else ''}Watch Trailer": _watch_trailer,
|
f"{'📼 ' if icons else ''}Watch Trailer": _watch_trailer,
|
||||||
f"{'✨ ' if icons else ''}Score Anime": _score_anime,
|
f"{'✨ ' if icons else ''}Score Anime": _score_anime,
|
||||||
|
f"{'✨ ' if icons else ''}Progress Tracking": _set_progress_tracking,
|
||||||
f"{'📥 ' if icons else ''}Add to List": _add_to_list,
|
f"{'📥 ' if icons else ''}Add to List": _add_to_list,
|
||||||
f"{'📤 ' if icons else ''}Remove from List": _remove_from_list,
|
f"{'📤 ' if icons else ''}Remove from List": _remove_from_list,
|
||||||
f"{'📖 ' if icons else ''}View Info": _view_info,
|
f"{'📖 ' if icons else ''}View Info": _view_info,
|
||||||
@@ -1261,7 +1361,7 @@ def media_actions_menu(
|
|||||||
}
|
}
|
||||||
choices = list(options.keys())
|
choices = list(options.keys())
|
||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
action = fzf.run(choices, prompt="Select Action:", header="Anime Menu")
|
action = fzf.run(choices, prompt="Select Action", header="Anime Menu")
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
action = Rofi.run(choices, "Select Action")
|
action = Rofi.run(choices, "Select Action")
|
||||||
else:
|
else:
|
||||||
@@ -1329,14 +1429,14 @@ def anilist_results_menu(
|
|||||||
preview = get_fzf_anime_preview(search_results, anime_data.keys())
|
preview = get_fzf_anime_preview(search_results, anime_data.keys())
|
||||||
selected_anime_title = fzf.run(
|
selected_anime_title = fzf.run(
|
||||||
choices,
|
choices,
|
||||||
prompt="Select Anime: ",
|
prompt="Select Anime",
|
||||||
header="Search Results",
|
header="Search Results",
|
||||||
preview=preview,
|
preview=preview,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
selected_anime_title = fzf.run(
|
selected_anime_title = fzf.run(
|
||||||
choices,
|
choices,
|
||||||
prompt="Select Anime: ",
|
prompt="Select Anime",
|
||||||
header="Search Results",
|
header="Search Results",
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
@@ -1347,7 +1447,7 @@ def anilist_results_menu(
|
|||||||
choices = []
|
choices = []
|
||||||
for title in anime_data.keys():
|
for title in anime_data.keys():
|
||||||
icon_path = os.path.join(IMAGES_CACHE_DIR, title)
|
icon_path = os.path.join(IMAGES_CACHE_DIR, title)
|
||||||
choices.append(f"{title}\0icon\x1f{icon_path}")
|
choices.append(f"{title}\0icon\x1f{icon_path}.png")
|
||||||
choices.append("Back")
|
choices.append("Back")
|
||||||
selected_anime_title = Rofi.run_with_icons(choices, "Select Anime")
|
selected_anime_title = Rofi.run_with_icons(choices, "Select Anime")
|
||||||
else:
|
else:
|
||||||
@@ -1489,6 +1589,9 @@ def fastanime_main_menu(
|
|||||||
watch_history = list(map(int, config.watch_history.keys()))
|
watch_history = list(map(int, config.watch_history.keys()))
|
||||||
return AniList.search(id_in=watch_history, sort="TRENDING_DESC")
|
return AniList.search(id_in=watch_history, sort="TRENDING_DESC")
|
||||||
|
|
||||||
|
def _recent():
|
||||||
|
return (True, {"data": {"Page": {"media": config.user_data["recent_anime"]}}})
|
||||||
|
|
||||||
# WARNING: Will probably be depracated
|
# WARNING: Will probably be depracated
|
||||||
def _anime_list():
|
def _anime_list():
|
||||||
anime_list = config.anime_list
|
anime_list = config.anime_list
|
||||||
@@ -1507,6 +1610,8 @@ def fastanime_main_menu(
|
|||||||
else:
|
else:
|
||||||
config.load_config()
|
config.load_config()
|
||||||
|
|
||||||
|
config.set_fastanime_config_environs()
|
||||||
|
|
||||||
config.anime_provider.provider = config.provider
|
config.anime_provider.provider = config.provider
|
||||||
config.anime_provider.lazyload_provider(config.provider)
|
config.anime_provider.lazyload_provider(config.provider)
|
||||||
|
|
||||||
@@ -1516,6 +1621,7 @@ def fastanime_main_menu(
|
|||||||
# each option maps to anilist data that is described by the option name
|
# each option maps to anilist data that is described by the option name
|
||||||
options = {
|
options = {
|
||||||
f"{'🔥 ' if icons else ''}Trending": AniList.get_trending,
|
f"{'🔥 ' if icons else ''}Trending": AniList.get_trending,
|
||||||
|
f"{'🎞️ ' if icons else ''}Recent": _recent,
|
||||||
f"{'📺 ' if icons else ''}Watching": lambda media_list_type="Watching": handle_animelist(
|
f"{'📺 ' if icons else ''}Watching": lambda media_list_type="Watching": handle_animelist(
|
||||||
config, fastanime_runtime_state, media_list_type
|
config, fastanime_runtime_state, media_list_type
|
||||||
),
|
),
|
||||||
@@ -1551,7 +1657,7 @@ def fastanime_main_menu(
|
|||||||
if config.use_fzf:
|
if config.use_fzf:
|
||||||
action = fzf.run(
|
action = fzf.run(
|
||||||
choices,
|
choices,
|
||||||
prompt="Select Action: ",
|
prompt="Select Action",
|
||||||
header="Anilist Menu",
|
header="Anilist Menu",
|
||||||
)
|
)
|
||||||
elif config.use_rofi:
|
elif config.use_rofi:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from threading import Thread
|
|||||||
import requests
|
import requests
|
||||||
from yt_dlp.utils import clean_html, sanitize_filename
|
from yt_dlp.utils import clean_html, sanitize_filename
|
||||||
|
|
||||||
from ...constants import APP_CACHE_DIR
|
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 fzf_preview
|
||||||
@@ -46,7 +46,9 @@ def aniskip(mal_id: int, episode: str):
|
|||||||
|
|
||||||
# NOTE: May change this to a temp dir but there were issues so later
|
# NOTE: May change this to a temp dir but there were issues so later
|
||||||
WORKING_DIR = APP_CACHE_DIR # tempfile.gettempdir()
|
WORKING_DIR = APP_CACHE_DIR # tempfile.gettempdir()
|
||||||
|
HEADER_COLOR = 215, 0, 95
|
||||||
|
SEPARATOR_COLOR = 208, 208, 208
|
||||||
|
SINGLE_QUOTE = "'"
|
||||||
IMAGES_CACHE_DIR = os.path.join(WORKING_DIR, "images")
|
IMAGES_CACHE_DIR = os.path.join(WORKING_DIR, "images")
|
||||||
if not os.path.exists(IMAGES_CACHE_DIR):
|
if not os.path.exists(IMAGES_CACHE_DIR):
|
||||||
os.mkdir(IMAGES_CACHE_DIR)
|
os.mkdir(IMAGES_CACHE_DIR)
|
||||||
@@ -63,7 +65,7 @@ def save_image_from_url(url: str, file_name: str):
|
|||||||
file_name: filename to use
|
file_name: filename to use
|
||||||
"""
|
"""
|
||||||
image = requests.get(url)
|
image = requests.get(url)
|
||||||
with open(f"{IMAGES_CACHE_DIR}/{file_name}", "wb") as f:
|
with open(os.path.join(IMAGES_CACHE_DIR,f"{file_name}.png"), "wb") as f:
|
||||||
f.write(image.content)
|
f.write(image.content)
|
||||||
|
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ def save_info_from_str(info: str, file_name: str):
|
|||||||
info: the information anilist has on the anime
|
info: the information anilist has on the anime
|
||||||
file_name: the filename to use
|
file_name: the filename to use
|
||||||
"""
|
"""
|
||||||
with open(f"{ANIME_INFO_CACHE_DIR}/{file_name}", "w") as f:
|
with open(os.path.join(ANIME_INFO_CACHE_DIR,file_name,), "w",encoding="utf-8") as f:
|
||||||
f.write(info)
|
f.write(info)
|
||||||
|
|
||||||
|
|
||||||
@@ -91,18 +93,16 @@ def write_search_results(
|
|||||||
workers:number of threads to use defaults to as many as possible
|
workers:number of threads to use defaults to as many as possible
|
||||||
"""
|
"""
|
||||||
# NOTE: Will probably make this a configuraable option
|
# NOTE: Will probably make this a configuraable option
|
||||||
HEADER_COLOR = 215, 0, 95
|
|
||||||
SEPARATOR_COLOR = 208, 208, 208
|
|
||||||
SEPARATOR_WIDTH = 30
|
|
||||||
# use concurency to download and write as fast as possible
|
# use concurency to download and write as fast as possible
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
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 = anime["coverImage"]["large"]
|
if os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower() == "true":
|
||||||
future_to_task[executor.submit(save_image_from_url, image_url, title)] = (
|
image_url = anime["coverImage"]["large"]
|
||||||
image_url
|
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"
|
||||||
@@ -111,28 +111,57 @@ def write_search_results(
|
|||||||
progress = anime_list["progress"]
|
progress = anime_list["progress"]
|
||||||
# handle the text data
|
# handle the text data
|
||||||
template = f"""
|
template = f"""
|
||||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
ll=2
|
||||||
{get_true_fg('Title(jp):',*HEADER_COLOR)} {anime['title']['romaji']}
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
{get_true_fg('Title(eng):',*HEADER_COLOR)} {anime['title']['english']}
|
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||||
{get_true_fg('Popularity:',*HEADER_COLOR)} {anime['popularity']}
|
((ll++))
|
||||||
{get_true_fg('Favourites:',*HEADER_COLOR)} {anime['favourites']}
|
done
|
||||||
{get_true_fg('Status:',*HEADER_COLOR)} {anime['status']}
|
echo
|
||||||
{get_true_fg('Episodes:',*HEADER_COLOR)} {anime['episodes']}
|
echo "{get_true_fg('Title(jp):',*HEADER_COLOR)} {(anime['title']['romaji'] or "").replace('"',SINGLE_QUOTE)}"
|
||||||
{get_true_fg('Genres:',*HEADER_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime['genres'])}
|
echo "{get_true_fg('Title(eng):',*HEADER_COLOR)} {(anime['title']['english'] or "").replace('"',SINGLE_QUOTE)}"
|
||||||
{get_true_fg('Next Episode:',*HEADER_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])}
|
ll=2
|
||||||
{get_true_fg('Start Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
{get_true_fg('End Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
|
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
((ll++))
|
||||||
{get_true_fg('Media List:',*HEADER_COLOR)} {mediaListName}
|
done
|
||||||
{get_true_fg('Progress:',*HEADER_COLOR)} {progress}
|
echo
|
||||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
echo "{get_true_fg('Popularity:',*HEADER_COLOR)} {anilist_data_helper.format_number_with_commas(anime['popularity'])}"
|
||||||
{get_true_fg('Description:',*HEADER_COLOR)}
|
echo "{get_true_fg('Favourites:',*HEADER_COLOR)} {anilist_data_helper.format_number_with_commas(anime['favourites'])}"
|
||||||
|
echo "{get_true_fg('Status:',*HEADER_COLOR)} {str(anime['status']).replace('"',SINGLE_QUOTE)}"
|
||||||
|
echo "{get_true_fg('Next Episode:',*HEADER_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode']).replace('"',SINGLE_QUOTE)}"
|
||||||
|
echo "{get_true_fg('Genres:',*HEADER_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime['genres']).replace('"',SINGLE_QUOTE)}"
|
||||||
|
ll=2
|
||||||
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
|
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||||
|
((ll++))
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "{get_true_fg('Episodes:',*HEADER_COLOR)} {(anime['episodes']) or 'UNKNOWN'}"
|
||||||
|
echo "{get_true_fg('Start Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate']).replace('"',SINGLE_QUOTE)}"
|
||||||
|
echo "{get_true_fg('End Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate']).replace('"',SINGLE_QUOTE)}"
|
||||||
|
ll=2
|
||||||
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
|
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||||
|
((ll++))
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
echo "{get_true_fg('Media List:',*HEADER_COLOR)} {mediaListName.replace('"',SINGLE_QUOTE)}"
|
||||||
|
echo "{get_true_fg('Progress:',*HEADER_COLOR)} {progress}"
|
||||||
|
ll=2
|
||||||
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
|
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||||
|
((ll++))
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
# echo "{get_true_fg('Description:',*HEADER_COLOR).replace('"',SINGLE_QUOTE)}"
|
||||||
"""
|
"""
|
||||||
template = textwrap.dedent(template)
|
template = textwrap.dedent(template)
|
||||||
template = f"""
|
template = f"""
|
||||||
{template}
|
{template}
|
||||||
|
echo "
|
||||||
{textwrap.fill(clean_html(
|
{textwrap.fill(clean_html(
|
||||||
str(anime['description'])), width=45)}
|
(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
|
||||||
|
|
||||||
@@ -212,8 +241,8 @@ def get_fzf_manga_preview(manga_results, workers=None, wait=False):
|
|||||||
background_worker = Thread(
|
background_worker = Thread(
|
||||||
target=_worker,
|
target=_worker,
|
||||||
)
|
)
|
||||||
# ensure images and info exists
|
|
||||||
background_worker.daemon = True
|
background_worker.daemon = True
|
||||||
|
# ensure images and info exists
|
||||||
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
|
||||||
@@ -270,8 +299,13 @@ def get_fzf_episode_preview(
|
|||||||
] = image_url
|
] = image_url
|
||||||
template = textwrap.dedent(
|
template = textwrap.dedent(
|
||||||
f"""
|
f"""
|
||||||
{get_true_fg('Anime Title:',*HEADER_COLOR)} {anilist_result['title']['romaji'] or anilist_result['title']['english']}
|
ll=2
|
||||||
{get_true_fg('Episode Title:',*HEADER_COLOR)} {episode_title}
|
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||||
|
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||||
|
((ll++))
|
||||||
|
done
|
||||||
|
echo "{get_true_fg('Anime Title:',*HEADER_COLOR)} {(anilist_result['title']['romaji'] or anilist_result['title']['english']).replace('"',SINGLE_QUOTE)}"
|
||||||
|
echo "{get_true_fg('Episode Title:',*HEADER_COLOR)} {str(episode_title).replace('"',SINGLE_QUOTE)}"
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
future_to_url[
|
future_to_url[
|
||||||
@@ -289,27 +323,61 @@ def get_fzf_episode_preview(
|
|||||||
background_worker = Thread(
|
background_worker = Thread(
|
||||||
target=_worker,
|
target=_worker,
|
||||||
)
|
)
|
||||||
# ensure images and info exists
|
|
||||||
background_worker.daemon = True
|
background_worker.daemon = True
|
||||||
|
# ensure images and info exists
|
||||||
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"
|
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||||
preview = """
|
if S_PLATFORM == "win32":
|
||||||
%s
|
preview = """
|
||||||
if [ -s %s/{} ]; then fzf-preview %s/{}
|
%s
|
||||||
else echo Loading...
|
title={}
|
||||||
fi
|
show_image_previews="%s"
|
||||||
if [ -s %s/{} ]; then cat %s/{}
|
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||||
else echo Loading...
|
if [ $show_image_previews = "true" ];then
|
||||||
fi
|
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||||
""" % (
|
if command -v "chafa">/dev/null;then
|
||||||
fzf_preview,
|
chafa -s $dim "%s\\\\\\${title}.png"
|
||||||
IMAGES_CACHE_DIR,
|
else
|
||||||
IMAGES_CACHE_DIR,
|
echo please install chafa to enjoy image previews
|
||||||
ANIME_INFO_CACHE_DIR,
|
fi
|
||||||
ANIME_INFO_CACHE_DIR,
|
echo
|
||||||
)
|
else
|
||||||
|
echo Loading...
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -s "%s\\\\\\$title" ]; then source "%s\\\\\\$title"
|
||||||
|
else echo Loading...
|
||||||
|
fi
|
||||||
|
""" % (
|
||||||
|
fzf_preview,
|
||||||
|
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
||||||
|
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
|
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
|
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
|
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
preview = """
|
||||||
|
%s
|
||||||
|
show_image_previews="%s"
|
||||||
|
if [ $show_image_previews = "true" ];then
|
||||||
|
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||||
|
else echo Loading...
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -s %s/{} ]; then source %s/{}
|
||||||
|
else echo Loading...
|
||||||
|
fi
|
||||||
|
""" % (
|
||||||
|
fzf_preview,
|
||||||
|
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
||||||
|
IMAGES_CACHE_DIR,
|
||||||
|
IMAGES_CACHE_DIR,
|
||||||
|
ANIME_INFO_CACHE_DIR,
|
||||||
|
ANIME_INFO_CACHE_DIR,
|
||||||
|
)
|
||||||
if wait:
|
if wait:
|
||||||
background_worker.join()
|
background_worker.join()
|
||||||
return preview
|
return preview
|
||||||
@@ -329,7 +397,7 @@ def get_fzf_anime_preview(
|
|||||||
THe fzf preview script to use
|
THe fzf preview script to use
|
||||||
"""
|
"""
|
||||||
# ensure images and info exists
|
# ensure images and info exists
|
||||||
from ...constants import S_PLATFORM
|
|
||||||
background_worker = Thread(
|
background_worker = Thread(
|
||||||
target=write_search_results, args=(anilist_results, titles)
|
target=write_search_results, args=(anilist_results, titles)
|
||||||
)
|
)
|
||||||
@@ -342,34 +410,47 @@ def get_fzf_anime_preview(
|
|||||||
preview = """
|
preview = """
|
||||||
%s
|
%s
|
||||||
title={}
|
title={}
|
||||||
|
show_image_previews="%s"
|
||||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||||
if [ -s "%s\\\\\\$title" ]; then
|
if [ $show_image_previews = "true" ];then
|
||||||
if command -v chafa >/dev/null;then
|
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||||
chafa -f kitty -s $dim "%s\\\\\\$title"
|
if command -v "chafa">/dev/null;then
|
||||||
|
chafa -s $dim "%s\\\\\\${title}.png"
|
||||||
|
else
|
||||||
|
echo please install chafa to enjoy image previews
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo Loading...
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else echo Loading...
|
if [ -s "%s\\\\\\$title" ]; then source "%s\\\\\\$title"
|
||||||
fi
|
else echo Loading...
|
||||||
if [ -s "%s\\\\\\$title" ]; then cat "%s\\\\\\$title"
|
|
||||||
else echo Loading...
|
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
fzf_preview,
|
||||||
IMAGES_CACHE_DIR.replace("\\","\\\\\\"),
|
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
|
||||||
IMAGES_CACHE_DIR.replace("\\","\\\\\\"),
|
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
ANIME_INFO_CACHE_DIR.replace("\\","\\\\\\"),
|
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
ANIME_INFO_CACHE_DIR.replace("\\","\\\\\\"),
|
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
|
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
preview = """
|
preview = """
|
||||||
%s
|
%s
|
||||||
if [ -s %s/{} ]; then fzf-preview %s/{}
|
title={}
|
||||||
else echo Loading...
|
show_image_previews="%s"
|
||||||
|
if [ $show_image_previews = "true" ];then
|
||||||
|
if [ -s "%s/${title}.png" ]; then fzf-preview "%s/${title}.png"
|
||||||
|
else echo Loading...
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -s %s/{} ]; then cat %s/{}
|
if [ -s "%s/$title" ]; then source "%s/$title"
|
||||||
else echo Loading...
|
else echo Loading...
|
||||||
fi
|
fi
|
||||||
""" % (
|
""" % (
|
||||||
fzf_preview,
|
fzf_preview,
|
||||||
|
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,50 +1,73 @@
|
|||||||
import re
|
import re
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
from fastanime.constants import S_PLATFORM
|
from ...constants import S_PLATFORM
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
mpv_av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)")
|
||||||
|
|
||||||
|
|
||||||
def stream_video(MPV, url, mpv_args, custom_args):
|
def stream_video(MPV, url, mpv_args, custom_args):
|
||||||
process = subprocess.Popen(
|
|
||||||
[MPV, url, *mpv_args, *custom_args],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
last_time = None
|
|
||||||
av_time_pattern = re.compile(r"AV: ([0-9:]*) / ([0-9:]*) \(([0-9]*)%\)")
|
|
||||||
last_time = "0"
|
last_time = "0"
|
||||||
total_time = "0"
|
total_time = "0"
|
||||||
|
if os.environ.get("FASTANIME_DISABLE_MPV_POPEN", "False") == "False":
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[
|
||||||
|
MPV,
|
||||||
|
url,
|
||||||
|
*mpv_args,
|
||||||
|
*custom_args,
|
||||||
|
"--no-terminal",
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
if not process.stderr:
|
if not process.stderr:
|
||||||
continue
|
time.sleep(0.1)
|
||||||
output = process.stderr.readline()
|
continue
|
||||||
|
output = process.stderr.readline()
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
# Match the timestamp in the output
|
# Match the timestamp in the output
|
||||||
match = av_time_pattern.search(output.strip())
|
match = mpv_av_time_pattern.search(output.strip())
|
||||||
|
if match:
|
||||||
|
current_time = match.group(1)
|
||||||
|
total_time = match.group(2)
|
||||||
|
last_time = current_time
|
||||||
|
|
||||||
|
# Check if the process has terminated
|
||||||
|
retcode = process.poll()
|
||||||
|
if retcode is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
logger.error(f"An error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
process.terminate()
|
||||||
|
process.wait()
|
||||||
|
else:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[MPV, url, *mpv_args, *custom_args], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if proc.stdout:
|
||||||
|
for line in reversed(proc.stdout.split("\n")):
|
||||||
|
match = mpv_av_time_pattern.search(line.strip())
|
||||||
if match:
|
if match:
|
||||||
current_time = match.group(1)
|
last_time = match.group(1)
|
||||||
total_time = match.group(2)
|
total_time = match.group(2)
|
||||||
match.group(3)
|
break
|
||||||
last_time = current_time
|
|
||||||
# print(f"Current stream time: {current_time}, Total time: {total_time}, Progress: {percentage}%")
|
|
||||||
|
|
||||||
# Check if the process has terminated
|
|
||||||
retcode = process.poll()
|
|
||||||
if retcode is not None:
|
|
||||||
print("Finshed at: ", last_time)
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"An error occurred: {e}")
|
|
||||||
finally:
|
|
||||||
process.terminate()
|
|
||||||
|
|
||||||
return last_time, total_time
|
return last_time, total_time
|
||||||
|
|
||||||
|
|
||||||
@@ -63,6 +86,19 @@ def run_mpv(
|
|||||||
# Regex to check if the link is a YouTube URL
|
# Regex to check if the link is a YouTube URL
|
||||||
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
|
youtube_regex = r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/.+"
|
||||||
|
|
||||||
|
if link.endswith(".torrent"):
|
||||||
|
WEBTORRENT_CLI = shutil.which("webtorrent")
|
||||||
|
if not WEBTORRENT_CLI:
|
||||||
|
import time
|
||||||
|
|
||||||
|
print(
|
||||||
|
"webtorrent cli is not installed which is required for downloading and streaming from nyaa\nplease install it or use another provider"
|
||||||
|
)
|
||||||
|
time.sleep(120)
|
||||||
|
return "0", "0"
|
||||||
|
cmd = [WEBTORRENT_CLI, link, f"--{player}"]
|
||||||
|
subprocess.run(cmd)
|
||||||
|
return "0", "0"
|
||||||
if player == "vlc":
|
if player == "vlc":
|
||||||
VLC = shutil.which("vlc")
|
VLC = shutil.which("vlc")
|
||||||
if not VLC and not S_PLATFORM == "win32":
|
if not VLC and not S_PLATFORM == "win32":
|
||||||
@@ -91,7 +127,8 @@ def run_mpv(
|
|||||||
"--user",
|
"--user",
|
||||||
"0",
|
"0",
|
||||||
"-a",
|
"-a",
|
||||||
"android.intent.action.VIEW" "-d",
|
"android.intent.action.VIEW",
|
||||||
|
"-d",
|
||||||
link,
|
link,
|
||||||
"-n",
|
"-n",
|
||||||
"org.videolan.vlc/org.videolan.vlc.gui.video.VideoPlayerActivity",
|
"org.videolan.vlc/org.videolan.vlc.gui.video.VideoPlayerActivity",
|
||||||
@@ -170,13 +207,3 @@ def run_mpv(
|
|||||||
mpv_args.append(f"--ytdl-format={ytdl_format}")
|
mpv_args.append(f"--ytdl-format={ytdl_format}")
|
||||||
stop_time, total_time = stream_video(MPV, link, mpv_args, custom_args)
|
stop_time, total_time = stream_video(MPV, link, mpv_args, custom_args)
|
||||||
return stop_time, total_time
|
return stop_time, total_time
|
||||||
|
|
||||||
|
|
||||||
# Example usage
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_mpv(
|
|
||||||
"https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
||||||
"Example Video",
|
|
||||||
"--fullscreen",
|
|
||||||
"--volume=50",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ class MpvPlayer(object):
|
|||||||
current_episode_number = (
|
current_episode_number = (
|
||||||
fastanime_runtime_state.provider_current_episode_number
|
fastanime_runtime_state.provider_current_episode_number
|
||||||
)
|
)
|
||||||
config.update_watch_history(anime_id_anilist, str(current_episode_number))
|
config.media_list_track(
|
||||||
|
anime_id_anilist,
|
||||||
|
episode_no=str(current_episode_number),
|
||||||
|
progress_tracking=fastanime_runtime_state.progress_tracking,
|
||||||
|
)
|
||||||
elif type == "reload":
|
elif type == "reload":
|
||||||
if current_episode_number not in total_episodes:
|
if current_episode_number not in total_episodes:
|
||||||
self.mpv_player.show_text("Episode not available")
|
self.mpv_player.show_text("Episode not available")
|
||||||
@@ -84,7 +88,11 @@ class MpvPlayer(object):
|
|||||||
|
|
||||||
self.mpv_player.show_text(f"Fetching episode {ep_no}")
|
self.mpv_player.show_text(f"Fetching episode {ep_no}")
|
||||||
current_episode_number = ep_no
|
current_episode_number = ep_no
|
||||||
config.update_watch_history(anime_id_anilist, str(ep_no))
|
config.media_list_track(
|
||||||
|
anime_id_anilist,
|
||||||
|
episode_no=str(ep_no),
|
||||||
|
progress_tracking=fastanime_runtime_state.progress_tracking,
|
||||||
|
)
|
||||||
fastanime_runtime_state.provider_current_episode_number = str(ep_no)
|
fastanime_runtime_state.provider_current_episode_number = str(ep_no)
|
||||||
else:
|
else:
|
||||||
self.mpv_player.show_text("Fetching previous episode...")
|
self.mpv_player.show_text("Fetching previous episode...")
|
||||||
@@ -97,7 +105,11 @@ class MpvPlayer(object):
|
|||||||
current_episode_number = (
|
current_episode_number = (
|
||||||
fastanime_runtime_state.provider_current_episode_number
|
fastanime_runtime_state.provider_current_episode_number
|
||||||
)
|
)
|
||||||
config.update_watch_history(anime_id_anilist, str(current_episode_number))
|
config.media_list_track(
|
||||||
|
anime_id_anilist,
|
||||||
|
episode_no=str(current_episode_number),
|
||||||
|
progress_tracking=fastanime_runtime_state.progress_tracking,
|
||||||
|
)
|
||||||
# update episode progress
|
# update episode progress
|
||||||
if config.user and current_episode_number:
|
if config.user and current_episode_number:
|
||||||
AniList.update_anime_list(
|
AniList.update_anime_list(
|
||||||
@@ -108,7 +120,7 @@ class MpvPlayer(object):
|
|||||||
)
|
)
|
||||||
# get them juicy streams
|
# get them juicy streams
|
||||||
episode_streams = anime_provider.get_episode_streams(
|
episode_streams = anime_provider.get_episode_streams(
|
||||||
provider_anime,
|
provider_anime["id"],
|
||||||
current_episode_number,
|
current_episode_number,
|
||||||
translation_type,
|
translation_type,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class FastAnimeRuntimeState(object):
|
|||||||
provider_anime_title: str
|
provider_anime_title: str
|
||||||
provider_anime: "Anime"
|
provider_anime: "Anime"
|
||||||
provider_anime_search_result: "SearchResult"
|
provider_anime_search_result: "SearchResult"
|
||||||
|
progress_tracking: str = ""
|
||||||
|
|
||||||
selected_anime_anilist: "AnilistBaseMediaDataSchema"
|
selected_anime_anilist: "AnilistBaseMediaDataSchema"
|
||||||
selected_anime_id_anilist: int
|
selected_anime_id_anilist: int
|
||||||
@@ -36,8 +37,13 @@ def exit_app(exit_code=0, *args):
|
|||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
if not console.is_terminal:
|
if not console.is_terminal:
|
||||||
from plyer import notification
|
try:
|
||||||
|
from plyer import notification
|
||||||
|
except ImportError:
|
||||||
|
print(
|
||||||
|
"Plyer is not installed; install it for desktop notifications to be enabled"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
notification.notify(
|
notification.notify(
|
||||||
app_name=APP_NAME,
|
app_name=APP_NAME,
|
||||||
app_icon=ICON_PATH,
|
app_icon=ICON_PATH,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ else:
|
|||||||
# ----- user configs and data -----
|
# ----- user configs and data -----
|
||||||
|
|
||||||
S_PLATFORM = sys.platform
|
S_PLATFORM = sys.platform
|
||||||
APP_DATA_DIR = click.get_app_dir(APP_NAME,roaming=False)
|
APP_DATA_DIR = click.get_app_dir(APP_NAME, roaming=False)
|
||||||
if S_PLATFORM == "win32":
|
if S_PLATFORM == "win32":
|
||||||
# app data
|
# app data
|
||||||
# app_data_dir_base = os.getenv("LOCALAPPDATA")
|
# app_data_dir_base = os.getenv("LOCALAPPDATA")
|
||||||
@@ -78,6 +78,7 @@ Path(USER_VIDEOS_DIR).mkdir(parents=True, exist_ok=True)
|
|||||||
|
|
||||||
# useful paths
|
# useful paths
|
||||||
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
|
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
|
||||||
|
USER_WATCH_HISTORY_PATH = os.path.join(APP_DATA_DIR, "watch_history.json")
|
||||||
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
|
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
|
||||||
LOG_FILE_PATH = os.path.join(APP_DATA_DIR, "fastanime.log")
|
LOG_FILE_PATH = os.path.join(APP_DATA_DIR, "fastanime.log")
|
||||||
|
|
||||||
|
|||||||
14
fastanime/fastanime.py
Executable file
14
fastanime/fastanime.py
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the application root directory to Python path
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
application_path = os.path.dirname(sys.executable)
|
||||||
|
sys.path.insert(0, application_path)
|
||||||
|
|
||||||
|
# Import and run the main application
|
||||||
|
from fastanime import FastAnime
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
FastAnime()
|
||||||
@@ -225,6 +225,7 @@ query ($userId: Int, $status: MediaListStatus, $type: MediaType) {
|
|||||||
averageScore
|
averageScore
|
||||||
episodes
|
episodes
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
studios {
|
studios {
|
||||||
nodes {
|
nodes {
|
||||||
name
|
name
|
||||||
@@ -369,6 +370,7 @@ query($query:String,%s){
|
|||||||
averageScore
|
averageScore
|
||||||
episodes
|
episodes
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
studios {
|
studios {
|
||||||
nodes {
|
nodes {
|
||||||
name
|
name
|
||||||
@@ -428,6 +430,7 @@ query ($type: MediaType) {
|
|||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
episodes
|
episodes
|
||||||
description
|
description
|
||||||
studios {
|
studios {
|
||||||
@@ -503,6 +506,7 @@ query ($type: MediaType) {
|
|||||||
episodes
|
episodes
|
||||||
description
|
description
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
studios {
|
studios {
|
||||||
nodes {
|
nodes {
|
||||||
name
|
name
|
||||||
@@ -566,6 +570,7 @@ query ($type: MediaType) {
|
|||||||
averageScore
|
averageScore
|
||||||
description
|
description
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
studios {
|
studios {
|
||||||
nodes {
|
nodes {
|
||||||
name
|
name
|
||||||
@@ -624,6 +629,7 @@ query ($type: MediaType) {
|
|||||||
description
|
description
|
||||||
episodes
|
episodes
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
mediaListEntry {
|
mediaListEntry {
|
||||||
status
|
status
|
||||||
id
|
id
|
||||||
@@ -698,6 +704,7 @@ query ($type: MediaType) {
|
|||||||
averageScore
|
averageScore
|
||||||
description
|
description
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
episodes
|
episodes
|
||||||
studios {
|
studios {
|
||||||
nodes {
|
nodes {
|
||||||
@@ -759,6 +766,7 @@ query ($type: MediaType) {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
averageScore
|
averageScore
|
||||||
popularity
|
popularity
|
||||||
streamingEpisodes {
|
streamingEpisodes {
|
||||||
@@ -862,6 +870,7 @@ query ($id: Int, $type: MediaType) {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
averageScore
|
averageScore
|
||||||
popularity
|
popularity
|
||||||
streamingEpisodes {
|
streamingEpisodes {
|
||||||
@@ -954,6 +963,7 @@ query ($page: Int, $type: MediaType) {
|
|||||||
favourites
|
favourites
|
||||||
averageScore
|
averageScore
|
||||||
genres
|
genres
|
||||||
|
synonyms
|
||||||
episodes
|
episodes
|
||||||
description
|
description
|
||||||
studios {
|
studios {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from .allanime.constants import SERVERS_AVAILABLE as ALLANIME_SERVERS
|
from .allanime.constants import SERVERS_AVAILABLE as ALLANIME_SERVERS
|
||||||
from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHESERVERS
|
from .animepahe.constants import SERVERS_AVAILABLE as ANIMEPAHE_SERVERS
|
||||||
from .aniwatch.constants import SERVERS_AVAILABLE as ANIWATCHSERVERS
|
from .hianime.constants import SERVERS_AVAILABLE as HIANIME_SERVERS
|
||||||
|
|
||||||
anime_sources = {
|
anime_sources = {
|
||||||
"allanime": "api.AllAnimeAPI",
|
"allanime": "api.AllAnimeAPI",
|
||||||
"animepahe": "api.AnimePaheApi",
|
"animepahe": "api.AnimePaheApi",
|
||||||
"aniwatch": "api.AniWatchApi",
|
"hianime": "api.HiAnimeApi",
|
||||||
"aniwave": "api.AniWaveApi",
|
"nyaa": "api.NyaaApi",
|
||||||
|
"yugen": "api.YugenApi"
|
||||||
}
|
}
|
||||||
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHESERVERS, *ANIWATCHSERVERS]
|
SERVERS_AVAILABLE = [*ALLANIME_SERVERS, *ANIMEPAHE_SERVERS, *HIANIME_SERVERS]
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from requests.exceptions import Timeout
|
|
||||||
|
|
||||||
from ...anime_provider.base_provider import AnimeProvider
|
from ...anime_provider.base_provider import AnimeProvider
|
||||||
|
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 ALLANIME_API_ENDPOINT, ALLANIME_BASE, ALLANIME_REFERER
|
||||||
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
||||||
@@ -27,6 +26,7 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
Provides a fast and effective interface to AllAnime site.
|
Provides a fast and effective interface to AllAnime site.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
PROVIDER = "allanime"
|
||||||
api_endpoint = ALLANIME_API_ENDPOINT
|
api_endpoint = ALLANIME_API_ENDPOINT
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
"Referer": ALLANIME_REFERER,
|
"Referer": ALLANIME_REFERER,
|
||||||
@@ -42,29 +42,21 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
Returns:
|
Returns:
|
||||||
[TODO:return]
|
[TODO:return]
|
||||||
"""
|
"""
|
||||||
try:
|
response = self.session.get(
|
||||||
response = self.session.get(
|
self.api_endpoint,
|
||||||
self.api_endpoint,
|
params={
|
||||||
params={
|
"variables": json.dumps(variables),
|
||||||
"variables": json.dumps(variables),
|
"query": query,
|
||||||
"query": query,
|
},
|
||||||
},
|
timeout=10,
|
||||||
timeout=10,
|
)
|
||||||
)
|
if response.ok:
|
||||||
if response.ok:
|
return response.json()["data"]
|
||||||
return response.json()["data"]
|
else:
|
||||||
else:
|
logger.error("[ALLANIME-ERROR]: ", response.text)
|
||||||
logger.error("[ALLANIME-ERROR]: ", response.text)
|
|
||||||
return {}
|
|
||||||
except Timeout:
|
|
||||||
logger.error(
|
|
||||||
"[ALLANIME-ERROR]: Timeout exceeded this could mean allanime is down or you have lost internet connection"
|
|
||||||
)
|
|
||||||
return {}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ALLANIME-ERROR]: {e}")
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
def search_for_anime(
|
def search_for_anime(
|
||||||
self,
|
self,
|
||||||
user_query: str,
|
user_query: str,
|
||||||
@@ -97,29 +89,25 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
"translationtype": translationtype,
|
"translationtype": translationtype,
|
||||||
"countryorigin": countryorigin,
|
"countryorigin": countryorigin,
|
||||||
}
|
}
|
||||||
try:
|
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
|
||||||
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
|
page_info = search_results["shows"]["pageInfo"]
|
||||||
page_info = search_results["shows"]["pageInfo"]
|
results = []
|
||||||
results = []
|
for result in search_results["shows"]["edges"]:
|
||||||
for result in search_results["shows"]["edges"]:
|
normalized_result = {
|
||||||
normalized_result = {
|
"id": result["_id"],
|
||||||
"id": result["_id"],
|
"title": result["name"],
|
||||||
"title": result["name"],
|
"type": result["__typename"],
|
||||||
"type": result["__typename"],
|
"availableEpisodes": result["availableEpisodes"],
|
||||||
"availableEpisodes": result["availableEpisodes"],
|
|
||||||
}
|
|
||||||
results.append(normalized_result)
|
|
||||||
|
|
||||||
normalized_search_results = {
|
|
||||||
"pageInfo": page_info,
|
|
||||||
"results": results,
|
|
||||||
}
|
}
|
||||||
return normalized_search_results
|
results.append(normalized_result)
|
||||||
|
|
||||||
except Exception as e:
|
normalized_search_results = {
|
||||||
logger.error(f"[ALLANIME-ERROR]: {e}")
|
"pageInfo": page_info,
|
||||||
return {}
|
"results": results,
|
||||||
|
}
|
||||||
|
return normalized_search_results
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
def get_anime(self, allanime_show_id: str):
|
def get_anime(self, allanime_show_id: str):
|
||||||
"""get an anime details given its id
|
"""get an anime details given its id
|
||||||
|
|
||||||
@@ -130,25 +118,23 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
[TODO:return]
|
[TODO:return]
|
||||||
"""
|
"""
|
||||||
variables = {"showId": allanime_show_id}
|
variables = {"showId": allanime_show_id}
|
||||||
try:
|
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
|
||||||
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
|
id: str = anime["show"]["_id"]
|
||||||
id: str = anime["show"]["_id"]
|
title: str = anime["show"]["name"]
|
||||||
title: str = anime["show"]["name"]
|
availableEpisodesDetail = anime["show"]["availableEpisodesDetail"]
|
||||||
availableEpisodesDetail = anime["show"]["availableEpisodesDetail"]
|
self.store.set(allanime_show_id, "anime_info", {"title": title})
|
||||||
type = anime.get("__typename")
|
type = anime.get("__typename")
|
||||||
normalized_anime = {
|
normalized_anime = {
|
||||||
"id": id,
|
"id": id,
|
||||||
"title": title,
|
"title": title,
|
||||||
"availableEpisodesDetail": availableEpisodesDetail,
|
"availableEpisodesDetail": availableEpisodesDetail,
|
||||||
"type": type,
|
"type": type,
|
||||||
}
|
}
|
||||||
return normalized_anime
|
return normalized_anime
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ALLANIME-ERROR]: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
def _get_anime_episode(
|
def _get_anime_episode(
|
||||||
self, allanime_show_id: str, episode_string: str, translation_type: str = "sub"
|
self, allanime_show_id: str, episode, translation_type: str = "sub"
|
||||||
) -> "AllAnimeEpisode | dict":
|
) -> "AllAnimeEpisode | dict":
|
||||||
"""get the episode details and sources info
|
"""get the episode details and sources info
|
||||||
|
|
||||||
@@ -163,16 +149,15 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
variables = {
|
variables = {
|
||||||
"showId": allanime_show_id,
|
"showId": allanime_show_id,
|
||||||
"translationType": translation_type,
|
"translationType": translation_type,
|
||||||
"episodeString": episode_string,
|
"episodeString": episode,
|
||||||
}
|
}
|
||||||
try:
|
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
|
||||||
episode = self._fetch_gql(ALLANIME_EPISODES_GQL, variables)
|
return episode["episode"]
|
||||||
return episode["episode"]
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ALLANIME-ERROR]: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_episode_streams(self, anime, episode_number: str, translation_type="sub"):
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def get_episode_streams(
|
||||||
|
self, anime_id, episode_number: str, translation_type="sub"
|
||||||
|
):
|
||||||
"""get the streams of an episode
|
"""get the streams of an episode
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -183,7 +168,10 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
Yields:
|
Yields:
|
||||||
[TODO:description]
|
[TODO:description]
|
||||||
"""
|
"""
|
||||||
anime_id = anime["id"]
|
|
||||||
|
anime_title = (self.store.get(anime_id, "anime_info", "") or {"title": ""})[
|
||||||
|
"title"
|
||||||
|
]
|
||||||
allanime_episode = self._get_anime_episode(
|
allanime_episode = self._get_anime_episode(
|
||||||
anime_id, episode_number, translation_type
|
anime_id, episode_number, translation_type
|
||||||
)
|
)
|
||||||
@@ -191,125 +179,117 @@ class AllAnimeAPI(AnimeProvider):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
embeds = allanime_episode["sourceUrls"]
|
embeds = allanime_episode["sourceUrls"]
|
||||||
try:
|
|
||||||
for embed in embeds:
|
|
||||||
try:
|
|
||||||
# filter the working streams no need to get all since the others are mostly hsl
|
|
||||||
# TODO: should i just get all the servers and handle the hsl??
|
|
||||||
if embed.get("sourceName", "") not in (
|
|
||||||
# priorities based on death note
|
|
||||||
"Sak", # 7
|
|
||||||
"S-mp4", # 7.9
|
|
||||||
"Luf-mp4", # 7.7
|
|
||||||
"Default", # 8.5
|
|
||||||
"Yt-mp4", # 7.9
|
|
||||||
"Kir", # NA
|
|
||||||
# "Vid-mp4" # 4
|
|
||||||
# "Ok", # 3.5
|
|
||||||
# "Ss-Hls", # 5.5
|
|
||||||
# "Mp4", # 4
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
url = embed.get("sourceUrl")
|
|
||||||
#
|
|
||||||
if not url:
|
|
||||||
continue
|
|
||||||
if url.startswith("--"):
|
|
||||||
url = url[2:]
|
|
||||||
url = one_digit_symmetric_xor(56, url)
|
|
||||||
|
|
||||||
if "tools.fast4speed.rsvp" in url:
|
@debug_provider(self.PROVIDER.upper())
|
||||||
yield {
|
def _get_server(embed):
|
||||||
"server": "Yt",
|
# filter the working streams no need to get all since the others are mostly hsl
|
||||||
"episode_title": f'{anime["title"]}; Episode {episode_number}',
|
# TODO: should i just get all the servers and handle the hsl??
|
||||||
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
if embed.get("sourceName", "") not in (
|
||||||
"subtitles": [],
|
# priorities based on death note
|
||||||
"links": [
|
"Sak", # 7
|
||||||
{
|
"S-mp4", # 7.9
|
||||||
"link": url,
|
"Luf-mp4", # 7.7
|
||||||
"quality": "1080",
|
"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:
|
||||||
|
return {
|
||||||
|
"server": "Yt",
|
||||||
|
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||||
|
"headers": {"Referer": f"https://{ALLANIME_BASE}/"},
|
||||||
|
"subtitles": [],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"link": url,
|
||||||
|
"quality": "1080",
|
||||||
}
|
}
|
||||||
continue
|
],
|
||||||
|
}
|
||||||
|
|
||||||
# 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 = (
|
embed_url = f"https://{ALLANIME_BASE}{url.replace('clock', 'clock.json')}"
|
||||||
f"https://{ALLANIME_BASE}{url.replace('clock', 'clock.json')}"
|
resp = self.session.get(
|
||||||
)
|
embed_url,
|
||||||
resp = self.session.get(
|
timeout=10,
|
||||||
embed_url,
|
)
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
if resp.ok:
|
if resp.ok:
|
||||||
match embed["sourceName"]:
|
match embed["sourceName"]:
|
||||||
case "Luf-mp4":
|
case "Luf-mp4":
|
||||||
logger.debug("allanime:Found streams from gogoanime")
|
logger.debug("allanime:Found streams from gogoanime")
|
||||||
yield {
|
return {
|
||||||
"server": "gogoanime",
|
"server": "gogoanime",
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"subtitles": [],
|
"subtitles": [],
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f"{anime_title}"
|
||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
}
|
}
|
||||||
case "Kir":
|
case "Kir":
|
||||||
logger.debug("allanime:Found streams from wetransfer")
|
logger.debug("allanime:Found streams from wetransfer")
|
||||||
yield {
|
return {
|
||||||
"server": "wetransfer",
|
"server": "wetransfer",
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"subtitles": [],
|
"subtitles": [],
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f"{anime_title}"
|
||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
}
|
}
|
||||||
case "S-mp4":
|
case "S-mp4":
|
||||||
logger.debug("allanime:Found streams from sharepoint")
|
logger.debug("allanime:Found streams from sharepoint")
|
||||||
yield {
|
return {
|
||||||
"server": "sharepoint",
|
"server": "sharepoint",
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"subtitles": [],
|
"subtitles": [],
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f"{anime_title}"
|
||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
}
|
}
|
||||||
case "Sak":
|
case "Sak":
|
||||||
logger.debug("allanime:Found streams from dropbox")
|
logger.debug("allanime:Found streams from dropbox")
|
||||||
yield {
|
return {
|
||||||
"server": "dropbox",
|
"server": "dropbox",
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"subtitles": [],
|
"subtitles": [],
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f"{anime_title}"
|
||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
}
|
}
|
||||||
case "Default":
|
case "Default":
|
||||||
logger.debug("allanime:Found streams from wixmp")
|
logger.debug("allanime:Found streams from wixmp")
|
||||||
yield {
|
return {
|
||||||
"server": "wixmp",
|
"server": "wixmp",
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"subtitles": [],
|
"subtitles": [],
|
||||||
"episode_title": (
|
"episode_title": (
|
||||||
allanime_episode["notes"] or f'{anime["title"]}'
|
allanime_episode["notes"] or f"{anime_title}"
|
||||||
)
|
)
|
||||||
+ f"; Episode {episode_number}",
|
+ f"; Episode {episode_number}",
|
||||||
"links": give_random_quality(resp.json()["links"]),
|
"links": give_random_quality(resp.json()["links"]),
|
||||||
}
|
}
|
||||||
except Timeout:
|
|
||||||
logger.error(
|
for embed in embeds:
|
||||||
"[ALLANIME-ERROR]: Timeout has been exceeded this could mean allanime is down or you have lost internet connection"
|
if server := _get_server(embed):
|
||||||
)
|
yield server
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ALLANIME-ERROR]: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ALLANIME-ERROR]: {e}")
|
|
||||||
return []
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from yt_dlp.utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from ..base_provider import AnimeProvider
|
from ..base_provider import AnimeProvider
|
||||||
|
from ..decorators import debug_provider
|
||||||
from .constants import (
|
from .constants import (
|
||||||
ANIMEPAHE_BASE,
|
ANIMEPAHE_BASE,
|
||||||
ANIMEPAHE_ENDPOINT,
|
ANIMEPAHE_ENDPOINT,
|
||||||
@@ -20,64 +21,63 @@ from .constants import (
|
|||||||
from .utils import process_animepahe_embed_page
|
from .utils import process_animepahe_embed_page
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimeSearchResult
|
from .types import AnimePaheAnimePage, AnimePaheSearchPage, AnimePaheSearchResult
|
||||||
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
JUICY_STREAM_REGEX = re.compile(r"source='(.*)';")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
KWIK_RE = re.compile(r"Player\|(.+?)'")
|
KWIK_RE = re.compile(r"Player\|(.+?)'")
|
||||||
|
|
||||||
|
|
||||||
# TODO: hack this to completion
|
|
||||||
class AnimePaheApi(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())
|
||||||
def search_for_anime(self, user_query: str, *args):
|
def search_for_anime(self, user_query: str, *args):
|
||||||
try:
|
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
||||||
url = f"{ANIMEPAHE_ENDPOINT}m=search&q={user_query}"
|
response = self.session.get(
|
||||||
response = self.session.get(
|
url,
|
||||||
url,
|
)
|
||||||
|
if not response.ok:
|
||||||
|
return
|
||||||
|
data: "AnimePaheSearchPage" = response.json()
|
||||||
|
self.search_page = data
|
||||||
|
for animepahe_search_result in data["data"]:
|
||||||
|
self.store.set(
|
||||||
|
str(animepahe_search_result["session"]),
|
||||||
|
"search_result",
|
||||||
|
animepahe_search_result,
|
||||||
)
|
)
|
||||||
if not response.ok:
|
|
||||||
return
|
|
||||||
data: "AnimePaheSearchPage" = response.json()
|
|
||||||
self.search_page = data
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"pageInfo": {
|
"pageInfo": {
|
||||||
"total": data["total"],
|
"total": data["total"],
|
||||||
"perPage": data["per_page"],
|
"perPage": data["per_page"],
|
||||||
"currentPage": data["current_page"],
|
"currentPage": data["current_page"],
|
||||||
},
|
},
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"availableEpisodes": list(range(result["episodes"])),
|
"availableEpisodes": list(range(result["episodes"])),
|
||||||
"id": result["session"],
|
"id": result["session"],
|
||||||
"title": result["title"],
|
"title": result["title"],
|
||||||
"type": result["type"],
|
"type": result["type"],
|
||||||
"year": result["year"],
|
"year": result["year"],
|
||||||
"score": result["score"],
|
"score": result["score"],
|
||||||
"status": result["status"],
|
"status": result["status"],
|
||||||
"season": result["season"],
|
"season": result["season"],
|
||||||
"poster": result["poster"],
|
"poster": result["poster"],
|
||||||
}
|
}
|
||||||
for result in data["data"]
|
for result in data["data"]
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIMEPAHE-ERROR]: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
def get_anime(self, session_id: str, *args):
|
def get_anime(self, session_id: str, *args):
|
||||||
page = 1
|
page = 1
|
||||||
try:
|
if d := self.store.get(str(session_id), "search_result"):
|
||||||
anime_result: "AnimeSearchResult" = [
|
anime_result: "AnimePaheSearchResult" = d
|
||||||
anime
|
|
||||||
for anime in self.search_page["data"]
|
|
||||||
if anime["session"] == session_id
|
|
||||||
][0]
|
|
||||||
data: "AnimePaheAnimePage" = {} # pyright:ignore
|
data: "AnimePaheAnimePage" = {} # pyright:ignore
|
||||||
|
|
||||||
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
|
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
|
||||||
@@ -122,7 +122,8 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return {}
|
return {}
|
||||||
self.anime = data # pyright:ignore
|
data["title"] = anime_result["title"] # pyright:ignore
|
||||||
|
self.store.set(str(session_id), "anime_info", data)
|
||||||
episodes = list(map(str, [episode["episode"] for episode in data["data"]]))
|
episodes = list(map(str, [episode["episode"] for episode in data["data"]]))
|
||||||
title = ""
|
title = ""
|
||||||
return {
|
return {
|
||||||
@@ -149,89 +150,87 @@ class AnimePaheApi(AnimeProvider):
|
|||||||
for episode in data["data"]
|
for episode in data["data"]
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIMEPAHE-ERROR]: {e}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_episode_streams(self, anime, episode_number: str, translation_type, *args):
|
@debug_provider(PROVIDER.upper())
|
||||||
try:
|
def get_episode_streams(
|
||||||
# extract episode details from memory
|
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 = [
|
||||||
episode
|
episode
|
||||||
for episode in self.anime["data"]
|
for episode in d["data"]
|
||||||
if float(episode["episode"]) == float(episode_number)
|
if float(episode["episode"]) == float(episode_number)
|
||||||
]
|
]
|
||||||
|
|
||||||
if not episode:
|
if not episode:
|
||||||
logger.error(
|
logger.error(f"[ANIMEPAHE-ERROR]: episode {episode_number} doesn't exist")
|
||||||
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
|
||||||
|
streams = {
|
||||||
|
"server": "kwik",
|
||||||
|
"links": [],
|
||||||
|
"episode_title": episode_title,
|
||||||
|
"subtitles": [],
|
||||||
|
"headers": {},
|
||||||
|
}
|
||||||
|
for res_dict in res_dicts:
|
||||||
|
# get embed url
|
||||||
|
embed_url = res_dict["data-src"]
|
||||||
|
data_audio = "dub" if res_dict["data-audio"] == "eng" else "sub"
|
||||||
|
# filter streams by translation_type
|
||||||
|
if data_audio != translation_type:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not embed_url:
|
||||||
|
logger.warning(
|
||||||
|
"[ANIMEPAHE-WARN]: embed url not found please report to the developers"
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
episode = episode[0]
|
# get embed page
|
||||||
|
embed_response = self.session.get(
|
||||||
anime_id = anime["id"]
|
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
|
||||||
# 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
|
if not response.ok:
|
||||||
streams = {
|
continue
|
||||||
"server": "kwik",
|
embed_page = embed_response.text
|
||||||
"links": [],
|
|
||||||
"episode_title": episode_title,
|
|
||||||
"subtitles": [],
|
|
||||||
"headers": {},
|
|
||||||
}
|
|
||||||
for res_dict in res_dicts:
|
|
||||||
# get embed url
|
|
||||||
embed_url = res_dict["data-src"]
|
|
||||||
data_audio = "dub" if res_dict["data-audio"] == "eng" else "sub"
|
|
||||||
# filter streams by translation_type
|
|
||||||
if data_audio != translation_type:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not embed_url:
|
decoded_js = process_animepahe_embed_page(embed_page)
|
||||||
logger.warn(
|
if not decoded_js:
|
||||||
"[ANIMEPAHE-WARN]: embed url not found please report to the developers"
|
logger.error("[ANIMEPAHE-ERROR]: failed to decode embed page")
|
||||||
)
|
return
|
||||||
return []
|
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
||||||
# get embed page
|
if not juicy_stream:
|
||||||
embed_response = self.session.get(
|
logger.error("[ANIMEPAHE-ERROR]: failed to find juicy stream")
|
||||||
embed_url, headers={"User-Agent": self.USER_AGENT, **SERVER_HEADERS}
|
return
|
||||||
)
|
juicy_stream = juicy_stream.group(1)
|
||||||
if not response.ok:
|
# add the link
|
||||||
continue
|
streams["links"].append(
|
||||||
embed_page = embed_response.text
|
{
|
||||||
|
"quality": res_dict["data-resolution"],
|
||||||
decoded_js = process_animepahe_embed_page(embed_page)
|
"translation_type": data_audio,
|
||||||
if not decoded_js:
|
"link": juicy_stream,
|
||||||
logger.error("[ANIMEPAHE-ERROR]: failed to decode embed page")
|
}
|
||||||
return
|
)
|
||||||
juicy_stream = JUICY_STREAM_REGEX.search(decoded_js)
|
yield streams
|
||||||
if not juicy_stream:
|
|
||||||
logger.error("[ANIMEPAHE-ERROR]: failed to find juicy stream")
|
|
||||||
return
|
|
||||||
juicy_stream = juicy_stream.group(1)
|
|
||||||
# add the link
|
|
||||||
streams["links"].append(
|
|
||||||
{
|
|
||||||
"quality": res_dict["data-resolution"],
|
|
||||||
"translation_type": data_audio,
|
|
||||||
"link": juicy_stream,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
yield streams
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIMEPAHE-ERROR]: {e}")
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import Literal, TypedDict
|
from typing import Literal, TypedDict
|
||||||
|
|
||||||
|
|
||||||
class AnimeSearchResult(TypedDict):
|
class AnimePaheSearchResult(TypedDict):
|
||||||
id: int
|
id: int
|
||||||
title: str
|
title: str
|
||||||
type: str
|
type: str
|
||||||
@@ -21,7 +21,7 @@ class AnimePaheSearchPage(TypedDict):
|
|||||||
last_page: int
|
last_page: int
|
||||||
_from: int
|
_from: int
|
||||||
to: int
|
to: int
|
||||||
data: list[AnimeSearchResult]
|
data: list[AnimePaheSearchResult]
|
||||||
|
|
||||||
|
|
||||||
class Episode(TypedDict):
|
class Episode(TypedDict):
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
from html.parser import HTMLParser
|
|
||||||
from itertools import cycle
|
|
||||||
from urllib.parse import quote_plus
|
|
||||||
|
|
||||||
from yt_dlp.utils import (
|
|
||||||
clean_html,
|
|
||||||
extract_attributes,
|
|
||||||
get_element_by_class,
|
|
||||||
get_element_html_by_class,
|
|
||||||
get_elements_by_class,
|
|
||||||
get_elements_html_by_class,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ..base_provider import AnimeProvider
|
|
||||||
from ..utils import give_random_quality
|
|
||||||
from .constants import SERVERS_AVAILABLE
|
|
||||||
from .types import AniWatchStream
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
LINK_TO_STREAMS_REGEX = re.compile(r".*://(.*)/embed-(2|4|6)/e-([0-9])/(.*)\?.*")
|
|
||||||
IMAGE_HTML_ELEMENT_REGEX = re.compile(r"<img.*?>")
|
|
||||||
|
|
||||||
|
|
||||||
class ParseAnchorAndImgTag(HTMLParser):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.img_tag = None
|
|
||||||
self.a_tag = None
|
|
||||||
|
|
||||||
def handle_starttag(self, tag, attrs):
|
|
||||||
if tag == "img":
|
|
||||||
self.img_tag = {attr[0]: attr[1] for attr in attrs}
|
|
||||||
if tag == "a":
|
|
||||||
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
|
||||||
|
|
||||||
|
|
||||||
class AniWatchApi(AnimeProvider):
|
|
||||||
# HEADERS = {"Referer": "https://hianime.to/home"}
|
|
||||||
|
|
||||||
def search_for_anime(self, anime_title: str, *args):
|
|
||||||
try:
|
|
||||||
query = quote_plus(anime_title)
|
|
||||||
url = f"https://hianime.to/search?keyword={query}"
|
|
||||||
response = self.session.get(url)
|
|
||||||
if not response.ok:
|
|
||||||
return
|
|
||||||
search_page = response.text
|
|
||||||
search_results_html_items = get_elements_by_class("flw-item", search_page)
|
|
||||||
results = []
|
|
||||||
for search_results_html_item in search_results_html_items:
|
|
||||||
film_poster_html = get_element_by_class(
|
|
||||||
"film-poster", search_results_html_item
|
|
||||||
)
|
|
||||||
|
|
||||||
if not film_poster_html:
|
|
||||||
continue
|
|
||||||
# get availableEpisodes
|
|
||||||
episodes_html = get_element_html_by_class("tick-sub", film_poster_html)
|
|
||||||
episodes = clean_html(episodes_html) or 12
|
|
||||||
|
|
||||||
# get anime id and poster image url
|
|
||||||
parser = ParseAnchorAndImgTag()
|
|
||||||
parser.feed(film_poster_html)
|
|
||||||
image_data = parser.img_tag
|
|
||||||
anime_link_data = parser.a_tag
|
|
||||||
if not image_data or not anime_link_data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
episodes = int(episodes)
|
|
||||||
|
|
||||||
# finally!!
|
|
||||||
image_link = image_data["data-src"]
|
|
||||||
anime_id = anime_link_data["data-id"]
|
|
||||||
title = anime_link_data["title"]
|
|
||||||
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"availableEpisodes": list(range(1, episodes)),
|
|
||||||
"id": anime_id,
|
|
||||||
"title": title,
|
|
||||||
"poster": image_link,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.search_results = results
|
|
||||||
return {"pageInfo": {}, "results": results}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIWATCH-ERROR]: {e}")
|
|
||||||
|
|
||||||
def get_anime(self, aniwatch_id, *args):
|
|
||||||
try:
|
|
||||||
anime_result = {}
|
|
||||||
for anime in self.search_results:
|
|
||||||
if anime["id"] == aniwatch_id:
|
|
||||||
anime_result = anime
|
|
||||||
break
|
|
||||||
anime_url = f"https://hianime.to/ajax/v2/episode/list/{aniwatch_id}"
|
|
||||||
response = self.session.get(anime_url, timeout=10)
|
|
||||||
if response.ok:
|
|
||||||
response_json = response.json()
|
|
||||||
aniwatch_anime_page = response_json["html"]
|
|
||||||
episodes_info_container_html = get_element_html_by_class(
|
|
||||||
"ss-list", aniwatch_anime_page
|
|
||||||
)
|
|
||||||
episodes_info_html_list = get_elements_html_by_class(
|
|
||||||
"ep-item", episodes_info_container_html
|
|
||||||
)
|
|
||||||
# keys: [ data-number: episode_number, data-id: episode_id, title: episode_title , href:episode_page_url]
|
|
||||||
episodes_info_dicts = [
|
|
||||||
extract_attributes(episode_dict)
|
|
||||||
for episode_dict in episodes_info_html_list
|
|
||||||
]
|
|
||||||
episodes = [episode["data-number"] for episode in episodes_info_dicts]
|
|
||||||
self.episodes_info = [
|
|
||||||
{
|
|
||||||
"id": episode["data-id"],
|
|
||||||
"title": (
|
|
||||||
(episode["title"] or "").replace(
|
|
||||||
f"Episode {episode['data-number']}", ""
|
|
||||||
)
|
|
||||||
or anime_result["title"]
|
|
||||||
)
|
|
||||||
+ f"; Episode {episode['data-number']}",
|
|
||||||
"episode": episode["data-number"],
|
|
||||||
}
|
|
||||||
for episode in episodes_info_dicts
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
"id": aniwatch_id,
|
|
||||||
"availableEpisodesDetail": {
|
|
||||||
"dub": episodes,
|
|
||||||
"sub": episodes,
|
|
||||||
"raw": episodes,
|
|
||||||
},
|
|
||||||
"poster": anime_result["poster"],
|
|
||||||
"title": anime_result["title"],
|
|
||||||
"episodes_info": self.episodes_info,
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIWACTCH-ERROR]: {e}")
|
|
||||||
|
|
||||||
def get_episode_streams(self, anime, episode, translation_type, *args):
|
|
||||||
try:
|
|
||||||
episode_details = [
|
|
||||||
episode_details
|
|
||||||
for episode_details in self.episodes_info
|
|
||||||
if episode_details["episode"] == episode
|
|
||||||
]
|
|
||||||
if not episode_details:
|
|
||||||
return
|
|
||||||
episode_details = episode_details[0]
|
|
||||||
episode_url = f"https://hianime.to/ajax/v2/episode/servers?episodeId={episode_details['id']}"
|
|
||||||
response = self.session.get(episode_url)
|
|
||||||
if response.ok:
|
|
||||||
response_json = response.json()
|
|
||||||
episode_page_html = response_json["html"]
|
|
||||||
servers_containers_html = get_elements_html_by_class(
|
|
||||||
"ps__-list", episode_page_html
|
|
||||||
)
|
|
||||||
if not servers_containers_html:
|
|
||||||
return
|
|
||||||
# sub servers
|
|
||||||
try:
|
|
||||||
servers_html_sub = get_elements_html_by_class(
|
|
||||||
"server-item", servers_containers_html[0]
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.warn("AniWatch: sub not found")
|
|
||||||
servers_html_sub = None
|
|
||||||
|
|
||||||
# dub servers
|
|
||||||
try:
|
|
||||||
servers_html_dub = get_elements_html_by_class(
|
|
||||||
"server-item", servers_containers_html[1]
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.warn("AniWatch: dub not found")
|
|
||||||
servers_html_dub = None
|
|
||||||
|
|
||||||
if translation_type == "dub":
|
|
||||||
servers_html = servers_html_dub
|
|
||||||
else:
|
|
||||||
servers_html = servers_html_sub
|
|
||||||
if not servers_html:
|
|
||||||
return
|
|
||||||
for server_name, server_html in zip(
|
|
||||||
cycle(SERVERS_AVAILABLE), servers_html
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
# keys: [ data-type: translation_type, data-id: embed_id, data-server-id: server_id ]
|
|
||||||
servers_info = extract_attributes(server_html)
|
|
||||||
embed_url = f"https://hianime.to/ajax/v2/episode/sources?id={servers_info['data-id']}"
|
|
||||||
embed_response = self.session.get(embed_url)
|
|
||||||
if embed_response.ok:
|
|
||||||
embed_json = embed_response.json()
|
|
||||||
raw_link_to_streams = embed_json["link"]
|
|
||||||
match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams)
|
|
||||||
if not match:
|
|
||||||
continue
|
|
||||||
provider_domain = match.group(1)
|
|
||||||
embed_type = match.group(2)
|
|
||||||
episode_number = match.group(3)
|
|
||||||
source_id = match.group(4)
|
|
||||||
|
|
||||||
link_to_streams = f"https://{provider_domain}/embed-{embed_type}/ajax/e-{episode_number}/getSources?id={source_id}"
|
|
||||||
link_to_streams_response = self.session.get(link_to_streams)
|
|
||||||
if link_to_streams_response.ok:
|
|
||||||
juicy_streams_json: "AniWatchStream" = (
|
|
||||||
link_to_streams_response.json()
|
|
||||||
)
|
|
||||||
yield {
|
|
||||||
"headers": {},
|
|
||||||
"subtitles": [
|
|
||||||
{
|
|
||||||
"url": track["file"],
|
|
||||||
"language": track["label"],
|
|
||||||
}
|
|
||||||
for track in juicy_streams_json["tracks"]
|
|
||||||
if track["kind"] == "captions"
|
|
||||||
],
|
|
||||||
"server": server_name,
|
|
||||||
"episode_title": episode_details["title"],
|
|
||||||
"links": give_random_quality(
|
|
||||||
[
|
|
||||||
{"link": link["file"], "type": link["type"]}
|
|
||||||
for link in juicy_streams_json["sources"]
|
|
||||||
]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIWATCH_ERROR]: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[ANIWATCH_ERROR]: {e}")
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from typing import Literal, TypedDict
|
|
||||||
|
|
||||||
|
|
||||||
class AniWatchSkipTime(TypedDict):
|
|
||||||
start: int
|
|
||||||
end: int
|
|
||||||
|
|
||||||
|
|
||||||
class AniWatchSource(TypedDict):
|
|
||||||
file: str
|
|
||||||
type: str
|
|
||||||
|
|
||||||
|
|
||||||
class AniWatchTrack(TypedDict):
|
|
||||||
file: str
|
|
||||||
label: str
|
|
||||||
kind: Literal["captions", "thumbnails", "audio"]
|
|
||||||
|
|
||||||
|
|
||||||
class AniWatchStream(TypedDict):
|
|
||||||
sources: list[AniWatchSource]
|
|
||||||
tracks: list[AniWatchTrack]
|
|
||||||
encrypted: bool
|
|
||||||
intro: AniWatchSkipTime
|
|
||||||
outro: AniWatchSkipTime
|
|
||||||
server: int
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
from html.parser import HTMLParser
|
|
||||||
|
|
||||||
from yt_dlp.utils import clean_html, get_element_by_class, get_elements_by_class
|
|
||||||
|
|
||||||
from ..base_provider import AnimeProvider
|
|
||||||
from .constants import ANIWAVE_BASE, SEARCH_HEADERS
|
|
||||||
|
|
||||||
|
|
||||||
class ParseAnchorAndImgTag(HTMLParser):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.img_tag = None
|
|
||||||
self.a_tag = None
|
|
||||||
|
|
||||||
def handle_starttag(self, tag, attrs):
|
|
||||||
if tag == "img":
|
|
||||||
self.img_tag = {attr[0]: attr[1] for attr in attrs}
|
|
||||||
if tag == "a":
|
|
||||||
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
|
||||||
|
|
||||||
|
|
||||||
class AniWaveApi(AnimeProvider):
|
|
||||||
def search_for_anime(self, anime_title, *args):
|
|
||||||
self.session.headers.update(SEARCH_HEADERS)
|
|
||||||
search_url = f"{ANIWAVE_BASE}/filter"
|
|
||||||
params = {"keyword": anime_title}
|
|
||||||
res = self.session.get(search_url, params=params)
|
|
||||||
search_page = res.text
|
|
||||||
search_results_html_list = get_elements_by_class("item", search_page)
|
|
||||||
results = []
|
|
||||||
for result_html in search_results_html_list:
|
|
||||||
aniposter_html = get_element_by_class("poster", result_html)
|
|
||||||
episode_html = get_element_by_class("sub", aniposter_html)
|
|
||||||
episodes = clean_html(episode_html) or 12
|
|
||||||
if not aniposter_html:
|
|
||||||
return
|
|
||||||
parser = ParseAnchorAndImgTag()
|
|
||||||
parser.feed(aniposter_html)
|
|
||||||
image_data = parser.img_tag
|
|
||||||
anime_link_data = parser.a_tag
|
|
||||||
if not image_data or not anime_link_data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
episodes = int(episodes)
|
|
||||||
|
|
||||||
# finally!!
|
|
||||||
image_link = image_data["src"]
|
|
||||||
title = image_data["alt"]
|
|
||||||
anime_id = anime_link_data["href"]
|
|
||||||
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"availableEpisodes": list(range(1, episodes)),
|
|
||||||
"id": anime_id,
|
|
||||||
"title": title,
|
|
||||||
"poster": image_link,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.search_results = results
|
|
||||||
return {"pageInfo": {}, "results": results}
|
|
||||||
|
|
||||||
def get_anime(self, anime_id, *args):
|
|
||||||
anime_page_url = f"{ANIWAVE_BASE}{anime_id}"
|
|
||||||
self.session.get(anime_page_url)
|
|
||||||
# TODO: to be continued; mostly js so very difficult
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
ANIWAVE_BASE = "https://aniwave.to"
|
|
||||||
|
|
||||||
SEARCH_HEADERS = {
|
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8",
|
|
||||||
"Accept-Language": "en-US,en;q=0.5",
|
|
||||||
# 'Accept-Encoding': 'Utf-8',
|
|
||||||
"Referer": "https://aniwave.to/filter",
|
|
||||||
"DNT": "1",
|
|
||||||
"Upgrade-Insecure-Requests": "1",
|
|
||||||
"Sec-Fetch-Dest": "document",
|
|
||||||
"Sec-Fetch-Mode": "navigate",
|
|
||||||
"Sec-Fetch-Site": "same-origin",
|
|
||||||
"Sec-Fetch-User": "?1",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
"Alt-Used": "aniwave.to",
|
|
||||||
# 'Cookie': '__pf=1; usertype=guest; session=BElk9DJdO3sFdDmLiGxuNiM9eGYO1TjktGsmdwjV',
|
|
||||||
"Priority": "u=0, i",
|
|
||||||
# Requests doesn't support trailers
|
|
||||||
# 'TE': 'trailers',
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,34 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from yt_dlp.utils.networking import random_user_agent
|
from yt_dlp.utils.networking import random_user_agent
|
||||||
|
|
||||||
|
from ...constants import APP_CACHE_DIR
|
||||||
|
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 = {}
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, cache_requests, use_persistent_provider_store) -> None:
|
||||||
self.session = requests.session()
|
if cache_requests.lower() == "true":
|
||||||
|
from ..common.requests_cacher import CachedRequestsSession
|
||||||
|
|
||||||
|
self.session = CachedRequestsSession(
|
||||||
|
os.path.join(APP_CACHE_DIR, "cached_requests.db")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.session = requests.session()
|
||||||
self.session.headers.update({"User-Agent": self.USER_AGENT, **self.HEADERS})
|
self.session.headers.update({"User-Agent": self.USER_AGENT, **self.HEADERS})
|
||||||
|
if use_persistent_provider_store.lower() == "true":
|
||||||
|
self.store = ProviderStore(
|
||||||
|
"persistent",
|
||||||
|
self.PROVIDER,
|
||||||
|
os.path.join(APP_CACHE_DIR, "anime_providers_store.db"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.store = ProviderStore("memory")
|
||||||
|
|||||||
39
fastanime/libs/anime_provider/decorators.py
Normal file
39
fastanime/libs/anime_provider/decorators.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def debug_provider(provider_name: str):
|
||||||
|
def _provider_function_decorator(provider_function):
|
||||||
|
@functools.wraps(provider_function)
|
||||||
|
def _provider_function_wrapper(*args, **kwargs):
|
||||||
|
if not os.environ.get("FASTANIME_DEBUG"):
|
||||||
|
try:
|
||||||
|
return provider_function(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[{provider_name}@{provider_function.__name__}]: {e}")
|
||||||
|
else:
|
||||||
|
return provider_function(*args, **kwargs)
|
||||||
|
|
||||||
|
return _provider_function_wrapper
|
||||||
|
|
||||||
|
return _provider_function_decorator
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_internet_connection(provider_function):
|
||||||
|
@functools.wraps(provider_function)
|
||||||
|
def _wrapper(*args, **kwargs):
|
||||||
|
import requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
requests.get("https://google.com", timeout=5)
|
||||||
|
except requests.ConnectionError:
|
||||||
|
from sys import exit
|
||||||
|
|
||||||
|
print("You are not connected to the internet;Aborting...")
|
||||||
|
exit(1)
|
||||||
|
return provider_function(*args, **kwargs)
|
||||||
|
|
||||||
|
return _wrapper
|
||||||
243
fastanime/libs/anime_provider/hianime/api.py
Normal file
243
fastanime/libs/anime_provider/hianime/api.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
from itertools import cycle
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
|
from yt_dlp.utils import (
|
||||||
|
clean_html,
|
||||||
|
extract_attributes,
|
||||||
|
get_element_by_class,
|
||||||
|
get_element_html_by_class,
|
||||||
|
get_elements_by_class,
|
||||||
|
get_elements_html_by_class,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..base_provider import AnimeProvider
|
||||||
|
from ..decorators import debug_provider
|
||||||
|
from ..utils import give_random_quality
|
||||||
|
from .constants import SERVERS_AVAILABLE
|
||||||
|
from .types import HiAnimeStream
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
LINK_TO_STREAMS_REGEX = re.compile(r".*://(.*)/embed-(2|4|6)/e-([0-9])/(.*)\?.*")
|
||||||
|
IMAGE_HTML_ELEMENT_REGEX = re.compile(r"<img.*?>")
|
||||||
|
|
||||||
|
|
||||||
|
class ParseAnchorAndImgTag(HTMLParser):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.img_tag = None
|
||||||
|
self.a_tag = None
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
if tag == "img":
|
||||||
|
self.img_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
if tag == "a":
|
||||||
|
self.a_tag = {attr[0]: attr[1] for attr in attrs}
|
||||||
|
|
||||||
|
|
||||||
|
class HiAnimeApi(AnimeProvider):
|
||||||
|
# HEADERS = {"Referer": "https://hianime.to/home"}
|
||||||
|
|
||||||
|
PROVIDER = "hianime"
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def search_for_anime(self, anime_title: str, *args):
|
||||||
|
query = quote_plus(anime_title)
|
||||||
|
url = f"https://hianime.to/search?keyword={query}"
|
||||||
|
response = self.session.get(url)
|
||||||
|
if not response.ok:
|
||||||
|
return
|
||||||
|
search_page = response.text
|
||||||
|
search_results_html_items = get_elements_by_class("flw-item", search_page)
|
||||||
|
results = []
|
||||||
|
for search_results_html_item in search_results_html_items:
|
||||||
|
film_poster_html = get_element_by_class(
|
||||||
|
"film-poster", search_results_html_item
|
||||||
|
)
|
||||||
|
|
||||||
|
if not film_poster_html:
|
||||||
|
continue
|
||||||
|
# get availableEpisodes
|
||||||
|
episodes_html = get_element_html_by_class("tick-sub", film_poster_html)
|
||||||
|
episodes = clean_html(episodes_html) or 12
|
||||||
|
|
||||||
|
# get anime id and poster image url
|
||||||
|
parser = ParseAnchorAndImgTag()
|
||||||
|
parser.feed(film_poster_html)
|
||||||
|
image_data = parser.img_tag
|
||||||
|
anime_link_data = parser.a_tag
|
||||||
|
if not image_data or not anime_link_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
episodes = int(episodes)
|
||||||
|
|
||||||
|
# finally!!
|
||||||
|
image_link = image_data["data-src"]
|
||||||
|
anime_id = anime_link_data["data-id"]
|
||||||
|
title = anime_link_data["title"]
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"availableEpisodes": list(range(1, episodes)),
|
||||||
|
"id": anime_id,
|
||||||
|
"title": title,
|
||||||
|
"poster": image_link,
|
||||||
|
}
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
self.store.set(result["id"], "search_result", result)
|
||||||
|
return {"pageInfo": {}, "results": results}
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def get_anime(self, hianime_id, *args):
|
||||||
|
anime_result = {}
|
||||||
|
if d := self.store.get(str(hianime_id), "search_result"):
|
||||||
|
anime_result = d
|
||||||
|
anime_url = f"https://hianime.to/ajax/v2/episode/list/{hianime_id}"
|
||||||
|
response = self.session.get(anime_url, timeout=10)
|
||||||
|
if response.ok:
|
||||||
|
response_json = response.json()
|
||||||
|
hianime_anime_page = response_json["html"]
|
||||||
|
episodes_info_container_html = get_element_html_by_class(
|
||||||
|
"ss-list", hianime_anime_page
|
||||||
|
)
|
||||||
|
episodes_info_html_list = get_elements_html_by_class(
|
||||||
|
"ep-item", episodes_info_container_html
|
||||||
|
)
|
||||||
|
# keys: [ data-number: episode_number, data-id: episode_id, title: episode_title , href:episode_page_url]
|
||||||
|
episodes_info_dicts = [
|
||||||
|
extract_attributes(episode_dict)
|
||||||
|
for episode_dict in episodes_info_html_list
|
||||||
|
]
|
||||||
|
episodes = [episode["data-number"] for episode in episodes_info_dicts]
|
||||||
|
episodes_info = [
|
||||||
|
{
|
||||||
|
"id": episode["data-id"],
|
||||||
|
"title": (
|
||||||
|
(episode["title"] or "").replace(
|
||||||
|
f"Episode {episode['data-number']}", ""
|
||||||
|
)
|
||||||
|
or anime_result["title"]
|
||||||
|
)
|
||||||
|
+ f"; Episode {episode['data-number']}",
|
||||||
|
"episode": episode["data-number"],
|
||||||
|
}
|
||||||
|
for episode in episodes_info_dicts
|
||||||
|
]
|
||||||
|
self.store.set(
|
||||||
|
str(hianime_id),
|
||||||
|
"anime_info",
|
||||||
|
episodes_info,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"id": hianime_id,
|
||||||
|
"availableEpisodesDetail": {
|
||||||
|
"dub": episodes,
|
||||||
|
"sub": episodes,
|
||||||
|
"raw": episodes,
|
||||||
|
},
|
||||||
|
"poster": anime_result["poster"],
|
||||||
|
"title": anime_result["title"],
|
||||||
|
"episodes_info": episodes_info,
|
||||||
|
}
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def get_episode_streams(self, anime_id, episode, translation_type, *args):
|
||||||
|
if d := self.store.get(str(anime_id), "anime_info"):
|
||||||
|
episodes_info = d
|
||||||
|
episode_details = [
|
||||||
|
episode_details
|
||||||
|
for episode_details in episodes_info
|
||||||
|
if episode_details["episode"] == episode
|
||||||
|
]
|
||||||
|
if not episode_details:
|
||||||
|
return
|
||||||
|
episode_details = episode_details[0]
|
||||||
|
episode_url = f"https://hianime.to/ajax/v2/episode/servers?episodeId={episode_details['id']}"
|
||||||
|
response = self.session.get(episode_url)
|
||||||
|
if response.ok:
|
||||||
|
response_json = response.json()
|
||||||
|
episode_page_html = response_json["html"]
|
||||||
|
servers_containers_html = get_elements_html_by_class(
|
||||||
|
"ps__-list", episode_page_html
|
||||||
|
)
|
||||||
|
if not servers_containers_html:
|
||||||
|
return
|
||||||
|
# sub servers
|
||||||
|
try:
|
||||||
|
servers_html_sub = get_elements_html_by_class(
|
||||||
|
"server-item", servers_containers_html[0]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("HiAnime: sub not found")
|
||||||
|
servers_html_sub = None
|
||||||
|
|
||||||
|
# dub servers
|
||||||
|
try:
|
||||||
|
servers_html_dub = get_elements_html_by_class(
|
||||||
|
"server-item", servers_containers_html[1]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("HiAnime: dub not found")
|
||||||
|
servers_html_dub = None
|
||||||
|
|
||||||
|
if translation_type == "dub":
|
||||||
|
servers_html = servers_html_dub
|
||||||
|
else:
|
||||||
|
servers_html = servers_html_sub
|
||||||
|
if not servers_html:
|
||||||
|
return
|
||||||
|
|
||||||
|
@debug_provider(self.PROVIDER.upper())
|
||||||
|
def _get_server(server_name, server_html):
|
||||||
|
# keys: [ data-type: translation_type, data-id: embed_id, data-server-id: server_id ]
|
||||||
|
servers_info = extract_attributes(server_html)
|
||||||
|
embed_url = f"https://hianime.to/ajax/v2/episode/sources?id={servers_info['data-id']}"
|
||||||
|
embed_response = self.session.get(embed_url)
|
||||||
|
if embed_response.ok:
|
||||||
|
embed_json = embed_response.json()
|
||||||
|
raw_link_to_streams = embed_json["link"]
|
||||||
|
match = LINK_TO_STREAMS_REGEX.match(raw_link_to_streams)
|
||||||
|
if not match:
|
||||||
|
return
|
||||||
|
provider_domain = match.group(1)
|
||||||
|
embed_type = match.group(2)
|
||||||
|
episode_number = match.group(3)
|
||||||
|
source_id = match.group(4)
|
||||||
|
|
||||||
|
link_to_streams = f"https://{provider_domain}/embed-{embed_type}/ajax/e-{episode_number}/getSources?id={source_id}"
|
||||||
|
link_to_streams_response = self.session.get(link_to_streams)
|
||||||
|
if link_to_streams_response.ok:
|
||||||
|
juicy_streams_json: "HiAnimeStream" = (
|
||||||
|
link_to_streams_response.json()
|
||||||
|
)
|
||||||
|
# TODO: Hianime decided to fucking encrypt shit
|
||||||
|
# so got to fix it later
|
||||||
|
return {
|
||||||
|
"headers": {},
|
||||||
|
"subtitles": [
|
||||||
|
{
|
||||||
|
"url": track["file"],
|
||||||
|
"language": track["label"],
|
||||||
|
}
|
||||||
|
for track in juicy_streams_json["tracks"]
|
||||||
|
if track["kind"] == "captions"
|
||||||
|
],
|
||||||
|
"server": server_name,
|
||||||
|
"episode_title": episode_details["title"],
|
||||||
|
"links": give_random_quality(
|
||||||
|
[
|
||||||
|
{"link": link["file"]}
|
||||||
|
for link in juicy_streams_json["tracks"]
|
||||||
|
]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
for server_name, server_html in zip(
|
||||||
|
cycle(SERVERS_AVAILABLE), servers_html
|
||||||
|
):
|
||||||
|
if server := _get_server(server_name, server_html):
|
||||||
|
yield server
|
||||||
26
fastanime/libs/anime_provider/hianime/types.py
Normal file
26
fastanime/libs/anime_provider/hianime/types.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from typing import Literal, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class HiAnimeSkipTime(TypedDict):
|
||||||
|
start: int
|
||||||
|
end: int
|
||||||
|
|
||||||
|
|
||||||
|
class HiAnimeSource(TypedDict):
|
||||||
|
file: str
|
||||||
|
type: str
|
||||||
|
|
||||||
|
|
||||||
|
class HiAnimeTrack(TypedDict):
|
||||||
|
file: str
|
||||||
|
label: str
|
||||||
|
kind: Literal["captions", "thumbnails", "audio"]
|
||||||
|
|
||||||
|
|
||||||
|
class HiAnimeStream(TypedDict):
|
||||||
|
sources: list[HiAnimeSource]
|
||||||
|
tracks: list[HiAnimeTrack]
|
||||||
|
encrypted: bool
|
||||||
|
intro: HiAnimeSkipTime
|
||||||
|
outro: HiAnimeSkipTime
|
||||||
|
server: int
|
||||||
345
fastanime/libs/anime_provider/nyaa/api.py
Normal file
345
fastanime/libs/anime_provider/nyaa/api.py
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from yt_dlp.utils import (
|
||||||
|
extract_attributes,
|
||||||
|
get_element_html_by_attribute,
|
||||||
|
get_element_html_by_class,
|
||||||
|
get_element_text_and_html_by_tag,
|
||||||
|
get_elements_html_by_class,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ...common.mini_anilist import search_for_anime_with_anilist
|
||||||
|
from ..base_provider import AnimeProvider
|
||||||
|
from ..decorators import debug_provider
|
||||||
|
from ..types import SearchResults
|
||||||
|
from .constants import NYAA_ENDPOINT
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
EXTRACT_USEFUL_INFO_PATTERN_1 = re.compile(
|
||||||
|
r"\[(\w+)\] (.+) - (\d+) [\[\(](\d+)p[\]\)].*"
|
||||||
|
)
|
||||||
|
|
||||||
|
EXTRACT_USEFUL_INFO_PATTERN_2 = re.compile(
|
||||||
|
r"\[(\w+)\] (.+)E(\d+) [\[\(]?(\d+)p.*[\]\)]?.*"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NyaaApi(AnimeProvider):
|
||||||
|
search_results: SearchResults
|
||||||
|
PROVIDER = "nyaa"
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def search_for_anime(self, user_query: str, *args, **_):
|
||||||
|
self.search_results = search_for_anime_with_anilist(
|
||||||
|
user_query, True
|
||||||
|
) # pyright: ignore
|
||||||
|
self.user_query = user_query
|
||||||
|
return self.search_results
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def get_anime(self, anilist_id: str, *_):
|
||||||
|
for anime in self.search_results["results"]:
|
||||||
|
if anime["id"] == anilist_id:
|
||||||
|
self.titles = [anime["title"], *anime["otherTitles"], self.user_query]
|
||||||
|
return {
|
||||||
|
"id": anime["id"],
|
||||||
|
"title": anime["title"],
|
||||||
|
"poster": anime["poster"],
|
||||||
|
"availableEpisodesDetail": {
|
||||||
|
"dub": anime["availableEpisodes"],
|
||||||
|
"sub": anime["availableEpisodes"],
|
||||||
|
"raw": anime["availableEpisodes"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def get_episode_streams(
|
||||||
|
self,
|
||||||
|
anime_id: str,
|
||||||
|
episode_number: str,
|
||||||
|
translation_type: str,
|
||||||
|
trusted_only=bool(int(os.environ.get("FA_NYAA_TRUSTED_ONLY", "0"))),
|
||||||
|
allow_dangerous=bool(int(os.environ.get("FA_NYAA_ALLOW_DANGEROUS", "0"))),
|
||||||
|
sort_by="seeders",
|
||||||
|
*args,
|
||||||
|
):
|
||||||
|
anime_title = self.titles[0]
|
||||||
|
logger.debug(f"Searching nyaa for query: '{anime_title} {episode_number}'")
|
||||||
|
servers = {}
|
||||||
|
|
||||||
|
torrents_table = ""
|
||||||
|
for title in self.titles:
|
||||||
|
try:
|
||||||
|
url_arguments: dict[str, str] = {
|
||||||
|
"c": "1_2", # Language (English)
|
||||||
|
"q": f"{title} {'0' if len(episode_number)==1 else ''}{episode_number}", # Search Query
|
||||||
|
}
|
||||||
|
# url_arguments["q"] = anime_title
|
||||||
|
|
||||||
|
# if trusted_only:
|
||||||
|
# url_arguments["f"] = "2" # Trusted uploaders only
|
||||||
|
|
||||||
|
# What to sort torrents by
|
||||||
|
if sort_by == "seeders":
|
||||||
|
url_arguments["s"] = "seeders"
|
||||||
|
elif sort_by == "date":
|
||||||
|
url_arguments["s"] = "id"
|
||||||
|
elif sort_by == "size":
|
||||||
|
url_arguments["s"] = "size"
|
||||||
|
elif sort_by == "comments":
|
||||||
|
url_arguments["s"] = "comments"
|
||||||
|
|
||||||
|
logger.debug(f"URL Arguments: {url_arguments}")
|
||||||
|
|
||||||
|
response = self.session.get(NYAA_ENDPOINT, params=url_arguments)
|
||||||
|
if not response.ok:
|
||||||
|
logger.error(f"[NYAA]: {response.text}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
torrents_table = get_element_text_and_html_by_tag(
|
||||||
|
"table", response.text
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[NYAA]: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not torrents_table:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for anime_torrent in get_elements_html_by_class(
|
||||||
|
"success", torrents_table[1]
|
||||||
|
):
|
||||||
|
td_title = get_element_html_by_attribute(
|
||||||
|
"colspan", "2", anime_torrent
|
||||||
|
)
|
||||||
|
if not td_title:
|
||||||
|
continue
|
||||||
|
title_anchor_tag = get_element_text_and_html_by_tag("a", td_title)
|
||||||
|
|
||||||
|
if not title_anchor_tag:
|
||||||
|
continue
|
||||||
|
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||||
|
if not title_anchor_tag_attrs:
|
||||||
|
continue
|
||||||
|
if "class" in title_anchor_tag_attrs:
|
||||||
|
td_title = td_title.replace(title_anchor_tag[1], "")
|
||||||
|
title_anchor_tag = get_element_text_and_html_by_tag(
|
||||||
|
"a", td_title
|
||||||
|
)
|
||||||
|
|
||||||
|
if not title_anchor_tag:
|
||||||
|
continue
|
||||||
|
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||||
|
if not title_anchor_tag_attrs:
|
||||||
|
continue
|
||||||
|
anime_title_info = title_anchor_tag_attrs["title"]
|
||||||
|
if not anime_title_info:
|
||||||
|
continue
|
||||||
|
match = EXTRACT_USEFUL_INFO_PATTERN_1.search(
|
||||||
|
anime_title_info.strip()
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
server = match[1]
|
||||||
|
match[2]
|
||||||
|
_episode_number = match[3]
|
||||||
|
quality = match[4]
|
||||||
|
if float(episode_number) != float(_episode_number):
|
||||||
|
continue
|
||||||
|
|
||||||
|
links_td = get_element_html_by_class("text-center", anime_torrent)
|
||||||
|
if not links_td:
|
||||||
|
continue
|
||||||
|
torrent_anchor_tag = get_element_text_and_html_by_tag("a", links_td)
|
||||||
|
if not torrent_anchor_tag:
|
||||||
|
continue
|
||||||
|
torrent_anchor_tag_atrrs = extract_attributes(torrent_anchor_tag[1])
|
||||||
|
if not torrent_anchor_tag_atrrs:
|
||||||
|
continue
|
||||||
|
torrent_file_url = (
|
||||||
|
f'{NYAA_ENDPOINT}{torrent_anchor_tag_atrrs["href"]}'
|
||||||
|
)
|
||||||
|
if server in servers:
|
||||||
|
link = {
|
||||||
|
"translation_type": "sub",
|
||||||
|
"link": torrent_file_url,
|
||||||
|
"quality": quality,
|
||||||
|
}
|
||||||
|
if link not in servers[server]["links"]:
|
||||||
|
servers[server]["links"].append(link)
|
||||||
|
else:
|
||||||
|
servers[server] = {
|
||||||
|
"server": server,
|
||||||
|
"headers": {},
|
||||||
|
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||||
|
"subtitles": [],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"translation_type": "sub",
|
||||||
|
"link": torrent_file_url,
|
||||||
|
"quality": quality,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
for anime_torrent in get_elements_html_by_class(
|
||||||
|
"default", torrents_table[1]
|
||||||
|
):
|
||||||
|
td_title = get_element_html_by_attribute(
|
||||||
|
"colspan", "2", anime_torrent
|
||||||
|
)
|
||||||
|
if not td_title:
|
||||||
|
continue
|
||||||
|
title_anchor_tag = get_element_text_and_html_by_tag("a", td_title)
|
||||||
|
|
||||||
|
if not title_anchor_tag:
|
||||||
|
continue
|
||||||
|
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||||
|
if not title_anchor_tag_attrs:
|
||||||
|
continue
|
||||||
|
if "class" in title_anchor_tag_attrs:
|
||||||
|
td_title = td_title.replace(title_anchor_tag[1], "")
|
||||||
|
title_anchor_tag = get_element_text_and_html_by_tag(
|
||||||
|
"a", td_title
|
||||||
|
)
|
||||||
|
|
||||||
|
if not title_anchor_tag:
|
||||||
|
continue
|
||||||
|
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||||
|
if not title_anchor_tag_attrs:
|
||||||
|
continue
|
||||||
|
anime_title_info = title_anchor_tag_attrs["title"]
|
||||||
|
if not anime_title_info:
|
||||||
|
continue
|
||||||
|
match = EXTRACT_USEFUL_INFO_PATTERN_2.search(
|
||||||
|
anime_title_info.strip()
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
server = match[1]
|
||||||
|
match[2]
|
||||||
|
_episode_number = match[3]
|
||||||
|
quality = match[4]
|
||||||
|
if float(episode_number) != float(_episode_number):
|
||||||
|
continue
|
||||||
|
|
||||||
|
links_td = get_element_html_by_class("text-center", anime_torrent)
|
||||||
|
if not links_td:
|
||||||
|
continue
|
||||||
|
torrent_anchor_tag = get_element_text_and_html_by_tag("a", links_td)
|
||||||
|
if not torrent_anchor_tag:
|
||||||
|
continue
|
||||||
|
torrent_anchor_tag_atrrs = extract_attributes(torrent_anchor_tag[1])
|
||||||
|
if not torrent_anchor_tag_atrrs:
|
||||||
|
continue
|
||||||
|
torrent_file_url = (
|
||||||
|
f'{NYAA_ENDPOINT}{torrent_anchor_tag_atrrs["href"]}'
|
||||||
|
)
|
||||||
|
if server in servers:
|
||||||
|
link = {
|
||||||
|
"translation_type": "sub",
|
||||||
|
"link": torrent_file_url,
|
||||||
|
"quality": quality,
|
||||||
|
}
|
||||||
|
if link not in servers[server]["links"]:
|
||||||
|
servers[server]["links"].append(link)
|
||||||
|
else:
|
||||||
|
servers[server] = {
|
||||||
|
"server": server,
|
||||||
|
"headers": {},
|
||||||
|
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||||
|
"subtitles": [],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"translation_type": "sub",
|
||||||
|
"link": torrent_file_url,
|
||||||
|
"quality": quality,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if not allow_dangerous:
|
||||||
|
break
|
||||||
|
for anime_torrent in get_elements_html_by_class(
|
||||||
|
"danger", torrents_table[1]
|
||||||
|
):
|
||||||
|
td_title = get_element_html_by_attribute(
|
||||||
|
"colspan", "2", anime_torrent
|
||||||
|
)
|
||||||
|
if not td_title:
|
||||||
|
continue
|
||||||
|
title_anchor_tag = get_element_text_and_html_by_tag("a", td_title)
|
||||||
|
|
||||||
|
if not title_anchor_tag:
|
||||||
|
continue
|
||||||
|
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||||
|
if not title_anchor_tag_attrs:
|
||||||
|
continue
|
||||||
|
if "class" in title_anchor_tag_attrs:
|
||||||
|
td_title = td_title.replace(title_anchor_tag[1], "")
|
||||||
|
title_anchor_tag = get_element_text_and_html_by_tag(
|
||||||
|
"a", td_title
|
||||||
|
)
|
||||||
|
|
||||||
|
if not title_anchor_tag:
|
||||||
|
continue
|
||||||
|
title_anchor_tag_attrs = extract_attributes(title_anchor_tag[1])
|
||||||
|
if not title_anchor_tag_attrs:
|
||||||
|
continue
|
||||||
|
anime_title_info = title_anchor_tag_attrs["title"]
|
||||||
|
if not anime_title_info:
|
||||||
|
continue
|
||||||
|
match = EXTRACT_USEFUL_INFO_PATTERN_2.search(
|
||||||
|
anime_title_info.strip()
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
server = match[1]
|
||||||
|
match[2]
|
||||||
|
_episode_number = match[3]
|
||||||
|
quality = match[4]
|
||||||
|
if float(episode_number) != float(_episode_number):
|
||||||
|
continue
|
||||||
|
|
||||||
|
links_td = get_element_html_by_class("text-center", anime_torrent)
|
||||||
|
if not links_td:
|
||||||
|
continue
|
||||||
|
torrent_anchor_tag = get_element_text_and_html_by_tag("a", links_td)
|
||||||
|
if not torrent_anchor_tag:
|
||||||
|
continue
|
||||||
|
torrent_anchor_tag_atrrs = extract_attributes(torrent_anchor_tag[1])
|
||||||
|
if not torrent_anchor_tag_atrrs:
|
||||||
|
continue
|
||||||
|
torrent_file_url = (
|
||||||
|
f'{NYAA_ENDPOINT}{torrent_anchor_tag_atrrs["href"]}'
|
||||||
|
)
|
||||||
|
if server in servers:
|
||||||
|
link = {
|
||||||
|
"translation_type": "sub",
|
||||||
|
"link": torrent_file_url,
|
||||||
|
"quality": quality,
|
||||||
|
}
|
||||||
|
if link not in servers[server]["links"]:
|
||||||
|
servers[server]["links"].append(link)
|
||||||
|
else:
|
||||||
|
servers[server] = {
|
||||||
|
"server": server,
|
||||||
|
"headers": {},
|
||||||
|
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||||
|
"subtitles": [],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"translation_type": "sub",
|
||||||
|
"link": torrent_file_url,
|
||||||
|
"quality": quality,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[NYAA]: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for server in servers:
|
||||||
|
yield servers[server]
|
||||||
1
fastanime/libs/anime_provider/nyaa/constants.py
Normal file
1
fastanime/libs/anime_provider/nyaa/constants.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
NYAA_ENDPOINT = "https://nyaa.si"
|
||||||
126
fastanime/libs/anime_provider/nyaa/utils.py
Normal file
126
fastanime/libs/anime_provider/nyaa/utils.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import libtorrent # pyright: ignore
|
||||||
|
from rich import print
|
||||||
|
from rich.progress import (
|
||||||
|
BarColumn,
|
||||||
|
DownloadColumn,
|
||||||
|
Progress,
|
||||||
|
TextColumn,
|
||||||
|
TimeRemainingColumn,
|
||||||
|
TransferSpeedColumn,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("nyaa")
|
||||||
|
|
||||||
|
|
||||||
|
def download_torrent(
|
||||||
|
filename: str,
|
||||||
|
result_filename: str | None = None,
|
||||||
|
show_progress: bool = True,
|
||||||
|
base_path: str = "Anime",
|
||||||
|
) -> str:
|
||||||
|
session = libtorrent.session({"listen_interfaces": "0.0.0.0:6881"})
|
||||||
|
logger.debug("Started libtorrent session")
|
||||||
|
|
||||||
|
base_path = os.path.expanduser(base_path)
|
||||||
|
logger.debug(f"Downloading output to: '{base_path}'")
|
||||||
|
|
||||||
|
info = libtorrent.torrent_info(filename)
|
||||||
|
|
||||||
|
logger.debug("Started downloading torrent")
|
||||||
|
handle: libtorrent.torrent_handle = session.add_torrent(
|
||||||
|
{"ti": info, "save_path": base_path}
|
||||||
|
)
|
||||||
|
|
||||||
|
status: libtorrent.session_status = handle.status()
|
||||||
|
|
||||||
|
progress_bar = Progress(
|
||||||
|
"[progress.description]{task.description}",
|
||||||
|
BarColumn(bar_width=None),
|
||||||
|
"[progress.percentage]{task.percentage:>3.1f}%",
|
||||||
|
"•",
|
||||||
|
DownloadColumn(),
|
||||||
|
"•",
|
||||||
|
TransferSpeedColumn(),
|
||||||
|
"•",
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
"•",
|
||||||
|
TextColumn("[green]Peers: {task.fields[peers]}[/green]"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if show_progress:
|
||||||
|
with progress_bar:
|
||||||
|
download_task = progress_bar.add_task(
|
||||||
|
"downloading",
|
||||||
|
filename=status.name,
|
||||||
|
total=status.total_wanted,
|
||||||
|
peers=0,
|
||||||
|
start=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
while not status.total_done:
|
||||||
|
# Checking files
|
||||||
|
status = handle.status()
|
||||||
|
description = "[bold yellow]Checking files[/bold yellow]"
|
||||||
|
progress_bar.update(
|
||||||
|
download_task,
|
||||||
|
completed=status.total_done,
|
||||||
|
peers=status.num_peers,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Started download
|
||||||
|
progress_bar.start_task(download_task)
|
||||||
|
description = f"[bold blue]Downloading[/bold blue] [bold yellow]{result_filename}[/bold yellow]"
|
||||||
|
|
||||||
|
while not status.is_seeding:
|
||||||
|
status = handle.status()
|
||||||
|
|
||||||
|
progress_bar.update(
|
||||||
|
download_task,
|
||||||
|
completed=status.total_done,
|
||||||
|
peers=status.num_peers,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
alerts = session.pop_alerts()
|
||||||
|
|
||||||
|
alert: libtorrent.alert
|
||||||
|
for alert in alerts:
|
||||||
|
if (
|
||||||
|
alert.category()
|
||||||
|
& libtorrent.alert.category_t.error_notification
|
||||||
|
):
|
||||||
|
logger.debug(f"[Alert] {alert}")
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
progress_bar.update(
|
||||||
|
download_task,
|
||||||
|
description=f"[bold blue]Finished Downloading[/bold blue] [bold green]{result_filename}[/bold green]",
|
||||||
|
completed=status.total_wanted,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result_filename:
|
||||||
|
old_name = f"{base_path}/{status.name}"
|
||||||
|
new_name = f"{base_path}/{result_filename}"
|
||||||
|
|
||||||
|
os.rename(old_name, new_name)
|
||||||
|
|
||||||
|
logger.debug(f"Finished torrent download, renamed '{old_name}' to '{new_name}'")
|
||||||
|
|
||||||
|
return new_name
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("You need to pass in the .torrent file path.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
download_torrent(sys.argv[1])
|
||||||
114
fastanime/libs/anime_provider/providers_store.py
Normal file
114
fastanime/libs/anime_provider/providers_store.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderStoreDB:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
provider_name,
|
||||||
|
cache_db_path: str,
|
||||||
|
max_lifetime: int = 604800,
|
||||||
|
max_size: int = (1024**2) * 10,
|
||||||
|
table_name: str = "fastanime_providers_store",
|
||||||
|
clean_db=False,
|
||||||
|
):
|
||||||
|
from ..common.sqlitedb_helper import SqliteDB
|
||||||
|
|
||||||
|
self.cache_db_path = cache_db_path
|
||||||
|
self.clean_db = clean_db
|
||||||
|
self.provider_name = provider_name
|
||||||
|
self.max_lifetime = max_lifetime
|
||||||
|
self.max_size = max_size
|
||||||
|
self.table_name = table_name
|
||||||
|
self.sqlite_db_connection = SqliteDB(self.cache_db_path)
|
||||||
|
|
||||||
|
# Prepare the cache table if it doesn't exist
|
||||||
|
self._create_store_table()
|
||||||
|
|
||||||
|
def _create_store_table(self):
|
||||||
|
"""Create cache table if it doesn't exist."""
|
||||||
|
with self.sqlite_db_connection as conn:
|
||||||
|
conn.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS {self.table_name} (
|
||||||
|
id TEXT,
|
||||||
|
data_type TEXT,
|
||||||
|
provider_name TEXT,
|
||||||
|
data TEXT,
|
||||||
|
cache_expiry INTEGER
|
||||||
|
)"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, id: str, data_type: str, default=None):
|
||||||
|
with self.sqlite_db_connection as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
data
|
||||||
|
FROM {self.table_name}
|
||||||
|
WHERE
|
||||||
|
id = ?
|
||||||
|
AND data_type = ?
|
||||||
|
AND provider_name = ?
|
||||||
|
AND cache_expiry > ?
|
||||||
|
""",
|
||||||
|
(id, data_type, self.provider_name, int(time.time())),
|
||||||
|
)
|
||||||
|
cached_data = cursor.fetchone()
|
||||||
|
|
||||||
|
if cached_data:
|
||||||
|
logger.debug("Found existing request in cache")
|
||||||
|
(json_data,) = cached_data
|
||||||
|
return json.loads(json_data)
|
||||||
|
return default
|
||||||
|
|
||||||
|
def set(self, id: str, data_type: str, data):
|
||||||
|
with self.sqlite_db_connection as connection:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO {self.table_name}
|
||||||
|
VALUES ( ?, ?,?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
id,
|
||||||
|
data_type,
|
||||||
|
self.provider_name,
|
||||||
|
json.dumps(data),
|
||||||
|
int(time.time()) + self.max_lifetime,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderStoreMem:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
self._store = defaultdict(dict)
|
||||||
|
|
||||||
|
def get(self, id: str, data_type: str, default=None):
|
||||||
|
return self._store[id][data_type]
|
||||||
|
|
||||||
|
def set(self, id: str, data_type: str, data):
|
||||||
|
self._store[id][data_type] = data
|
||||||
|
|
||||||
|
|
||||||
|
def ProviderStore(store_type, *args, **kwargs):
|
||||||
|
if store_type == "persistent":
|
||||||
|
return ProviderStoreDB(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return ProviderStoreMem()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
store = ProviderStore("persistent", "test_provider", "provider_store")
|
||||||
|
store.set("123", "test", {"hello": "world"})
|
||||||
|
print(store.get("123", "test"))
|
||||||
|
print("-------------------------------")
|
||||||
|
store = ProviderStore("memory")
|
||||||
|
store.set("1", "test", {"hello": "world"})
|
||||||
|
print(store.get("1", "test"))
|
||||||
@@ -19,6 +19,7 @@ class PageInfo(TypedDict):
|
|||||||
class SearchResult(TypedDict):
|
class SearchResult(TypedDict):
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
|
otherTitles: list[str]
|
||||||
availableEpisodes: list[str]
|
availableEpisodes: list[str]
|
||||||
type: str
|
type: str
|
||||||
score: int
|
score: int
|
||||||
|
|||||||
216
fastanime/libs/anime_provider/yugen/api.py
Normal file
216
fastanime/libs/anime_provider/yugen/api.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import base64
|
||||||
|
from itertools import cycle
|
||||||
|
from yt_dlp.utils import (
|
||||||
|
get_element_text_and_html_by_tag,
|
||||||
|
get_elements_text_and_html_by_attribute,
|
||||||
|
extract_attributes,
|
||||||
|
get_element_by_attribute,
|
||||||
|
)
|
||||||
|
import re
|
||||||
|
|
||||||
|
from yt_dlp.utils.traversal import get_element_html_by_attribute
|
||||||
|
from .constants import YUGEN_ENDPOINT, SEARCH_URL
|
||||||
|
from ..decorators import debug_provider
|
||||||
|
from ..base_provider import AnimeProvider
|
||||||
|
|
||||||
|
|
||||||
|
# ** Adapted from anipy-cli **
|
||||||
|
class YugenApi(AnimeProvider):
|
||||||
|
"""
|
||||||
|
Provides a fast and effective interface to YugenApi site.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PROVIDER = "yugen"
|
||||||
|
api_endpoint = YUGEN_ENDPOINT
|
||||||
|
# HEADERS = {
|
||||||
|
# "Referer": ALLANIME_REFERER,
|
||||||
|
# }
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def search_for_anime(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
translation_type: str = "sub",
|
||||||
|
nsfw=True,
|
||||||
|
unknown=True,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
results = []
|
||||||
|
has_next = True
|
||||||
|
page = 0
|
||||||
|
while has_next:
|
||||||
|
page += 1
|
||||||
|
response = self.session.get(
|
||||||
|
SEARCH_URL, params={"q": user_query, "page": page}
|
||||||
|
)
|
||||||
|
search_results = response.json()
|
||||||
|
has_next = search_results["hasNext"]
|
||||||
|
|
||||||
|
results_html = search_results["query"]
|
||||||
|
anime = get_elements_text_and_html_by_attribute(
|
||||||
|
"class", "anime-meta", results_html, tag="a"
|
||||||
|
)
|
||||||
|
id_regex = re.compile(r"(\d+)\/([^\/]+)")
|
||||||
|
for _a in anime:
|
||||||
|
if not _a:
|
||||||
|
continue
|
||||||
|
a = extract_attributes(_a[1])
|
||||||
|
|
||||||
|
if not a:
|
||||||
|
continue
|
||||||
|
uri = a["href"]
|
||||||
|
identifier = id_regex.search(uri) # pyright:ignore
|
||||||
|
if identifier is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(identifier.groups()) != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
identifier = base64.b64encode(
|
||||||
|
f"{identifier.group(1)}/{identifier.group(2)}".encode()
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
anime_title = a["title"]
|
||||||
|
languages = {"sub": 1, "dub": 0}
|
||||||
|
excl = get_element_by_attribute(
|
||||||
|
"class", "ani-exclamation", _a[1], tag="div"
|
||||||
|
)
|
||||||
|
if excl is not None:
|
||||||
|
if "dub" in excl.lower():
|
||||||
|
languages["dub"] = 1
|
||||||
|
#
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"id": identifier,
|
||||||
|
"title": anime_title,
|
||||||
|
"availableEpisodes": languages,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pageInfo": {"total": len(results)},
|
||||||
|
"results": results,
|
||||||
|
}
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def get_anime(self, anime_id: str, **kwargs):
|
||||||
|
identifier = base64.b64decode(anime_id).decode()
|
||||||
|
response = self.session.get(f"{YUGEN_ENDPOINT}/anime/{identifier}")
|
||||||
|
html_page = response.text
|
||||||
|
data_map = {
|
||||||
|
"id": anime_id,
|
||||||
|
"title": None,
|
||||||
|
"poster": None,
|
||||||
|
"genres": [],
|
||||||
|
"synopsis": None,
|
||||||
|
"release_year": None,
|
||||||
|
"status": None,
|
||||||
|
"otherTitles": [],
|
||||||
|
"availableEpisodesDetail": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_match = re.search(
|
||||||
|
r'<div class="ap-.+?">Episodes</div><span class="description" .+?>(\d+)</span></div>',
|
||||||
|
html_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
if sub_match:
|
||||||
|
eps = int(sub_match.group(1))
|
||||||
|
data_map["availableEpisodesDetail"]["sub"] = list(map(str,range(1, eps + 1)))
|
||||||
|
|
||||||
|
dub_match = re.search(
|
||||||
|
r'<div class="ap-.+?">Episodes \(Dub\)</div><span class="description" .+?>(\d+)</span></div>',
|
||||||
|
html_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
if dub_match:
|
||||||
|
eps = int(dub_match.group(1))
|
||||||
|
data_map["availableEpisodesDetail"]["dub"] = list(map(str,range(1, eps + 1)))
|
||||||
|
|
||||||
|
name = get_element_text_and_html_by_tag("h1", html_page)
|
||||||
|
if name is not None:
|
||||||
|
data_map["title"] = name[0].strip()
|
||||||
|
|
||||||
|
synopsis = get_element_by_attribute("class", "description", html_page, tag="p")
|
||||||
|
if synopsis is not None:
|
||||||
|
data_map["synopsis"] = synopsis
|
||||||
|
|
||||||
|
# FIXME: This is not working because ytdl is too strict on also getting a closing tag
|
||||||
|
try:
|
||||||
|
image = get_element_html_by_attribute(
|
||||||
|
"class", "cover", html_page, tag="img"
|
||||||
|
)
|
||||||
|
img_attrs = extract_attributes(image)
|
||||||
|
if img_attrs is not None:
|
||||||
|
data_map["image"] = img_attrs.get("src")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
data = get_elements_text_and_html_by_attribute(
|
||||||
|
"class", "data", html_page, tag="div"
|
||||||
|
)
|
||||||
|
for d in data:
|
||||||
|
title = get_element_text_and_html_by_tag("div", d[1])
|
||||||
|
desc = get_element_text_and_html_by_tag("span", d[1])
|
||||||
|
if title is None or desc is None:
|
||||||
|
continue
|
||||||
|
title = title[0]
|
||||||
|
desc = desc[0]
|
||||||
|
if title in ["Native", "Romaji"]:
|
||||||
|
data_map["alternative_names"].append(desc)
|
||||||
|
elif title == "Synonyms":
|
||||||
|
data_map["alternative_names"].extend(desc.split(","))
|
||||||
|
elif title == "Premiered":
|
||||||
|
try:
|
||||||
|
data_map["release_year"] = int(desc.split()[-1])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
elif title == "Status":
|
||||||
|
data_map["status"] = title
|
||||||
|
elif title == "Genres":
|
||||||
|
data_map["genres"].extend([g.strip() for g in desc.split(",")])
|
||||||
|
|
||||||
|
return data_map
|
||||||
|
|
||||||
|
@debug_provider(PROVIDER.upper())
|
||||||
|
def get_episode_streams(
|
||||||
|
self, anime_id, episode_number: str, translation_type="sub"
|
||||||
|
):
|
||||||
|
"""get the streams of an episode
|
||||||
|
|
||||||
|
Args:
|
||||||
|
translation_type ([TODO:parameter]): [TODO:description]
|
||||||
|
anime: [TODO:description]
|
||||||
|
episode_number: [TODO:description]
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
[TODO:description]
|
||||||
|
"""
|
||||||
|
|
||||||
|
identifier = base64.b64decode(anime_id).decode()
|
||||||
|
|
||||||
|
id_num, anime_title = identifier.split("/")
|
||||||
|
if translation_type == "dub":
|
||||||
|
video_query = f"{id_num}|{episode_number}|dub"
|
||||||
|
else:
|
||||||
|
video_query = f"{id_num}|{episode_number}"
|
||||||
|
#
|
||||||
|
|
||||||
|
res = self.session.post(
|
||||||
|
f"{YUGEN_ENDPOINT}/api/embed/",
|
||||||
|
data={
|
||||||
|
"id": base64.b64encode(video_query.encode()).decode(),
|
||||||
|
"ac": "0",
|
||||||
|
},
|
||||||
|
headers={"x-requested-with": "XMLHttpRequest"},
|
||||||
|
)
|
||||||
|
res = res.json()
|
||||||
|
yield {
|
||||||
|
"server": "gogoanime",
|
||||||
|
"episode_title": f"{anime_title}; Episode {episode_number}",
|
||||||
|
"headers": {},
|
||||||
|
"subtitles": [],
|
||||||
|
"links": [{"quality": quality, "link": link} for quality,link in zip(cycle(["1080","720","480","360"]),res["hls"])],
|
||||||
|
}
|
||||||
5
fastanime/libs/anime_provider/yugen/constants.py
Normal file
5
fastanime/libs/anime_provider/yugen/constants.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
YUGEN_ENDPOINT: str = "https://yugenanime.tv"
|
||||||
|
|
||||||
|
SEARCH_URL = YUGEN_ENDPOINT + "/api/discover/"
|
||||||
|
SERVERS_AVAILABLE = ["gogoanime"]
|
||||||
@@ -44,7 +44,7 @@ def search_for_manga_with_anilist(manga_title: str):
|
|||||||
pageInfo {
|
pageInfo {
|
||||||
currentPage
|
currentPage
|
||||||
}
|
}
|
||||||
media(search: $query, type: MANGA) {
|
media(search: $query, type: MANGA,genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title {
|
title {
|
||||||
@@ -96,32 +96,36 @@ def search_for_manga_with_anilist(manga_title: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def search_for_anime_with_anilist(anime_title: str):
|
def search_for_anime_with_anilist(anime_title: str, prefer_eng_titles=False):
|
||||||
query = """
|
query = """
|
||||||
query ($query: String) {
|
query ($query: String) {
|
||||||
Page(perPage: 50) {
|
Page(perPage: 50) {
|
||||||
pageInfo {
|
pageInfo {
|
||||||
total
|
total
|
||||||
currentPage
|
currentPage
|
||||||
hasNextPage
|
hasNextPage
|
||||||
}
|
}
|
||||||
media(search: $query, type: ANIME) {
|
media(search: $query, type: ANIME, genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title {
|
title {
|
||||||
romaji
|
romaji
|
||||||
english
|
english
|
||||||
}
|
}
|
||||||
episodes
|
episodes
|
||||||
status
|
status
|
||||||
nextAiringEpisode {
|
synonyms
|
||||||
timeUntilAiring
|
nextAiringEpisode {
|
||||||
airingAt
|
timeUntilAiring
|
||||||
episode
|
airingAt
|
||||||
}
|
episode
|
||||||
}
|
}
|
||||||
}
|
coverImage {
|
||||||
}
|
large
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
response = post(
|
response = post(
|
||||||
ANILIST_ENDPOINT,
|
ANILIST_ENDPOINT,
|
||||||
@@ -134,22 +138,55 @@ def search_for_anime_with_anilist(anime_title: str):
|
|||||||
"pageInfo": anilist_data["data"]["Page"]["pageInfo"],
|
"pageInfo": anilist_data["data"]["Page"]["pageInfo"],
|
||||||
"results": [
|
"results": [
|
||||||
{
|
{
|
||||||
"id": anime_result["id"],
|
"id": str(anime_result["id"]),
|
||||||
"title": anime_result["title"]["romaji"]
|
"title": (
|
||||||
or anime_result["title"]["english"],
|
(
|
||||||
"type": "anime",
|
anime_result["title"]["english"]
|
||||||
"availableEpisodes": list(
|
or anime_result["title"]["romaji"]
|
||||||
range(
|
)
|
||||||
1,
|
if prefer_eng_titles
|
||||||
|
else (
|
||||||
|
anime_result["title"]["romaji"]
|
||||||
|
or anime_result["title"]["english"]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"otherTitles": [
|
||||||
|
(
|
||||||
(
|
(
|
||||||
anime_result["episodes"]
|
anime_result["title"]["romaji"]
|
||||||
if not anime_result["status"] == "RELEASING"
|
or anime_result["title"]["english"]
|
||||||
and anime_result["episodes"]
|
)
|
||||||
else (
|
if prefer_eng_titles
|
||||||
anime_result["nextAiringEpisode"]["episode"] - 1
|
else (
|
||||||
if anime_result["nextAiringEpisode"]
|
anime_result["title"]["english"]
|
||||||
else 0
|
or anime_result["title"]["romaji"]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
*(anime_result["synonyms"] or []),
|
||||||
|
],
|
||||||
|
"type": "anime",
|
||||||
|
"poster": anime_result["coverImage"]["large"],
|
||||||
|
"availableEpisodes": list(
|
||||||
|
map(
|
||||||
|
str,
|
||||||
|
range(
|
||||||
|
1,
|
||||||
|
(
|
||||||
|
anime_result["episodes"]
|
||||||
|
if not anime_result["status"] == "RELEASING"
|
||||||
|
and anime_result["episodes"]
|
||||||
|
else (
|
||||||
|
(
|
||||||
|
anime_result["nextAiringEpisode"]["episode"]
|
||||||
|
- 1
|
||||||
|
if anime_result["nextAiringEpisode"]
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
if not anime_result["episodes"]
|
||||||
|
else anime_result["episodes"]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
+ 1,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -228,7 +265,7 @@ def get_basic_anime_info_by_title(anime_title: str):
|
|||||||
pageInfo {
|
pageInfo {
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
media(search: $query, type: ANIME) {
|
media(search: $query, type: ANIME,genre_not_in: ["hentai"]) {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
title {
|
title {
|
||||||
|
|||||||
209
fastanime/libs/common/requests_cacher.py
Normal file
209
fastanime/libs/common/requests_cacher.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .sqlitedb_helper import SqliteDB
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
caching_mimetypes = {
|
||||||
|
"application": {
|
||||||
|
"json",
|
||||||
|
"xml",
|
||||||
|
"x-www-form-urlencoded",
|
||||||
|
"x-javascript",
|
||||||
|
"javascript",
|
||||||
|
},
|
||||||
|
"text": {"html", "css", "javascript", "plain", "xml", "xsl", "x-javascript"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CachedRequestsSession(requests.Session):
|
||||||
|
__request_functions__ = (
|
||||||
|
"get",
|
||||||
|
"options",
|
||||||
|
"head",
|
||||||
|
"post",
|
||||||
|
"put",
|
||||||
|
"patch",
|
||||||
|
"delete",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
def caching_params(name: str):
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
return cls.request(self, name, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
for func in cls.__request_functions__:
|
||||||
|
setattr(cls, func, caching_params(func))
|
||||||
|
|
||||||
|
return super().__new__(cls)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cache_db_path: str,
|
||||||
|
max_lifetime: int = 259200,
|
||||||
|
max_size: int = (1024**2) * 10,
|
||||||
|
table_name: str = "fastanime_requests_cache",
|
||||||
|
clean_db=False,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.cache_db_path = cache_db_path
|
||||||
|
self.max_lifetime = max_lifetime
|
||||||
|
self.max_size = max_size
|
||||||
|
self.table_name = table_name
|
||||||
|
self.sqlite_db_connection = SqliteDB(self.cache_db_path)
|
||||||
|
|
||||||
|
# Prepare the cache table if it doesn't exist
|
||||||
|
self._create_cache_table()
|
||||||
|
|
||||||
|
def _create_cache_table(self):
|
||||||
|
"""Create cache table if it doesn't exist."""
|
||||||
|
with self.sqlite_db_connection as conn:
|
||||||
|
conn.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE IF NOT EXISTS {self.table_name} (
|
||||||
|
url TEXT,
|
||||||
|
status_code INTEGER,
|
||||||
|
request_headers TEXT,
|
||||||
|
response_headers TEXT,
|
||||||
|
data BLOB,
|
||||||
|
redirection_policy INT,
|
||||||
|
cache_expiry INTEGER
|
||||||
|
)"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def request(
|
||||||
|
self,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
params=None,
|
||||||
|
force_caching=False,
|
||||||
|
fresh=int(os.environ.get("FASTANIME_FRESH_REQUESTS", 0)),
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
if params:
|
||||||
|
url += "?" + urlencode(params)
|
||||||
|
|
||||||
|
redirection_policy = int(kwargs.get("force_redirects", False))
|
||||||
|
|
||||||
|
with self.sqlite_db_connection as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
time_before_access_db = datetime.now()
|
||||||
|
|
||||||
|
logger.debug("Checking for existing request in cache")
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
status_code,
|
||||||
|
request_headers,
|
||||||
|
response_headers,
|
||||||
|
data,
|
||||||
|
redirection_policy
|
||||||
|
FROM {self.table_name}
|
||||||
|
WHERE
|
||||||
|
url = ?
|
||||||
|
AND redirection_policy = ?
|
||||||
|
AND cache_expiry > ?
|
||||||
|
""",
|
||||||
|
(url, redirection_policy, int(time.time())),
|
||||||
|
)
|
||||||
|
cached_request = cursor.fetchone()
|
||||||
|
time_after_access_db = datetime.now()
|
||||||
|
|
||||||
|
if cached_request and not fresh:
|
||||||
|
logger.debug("Found existing request in cache")
|
||||||
|
(
|
||||||
|
status_code,
|
||||||
|
request_headers,
|
||||||
|
response_headers,
|
||||||
|
data,
|
||||||
|
redirection_policy,
|
||||||
|
) = cached_request
|
||||||
|
|
||||||
|
response = requests.Response()
|
||||||
|
response.headers.update(json.loads(response_headers))
|
||||||
|
response.status_code = status_code
|
||||||
|
response._content = data
|
||||||
|
|
||||||
|
if "timeout" in kwargs:
|
||||||
|
kwargs.pop("timeout")
|
||||||
|
if "headers" in kwargs:
|
||||||
|
kwargs.pop("headers")
|
||||||
|
_request = requests.Request(
|
||||||
|
method, url, headers=json.loads(request_headers), *args, **kwargs
|
||||||
|
)
|
||||||
|
response.request = _request.prepare()
|
||||||
|
response.elapsed = time_after_access_db - time_before_access_db
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Perform the request and cache it
|
||||||
|
response = super().request(method, url, *args, **kwargs)
|
||||||
|
if response.ok and (
|
||||||
|
force_caching
|
||||||
|
or self.is_content_type_cachable(
|
||||||
|
response.headers.get("content-type"), caching_mimetypes
|
||||||
|
)
|
||||||
|
and len(response.content) < self.max_size
|
||||||
|
):
|
||||||
|
logger.debug("Caching the current request")
|
||||||
|
cursor.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO {self.table_name}
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
url,
|
||||||
|
response.status_code,
|
||||||
|
json.dumps(dict(response.request.headers)),
|
||||||
|
json.dumps(dict(response.headers)),
|
||||||
|
response.content,
|
||||||
|
redirection_policy,
|
||||||
|
int(time.time()) + self.max_lifetime,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_content_type_cachable(content_type, caching_mimetypes):
|
||||||
|
"""Checks whether the given encoding is supported by the cacher"""
|
||||||
|
if content_type is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
mime, contents = content_type.split("/")
|
||||||
|
|
||||||
|
contents = re.sub(r";.*$", "", contents)
|
||||||
|
|
||||||
|
return mime in caching_mimetypes and any(
|
||||||
|
content in caching_mimetypes[mime] for content in contents.split("+")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with CachedRequestsSession("cache.db") as session:
|
||||||
|
response = session.get(
|
||||||
|
"https://google.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
response_b = session.get(
|
||||||
|
"https://google.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("A: ", response.elapsed)
|
||||||
|
print("B: ", response_b.elapsed)
|
||||||
|
|
||||||
|
print(response_b.text[0:30])
|
||||||
34
fastanime/libs/common/sqlitedb_helper.py
Normal file
34
fastanime/libs/common/sqlitedb_helper.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SqliteDB:
|
||||||
|
def __init__(self, db_path: str) -> None:
|
||||||
|
self.db_path = db_path
|
||||||
|
self.connection = sqlite3.connect(self.db_path)
|
||||||
|
logger.debug("Enabling WAL mode for concurrent access")
|
||||||
|
self.connection.execute("PRAGMA journal_mode=WAL;")
|
||||||
|
self.connection.close()
|
||||||
|
self.connection = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
logger.debug("Starting new connection...")
|
||||||
|
start_time = time.time()
|
||||||
|
self.connection = sqlite3.connect(self.db_path)
|
||||||
|
logger.debug(
|
||||||
|
"Successfully got a new connection in {} seconds".format(
|
||||||
|
time.time() - start_time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return self.connection
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if self.connection:
|
||||||
|
logger.debug("Closing connection to cache db")
|
||||||
|
self.connection.commit()
|
||||||
|
self.connection.close()
|
||||||
|
self.connection = None
|
||||||
|
logger.debug("Successfully closed connection to cache db")
|
||||||
@@ -49,7 +49,7 @@ class FZF:
|
|||||||
"--info=hidden",
|
"--info=hidden",
|
||||||
"--layout=reverse",
|
"--layout=reverse",
|
||||||
"--height=100%",
|
"--height=100%",
|
||||||
"--bind=right:accept",
|
"--bind=right:accept,ctrl-/:toggle-preview,ctrl-space:toggle-wrap+toggle-preview-wrap",
|
||||||
"--no-margin",
|
"--no-margin",
|
||||||
"+m",
|
"+m",
|
||||||
"-i",
|
"-i",
|
||||||
@@ -124,7 +124,7 @@ class FZF:
|
|||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
universal_newlines=True,
|
universal_newlines=True,
|
||||||
text=True,
|
text=True,
|
||||||
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:")
|
print("sth went wrong:confused:")
|
||||||
@@ -163,7 +163,7 @@ class FZF:
|
|||||||
HEADER,
|
HEADER,
|
||||||
"--header-first",
|
"--header-first",
|
||||||
"--prompt",
|
"--prompt",
|
||||||
prompt.title(),
|
f"{prompt.title()}: ",
|
||||||
] # pyright:ignore
|
] # pyright:ignore
|
||||||
|
|
||||||
if preview:
|
if preview:
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import subprocess
|
|||||||
from shutil import which
|
from shutil import which
|
||||||
from sys import exit
|
from sys import exit
|
||||||
|
|
||||||
from plyer import notification
|
|
||||||
|
|
||||||
from fastanime import APP_NAME
|
from fastanime import APP_NAME
|
||||||
|
|
||||||
from ...constants import ICON_PATH
|
from ...constants import ICON_PATH
|
||||||
@@ -13,6 +11,7 @@ class RofiApi:
|
|||||||
ROFI_EXECUTABLE = which("rofi")
|
ROFI_EXECUTABLE = which("rofi")
|
||||||
|
|
||||||
rofi_theme = ""
|
rofi_theme = ""
|
||||||
|
rofi_theme_preview = ""
|
||||||
rofi_theme_confirm = ""
|
rofi_theme_confirm = ""
|
||||||
rofi_theme_input = ""
|
rofi_theme_input = ""
|
||||||
|
|
||||||
@@ -23,9 +22,9 @@ class RofiApi:
|
|||||||
raise Exception("Rofi not found")
|
raise Exception("Rofi not found")
|
||||||
|
|
||||||
args = [self.ROFI_EXECUTABLE]
|
args = [self.ROFI_EXECUTABLE]
|
||||||
if self.rofi_theme:
|
if self.rofi_theme_preview:
|
||||||
args.extend(["-no-config", "-theme", self.rofi_theme])
|
args.extend(["-no-config", "-theme", self.rofi_theme_preview])
|
||||||
args.extend(["-p", prompt_text, "-i", "-show-icons", "-dmenu"])
|
args.extend(["-p", f"{prompt_text.title()}", "-i", "-show-icons", "-dmenu"])
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
args,
|
args,
|
||||||
input=rofi_input,
|
input=rofi_input,
|
||||||
@@ -35,6 +34,13 @@ class RofiApi:
|
|||||||
|
|
||||||
choice = result.stdout.strip()
|
choice = result.stdout.strip()
|
||||||
if not choice:
|
if not choice:
|
||||||
|
try:
|
||||||
|
from plyer import notification
|
||||||
|
except ImportError:
|
||||||
|
print(
|
||||||
|
"Plyer is not installed; install it for desktop notifications to be enabled"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
notification.notify(
|
notification.notify(
|
||||||
app_name=APP_NAME,
|
app_name=APP_NAME,
|
||||||
app_icon=ICON_PATH,
|
app_icon=ICON_PATH,
|
||||||
@@ -64,6 +70,13 @@ class RofiApi:
|
|||||||
|
|
||||||
choice = result.stdout.strip()
|
choice = result.stdout.strip()
|
||||||
if not choice or choice not in options:
|
if not choice or choice not in options:
|
||||||
|
try:
|
||||||
|
from plyer import notification
|
||||||
|
except ImportError:
|
||||||
|
print(
|
||||||
|
"Plyer is not installed; install it for desktop notifications to be enabled"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
notification.notify(
|
notification.notify(
|
||||||
app_name=APP_NAME,
|
app_name=APP_NAME,
|
||||||
app_icon=ICON_PATH,
|
app_icon=ICON_PATH,
|
||||||
@@ -91,6 +104,13 @@ class RofiApi:
|
|||||||
|
|
||||||
choice = result.stdout.strip()
|
choice = result.stdout.strip()
|
||||||
if not choice:
|
if not choice:
|
||||||
|
try:
|
||||||
|
from plyer import notification
|
||||||
|
except ImportError:
|
||||||
|
print(
|
||||||
|
"Plyer is not installed; install it for desktop notifications to be enabled"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
notification.notify(
|
notification.notify(
|
||||||
app_name=APP_NAME,
|
app_name=APP_NAME,
|
||||||
app_icon=ICON_PATH,
|
app_icon=ICON_PATH,
|
||||||
@@ -120,6 +140,13 @@ class RofiApi:
|
|||||||
|
|
||||||
user_input = result.stdout.strip()
|
user_input = result.stdout.strip()
|
||||||
if not user_input:
|
if not user_input:
|
||||||
|
try:
|
||||||
|
from plyer import notification
|
||||||
|
except ImportError:
|
||||||
|
print(
|
||||||
|
"Plyer is not installed; install it for desktop notifications to be enabled"
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
notification.notify(
|
notification.notify(
|
||||||
app_name=APP_NAME,
|
app_name=APP_NAME,
|
||||||
app_icon=ICON_PATH,
|
app_icon=ICON_PATH,
|
||||||
|
|||||||
11
make_release
Executable file
11
make_release
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#! /usr/bin/env sh
|
||||||
|
CLI_DIR="$(dirname "$(realpath "$0")")"
|
||||||
|
VERSION=$1
|
||||||
|
[ -z "$VERSION" ] && echo no version provided && exit 1
|
||||||
|
[ "$VERSION" = "current" ] && fastanime --version && exit 0
|
||||||
|
sed -i "s/^version.*/version = \"$VERSION\"/" "$CLI_DIR/pyproject.toml" &&
|
||||||
|
sed -i "s/__version__.*/__version__ = \"v$VERSION\"/" "$CLI_DIR/fastanime/__init__.py" &&
|
||||||
|
git stage "$CLI_DIR/pyproject.toml" "$CLI_DIR/fastanime/__init__.py" &&
|
||||||
|
git commit -m "chore: bump version (v$VERSION)" &&
|
||||||
|
git push &&
|
||||||
|
gh release create "v$VERSION"
|
||||||
1373
poetry.lock
generated
1373
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
65
pyinstaller.spec
Normal file
65
pyinstaller.spec
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
# Collect all required data files
|
||||||
|
datas = [
|
||||||
|
('fastanime/assets/*', 'fastanime/assets'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Collect all required hidden imports
|
||||||
|
hiddenimports = [
|
||||||
|
'click',
|
||||||
|
'rich',
|
||||||
|
'requests',
|
||||||
|
'yt_dlp',
|
||||||
|
'python_mpv',
|
||||||
|
'fuzzywuzzy',
|
||||||
|
'fastanime',
|
||||||
|
] + collect_submodules('fastanime')
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['./fastanime/fastanime.py'], # Changed entry point
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher,
|
||||||
|
strip=True, # Strip debug information
|
||||||
|
optimize=2 # Optimize bytecode noarchive=False
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(
|
||||||
|
a.pure,
|
||||||
|
a.zipped_data,
|
||||||
|
optimize=2 # Optimize bytecode cipher=block_cipher
|
||||||
|
)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='fastanime',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=True,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon='fastanime/assets/logo.ico'
|
||||||
|
)
|
||||||
@@ -1,35 +1,36 @@
|
|||||||
[tool.poetry]
|
[project]
|
||||||
name = "fastanime"
|
name = "fastanime"
|
||||||
version = "2.5.2"
|
version = "2.7.5"
|
||||||
description = "A browser anime site experience from the terminal"
|
description = "A browser anime site experience from the terminal"
|
||||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
|
||||||
license = "UNLICENSE"
|
license = "UNLICENSE"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"click>=8.1.7",
|
||||||
|
"inquirerpy>=0.3.4",
|
||||||
|
"requests>=2.32.3",
|
||||||
|
"rich>=13.9.2",
|
||||||
|
"thefuzz>=0.22.1",
|
||||||
|
"yt-dlp>=2024.10.7",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[project.scripts]
|
||||||
python = "^3.10"
|
|
||||||
yt-dlp = "^2024.5.27"
|
|
||||||
rich = "^13.7.1"
|
|
||||||
click = "^8.1.7"
|
|
||||||
inquirerpy = "^0.3.4"
|
|
||||||
thefuzz = "^0.22.1"
|
|
||||||
requests = "^2.32.3"
|
|
||||||
plyer = "^2.1.0"
|
|
||||||
|
|
||||||
mpv = "^1.0.7"
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
black = "^24.4.2"
|
|
||||||
isort = "^5.13.2"
|
|
||||||
pytest = "^8.2.2"
|
|
||||||
ruff = "^0.4.10"
|
|
||||||
pre-commit = "^3.7.1"
|
|
||||||
autoflake = "^2.3.1"
|
|
||||||
tox = "^4.16.0"
|
|
||||||
|
|
||||||
pyright = "^1.1.374"
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
|
||||||
fastanime = 'fastanime:FastAnime'
|
fastanime = 'fastanime:FastAnime'
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
standard = ["fastapi[standard]>=0.115.0", "mpv>=1.0.7", "plyer>=2.1.0"]
|
||||||
|
api = ["fastapi[standard]>=0.115.0"]
|
||||||
|
notifications = ["plyer>=2.1.0"]
|
||||||
|
mpv = ["mpv>=1.0.7"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
dev-dependencies = [
|
||||||
|
"pyinstaller>=6.11.1",
|
||||||
|
"pyright>=1.1.384",
|
||||||
|
"pytest>=8.3.3",
|
||||||
|
"ruff>=0.6.9",
|
||||||
|
]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from fastanime.cli import run_cli
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def runner():
|
def runner():
|
||||||
return CliRunner()
|
return CliRunner(env={"FASTANIME_CACHE_REQUESTS": "false"})
|
||||||
|
|
||||||
|
|
||||||
def test_main_help(runner: CliRunner):
|
def test_main_help(runner: CliRunner):
|
||||||
|
|||||||
18
tox.ini
18
tox.ini
@@ -5,23 +5,23 @@ env_list = lint, pyright, py{310,311}
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
description = run unit tests
|
description = run unit tests
|
||||||
deps =poetry
|
deps =uv
|
||||||
commands =
|
commands =
|
||||||
poetry install
|
uv sync --dev --all-extras
|
||||||
poetry run pytest
|
uv run pytest
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
description = run linters
|
description = run linters
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps =poetry
|
deps =uv
|
||||||
commands =
|
commands =
|
||||||
poetry install
|
uv sync --dev --all-extras
|
||||||
poetry run black .
|
uv run ruff format .
|
||||||
|
|
||||||
[testenv:pyright]
|
[testenv:pyright]
|
||||||
description = run type checking
|
description = run type checking
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps =poetry
|
deps =uv
|
||||||
commands =
|
commands =
|
||||||
poetry install --no-root
|
uv sync --dev --all-extras
|
||||||
poetry run pyright
|
uv run pyright
|
||||||
|
|||||||
Reference in New Issue
Block a user