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. 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 - `--server <server>` or `-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 - `--continue/--no-continue` or `-c/-no-c` whether to continue from the last episode you were watching
- `--quality;-q <0|1|2|3>` the link to choose from server - `--quality <1080/720/480/360>` or `-q <1080/720/480/360>` the link to choose from server
- `--translation-type;- <dub|sub` what language for anime - `--translation-type <dub/sub>` or `-t <dub/sub>` what language for anime
- `--auto-select;-a/--no-auto-select;-no-a` auto select title from provider results - `--dub` dubbed anime
- `--auto-next;-A;/--no-auto-next;-no-A` auto select next episode - `--sub` subbed anime
- `-downloads-dir;-d <path>` set the folder to download anime into - `--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 - `--fzf` use fzf for the ui
- `--default` use the default ui - `--default` use the default ui
- `--preview` show a preview when using fzf - `--preview` show a preview when using fzf
- `--no-preview` dont 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 - `--icons/--no-icons` toggle the visibility of the icons
- `--skip/--no-skip` whether to skip the opening and ending theme songs. - `--skip/--no-skip` whether to skip the opening and ending theme songs.
- `--rofi` use rofi for the ui - `--rofi` use rofi for the ui
@@ -220,6 +222,19 @@ Available options include:
- `--log-file` allow logging to a file - `--log-file` allow logging to a file
- `--rich-traceback` allow rich traceback - `--rich-traceback` allow rich traceback
- `--use-mpv-mod/--use-default-player` whether to use python-mpv - `--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: #### The anilist command :fire: :fire: :fire:

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import click import click
from ...cli.config import Config from ...cli.config import Config
from ..utils.completion_types import anime_titles_shell_complete
@click.command( @click.command(
@@ -12,7 +13,9 @@ from ...cli.config import Config
"-r", "-r",
help="A range of episodes to binge", 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 @click.pass_obj
def search(config: Config, anime_title: str, episode_range: str): def search(config: Config, anime_title: str, episode_range: str):
from click import clear 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 will try to update the episode to be the next episode if delta has reached a specific threshhold
# this update will only apply locally # this update will only apply locally
# the remote(anilist) is only updated when its certain you are going to open the player # the remote(anilist) is only updated when its certain you are going to open the player
available_episodes: list = sorted(
fastanime_runtime_state.provider_available_episodes, key=float
)
if stop_time == "0" or total_time == "0": if stop_time == "0" or total_time == "0":
# increment the episode # increment the episodes
episode = str(int(current_episode_number) + 1) 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: else:
error = config.error * 60 error = config.error * 60
delta = calculate_time_delta(stop_time, total_time) delta = calculate_time_delta(stop_time, total_time)
if delta.total_seconds() > error: if delta.total_seconds() > error:
episode = current_episode_number episode = current_episode_number
else: 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" stop_time = "0"
total_time = "0" total_time = "0"
@@ -672,8 +682,7 @@ def fetch_anime_episode(config, fastanime_runtime_state: "FastAnimeRuntimeState"
else: else:
if not Rofi.confirm("Sth went wrong!!Enter to continue..."): if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
exit(1) exit(1)
fetch_anime_episode(config, fastanime_runtime_state) return fetch_anime_episode(config, fastanime_runtime_state)
return
fastanime_runtime_state.provider_anime = provider_anime fastanime_runtime_state.provider_anime = provider_anime
provider_anime_episodes_menu(config, fastanime_runtime_state) provider_anime_episodes_menu(config, fastanime_runtime_state)
@@ -719,8 +728,7 @@ def anime_provider_search_results_menu(
else: else:
if not Rofi.confirm("Sth went wrong!!Enter to continue..."): if not Rofi.confirm("Sth went wrong!!Enter to continue..."):
exit(1) exit(1)
anime_provider_search_results_menu(config, fastanime_runtime_state) return anime_provider_search_results_menu(config, fastanime_runtime_state)
return
provider_search_results = { provider_search_results = {
anime["title"]: anime for anime in provider_search_results["results"] anime["title"]: anime for anime in provider_search_results["results"]
@@ -1176,6 +1184,7 @@ def anilist_results_menu(
anime["status"] == "RELEASING" anime["status"] == "RELEASING"
and anime["nextAiringEpisode"] and anime["nextAiringEpisode"]
and progress > 0 and progress > 0
and anime["mediaListEntry"]
): ):
last_aired_episode = anime["nextAiringEpisode"]["episode"] - 1 last_aired_episode = anime["nextAiringEpisode"]["episode"] - 1
if last_aired_episode - progress > 0: 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") @mpv_player.on_key_press("shift+t")
def _toggle_translation_type(): def _toggle_translation_type():
translation_type = "sub" if config.translation_type == "dub" else "dub" translation_type = "sub" if config.translation_type == "dub" else "dub"
mpv_player.show_text("Changing translation type...")
anime = anime_provider.get_anime( anime = anime_provider.get_anime(
fastanime_runtime_state.provider_anime_search_result["id"], fastanime_runtime_state.provider_anime_search_result["id"],
fastanime_runtime_state.selected_anime_anilist, 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" 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 """Helper function used to filter a list of EpisodeStream objects to one that has a corresponding quality
Args: 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 an EpisodeStream object or None incase the quality was not found
""" """
for stream_link in stream_links: 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 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"): def format_bytes_to_human(num_of_bytes: float, suffix: str = "B"):

View File

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

View File

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

View File

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

View File

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

View File

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