mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-26 20:53:34 -08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
603efd56e8 | ||
|
|
4d74dfa339 | ||
|
|
4681e38153 | ||
|
|
242003500d | ||
|
|
66ab365657 | ||
|
|
bc8d7b2e28 | ||
|
|
db3a1f7175 | ||
|
|
3a51e0225e | ||
|
|
7b3388939c | ||
|
|
fcf875bdb2 | ||
|
|
0e4624297c | ||
|
|
91b54dfcb9 | ||
|
|
7011489ce5 | ||
|
|
316ca62ca4 |
80
README.md
80
README.md
@@ -2,18 +2,17 @@
|
||||
|
||||
Welcome to **FastAnime**, anime site experience from the terminal.
|
||||
|
||||
|
||||
**fzf mode**
|
||||
|
||||
[fa_fzf_demo.webm](https://github.com/user-attachments/assets/b1fecf25-e358-4e8b-a144-bcb7947210cf)
|
||||
|
||||
|
||||
**other modes:**
|
||||
|
||||
<details>
|
||||
<summary><b>rofi mode</b></summary>
|
||||
|
||||
[fa_rofi_mode.webm](https://github.com/user-attachments/assets/2ce669bf-b62f-4c44-bd79-cf0dcaddf37a)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -23,11 +22,8 @@ Welcome to **FastAnime**, anime site experience from the terminal.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magic-tape](https://gitlab.com/christosangel/magic-tape/-/tree/main?ref_type=heads) and [ani-cli](https://github.com/pystardust/ani-cli).
|
||||
|
||||
|
||||
|
||||
<!--toc:start-->
|
||||
|
||||
- [FastAnime](#fastanime)
|
||||
@@ -40,13 +36,17 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
||||
- [External Dependencies](#external-dependencies)
|
||||
- [Usage](#usage)
|
||||
- [The Commandline interface :fire:](#the-commandline-interface-fire)
|
||||
- [The anilist command](#the-anilist-command)
|
||||
- [The anilist command :fire: :fire: :fire:](#the-anilist-command-fire-fire-fire)
|
||||
- [Running without any subcommand](#running-without-any-subcommand)
|
||||
- [Subcommands](#subcommands)
|
||||
- [download subcommand](#download-subcommand)
|
||||
- [search subcommand](#search-subcommand)
|
||||
- [downloads subcommand](#downloads-subcommand)
|
||||
- [config subcommand](#config-subcommand)
|
||||
- [cache subcommand](#cache-subcommand)
|
||||
- [MPV specific commands](#mpv-specific-commands)
|
||||
- [Added keybindings](#added-keybindings)
|
||||
- [Added script messages](#added-script-messages)
|
||||
- [Configuration](#configuration)
|
||||
- [Contributing](#contributing)
|
||||
- [Receiving Support](#receiving-support)
|
||||
@@ -57,7 +57,6 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
||||
>
|
||||
> This project currently scrapes allanime and is in no way related to them. The site is in the public domain and can be access by any one with a browser.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
The app can run wherever python can run. So all you need to have is python installed on your device.
|
||||
@@ -75,12 +74,21 @@ Preferred method of installation since [Pipx](https://github.com/pypa/pipx) crea
|
||||
```bash
|
||||
|
||||
pipx install fastanime
|
||||
# -- or for the development version --
|
||||
pipx install 'fastanime==<latest-pre-release-tag>.dev1'
|
||||
# example
|
||||
# pipx install 'fastanime==0.60.1.dev1'
|
||||
|
||||
```
|
||||
|
||||
#### Using pip
|
||||
|
||||
```bash
|
||||
pip install fastanime
|
||||
# -- or for the development version --
|
||||
pip install 'fastanime==<latest-pre-release-tag>.dev1'
|
||||
# example
|
||||
# pip install 'fastanime==0.60.1.dev1'
|
||||
```
|
||||
|
||||
### Installing the bleeding edge version
|
||||
@@ -152,7 +160,7 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
|
||||
> everything you could ever need with a small footprint.
|
||||
> But if you have a reason feel free to encourage as to do so.
|
||||
|
||||
**Other dependencies that will just make your experience better:**
|
||||
**Other external dependencies that will just make your experience better:**
|
||||
|
||||
- [fzf](https://github.com/junegunn/fzf) :fire: which is used as a better alternative to the ui.
|
||||
- [chafa](https://github.com/hpjansson/chafa) currently the best cross platform and cross terminal image viewer for the terminal.
|
||||
@@ -181,6 +189,7 @@ Overview of main commands:
|
||||
- `fastanime search`: Powerful command meant for binging since it doesn't require the interfaces
|
||||
- `fastanime downloads`: View downloaded anime and watch with MPV.
|
||||
- `fastanime config`: Quickly edit configuration settings.
|
||||
- `fastanime cache`: Quickly manage the cache fastanime uses
|
||||
|
||||
Configuration is directly passed into this command at run time to override your config.
|
||||
|
||||
@@ -204,6 +213,9 @@ Available options include:
|
||||
- `--rofi-theme <path>` theme to use with rofi
|
||||
- `--rofi-theme-input <path>` theme to use with rofi input
|
||||
- `--rofi-theme-confirm <path>` theme to use with rofi confirm
|
||||
- `--log` allow logging to stdout
|
||||
- `--log-file` allow logging to a file
|
||||
- `--rich-traceback` allow rich traceback
|
||||
|
||||
#### The anilist command :fire: :fire: :fire:
|
||||
|
||||
@@ -331,11 +343,57 @@ fastanime config
|
||||
|
||||
# to get config path which is useful if you want to use it for another program.
|
||||
fastanime config --path
|
||||
|
||||
# add a desktop entry
|
||||
fastanime config --desktop-entry
|
||||
```
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` in case you don't know.
|
||||
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` .
|
||||
|
||||
#### cache subcommand
|
||||
|
||||
Easily manage the data fastanime has cached; for the previews.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# delete everything in the cache dir
|
||||
fastanime cache --clean
|
||||
|
||||
# print the path to the cache dir and exit
|
||||
fastanime cache --path
|
||||
|
||||
# print the current size of the cache dir and exit
|
||||
fastanime cache --size
|
||||
|
||||
# open the cache dir and exit
|
||||
fastanime cache
|
||||
```
|
||||
|
||||
## 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.
|
||||
This is all powered with [python-mpv]() which enables writing mpv scripts with python just like how it would be done in lua.
|
||||
|
||||
### Added keybindings
|
||||
|
||||
`<shift>+n` fetch the next episode
|
||||
|
||||
`<shift>+p` fetch the previous episode
|
||||
|
||||
`<shift>+t` toggle the translation type from dub to sub
|
||||
|
||||
`<shift>+a` toggle auto next episode
|
||||
|
||||
### Added script messages
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
script-message select-episode <episode-number>
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -356,6 +414,7 @@ skip=false
|
||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||
# used in the continue from time stamp
|
||||
error=3
|
||||
use_mpv_mod=True
|
||||
|
||||
# the format of downloaded anime and trailer
|
||||
# based on yt-dlp format and passed directly to it
|
||||
@@ -393,7 +452,7 @@ notification_duration=2
|
||||
|
||||
We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider.
|
||||
|
||||
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed.
|
||||
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed or if you are in a rush for the feature to be merged just open a pr.
|
||||
|
||||
## Receiving Support
|
||||
|
||||
@@ -405,7 +464,6 @@ For inquiries, join our [Discord Server](https://discord.gg/4NUTj5Pt).
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
## Supporting the Project
|
||||
|
||||
Show your support by starring our GitHub repository or [buying us a coffee](https://ko-fi.com/benex254).
|
||||
|
||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v0.50.0"
|
||||
__version__ = "v0.60.4"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
|
||||
@@ -33,3 +33,7 @@ def cache(clean, path, size):
|
||||
fp = os.path.join(dirpath, f)
|
||||
total_size += os.path.getsize(fp)
|
||||
print("Total Size: ", sizeof_fmt(total_size))
|
||||
else:
|
||||
import click
|
||||
|
||||
click.launch(APP_CACHE_DIR)
|
||||
|
||||
@@ -48,6 +48,7 @@ class Config(object):
|
||||
"rofi_theme_input": "",
|
||||
"rofi_theme_confirm": "",
|
||||
"use_mpv_mod": "true",
|
||||
"force_window": "immediate",
|
||||
}
|
||||
)
|
||||
self.configparser.add_section("stream")
|
||||
@@ -78,6 +79,7 @@ class Config(object):
|
||||
self.error = self.get_error()
|
||||
self.server = self.get_server()
|
||||
self.format = self.get_format()
|
||||
self.force_window = self.get_force_window()
|
||||
self.preferred_language = self.get_preferred_language()
|
||||
self.rofi_theme = self.get_rofi_theme()
|
||||
Rofi.rofi_theme = self.rofi_theme
|
||||
@@ -145,6 +147,9 @@ class Config(object):
|
||||
def get_skip(self):
|
||||
return self.configparser.getboolean("stream", "skip")
|
||||
|
||||
def get_force_window(self):
|
||||
return self.configparser.get("stream", "force_window")
|
||||
|
||||
def get_icons(self):
|
||||
return self.configparser.getboolean("general", "icons")
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ def player_controls(config: "Config", anilist_config: QueryDict):
|
||||
from ..utils.player import player
|
||||
|
||||
mpv = player.create_player(
|
||||
current_link,
|
||||
config.anime_provider,
|
||||
anilist_config,
|
||||
config,
|
||||
@@ -92,7 +93,6 @@ def player_controls(config: "Config", anilist_config: QueryDict):
|
||||
mpv._set_property("chapters-file", chapters_file[1])
|
||||
mpv._set_property("script-opts", script_opts[1])
|
||||
mpv.start = start_time
|
||||
mpv.play(current_link)
|
||||
mpv.wait_for_shutdown()
|
||||
mpv.terminate()
|
||||
stop_time = player.last_stop_time
|
||||
@@ -373,7 +373,11 @@ def fetch_streams(config: "Config", anilist_config: QueryDict):
|
||||
from ..utils.player import player
|
||||
|
||||
mpv = player.create_player(
|
||||
anime_provider, anilist_config, config, selected_server["episode_title"]
|
||||
stream_link,
|
||||
anime_provider,
|
||||
anilist_config,
|
||||
config,
|
||||
selected_server["episode_title"],
|
||||
)
|
||||
|
||||
if custom_args and None:
|
||||
@@ -382,7 +386,6 @@ def fetch_streams(config: "Config", anilist_config: QueryDict):
|
||||
mpv._set_property("chapters-file", chapters_file[1])
|
||||
mpv._set_property("script-opts", script_opts[1])
|
||||
mpv.start = start_time
|
||||
mpv.play(stream_link)
|
||||
mpv.wait_for_shutdown()
|
||||
mpv.terminate()
|
||||
stop_time = player.last_stop_time
|
||||
|
||||
@@ -27,9 +27,13 @@ class MpvPlayer(object):
|
||||
last_stop_time_secs = 0
|
||||
last_total_time_secs = 0
|
||||
current_media_title = ""
|
||||
player_fetching = False
|
||||
|
||||
def get_episode(
|
||||
self, type: "Literal['next','previous','reload','custom']", ep_no=None
|
||||
self,
|
||||
type: "Literal['next','previous','reload','custom']",
|
||||
ep_no=None,
|
||||
server="top",
|
||||
):
|
||||
anilist_config = self.anilist_config
|
||||
config = self.config
|
||||
@@ -62,7 +66,9 @@ class MpvPlayer(object):
|
||||
elif type == "custom":
|
||||
if not ep_no or ep_no not in episodes:
|
||||
self.mpv_player.show_text("Episode number not specified or invalid")
|
||||
self.mpv_player.show_text(f"Acceptable episodes are: {episodes}")
|
||||
self.mpv_player.show_text(
|
||||
f"Acceptable episodes are: {episodes}",
|
||||
)
|
||||
return
|
||||
|
||||
self.mpv_player.show_text(f"Fetching episode {ep_no}")
|
||||
@@ -97,7 +103,19 @@ class MpvPlayer(object):
|
||||
return None
|
||||
|
||||
# always select the first
|
||||
selected_server = next(episode_streams)
|
||||
if server == "top":
|
||||
selected_server = next(episode_streams)
|
||||
else:
|
||||
episode_streams_dict = {
|
||||
episode_stream["server"]: episode_stream
|
||||
for episode_stream in episode_streams
|
||||
}
|
||||
selected_server = episode_streams_dict.get(server)
|
||||
if selected_server is None:
|
||||
self.mpv_player.show_text(
|
||||
f"Invalid server!!; servers available are: {episode_streams_dict.keys()}",
|
||||
)
|
||||
return None
|
||||
self.current_media_title = selected_server["episode_title"]
|
||||
links = selected_server["links"]
|
||||
if quality > len(links) - 1:
|
||||
@@ -108,7 +126,12 @@ class MpvPlayer(object):
|
||||
return stream_link
|
||||
|
||||
def create_player(
|
||||
self, anime_provider: "AnimeProvider", anilist_config, config: "Config", title
|
||||
self,
|
||||
stream_link,
|
||||
anime_provider: "AnimeProvider",
|
||||
anilist_config,
|
||||
config: "Config",
|
||||
title,
|
||||
):
|
||||
self.anime_provider = anime_provider
|
||||
self.anilist_config = anilist_config
|
||||
@@ -120,13 +143,53 @@ class MpvPlayer(object):
|
||||
self.current_media_title = ""
|
||||
|
||||
mpv_player = mpv.MPV(
|
||||
log_handler=print,
|
||||
loglevel="error",
|
||||
config=True,
|
||||
input_default_bindings=True,
|
||||
input_vo_keyboard=True,
|
||||
osc=True,
|
||||
ytdl=True,
|
||||
)
|
||||
mpv_player.force_window = config.force_window
|
||||
# mpv_player.cache = "yes"
|
||||
# mpv_player.cache_pause = "no"
|
||||
mpv_player.title = title
|
||||
|
||||
mpv_player.play(stream_link)
|
||||
|
||||
# -- events --
|
||||
@mpv_player.event_callback("file-loaded")
|
||||
def set_total_time(event, *args):
|
||||
d = mpv_player._get_property("duration")
|
||||
self.player_fetching = False
|
||||
if isinstance(d, float):
|
||||
self.last_total_time = format_time(d)
|
||||
|
||||
@mpv_player.property_observer("time-pos")
|
||||
def handle_time_start_update(*args):
|
||||
if len(args) > 1:
|
||||
value = args[1]
|
||||
if value is not None:
|
||||
self.last_stop_time = format_time(value)
|
||||
|
||||
@mpv_player.property_observer("time-remaining")
|
||||
def handle_time_remaining_update(
|
||||
property, time_remaining: float | None = None, *args
|
||||
):
|
||||
if time_remaining is not None:
|
||||
print(time_remaining, self.player_fetching)
|
||||
if time_remaining < 1 and config.auto_next and not self.player_fetching:
|
||||
print("Auto Fetching Next Episode")
|
||||
self.player_fetching = True
|
||||
url = self.get_episode("next")
|
||||
if url:
|
||||
mpv_player.loadfile(
|
||||
url,
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
# -- keybindings --
|
||||
@mpv_player.on_key_press("shift+n")
|
||||
def _next_episode():
|
||||
url = self.get_episode("next")
|
||||
@@ -134,18 +197,6 @@ class MpvPlayer(object):
|
||||
mpv_player.loadfile(url, options=f"title={self.current_media_title}")
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
@mpv_player.event_callback("file-loaded")
|
||||
def set_total_time(event, *args):
|
||||
d = mpv_player._get_property("duration")
|
||||
if isinstance(d, float):
|
||||
self.last_total_time = format_time(d)
|
||||
|
||||
@mpv_player.event_callback("shutdown")
|
||||
def set_total_time_on_shutdown(event, *args):
|
||||
d = mpv_player._get_property("duration")
|
||||
if isinstance(d, float):
|
||||
self.last_total_time = format_time(d)
|
||||
|
||||
@mpv_player.on_key_press("shift+p")
|
||||
def _previous_episode():
|
||||
url = self.get_episode("previous")
|
||||
@@ -190,31 +241,11 @@ class MpvPlayer(object):
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
@mpv_player.property_observer("time-pos")
|
||||
def handle_time_start_update(*args):
|
||||
if len(args) > 1:
|
||||
value = args[1]
|
||||
if value is not None:
|
||||
self.last_stop_time_secs = value
|
||||
self.last_stop_time = format_time(value)
|
||||
|
||||
@mpv_player.property_observer("time-remaining")
|
||||
def handle_time_remaining_update(*args):
|
||||
if len(args) > 1:
|
||||
value = args[1]
|
||||
if value is not None:
|
||||
rem_time = value
|
||||
if rem_time < 10 and config.auto_next:
|
||||
url = self.get_episode("next")
|
||||
if url:
|
||||
mpv_player.loadfile(
|
||||
url,
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
# -- script messages --
|
||||
@mpv_player.message_handler("select-episode")
|
||||
def select_episode(episode: bytes | None = None, *args):
|
||||
if not episode:
|
||||
mpv_player.show_text("No episode was selected")
|
||||
return
|
||||
url = self.get_episode("custom", episode.decode())
|
||||
if url:
|
||||
@@ -223,11 +254,29 @@ class MpvPlayer(object):
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
|
||||
mpv_player.register_message_handler("select-episode", select_episode)
|
||||
@mpv_player.message_handler("select-server")
|
||||
def select_server(server: bytes | None = None, *args):
|
||||
if not server:
|
||||
mpv_player.show_text("No server was selected")
|
||||
return
|
||||
url = self.get_episode("reload", server=server.decode())
|
||||
if url:
|
||||
mpv_player.loadfile(
|
||||
url,
|
||||
)
|
||||
mpv_player.title = self.current_media_title
|
||||
else:
|
||||
pass
|
||||
|
||||
# -- events --
|
||||
mpv_player.observe_property("time-pos", handle_time_start_update)
|
||||
mpv_player.register_event_callback(set_total_time)
|
||||
mpv_player.register_event_callback(set_total_time_on_shutdown)
|
||||
mpv_player.observe_property("time-remaining", handle_time_remaining_update)
|
||||
mpv_player.register_event_callback(set_total_time)
|
||||
|
||||
# --script-messages --
|
||||
mpv_player.register_message_handler("select-episode", select_episode)
|
||||
mpv_player.register_message_handler("select-server", select_server)
|
||||
|
||||
self.mpv_player = mpv_player
|
||||
return mpv_player
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ if PLATFORM == "Windows":
|
||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.ico")
|
||||
else:
|
||||
ICON_PATH = os.path.join(ASSETS_DIR, "logo.png")
|
||||
PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
|
||||
|
||||
|
||||
# ----- user configs and data -----
|
||||
APP_DATA_DIR = dirs.user_config_dir
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "0.50.0"
|
||||
version = "0.60.4.dev1"
|
||||
description = "A browser anime site experience from the terminal"
|
||||
authors = ["Benextempest <benextempest@gmail.com>"]
|
||||
license = "UNLICENSE"
|
||||
|
||||
Reference in New Issue
Block a user