mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-06 04:41:06 -08:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -8,31 +8,24 @@ jobs:
|
||||
debug_build:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v5
|
||||
- name: Install poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||
run: |
|
||||
poetry config virtualenvs.create true --local
|
||||
poetry config virtualenvs.in-project true --local
|
||||
- uses: actions/cache@v3
|
||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
path: ./.venv
|
||||
key: venv-${{ hashFiles('poetry.lock') }}
|
||||
- name: Install the project dependencies
|
||||
run: poetry install --all-extras
|
||||
- name: build app
|
||||
run: poetry build
|
||||
enable-cache: true
|
||||
|
||||
- name: Build fastanime
|
||||
run: uv build
|
||||
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fastanime_debug_build
|
||||
path: |
|
||||
dist
|
||||
!dist/*.whl
|
||||
# - name: Run the automated tests (for example)
|
||||
# run: poetry run pytest -v
|
||||
|
||||
12
.github/workflows/publish.yml
vendored
12
.github/workflows/publish.yml
vendored
@@ -27,11 +27,13 @@ jobs:
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Build release distributions
|
||||
run: |
|
||||
# NOTE: put your own distribution build steps here.
|
||||
python -m pip install build
|
||||
python -m build
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Build fastanime
|
||||
run: uv build
|
||||
|
||||
- name: Upload distributions
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
40
.github/workflows/test.yml
vendored
40
.github/workflows/test.yml
vendored
@@ -6,37 +6,35 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11"] # List the Python versions you want to test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||
run: |
|
||||
poetry config virtualenvs.create true --local
|
||||
poetry config virtualenvs.in-project true --local
|
||||
- uses: actions/cache@v3
|
||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
path: ./.venv
|
||||
key: venv-${{ hashFiles('poetry.lock') }}
|
||||
- name: Install the project dependencies
|
||||
run: poetry install --all-extras
|
||||
- name: run linter, formatters and sort imports
|
||||
run: |
|
||||
poetry run black .
|
||||
poetry run ruff check --output-format=github . --fix
|
||||
poetry run isort . --profile black
|
||||
- name: run type checking
|
||||
run: poetry run pyright
|
||||
- name: run tests
|
||||
run: poetry run pytest
|
||||
enable-cache: true
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Run linter and formater
|
||||
run: uv run ruff check --output-format=github
|
||||
|
||||
- name: Run type checking
|
||||
run: uv run pyright
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest tests
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,10 +1,7 @@
|
||||
FROM ubuntu
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install python3
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install pipx
|
||||
RUN pipx ensurepath
|
||||
FROM python:3.12-slim-bookworm
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
COPY . /fastanime
|
||||
ENV PATH=/root/.local/bin:$PATH
|
||||
WORKDIR /fastanime
|
||||
RUN pipx install .
|
||||
RUN uv tool install .
|
||||
CMD ["bash"]
|
||||
|
||||
57
README.md
57
README.md
@@ -8,12 +8,12 @@
|
||||
|
||||
Welcome to **FastAnime**, anime site experience from the terminal.
|
||||
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary><b>fzf mode</b></summary>
|
||||
|
||||
[fa_fzf_demo.webm](https://github.com/user-attachments/assets/b1fecf25-e358-4e8b-a144-bcb7947210cf)
|
||||
[fastanime-fzf.webm](https://github.com/user-attachments/assets/90875a57-198b-4c78-98d5-10a459001edd)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -85,11 +85,31 @@ If you have any difficulty consult for help on the [discord channel](https://dis
|
||||
### Installation using your favourite package manager
|
||||
|
||||
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
|
||||
With the following extras available:
|
||||
|
||||
- standard -which installs all dependencies
|
||||
- api - which installs dependencies required to use `fastanime serve`
|
||||
- mpv - which installs python mpv
|
||||
- notifications - which installs plyer required for desktop notifications
|
||||
|
||||
#### Using uv
|
||||
|
||||
Recommended method of installation
|
||||
|
||||
```bash
|
||||
# generally:
|
||||
uv tool install fastanime
|
||||
|
||||
# if you want other functionality:
|
||||
uv tool install fastanime[standard]
|
||||
uv tool install fastanime[api]
|
||||
uv tool install fastanime[mpv]
|
||||
uv tool install fastanime[notifications]
|
||||
|
||||
```
|
||||
|
||||
#### Using pipx
|
||||
|
||||
Preferred method of installation since [Pipx](https://github.com/pypa/pipx) creates an isolated environment for each app it installs.
|
||||
|
||||
```bash
|
||||
|
||||
pipx install fastanime
|
||||
@@ -133,7 +153,7 @@ Requirements:
|
||||
|
||||
- [git](https://git-scm.com/)
|
||||
- [python 3.10 and above](https://www.python.org/)
|
||||
- [poetry](https://python-poetry.org/docs/#installation)
|
||||
- [uv](https://astral.sh/blog/uv)
|
||||
|
||||
To build from the source, follow these steps:
|
||||
|
||||
@@ -142,15 +162,8 @@ To build from the source, follow these steps:
|
||||
3. Then build and Install the app:
|
||||
|
||||
```bash
|
||||
# Normal Installation
|
||||
poetry build
|
||||
cd dist
|
||||
pip install fastanime<version>.whl
|
||||
|
||||
# Editable installation (easiest for updates)
|
||||
# just do a git pull in the Project dir
|
||||
# the latter will require rebuilding the app
|
||||
pip install -e .
|
||||
# build and install fastanime with uv
|
||||
uv tool install .
|
||||
```
|
||||
|
||||
4. Enjoy! Verify installation with:
|
||||
@@ -167,6 +180,7 @@ fastanime --version
|
||||
> - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/`
|
||||
> - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc`
|
||||
> - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc`
|
||||
> or using the built in command `fastanime completions`
|
||||
|
||||
### External Dependencies
|
||||
|
||||
@@ -637,6 +651,19 @@ fastanime completions --bash
|
||||
fastanime completions --zsh
|
||||
```
|
||||
|
||||
#### fastanime serve
|
||||
|
||||
Helper command that starts a rest server.
|
||||
This requires you to install fastanime with the api extra or standard extra.
|
||||
|
||||
```bash
|
||||
# default options
|
||||
fastanime serve
|
||||
|
||||
# specify host and port
|
||||
fastanime serve --host <host> --port <port>
|
||||
```
|
||||
|
||||
### MPV specific commands
|
||||
|
||||
The project now allows on the fly media controls directly from mpv. This means you can go to the next or previous episode without the window ever closing thus offering a seamless experience.
|
||||
@@ -728,6 +755,8 @@ cache_requests = True
|
||||
|
||||
use_persistent_provider_store = False
|
||||
|
||||
recent = 50
|
||||
|
||||
|
||||
[stream]
|
||||
continue_from_history = True
|
||||
|
||||
5
fa
5
fa
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
# exec "${PYTHON:-python3}" -Werror -Xdev -m "$(dirname "$(realpath "$0")")/fastanime" "$@"
|
||||
cd "$(dirname "$(realpath "$0")")" || exit 1
|
||||
exec python -m fastanime "$@"
|
||||
CLI_DIR="$(dirname "$(realpath "$0")")"
|
||||
exec uv run --directory "$CLI_DIR/../" fastanime "$@"
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
|
||||
|
||||
COMMA_REGEX = re.compile(r"([0-9]{3})(?=\d)")
|
||||
|
||||
|
||||
# TODO: Add formating options for the final date
|
||||
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']}"
|
||||
else:
|
||||
return "Unknown"
|
||||
@@ -27,6 +30,12 @@ def format_list_data_with_comma(data: list | 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"):
|
||||
if airing_episode:
|
||||
return f"{airing_episode['episode']} on {format_anilist_timestamp(airing_episode['airingAt'])}"
|
||||
|
||||
@@ -9,6 +9,7 @@ anime_normalizer_raw = {
|
||||
"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",
|
||||
'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",
|
||||
},
|
||||
"hianime": {"My Star": "Oshi no Ko"},
|
||||
"animepahe": {"Azumanga Daiou The Animation": "Azumanga Daioh"},
|
||||
@@ -20,7 +21,7 @@ def get_anime_normalizer():
|
||||
"""Used because there are different providers"""
|
||||
import os
|
||||
|
||||
current_provider = os.environ["FASTANIME_PROVIDER"]
|
||||
current_provider = os.environ.get("FASTANIME_PROVIDER", "allanime")
|
||||
return anime_normalizer_raw[current_provider]
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class YtDLPDownloader:
|
||||
downloads_queue = Queue()
|
||||
_thread = None
|
||||
|
||||
def _worker(self):
|
||||
while True:
|
||||
@@ -26,11 +27,6 @@ class YtDLPDownloader:
|
||||
logger.error(f"Something went wrong {e}")
|
||||
self.downloads_queue.task_done()
|
||||
|
||||
def __init__(self):
|
||||
self._thread = Thread(target=self._worker)
|
||||
self._thread.daemon = True
|
||||
self._thread.start()
|
||||
|
||||
def _download_file(
|
||||
self,
|
||||
url: str,
|
||||
@@ -38,6 +34,7 @@ class YtDLPDownloader:
|
||||
episode_title: str,
|
||||
download_dir: str,
|
||||
silent: bool,
|
||||
progress_hooks=[],
|
||||
vid_format: str = "best",
|
||||
force_unknown_ext=False,
|
||||
verbose=False,
|
||||
@@ -86,6 +83,7 @@ class YtDLPDownloader:
|
||||
"verbose": verbose,
|
||||
"format": vid_format,
|
||||
"compat_opts": ("allow-unsafe-ext",) if force_unknown_ext else tuple(),
|
||||
"progress_hooks": progress_hooks,
|
||||
}
|
||||
urls = [url]
|
||||
if sub:
|
||||
@@ -174,8 +172,15 @@ class YtDLPDownloader:
|
||||
except Exception as e:
|
||||
print(f"[red bold]An error[/] occurred: {e}")
|
||||
|
||||
# WARN: May remove this legacy functionality
|
||||
def download_file(self, url: str, title, silent=True):
|
||||
def download_file(
|
||||
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
|
||||
|
||||
Args:
|
||||
@@ -183,7 +188,17 @@ class YtDLPDownloader:
|
||||
silent ([TODO:parameter]): [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()
|
||||
|
||||
@@ -37,6 +37,10 @@ def anime_title_percentage_match(
|
||||
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()),
|
||||
)
|
||||
|
||||
@@ -2,11 +2,11 @@ import sys
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
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
|
||||
|
||||
|
||||
__version__ = "v2.6.0"
|
||||
__version__ = "v2.6.9"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
|
||||
25
fastanime/api/__init__.py
Normal file
25
fastanime/api/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from ..AnimeProvider import AnimeProvider
|
||||
|
||||
app = FastAPI()
|
||||
anime_provider = AnimeProvider("allanime", "true", "true")
|
||||
|
||||
|
||||
@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)
|
||||
@@ -16,6 +16,7 @@ commands = {
|
||||
"completions": "completions.completions",
|
||||
"update": "update.update",
|
||||
"grab": "grab.grab",
|
||||
"serve": "serve.serve",
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +178,9 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
help="the player to use when streaming",
|
||||
type=click.Choice(["mpv", "vlc"]),
|
||||
)
|
||||
@click.option(
|
||||
"--fresh-requests", is_flag=True, help="Force the requests cache to be updated"
|
||||
)
|
||||
@click.pass_context
|
||||
def run_cli(
|
||||
ctx: click.Context,
|
||||
@@ -211,7 +215,10 @@ def run_cli(
|
||||
use_python_mpv,
|
||||
sync_play,
|
||||
player,
|
||||
fresh_requests,
|
||||
):
|
||||
import os
|
||||
|
||||
from .config import Config
|
||||
|
||||
ctx.obj = Config()
|
||||
@@ -250,6 +257,8 @@ def run_cli(
|
||||
|
||||
install()
|
||||
|
||||
if fresh_requests:
|
||||
os.environ["FASTANIME_FRESH_REQUESTS"] = "1"
|
||||
if sync_play:
|
||||
ctx.obj.sync_play = sync_play
|
||||
if provider:
|
||||
|
||||
@@ -75,9 +75,9 @@ def is_git_repo(author, repository):
|
||||
return bool(match) and match.group(1) == f"{author}/{repository}"
|
||||
|
||||
|
||||
def update_app():
|
||||
def update_app(force=False):
|
||||
is_latest, release_json = check_for_updates()
|
||||
if is_latest:
|
||||
if is_latest and not force:
|
||||
print("[green]App is up to date[/]")
|
||||
return False, release_json
|
||||
tag_name = release_json["tag_name"]
|
||||
@@ -101,8 +101,10 @@ def update_app():
|
||||
)
|
||||
|
||||
else:
|
||||
if PIPX_EXECUTABLE := shutil.which("pipx"):
|
||||
process = subprocess.run([PIPX_EXECUTABLE, "upgrade", APP_NAME])
|
||||
if UV := shutil.which("uv"):
|
||||
process = subprocess.run([UV, "tool", "upgrade", APP_NAME])
|
||||
elif PIPX := shutil.which("pipx"):
|
||||
process = subprocess.run([PIPX, "upgrade", APP_NAME])
|
||||
else:
|
||||
PYTHON_EXECUTABLE = sys.executable
|
||||
|
||||
|
||||
@@ -361,9 +361,9 @@ def download(
|
||||
episode_title,
|
||||
download_dir,
|
||||
silent,
|
||||
config.format,
|
||||
force_unknown_ext,
|
||||
verbose,
|
||||
vid_format=config.format,
|
||||
force_unknown_ext=force_unknown_ext,
|
||||
verbose=verbose,
|
||||
headers=provider_headers,
|
||||
sub=subtitles[0]["url"] if subtitles else "",
|
||||
merge=merge,
|
||||
|
||||
@@ -169,6 +169,7 @@ def downloads(
|
||||
from threading import Thread
|
||||
|
||||
worker = Thread(target=_worker)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
else:
|
||||
_worker()
|
||||
@@ -241,6 +242,7 @@ def downloads(
|
||||
from threading import Thread
|
||||
|
||||
worker = Thread(target=_worker)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
else:
|
||||
_worker()
|
||||
|
||||
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)
|
||||
@@ -11,12 +11,14 @@ import click
|
||||
\b
|
||||
# check for latest release
|
||||
fastanime update --check
|
||||
|
||||
# Force an update regardless of the current version
|
||||
fastanime update --force
|
||||
""",
|
||||
)
|
||||
@click.option("--check", "-c", help="Check for the latest release", is_flag=True)
|
||||
def update(
|
||||
check,
|
||||
):
|
||||
@click.option("--force", "-c", help="Force update", is_flag=True)
|
||||
def update(check, force):
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
|
||||
@@ -45,7 +47,7 @@ def update(
|
||||
print(f"You are running the latest version ({__version__}) of fastanime")
|
||||
_print_release(github_release_data)
|
||||
else:
|
||||
success, github_release_data = update_app()
|
||||
success, github_release_data = update_app(force)
|
||||
_print_release(github_release_data)
|
||||
if success:
|
||||
print("Successfully updated")
|
||||
|
||||
@@ -26,7 +26,7 @@ class Config(object):
|
||||
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
|
||||
)
|
||||
anime_provider: "AnimeProvider"
|
||||
user_data = {"watch_history": {}, "animelist": [], "user": {}}
|
||||
user_data = {"recent_anime": [], "animelist": [], "user": {}}
|
||||
default_config = {
|
||||
"auto_next": "False",
|
||||
"auto_select": "True",
|
||||
@@ -40,6 +40,7 @@ class Config(object):
|
||||
"force_window": "immediate",
|
||||
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
||||
"icons": "false",
|
||||
"image_previews": "true",
|
||||
"normalize_titles": "true",
|
||||
"notification_duration": "2",
|
||||
"player": "mpv",
|
||||
@@ -48,6 +49,7 @@ class Config(object):
|
||||
"preview": "False",
|
||||
"provider": "allanime",
|
||||
"quality": "1080",
|
||||
"recent": "50",
|
||||
"rofi_theme": "",
|
||||
"rofi_theme_confirm": "",
|
||||
"rofi_theme_input": "",
|
||||
@@ -63,7 +65,7 @@ class Config(object):
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.initialize_user_data_and_watch_history()
|
||||
self.initialize_user_data_and_watch_history_recent_anime()
|
||||
self.load_config()
|
||||
|
||||
def load_config(self):
|
||||
@@ -88,6 +90,7 @@ class Config(object):
|
||||
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()
|
||||
@@ -97,6 +100,7 @@ class Config(object):
|
||||
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()
|
||||
@@ -134,6 +138,20 @@ class Config(object):
|
||||
self.user_data["user"] = user
|
||||
self._update_user_data()
|
||||
|
||||
def update_recent(self, recent_anime: list):
|
||||
recent_anime_ids = []
|
||||
_recent_anime = []
|
||||
for anime in recent_anime[::-1]:
|
||||
if (
|
||||
anime["id"] not in recent_anime_ids
|
||||
and len(recent_anime_ids) <= self.recent
|
||||
):
|
||||
_recent_anime.append(anime)
|
||||
recent_anime_ids.append(anime["id"])
|
||||
|
||||
self.user_data["recent_anime"] = _recent_anime
|
||||
self._update_user_data()
|
||||
|
||||
def media_list_track(
|
||||
self,
|
||||
anime_id: int,
|
||||
@@ -155,7 +173,7 @@ class Config(object):
|
||||
with open(USER_WATCH_HISTORY_PATH, "w") as f:
|
||||
json.dump(self.watch_history, f)
|
||||
|
||||
def initialize_user_data_and_watch_history(self):
|
||||
def initialize_user_data_and_watch_history_recent_anime(self):
|
||||
try:
|
||||
if os.path.isfile(USER_DATA_PATH):
|
||||
with open(USER_DATA_PATH, "r") as f:
|
||||
@@ -197,6 +215,9 @@ class Config(object):
|
||||
def get_icons(self):
|
||||
return self.configparser.getboolean("general", "icons")
|
||||
|
||||
def get_image_previews(self):
|
||||
return self.configparser.getboolean("general", "image_previews")
|
||||
|
||||
def get_preview(self):
|
||||
return self.configparser.getboolean("general", "preview")
|
||||
|
||||
@@ -231,6 +252,9 @@ class Config(object):
|
||||
def get_normalize_titles(self):
|
||||
return self.configparser.getboolean("general", "normalize_titles")
|
||||
|
||||
def get_recent(self):
|
||||
return self.configparser.getint("general", "recent")
|
||||
|
||||
# --- stream section ---
|
||||
def get_skip(self):
|
||||
return self.configparser.getboolean("stream", "skip")
|
||||
@@ -301,13 +325,6 @@ class Config(object):
|
||||
# be sure to also give the replacement emoji
|
||||
icons = {self.icons}
|
||||
|
||||
# the quality of the stream [1080,720,480,360]
|
||||
# this option is usually only reliable when:
|
||||
# provider=animepahe
|
||||
# since it provides links that actually point to streams of different qualities
|
||||
# while the rest just point to another link that can provide the anime from the same server
|
||||
quality = {self.quality}
|
||||
|
||||
# whether to normalize provider titles [True/False]
|
||||
# basically takes the provider titles and finds the corresponding anilist title then changes the title to that
|
||||
# useful for uniformity especially when downloading from different providers
|
||||
@@ -337,6 +354,9 @@ downloads_dir = {self.downloads_dir}
|
||||
# try it and you will see
|
||||
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]
|
||||
# -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
|
||||
@@ -400,8 +420,19 @@ cache_requests = {self.cache_requests}
|
||||
# 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]
|
||||
# the quality of the stream [1080,720,480,360]
|
||||
# this option is usually only reliable when:
|
||||
# provider=animepahe
|
||||
# since it provides links that actually point to streams of different qualities
|
||||
# while the rest just point to another link that can provide the anime from the same server
|
||||
quality = {self.quality}
|
||||
|
||||
# Auto continue from watch history [True/False]
|
||||
# this will make fastanime to choose the episode that you last watched to completion
|
||||
# and increment it by one
|
||||
|
||||
@@ -539,6 +539,14 @@ def provider_anime_episode_servers_menu(
|
||||
episode_title = episode_detail["title"]
|
||||
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:
|
||||
from ..utils.syncplay import SyncPlayer
|
||||
|
||||
@@ -1420,7 +1428,7 @@ def anilist_results_menu(
|
||||
choices = []
|
||||
for title in anime_data.keys():
|
||||
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")
|
||||
selected_anime_title = Rofi.run_with_icons(choices, "Select Anime")
|
||||
else:
|
||||
@@ -1562,6 +1570,9 @@ def fastanime_main_menu(
|
||||
watch_history = list(map(int, config.watch_history.keys()))
|
||||
return AniList.search(id_in=watch_history, sort="TRENDING_DESC")
|
||||
|
||||
def _recent():
|
||||
return (True, {"data": {"Page": {"media": config.user_data["recent_anime"]}}})
|
||||
|
||||
# WARNING: Will probably be depracated
|
||||
def _anime_list():
|
||||
anime_list = config.anime_list
|
||||
@@ -1589,6 +1600,7 @@ def fastanime_main_menu(
|
||||
# each option maps to anilist data that is described by the option name
|
||||
options = {
|
||||
f"{'🔥 ' if icons else ''}Trending": AniList.get_trending,
|
||||
f"{'🎞️ ' if icons else ''}Recent": _recent,
|
||||
f"{'📺 ' if icons else ''}Watching": lambda media_list_type="Watching": handle_animelist(
|
||||
config, fastanime_runtime_state, media_list_type
|
||||
),
|
||||
|
||||
@@ -9,7 +9,7 @@ from threading import Thread
|
||||
import requests
|
||||
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 ...Utility import anilist_data_helper
|
||||
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
|
||||
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")
|
||||
if not os.path.exists(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
|
||||
"""
|
||||
image = requests.get(url)
|
||||
with open(f"{IMAGES_CACHE_DIR}/{file_name}", "wb") as f:
|
||||
with open(f"{IMAGES_CACHE_DIR}/{file_name}.png", "wb") as f:
|
||||
f.write(image.content)
|
||||
|
||||
|
||||
@@ -91,18 +93,16 @@ def write_search_results(
|
||||
workers:number of threads to use defaults to as many as possible
|
||||
"""
|
||||
# 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
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
future_to_task = {}
|
||||
for anime, title in zip(anilist_results, titles):
|
||||
# actual image url
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_task[executor.submit(save_image_from_url, image_url, title)] = (
|
||||
image_url
|
||||
)
|
||||
if os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower() == "true":
|
||||
image_url = anime["coverImage"]["large"]
|
||||
future_to_task[
|
||||
executor.submit(save_image_from_url, image_url, title)
|
||||
] = image_url
|
||||
|
||||
mediaListName = "Not in any of your lists"
|
||||
progress = "UNKNOWN"
|
||||
@@ -111,28 +111,57 @@ def write_search_results(
|
||||
progress = anime_list["progress"]
|
||||
# handle the text data
|
||||
template = f"""
|
||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
||||
{get_true_fg('Title(jp):',*HEADER_COLOR)} {anime['title']['romaji']}
|
||||
{get_true_fg('Title(eng):',*HEADER_COLOR)} {anime['title']['english']}
|
||||
{get_true_fg('Popularity:',*HEADER_COLOR)} {anime['popularity']}
|
||||
{get_true_fg('Favourites:',*HEADER_COLOR)} {anime['favourites']}
|
||||
{get_true_fg('Status:',*HEADER_COLOR)} {anime['status']}
|
||||
{get_true_fg('Episodes:',*HEADER_COLOR)} {anime['episodes']}
|
||||
{get_true_fg('Genres:',*HEADER_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime['genres'])}
|
||||
{get_true_fg('Next Episode:',*HEADER_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode'])}
|
||||
{get_true_fg('Start Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['startDate'])}
|
||||
{get_true_fg('End Date:',*HEADER_COLOR)} {anilist_data_helper.format_anilist_date_object(anime['endDate'])}
|
||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
||||
{get_true_fg('Media List:',*HEADER_COLOR)} {mediaListName}
|
||||
{get_true_fg('Progress:',*HEADER_COLOR)} {progress}
|
||||
{get_true_fg("-"*SEPARATOR_WIDTH,*SEPARATOR_COLOR,bold=False)}
|
||||
{get_true_fg('Description:',*HEADER_COLOR)}
|
||||
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('Title(jp):',*HEADER_COLOR)} {(anime['title']['romaji'] or "").replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Title(eng):',*HEADER_COLOR)} {(anime['title']['english'] or "").replace('"',SINGLE_QUOTE)}"
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{get_true_fg("─",*SEPARATOR_COLOR,bold=False)}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
echo "{get_true_fg('Popularity:',*HEADER_COLOR)} {anilist_data_helper.format_number_with_commas(anime['popularity'])}"
|
||||
echo "{get_true_fg('Favourites:',*HEADER_COLOR)} {anilist_data_helper.format_number_with_commas(anime['favourites'])}"
|
||||
echo "{get_true_fg('Status:',*HEADER_COLOR)} {str(anime['status']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Next Episode:',*HEADER_COLOR)} {anilist_data_helper.extract_next_airing_episode(anime['nextAiringEpisode']).replace('"',SINGLE_QUOTE)}"
|
||||
echo "{get_true_fg('Genres:',*HEADER_COLOR)} {anilist_data_helper.format_list_data_with_comma(anime['genres']).replace('"',SINGLE_QUOTE)}"
|
||||
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 = f"""
|
||||
{template}
|
||||
echo "
|
||||
{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
|
||||
|
||||
@@ -212,6 +241,7 @@ def get_fzf_manga_preview(manga_results, workers=None, wait=False):
|
||||
background_worker = Thread(
|
||||
target=_worker,
|
||||
)
|
||||
background_worker.daemon = True
|
||||
# ensure images and info exists
|
||||
background_worker.start()
|
||||
|
||||
@@ -269,8 +299,13 @@ def get_fzf_episode_preview(
|
||||
] = image_url
|
||||
template = textwrap.dedent(
|
||||
f"""
|
||||
{get_true_fg('Anime Title:',*HEADER_COLOR)} {anilist_result['title']['romaji'] or anilist_result['title']['english']}
|
||||
{get_true_fg('Episode Title:',*HEADER_COLOR)} {episode_title}
|
||||
ll=2
|
||||
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[
|
||||
@@ -288,26 +323,61 @@ def get_fzf_episode_preview(
|
||||
background_worker = Thread(
|
||||
target=_worker,
|
||||
)
|
||||
background_worker.daemon = True
|
||||
# ensure images and info exists
|
||||
background_worker.start()
|
||||
|
||||
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
|
||||
os.environ["SHELL"] = shutil.which("bash") or "bash"
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||
else echo Loading...
|
||||
fi
|
||||
if [ -s %s/{} ]; then cat %s/{}
|
||||
else echo Loading...
|
||||
fi
|
||||
""" % (
|
||||
fzf_preview,
|
||||
IMAGES_CACHE_DIR,
|
||||
IMAGES_CACHE_DIR,
|
||||
ANIME_INFO_CACHE_DIR,
|
||||
ANIME_INFO_CACHE_DIR,
|
||||
)
|
||||
if S_PLATFORM == "win32":
|
||||
preview = """
|
||||
%s
|
||||
title={}
|
||||
show_image_previews="%s"
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [ $show_image_previews = "true" ];then
|
||||
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||
if command -v "chafa">/dev/null;then
|
||||
chafa -s $dim "%s\\\\\\${title}.png"
|
||||
else
|
||||
echo please install chafa to enjoy image previews
|
||||
fi
|
||||
echo
|
||||
else
|
||||
echo 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:
|
||||
background_worker.join()
|
||||
return preview
|
||||
@@ -327,11 +397,11 @@ def get_fzf_anime_preview(
|
||||
THe fzf preview script to use
|
||||
"""
|
||||
# ensure images and info exists
|
||||
from ...constants import S_PLATFORM
|
||||
|
||||
background_worker = Thread(
|
||||
target=write_search_results, args=(anilist_results, titles)
|
||||
)
|
||||
background_worker.daemon = True
|
||||
background_worker.start()
|
||||
|
||||
# the preview script is in bash so making sure fzf doesnt use any other shell lang to process the preview script
|
||||
@@ -340,18 +410,26 @@ def get_fzf_anime_preview(
|
||||
preview = """
|
||||
%s
|
||||
title={}
|
||||
show_image_previews="%s"
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [ -s "%s\\\\\\$title" ]; then
|
||||
if command -v chafa >/dev/null;then
|
||||
chafa -f kitty -s $dim "%s\\\\\\$title"
|
||||
if [ $show_image_previews = "true" ];then
|
||||
if [ -s "%s\\\\\\${title}.png" ]; then
|
||||
if command -v "chafa">/dev/null;then
|
||||
chafa -s $dim "%s\\\\\\${title}.png"
|
||||
else
|
||||
echo please install chafa to enjoy image previews
|
||||
fi
|
||||
echo
|
||||
else
|
||||
echo Loading...
|
||||
fi
|
||||
fi
|
||||
else echo Loading...
|
||||
fi
|
||||
if [ -s "%s\\\\\\$title" ]; then cat "%s\\\\\\$title"
|
||||
else echo Loading...
|
||||
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("\\", "\\\\\\"),
|
||||
@@ -360,14 +438,19 @@ def get_fzf_anime_preview(
|
||||
else:
|
||||
preview = """
|
||||
%s
|
||||
if [ -s %s/{} ]; then fzf-preview %s/{}
|
||||
else echo Loading...
|
||||
title={}
|
||||
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
|
||||
if [ -s %s/{} ]; then cat %s/{}
|
||||
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,
|
||||
IMAGES_CACHE_DIR,
|
||||
ANIME_INFO_CACHE_DIR,
|
||||
|
||||
@@ -225,6 +225,7 @@ query ($userId: Int, $status: MediaListStatus, $type: MediaType) {
|
||||
averageScore
|
||||
episodes
|
||||
genres
|
||||
synonyms
|
||||
studios {
|
||||
nodes {
|
||||
name
|
||||
@@ -369,6 +370,7 @@ query($query:String,%s){
|
||||
averageScore
|
||||
episodes
|
||||
genres
|
||||
synonyms
|
||||
studios {
|
||||
nodes {
|
||||
name
|
||||
@@ -428,6 +430,7 @@ query ($type: MediaType) {
|
||||
favourites
|
||||
averageScore
|
||||
genres
|
||||
synonyms
|
||||
episodes
|
||||
description
|
||||
studios {
|
||||
@@ -503,6 +506,7 @@ query ($type: MediaType) {
|
||||
episodes
|
||||
description
|
||||
genres
|
||||
synonyms
|
||||
studios {
|
||||
nodes {
|
||||
name
|
||||
@@ -566,6 +570,7 @@ query ($type: MediaType) {
|
||||
averageScore
|
||||
description
|
||||
genres
|
||||
synonyms
|
||||
studios {
|
||||
nodes {
|
||||
name
|
||||
@@ -624,6 +629,7 @@ query ($type: MediaType) {
|
||||
description
|
||||
episodes
|
||||
genres
|
||||
synonyms
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
@@ -698,6 +704,7 @@ query ($type: MediaType) {
|
||||
averageScore
|
||||
description
|
||||
genres
|
||||
synonyms
|
||||
episodes
|
||||
studios {
|
||||
nodes {
|
||||
@@ -759,6 +766,7 @@ query ($type: MediaType) {
|
||||
id
|
||||
}
|
||||
genres
|
||||
synonyms
|
||||
averageScore
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
@@ -862,6 +870,7 @@ query ($id: Int, $type: MediaType) {
|
||||
id
|
||||
}
|
||||
genres
|
||||
synonyms
|
||||
averageScore
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
@@ -954,6 +963,7 @@ query ($page: Int, $type: MediaType) {
|
||||
favourites
|
||||
averageScore
|
||||
genres
|
||||
synonyms
|
||||
episodes
|
||||
description
|
||||
studios {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
@@ -49,7 +50,7 @@ class CachedRequestsSession(requests.Session):
|
||||
def __init__(
|
||||
self,
|
||||
cache_db_path: str,
|
||||
max_lifetime: int = 604800,
|
||||
max_lifetime: int = 259200,
|
||||
max_size: int = (1024**2) * 10,
|
||||
table_name: str = "fastanime_requests_cache",
|
||||
clean_db=False,
|
||||
@@ -89,16 +90,10 @@ class CachedRequestsSession(requests.Session):
|
||||
url,
|
||||
params=None,
|
||||
force_caching=False,
|
||||
fresh=0,
|
||||
fresh=int(os.environ.get("FASTANIME_FRESH_REQUESTS", 0)),
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
# TODO: improve the caching functionality and add a layer to auto delete
|
||||
# expired requests
|
||||
if fresh:
|
||||
logger.debug("Executing fresh request")
|
||||
return super().request(method, url, params=params, *args, **kwargs)
|
||||
|
||||
if params:
|
||||
url += "?" + urlencode(params)
|
||||
|
||||
@@ -128,7 +123,7 @@ class CachedRequestsSession(requests.Session):
|
||||
cached_request = cursor.fetchone()
|
||||
time_after_access_db = datetime.now()
|
||||
|
||||
if cached_request:
|
||||
if cached_request and not fresh:
|
||||
logger.debug("Found existing request in cache")
|
||||
(
|
||||
status_code,
|
||||
|
||||
@@ -49,7 +49,7 @@ class FZF:
|
||||
"--info=hidden",
|
||||
"--layout=reverse",
|
||||
"--height=100%",
|
||||
"--bind=right:accept",
|
||||
"--bind=right:accept,ctrl-/:toggle-preview,ctrl-space:toggle-wrap+toggle-preview-wrap",
|
||||
"--no-margin",
|
||||
"+m",
|
||||
"-i",
|
||||
|
||||
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"
|
||||
1365
poetry.lock
generated
1365
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,41 +1,44 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "fastanime"
|
||||
version = "2.6.0"
|
||||
version = "2.6.9"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"click>=8.1.7",
|
||||
"inquirerpy>=0.3.4",
|
||||
"rich>=13.9.2",
|
||||
"thefuzz>=0.22.1",
|
||||
"yt-dlp>=2024.10.7",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
yt-dlp = "^2024.5.27"
|
||||
thefuzz = "^0.22.1"
|
||||
requests = "^2.32.3"
|
||||
rich = { version = "^13.7.1", optional = false }
|
||||
click = { version = "^8.1.7", optional = false }
|
||||
inquirerpy = { version = "^0.3.4", optional = false }
|
||||
mpv = { version = "^1.0.7", optional = true }
|
||||
plyer = { version = "^2.1.0", optional = true }
|
||||
|
||||
[tool.poetry.extras]
|
||||
full = ["plyer", "mpv"]
|
||||
# cli = ["rich", "click", "inquirerpy"]
|
||||
mpv = ["mpv"]
|
||||
notifications = ["plyer"]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.4.2"
|
||||
isort = "^5.13.2"
|
||||
pytest = "^8.2.2"
|
||||
ruff = "^0.4.10"
|
||||
pre-commit = "^3.7.1"
|
||||
autoflake = "^2.3.1"
|
||||
tox = "^4.16.0"
|
||||
|
||||
pyright = "^1.1.374"
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
[project.scripts]
|
||||
fastanime = 'fastanime:FastAnime'
|
||||
|
||||
[project.optional-dependencies]
|
||||
standard = [
|
||||
"fastapi[standard]>=0.115.0",
|
||||
"mpv>=1.0.7",
|
||||
"plyer>=2.1.0",
|
||||
]
|
||||
api = [
|
||||
"fastapi[standard]>=0.115.0",
|
||||
]
|
||||
notifications = [
|
||||
"plyer>=2.1.0",
|
||||
]
|
||||
mpv = [
|
||||
"mpv>=1.0.7",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"pyright>=1.1.384",
|
||||
"pytest>=8.3.3",
|
||||
"ruff>=0.6.9",
|
||||
]
|
||||
|
||||
18
tox.ini
18
tox.ini
@@ -5,23 +5,23 @@ env_list = lint, pyright, py{310,311}
|
||||
|
||||
[testenv]
|
||||
description = run unit tests
|
||||
deps =poetry
|
||||
deps =uv
|
||||
commands =
|
||||
poetry install --all-extras
|
||||
poetry run pytest
|
||||
uv sync --dev --all-extras
|
||||
uv run pytest
|
||||
|
||||
[testenv:lint]
|
||||
description = run linters
|
||||
skip_install = true
|
||||
deps =poetry
|
||||
deps =uv
|
||||
commands =
|
||||
poetry install --all-extras
|
||||
poetry run black .
|
||||
uv sync --dev --all-extras
|
||||
uv run ruff format .
|
||||
|
||||
[testenv:pyright]
|
||||
description = run type checking
|
||||
skip_install = true
|
||||
deps =poetry
|
||||
deps =uv
|
||||
commands =
|
||||
poetry install --no-root --all-extras
|
||||
poetry run pyright
|
||||
uv sync --dev --all-extras
|
||||
uv run pyright
|
||||
|
||||
Reference in New Issue
Block a user