Compare commits

...

41 Commits

Author SHA1 Message Date
Benex254
1ee50e8a55 chore: bump version (v2.6.9) 2024-10-20 10:06:02 +03:00
Benex254
ae95c5ea3d docs: update readme 2024-10-20 10:04:58 +03:00
Benex254
d64ad5e11d fix: move quality to stream section in config 2024-10-20 10:03:49 +03:00
Benex254
d1a47c6d44 chore: bump version (v2.6.8) 2024-10-18 22:59:18 +03:00
Benex254
51a834a62f chore: update deps 2024-10-18 22:53:39 +03:00
Benex254
3a030bf6f7 feat: add ability to update fastanime uv installations 2024-10-18 22:53:26 +03:00
Benex254
eb6a6fc82c chore: use uv in fa script 2024-10-18 22:46:44 +03:00
Benex254
437ccd94e4 ci: update to use uv 2024-10-18 22:37:14 +03:00
Benex254
d65868cc30 chore: update workflows to work with uv 2024-10-18 21:50:20 +03:00
Benex254
8678aa6544 Merge branch 'master' into uv 2024-10-18 20:26:55 +03:00
Benex254
00e5141152 chore: bump version (v2.6.7) 2024-10-12 01:08:14 +03:00
Benex254
90e757dfe1 feat: init switch to uv 2024-10-11 11:57:29 +03:00
Benex254
8b471b08e8 chore: init switch to uv 2024-10-11 10:52:18 +03:00
Benex254
158bc5710f docs: update readme 2024-10-11 10:49:53 +03:00
Benex254
a0b946a13d feat: add recent menu 2024-10-11 10:22:23 +03:00
Benex254
b547b75f03 feat: add environment variable that force updating of the cache db 2024-10-11 09:34:40 +03:00
Benex254
58c7427a47 feat(cli:serve): use the full executable path to python 2024-10-06 01:25:22 +03:00
Benex254
6220b9c55d chore: bump version (v2.6.6) 2024-10-06 01:15:15 +03:00
Benex254
6b9b5c131c fix(cli): use str instead of ints in serve 2024-10-06 01:15:05 +03:00
Benex254
212f2af39c chore: bump version (v2.6.5) 2024-10-06 01:05:28 +03:00
Benex254
f7b2b4e0c9 feat: add serve command 2024-10-06 01:04:20 +03:00
Benedict Xavier
a747529279 Update README.md 2024-10-05 19:37:19 +03:00
Benex254
1dfdcc27ce chore: bump version (v2.6.4) 2024-10-05 12:33:32 +03:00
Benex254
3c03289453 fix: add git push to make_release 2024-10-05 12:33:23 +03:00
Benex254
06fd446a72 chore: bump version (v2.6.3) 2024-10-05 12:29:29 +03:00
Benex254
172d912d8b chore(release): improve the make release script to also stage changes after bumping version 2024-10-05 12:29:15 +03:00
Benex254
2396018607 feat: make script to automate releases 2024-10-05 12:19:03 +03:00
Benex254
a9be9779c5 feat(fa): improve fa script 2024-10-05 12:14:45 +03:00
Benex254
2f76b26a99 feat(fzf): add some bindings 2024-10-05 11:54:22 +03:00
Benex254
2fe5edf810 feat(cli): make all threads daemon threads 2024-10-05 11:47:52 +03:00
Benex254
d67ee6a779 feat(downloader): add progress hook option to be passed to yt-dlp 2024-10-05 11:47:30 +03:00
Benex254
e06ec5dbd4 feat(cli): make the image previews optional 2024-10-05 11:31:13 +03:00
Benex254
c1b24ba2aa feat(cli): save images with .png extenstion to enable easier viewing by external apps 2024-10-05 11:05:07 +03:00
Benex254
59e9cf9fd0 feat: improve previews 2024-10-05 10:12:14 +03:00
Benex254
58761f5b96 chore: bump version 2024-10-04 19:44:54 +03:00
Benex254
ac959da229 feat: renable bg downloading function 2024-10-04 19:42:53 +03:00
benex
bacc8c48ec fix: image previews not showing up on windows 2024-10-04 11:03:54 +03:00
Benex254
905a159428 chore: add a mapping for re:zero s3 in normalizer 2024-10-03 15:09:51 +03:00
Benex254
20f734cab2 feat: also compare synonymns 2024-10-03 15:09:14 +03:00
Benex254
7c2c644aef chore: bump version 2024-10-03 14:18:22 +03:00
Benex254
0efc92081a feat: use .get in normlizer 2024-10-03 14:18:05 +03:00
29 changed files with 1789 additions and 1576 deletions

View File

@@ -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 --all-extras 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

View File

@@ -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

View File

@@ -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 --all-extras 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

View File

@@ -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"]

View File

@@ -8,12 +8,12 @@
Welcome to **FastAnime**, anime site experience from the terminal. Welcome to **FastAnime**, anime site experience from the terminal.
![fastanime-demo](https://github.com/user-attachments/assets/16e29f54-e9fa-48c7-b944-bfacb31ae1b5) ![fastanime](https://github.com/user-attachments/assets/9ab09f26-e4a8-4b70-a315-7def998cec63)
<details> <details>
<summary><b>fzf mode</b></summary> <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> </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 ### Installation using your favourite package manager
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/). Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
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 #### Using pipx
Preferred method of installation since [Pipx](https://github.com/pypa/pipx) creates an isolated environment for each app it installs.
```bash ```bash
pipx install fastanime pipx install fastanime
@@ -133,7 +153,7 @@ Requirements:
- [git](https://git-scm.com/) - [git](https://git-scm.com/)
- [python 3.10 and above](https://www.python.org/) - [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: 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: 3. Then build and Install the app:
```bash ```bash
# Normal Installation # build and install fastanime with uv
poetry build uv tool install .
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 .
``` ```
4. Enjoy! Verify installation with: 4. Enjoy! Verify installation with:
@@ -167,6 +180,7 @@ fastanime --version
> - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/` > - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/`
> - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc` > - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc`
> - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc` > - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc`
> or using the built in command `fastanime completions`
### External Dependencies ### External Dependencies
@@ -637,6 +651,19 @@ fastanime completions --bash
fastanime completions --zsh 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 ### 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. 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 use_persistent_provider_store = False
recent = 50
[stream] [stream]
continue_from_history = True continue_from_history = True

5
fa
View File

@@ -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 "$@"

View File

@@ -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'])}"

View File

@@ -9,6 +9,7 @@ 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",
}, },
"hianime": {"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"},
@@ -20,7 +21,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["FASTANIME_PROVIDER"] current_provider = os.environ.get("FASTANIME_PROVIDER", "allanime")
return anime_normalizer_raw[current_provider] return anime_normalizer_raw[current_provider]

View File

@@ -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,
@@ -86,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:
@@ -174,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:
@@ -183,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()

View File

@@ -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()),
) )

View File

@@ -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.6.0" __version__ = "v2.6.9"
APP_NAME = "FastAnime" APP_NAME = "FastAnime"
AUTHOR = "Benex254" AUTHOR = "Benex254"

25
fastanime/api/__init__.py Normal file
View 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)

View File

@@ -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",
} }
@@ -177,6 +178,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,
@@ -211,7 +215,10 @@ def run_cli(
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()
@@ -250,6 +257,8 @@ 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:

View File

@@ -75,9 +75,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 +101,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

View File

@@ -361,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,

View File

@@ -169,6 +169,7 @@ def downloads(
from threading import Thread from threading import Thread
worker = Thread(target=_worker) worker = Thread(target=_worker)
worker.daemon = True
worker.start() worker.start()
else: else:
_worker() _worker()
@@ -241,6 +242,7 @@ def downloads(
from threading import Thread from threading import Thread
worker = Thread(target=_worker) worker = Thread(target=_worker)
worker.daemon = True
worker.start() worker.start()
else: else:
_worker() _worker()

View 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)

View File

@@ -11,12 +11,14 @@ import click
\b \b
# check for latest release # check for latest release
fastanime update --check 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
@@ -45,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")

View File

@@ -26,7 +26,7 @@ class Config(object):
"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_config = { default_config = {
"auto_next": "False", "auto_next": "False",
"auto_select": "True", "auto_select": "True",
@@ -40,6 +40,7 @@ class Config(object):
"force_window": "immediate", "force_window": "immediate",
"format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best", "format": "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
"icons": "false", "icons": "false",
"image_previews": "true",
"normalize_titles": "true", "normalize_titles": "true",
"notification_duration": "2", "notification_duration": "2",
"player": "mpv", "player": "mpv",
@@ -48,6 +49,7 @@ class Config(object):
"preview": "False", "preview": "False",
"provider": "allanime", "provider": "allanime",
"quality": "1080", "quality": "1080",
"recent": "50",
"rofi_theme": "", "rofi_theme": "",
"rofi_theme_confirm": "", "rofi_theme_confirm": "",
"rofi_theme_input": "", "rofi_theme_input": "",
@@ -63,7 +65,7 @@ class Config(object):
} }
def __init__(self) -> None: def __init__(self) -> None:
self.initialize_user_data_and_watch_history() self.initialize_user_data_and_watch_history_recent_anime()
self.load_config() self.load_config()
def load_config(self): def load_config(self):
@@ -88,6 +90,7 @@ class Config(object):
self.force_window = self.get_force_window() self.force_window = self.get_force_window()
self.format = self.get_format() self.format = self.get_format()
self.icons = self.get_icons() self.icons = self.get_icons()
self.image_previews = self.get_image_previews()
self.normalize_titles = self.get_normalize_titles() self.normalize_titles = self.get_normalize_titles()
self.notification_duration = self.get_notification_duration() self.notification_duration = self.get_notification_duration()
self.player = self.get_player() self.player = self.get_player()
@@ -97,6 +100,7 @@ class Config(object):
self.provider = self.get_provider() self.provider = self.get_provider()
self.quality = self.get_quality() self.quality = self.get_quality()
self.recent = self.get_recent()
self.rofi_theme_confirm = self.get_rofi_theme_confirm() self.rofi_theme_confirm = self.get_rofi_theme_confirm()
self.rofi_theme_input = self.get_rofi_theme_input() self.rofi_theme_input = self.get_rofi_theme_input()
self.rofi_theme = self.get_rofi_theme() self.rofi_theme = self.get_rofi_theme()
@@ -134,6 +138,20 @@ class Config(object):
self.user_data["user"] = user self.user_data["user"] = user
self._update_user_data() 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( def media_list_track(
self, self,
anime_id: int, anime_id: int,
@@ -155,7 +173,7 @@ class Config(object):
with open(USER_WATCH_HISTORY_PATH, "w") as f: with open(USER_WATCH_HISTORY_PATH, "w") as f:
json.dump(self.watch_history, 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: 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:
@@ -197,6 +215,9 @@ 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")
@@ -231,6 +252,9 @@ class Config(object):
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")
@@ -301,13 +325,6 @@ 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
@@ -337,6 +354,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
@@ -400,8 +420,19 @@ cache_requests = {self.cache_requests}
# leave it as is # leave it as is
use_persistent_provider_store = {self.use_persistent_provider_store} use_persistent_provider_store = {self.use_persistent_provider_store}
# no of recent anime to keep [0-50]
# 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

View File

@@ -539,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
@@ -1420,7 +1428,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:
@@ -1562,6 +1570,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
@@ -1589,6 +1600,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
), ),

View File

@@ -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(f"{IMAGES_CACHE_DIR}/{file_name}.png", "wb") as f:
f.write(image.content) f.write(image.content)
@@ -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,6 +241,7 @@ def get_fzf_manga_preview(manga_results, workers=None, wait=False):
background_worker = Thread( background_worker = Thread(
target=_worker, target=_worker,
) )
background_worker.daemon = True
# ensure images and info exists # ensure images and info exists
background_worker.start() background_worker.start()
@@ -269,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[
@@ -288,26 +323,61 @@ def get_fzf_episode_preview(
background_worker = Thread( background_worker = Thread(
target=_worker, target=_worker,
) )
background_worker.daemon = True
# ensure images and info exists # 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
@@ -327,11 +397,11 @@ 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)
) )
background_worker.daemon = True
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
@@ -340,18 +410,26 @@ 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,
os.environ.get("FASTANIME_IMAGE_PREVIEWS", "true").lower(),
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"), IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
IMAGES_CACHE_DIR.replace("\\", "\\\\\\"), IMAGES_CACHE_DIR.replace("\\", "\\\\\\"),
ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"), ANIME_INFO_CACHE_DIR.replace("\\", "\\\\\\"),
@@ -360,14 +438,19 @@ def get_fzf_anime_preview(
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,

View File

@@ -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 {

View File

@@ -1,5 +1,6 @@
import json import json
import logging import logging
import os
import re import re
import time import time
from datetime import datetime from datetime import datetime
@@ -49,7 +50,7 @@ class CachedRequestsSession(requests.Session):
def __init__( def __init__(
self, self,
cache_db_path: str, cache_db_path: str,
max_lifetime: int = 604800, max_lifetime: int = 259200,
max_size: int = (1024**2) * 10, max_size: int = (1024**2) * 10,
table_name: str = "fastanime_requests_cache", table_name: str = "fastanime_requests_cache",
clean_db=False, clean_db=False,
@@ -89,16 +90,10 @@ class CachedRequestsSession(requests.Session):
url, url,
params=None, params=None,
force_caching=False, force_caching=False,
fresh=0, fresh=int(os.environ.get("FASTANIME_FRESH_REQUESTS", 0)),
*args, *args,
**kwargs, **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: if params:
url += "?" + urlencode(params) url += "?" + urlencode(params)
@@ -128,7 +123,7 @@ class CachedRequestsSession(requests.Session):
cached_request = cursor.fetchone() cached_request = cursor.fetchone()
time_after_access_db = datetime.now() time_after_access_db = datetime.now()
if cached_request: if cached_request and not fresh:
logger.debug("Found existing request in cache") logger.debug("Found existing request in cache")
( (
status_code, status_code,

View File

@@ -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",

11
make_release Executable file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,44 @@
[tool.poetry] [project]
name = "fastanime" name = "fastanime"
version = "2.6.0" version = "2.6.9"
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",
"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"
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]
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 = [
"pyright>=1.1.384",
"pytest>=8.3.3",
"ruff>=0.6.9",
]

18
tox.ini
View File

@@ -5,23 +5,23 @@ env_list = lint, pyright, py{310,311}
[testenv] [testenv]
description = run unit tests description = run unit tests
deps =poetry deps =uv
commands = commands =
poetry install --all-extras 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 --all-extras 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 --all-extras uv sync --dev --all-extras
poetry run pyright uv run pyright

1315
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff