Compare commits

...

21 Commits

Author SHA1 Message Date
Benex254
83c98936d1 chore: bump version 2024-08-15 11:33:22 +03:00
Benex254
0891cb279a docs: update readme 2024-08-15 11:32:21 +03:00
Benex254
95ba96f537 chore: remove plyer 2024-08-15 11:25:51 +03:00
Benex254
586790173b docs: update readme 2024-08-15 11:25:22 +03:00
Benex254
1d19449ab7 chore: bump version 2024-08-15 10:52:59 +03:00
Benex254
e1f73334ef feat(cli): remove unknown as possible quality 2024-08-15 10:50:32 +03:00
Benex254
4faac017b5 feat(utils): add 480 as possible quality 2024-08-15 10:49:29 +03:00
Benex254
bfbd2a57a0 feat(cli): add 480 as a possible quality 2024-08-15 10:48:39 +03:00
Benex254
9519472f83 feat(utils): pretty colors when defaulting to quality 2024-08-15 10:48:15 +03:00
Benex254
5c0c119cbc feat(download command): use actual episodes if downloading all 2024-08-15 10:47:35 +03:00
Benex254
87eb257a10 chore: use plyer.sttoragepath if possible 2024-08-15 10:29:59 +03:00
Benex254
4a08076c3b feat: use actual episodes list than inference 2024-08-15 00:20:06 +03:00
Benex254
0d239e6793 chore: bump version 2024-08-14 22:43:05 +03:00
Benex254
0a0d47ae88 chore: raise search results for anilist 2024-08-14 22:42:35 +03:00
Benex254
2ba07d47b3 feat(cli): add anime title completions 2024-08-14 22:32:47 +03:00
Benex254
f1b520fe3c chore: bump version 2024-08-14 21:22:40 +03:00
Benex254
8cfcc26468 feat(animepahe): use true episodes 2024-08-14 21:20:49 +03:00
Benex254
cd51edf0b8 feat(interface): better post error response 2024-08-14 21:10:32 +03:00
Benex254
6eb28cfa3d feat(mpv): show feedback on toggle translation type 2024-08-14 20:43:26 +03:00
Benex254
542d39fa6a chore: bump version 2024-08-14 20:40:45 +03:00
Benex254
e5e328148f fix: failed quality selection 2024-08-14 20:39:58 +03:00
15 changed files with 206 additions and 54 deletions

View File

@@ -196,20 +196,22 @@ Overview of main commands:
Configuration is directly passed into this command at run time to override your config.
Available options include:
Available options for the fastanime command include:
- `--server;-s <server>` set the default server to auto select
- `--continue;-c/--no-continue;-no-c` whether to continue from the last episode you were watching
- `--quality;-q <0|1|2|3>` the link to choose from server
- `--translation-type;- <dub|sub` what language for anime
- `--auto-select;-a/--no-auto-select;-no-a` auto select title from provider results
- `--auto-next;-A;/--no-auto-next;-no-A` auto select next episode
- `-downloads-dir;-d <path>` set the folder to download anime into
- `--server <server>` or `-s <server>` set the default server to auto select
- `--continue/--no-continue` or `-c/-no-c` whether to continue from the last episode you were watching
- `--quality <1080/720/480/360>` or `-q <1080/720/480/360>` the link to choose from server
- `--translation-type <dub/sub>` or `-t <dub/sub>` what language for anime
- `--dub` dubbed anime
- `--sub` subbed anime
- `--auto-select/--no-auto-select` or `-a/-no-a` auto select title from provider results
- `--auto-next/--no-auto-next` or `-A/-no-A` auto select next episode
- `-downloads-dir <path>` or `-d <path>` set the folder to download anime into
- `--fzf` use fzf for the ui
- `--default` use the default ui
- `--preview` show a preview when using fzf
- `--no-preview` dont show a preview when using fzf
- `--format <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
- `--format <yt-dlp format string>` or `-f <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
- `--icons/--no-icons` toggle the visibility of the icons
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
- `--rofi` use rofi for the ui
@@ -220,6 +222,19 @@ Available options include:
- `--log-file` allow logging to a file
- `--rich-traceback` allow rich traceback
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
- `--provider <allanime/animepahe>` anime site of choice to scrape from **NOTE:** animepahe is still experimental and requires node to decode one line of js thats hard to decode manually
Example usage of the above options
```bash
# downloading dubbed anime
fastanime --dub download <anime>
# use icons and fzf for a more elegant ui with preview
# only for anilist
fastanime --icons --preview --fzf anilist
# use icons with default ui
fastanime --icons --default anilist
```
#### The anilist command :fire: :fire: :fire:

View File

@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
) # noqa: F541
__version__ = "v1.1.5"
__version__ = "v1.6.3"
APP_NAME = "FastAnime"
AUTHOR = "Benex254"

View File

@@ -76,7 +76,14 @@ signal.signal(signal.SIGINT, handle_exit)
@click.option(
"-q",
"--quality",
type=click.Choice(["360", "720", "1080", "unknown"]),
type=click.Choice(
[
"360",
"480",
"720",
"1080",
]
),
help="set the quality of the stream",
)
@click.option(

View File

@@ -1,13 +1,13 @@
import click
from ...utils.completion_types import anime_titles_shell_complete
@click.command(
help="Search for anime using anilists api and get top ~50 results",
short_help="Search for anime",
)
@click.argument(
"title",
)
@click.argument("title", shell_complete=anime_titles_shell_complete)
@click.pass_obj
def search(config, title):
from ....anilist import AniList

View File

@@ -3,6 +3,8 @@ from typing import TYPE_CHECKING
import click
from ..utils.completion_types import anime_titles_shell_complete
if TYPE_CHECKING:
from ..config import Config
@@ -12,8 +14,7 @@ if TYPE_CHECKING:
short_help="Download anime",
)
@click.argument(
"anime-title",
required=True,
"anime-title", required=True, shell_complete=anime_titles_shell_complete
)
@click.option(
"--episode-range",
@@ -91,10 +92,12 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
episodes = anime["availableEpisodesDetail"][config.translation_type]
if episode_range:
episodes_start, episodes_end = episode_range.split("-")
episodes_range = range(round(float(episodes_start)), round(float(episodes_end)))
else:
episodes_start, episodes_end = 0, len(episodes)
for episode in range(round(float(episodes_start)), round(float(episodes_end))):
episodes_range = sorted(episodes, key=float)
for episode in episodes_range:
try:
episode = str(episode)
if episode not in episodes:

View File

@@ -1,6 +1,7 @@
import click
from ...cli.config import Config
from ..utils.completion_types import anime_titles_shell_complete
@click.command(
@@ -12,7 +13,9 @@ from ...cli.config import Config
"-r",
help="A range of episodes to binge",
)
@click.argument("anime_title", required=True, type=str)
@click.argument(
"anime_title", required=True, shell_complete=anime_titles_shell_complete
)
@click.pass_obj
def search(config: Config, anime_title: str, episode_range: str):
from click import clear

View File

@@ -532,16 +532,26 @@ def provider_anime_episode_servers_menu(
# this will try to update the episode to be the next episode if delta has reached a specific threshhold
# this update will only apply locally
# the remote(anilist) is only updated when its certain you are going to open the player
available_episodes: list = sorted(
fastanime_runtime_state.provider_available_episodes, key=float
)
if stop_time == "0" or total_time == "0":
# increment the episode
episode = str(int(current_episode_number) + 1)
# increment the episodes
next_episode = available_episodes.index(current_episode_number) + 1
if next_episode >= len(available_episodes):
next_episode = len(available_episodes) - 1
episode = available_episodes[next_episode]
else:
error = config.error * 60
delta = calculate_time_delta(stop_time, total_time)
if delta.total_seconds() > error:
episode = current_episode_number
else:
episode = str(int(current_episode_number) + 1)
# increment the episodes
next_episode = available_episodes.index(current_episode_number) + 1
if next_episode >= len(available_episodes):
next_episode = len(available_episodes) - 1
episode = available_episodes[next_episode]
stop_time = "0"
total_time = "0"
@@ -672,8 +682,7 @@ def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"
else:
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
exit(1)
fetch_anime_episode(config, fastanime_runtime_state)
return
return fetch_anime_episode(config, fastanime_runtime_state)
fastanime_runtime_state.provider_anime = provider_anime
provider_anime_episodes_menu(config, fastanime_runtime_state)
@@ -719,8 +728,7 @@ def anime_provider_search_results_menu(
else:
if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
exit(1)
anime_provider_search_results_menu(config, fastanime_runtime_state)
return
return anime_provider_search_results_menu(config, fastanime_runtime_state)
provider_search_results = {
anime["title"]: anime for anime in provider_search_results["results"]
@@ -1176,6 +1184,7 @@ def anilist_results_menu(
anime["status"] == "RELEASING"
and anime["nextAiringEpisode"]
and progress > 0
and anime["mediaListEntry"]
):
last_aired_episode = anime["nextAiringEpisode"]["episode"] - 1
if last_aired_episode - progress > 0:

View File

@@ -0,0 +1,98 @@
from typing import TYPE_CHECKING
import requests
if TYPE_CHECKING:
from ...libs.anilist.types import AnilistDataSchema
import logging
logger = logging.getLogger(__name__)
ANILIST_ENDPOINT = "https://graphql.anilist.co"
anime_title_query = """
query($query:String){
Page(perPage:50){
pageInfo{
total
currentPage
hasNextPage
}
media(search:$query,type:ANIME){
id
idMal
title{
romaji
english
}
}
}
}
"""
def get_anime_titles(query: str, variables: dict = {}):
"""the abstraction over all none authenticated requests and that returns data of a similar type
Args:
query: the anilist query
variables: the anilist api variables
Returns:
a boolean indicating success and none or an anilist object depending on success
"""
try:
response = requests.post(
ANILIST_ENDPOINT,
json={"query": query, "variables": variables},
timeout=10,
)
anilist_data: AnilistDataSchema = response.json()
# ensuring you dont get blocked
if (
int(response.headers.get("X-RateLimit-Remaining", 0)) < 30
and not response.status_code == 500
):
print("Warning you are exceeding the allowed number of calls per minute")
logger.warning(
"You are exceeding the allowed number of calls per minute for the AniList api enforcing timeout"
)
print("Forced timeout will now be initiated")
import time
print("sleeping...")
time.sleep(1 * 60)
if response.status_code == 200:
eng_titles = [
anime["title"]["english"]
for anime in anilist_data["data"]["Page"]["media"]
if anime["title"]["english"]
]
romaji_titles = [
anime["title"]["romaji"]
for anime in anilist_data["data"]["Page"]["media"]
if anime["title"]["romaji"]
]
return [*eng_titles, *romaji_titles]
else:
return ["non 200 status code"]
except requests.exceptions.Timeout:
logger.warning(
"Timeout has been exceeded this could mean anilist is down or you have lost internet connection"
)
return ["timeout exceeded"]
except requests.exceptions.ConnectionError:
logger.warning(
"ConnectionError this could mean anilist is down or you have lost internet connection"
)
return ["connection error"]
except Exception as e:
logger.error(f"Something unexpected occured {e}")
return ["unexpected error"]
def anime_titles_shell_complete(ctx, param, incomplete):
return [name for name in get_anime_titles(anime_title_query, {"query": incomplete})]

View File

@@ -234,6 +234,7 @@ class MpvPlayer(object):
@mpv_player.on_key_press("shift+t")
def _toggle_translation_type():
translation_type = "sub" if config.translation_type == "dub" else "dub"
mpv_player.show_text("Changing translation type...")
anime = anime_provider.get_anime(
fastanime_runtime_state.provider_anime_search_result["id"],
fastanime_runtime_state.selected_anime_anilist,

View File

@@ -19,7 +19,7 @@ BG_GREEN = "\033[48;2;120;233;12;m"
GREEN = "\033[38;2;45;24;45;m"
def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]"):
def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]", default=True):
"""Helper function used to filter a list of EpisodeStream objects to one that has a corresponding quality
Args:
@@ -30,8 +30,25 @@ def filter_by_quality(quality: str, stream_links: "list[EpisodeStream]"):
an EpisodeStream object or None incase the quality was not found
"""
for stream_link in stream_links:
if stream_link["quality"] == quality:
q = float(quality)
Q = float(stream_link["quality"])
# some providers have inaccurate eg qualities 718 instead of 720
if Q < q + 80 and Q > q - 80:
return stream_link
else:
if stream_links and default:
from rich import print
try:
print("[yellow bold]WARNING Qualities were:[/] ", stream_links)
print(
"[cyan bold]Using default of quality:[/] ",
stream_links[0]["quality"],
)
return stream_links[0]
except Exception as e:
print(e)
return
def format_bytes_to_human(num_of_bytes: float, suffix: str = "B"):

View File

@@ -9,7 +9,6 @@ PLATFORM = system()
# ---- app deps ----
APP_DIR = os.path.abspath(os.path.dirname(__file__))
CONFIGS_DIR = os.path.join(APP_DIR, "configs")
ASSETS_DIR = os.path.join(APP_DIR, "assets")
@@ -24,7 +23,6 @@ PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
# ----- user configs and data -----
S_PLATFORM = sys.platform
if S_PLATFORM == "win32":
# app data
app_data_dir_base = os.getenv("LOCALAPPDATA")

View File

@@ -231,7 +231,7 @@ $type:MediaType\
search_query = (
"""
query($query:String,%s){
Page(perPage:30,page:$page){
Page(perPage:50,page:$page){
pageInfo{
total
currentPage

View File

@@ -89,27 +89,28 @@ class AnimePaheApi(AnimeProvider):
if response.status_code == 200:
if not data:
data.update(response.json())
if ep_data := response.json().get("data"):
data["data"].extend(ep_data)
if data["next_page_url"]:
# TODO: Refine this
time.sleep(
random.choice(
[
0.25,
0.1,
0.5,
0.75,
1,
]
)
)
page += 1
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
_pages_loader(
url,
page,
else:
if ep_data := response.json().get("data"):
data["data"].extend(ep_data)
if response.json()["next_page_url"]:
# TODO: Refine this
time.sleep(
random.choice(
[
0.25,
0.1,
0.5,
0.75,
1,
]
)
)
page += 1
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
_pages_loader(
url,
page,
)
_pages_loader(
url,
@@ -119,7 +120,7 @@ class AnimePaheApi(AnimeProvider):
if not data:
return {}
self.anime = data # pyright:ignore
episodes = list(map(str, range(data["total"])))
episodes = list(map(str, [episode["episode"] for episode in data["data"]]))
title = ""
return {
"id": session_id,

View File

@@ -36,7 +36,7 @@ hex_to_char = {
def give_random_quality(links: list[dict]):
qualities = cycle(["1080", "720", "360"])
qualities = cycle(["1080", "720", "480", "360"])
return [
{"link": link["link"], "quality": quality}

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "fastanime"
version = "1.1.5.dev1"
version = "1.6.3.dev1"
description = "A browser anime site experience from the terminal"
authors = ["Benextempest <benextempest@gmail.com>"]
license = "UNLICENSE"