Compare commits

..

30 Commits

Author SHA1 Message Date
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
Benex254
cea1a67d64 chore: bump version 2024-08-14 20:07:35 +03:00
Benex254
97c6dc7968 feat(animepahe): make it random 2024-08-14 20:07:35 +03:00
Benex254
d97072e298 feat(anime_pahe): load all pages 2024-08-14 20:07:35 +03:00
Benex254
7cd246478e feat: improve error handling when fetching servers 2024-08-14 20:07:35 +03:00
BenedictX
8afe1df3a9 Update README.md 2024-08-13 20:57:16 +03:00
Benex254
452c2a3569 chore: bump version 2024-08-13 20:25:29 +03:00
Benex254
f738069794 docs: fix doc on search command 2024-08-13 20:22:51 +03:00
Benex254
d178eb976e fix(mpv): not starting from begining of episode 2024-08-13 20:22:27 +03:00
Benex254
d58dae6d6b fix(mpv): watch history not updating to correct position 2024-08-13 20:21:48 +03:00
Benex254
136cf841e1 feat: drop pyshortcuts and python-dotenv as a dependency 2024-08-13 19:58:14 +03:00
Benex254
748d321f36 feat: remove platformdirs as dep 2024-08-13 19:01:57 +03:00
Benex254
3e71239981 Revert "feat(mpv): update episode progress timestamp"
This reverts commit 571ab488f8.
2024-08-13 01:36:06 +03:00
Benex254
571ab488f8 feat(mpv): update episode progress timestamp 2024-08-13 01:05:21 +03:00
18 changed files with 391 additions and 132 deletions

View File

@@ -164,7 +164,8 @@ The only required external dependency, unless you won't be streaming, is [MPV](h
**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.
- [fzf](https://github.com/junegunn/fzf) 🔥 which is used as a better alternative to the ui.
- [rofi](https://github.com/davatorium/rofi) 🔥 which is used as another alternative ui + the the desktop entry ui
- [chafa](https://github.com/hpjansson/chafa) currently the best cross platform and cross terminal image viewer for the terminal.
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it](https://github.com/chase/awrit?tab=readme-ov-file)!!
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
@@ -310,15 +311,15 @@ Powerful command mainly aimed at binging anime. Since it doesn't require interac
**Syntax:**
```bash
# basic form where you will still be promted for the episode number
# basic form where you will still be prompted for the episode number
fastanime search <anime-title>
# binge all episodes with this command
fastanime search <anime-title> -
fastanime search <anime-title> -r -
# binge a specific episode range with this command
# be sure to observe the range Syntax
fastanime search <anime-title> <episodes-start>-<episodes-end>
fastanime search <anime-title> -r <episodes-start>-<episodes-end>
```
#### downloads subcommand

View File

@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
) # noqa: F541
__version__ = "v1.0.0"
__version__ = "v1.6.2"
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

@@ -22,34 +22,71 @@ if TYPE_CHECKING:
)
@click.pass_obj
def config(config: "Config", path, view, desktop_entry):
from pyshortcuts import make_shortcut
import sys
from rich import print
from ...constants import APP_NAME, ICON_PATH, USER_CONFIG_PATH
from ... import __version__
from ...constants import APP_NAME, ICON_PATH, S_PLATFORM, USER_CONFIG_PATH
if path:
print(USER_CONFIG_PATH)
elif view:
print(config)
elif desktop_entry:
import os
import shutil
from pathlib import Path
from textwrap import dedent
from rich import print
from rich.prompt import Confirm
from ..utils.tools import exit_app
FASTANIME_EXECUTABLE = shutil.which("fastanime")
if FASTANIME_EXECUTABLE:
cmds = f"{FASTANIME_EXECUTABLE} --rofi anilist"
else:
cmds = "_ -m fastanime --rofi anilist"
shortcut = make_shortcut(
name=APP_NAME,
description="Watch Anime from the terminal",
icon=ICON_PATH,
script=cmds,
terminal=False,
)
if shortcut:
print("Success", shortcut)
cmds = f"{sys.executable} -m fastanime --rofi anilist"
# TODO: Get funs of the other platforms to complete this lol
if S_PLATFORM == "win32":
print(
"Not implemented; the author thinks its not straight forward so welcomes lovers of windows to try and implement it themselves or to switch to a proper os like arch linux or pray the author gets bored 😜"
)
elif S_PLATFORM == "darwin":
print(
"Not implemented; the author thinks its not straight forward so welcomes lovers of mac to try and implement it themselves or to switch to a proper os like arch linux or pray the author gets bored 😜"
)
else:
print("Failed")
desktop_entry = dedent(
f"""
[Desktop Entry]
Name={APP_NAME}
Type=Application
version={__version__}
Path={Path().home()}
Comment=Watch anime from your terminal
Terminal=false
Icon={ICON_PATH}
Exec={cmds}
Categories=Entertainment
"""
)
base = os.path.expanduser("~/.local/share/applications")
desktop_entry_path = os.path.join(base, f"{APP_NAME}.desktop")
if os.path.exists(desktop_entry_path):
if not Confirm.ask(
f"The file already exists {desktop_entry_path}; or would you like to rewrite it",
default=False,
):
exit_app(1)
with open(desktop_entry_path, "w") as f:
f.write(desktop_entry)
with open(desktop_entry_path) as f:
print(f"Successfully wrote \n{f.read()}")
exit_app(0)
else:
import click

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:
@@ -112,7 +115,10 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
if config.server == "top":
with Progress() as progress:
progress.add_task("Fetching top server...", total=None)
server = next(streams)
server = next(streams, None)
if not server:
print("Sth went wrong when fetching the server")
continue
stream_link = filter_by_quality(config.quality, server["links"])
if not stream_link:
print("Quality not found")

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
@@ -130,7 +133,12 @@ def search(config: Config, anime_title: str, episode_range: str):
if config.server == "top":
with Progress() as progress:
progress.add_task("Fetching top server...", total=None)
server = next(streams)
server = next(streams, None)
if not server:
print("Sth went wrong when fetching the episode")
input("Enter to continue")
stream_anime()
return
stream_link = filter_by_quality(config.quality, server["links"])
if not stream_link:
print("Quality not found")

View File

@@ -20,7 +20,7 @@ from ...libs.rofi import Rofi
from ...Utility.data import anime_normalizer
from ...Utility.utils import anime_title_percentage_match
from ..utils.mpv import run_mpv
from ..utils.tools import FastAnimeRuntimeState, exit_app
from ..utils.tools import exit_app
from ..utils.utils import filter_by_quality, fuzzy_inquirer
from .utils import aniskip
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
from ...libs.anilist.types import AnilistBaseMediaDataSchema
from ...libs.anime_provider.types import Anime, SearchResult, Server
from ..config import Config
from ..utils.tools import FastAnimeRuntimeState
def calculate_time_delta(start_time, end_time):
@@ -316,7 +317,7 @@ def media_player_controls(
def provider_anime_episode_servers_menu(
config: "Config", fastanime_runtime_state: FastAnimeRuntimeState
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""Menu that enables selection of a server either manually or automatically based on user config then plays the stream link of the quality the user prefers
@@ -365,7 +366,22 @@ def provider_anime_episode_servers_menu(
with Progress() as progress:
progress.add_task("Fetching top server...", total=None)
try:
selected_server = next(episode_streams_generator)
selected_server = next(episode_streams_generator, None)
if not selected_server:
if config.use_rofi:
if Rofi.confirm("Sth went wrong enter to continue"):
provider_anime_episode_servers_menu(
config, fastanime_runtime_state
)
else:
exit_app(1)
else:
print("Sth went wrong")
input("Enter to continue...")
provider_anime_episode_servers_menu(
config, fastanime_runtime_state
)
return
server_name = "top"
except Exception as e:
print("Failed to get streams. Reason:", e)
@@ -502,7 +518,7 @@ def provider_anime_episode_servers_menu(
mpv.terminate()
stop_time = player.last_stop_time
total_time = player.last_total_time
current_episode_number = fastanime_runtime_state.provider_current_episode_number
else:
stop_time, total_time = run_mpv(
current_stream_link,
@@ -516,21 +532,34 @@ 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"
config.update_watch_history(
anime_id_anilist, episode, start_time=stop_time, total_time=total_time
anime_id_anilist,
episode,
start_time=stop_time,
total_time=total_time,
)
# switch to controls
@@ -653,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)
@@ -700,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"]
@@ -759,7 +786,7 @@ def anime_provider_search_results_menu(
# ---- ANILIST MEDIA ACTIONS MENU ----
#
def media_actions_menu(
config: "Config", fastanime_runtime_state: FastAnimeRuntimeState
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""The menu responsible for handling all media actions such as watching a trailer or streaming it
@@ -781,7 +808,7 @@ def media_actions_menu(
episodes_total = selected_anime_anilist["episodes"] or "Inf"
def _watch_trailer(
config: "Config", fastanime_runtime_state: FastAnimeRuntimeState
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""Helper function to watch trailers with
@@ -806,7 +833,9 @@ def media_actions_menu(
exit(0)
media_actions_menu(config, fastanime_runtime_state)
def _add_to_list(config: "Config", fastanime_runtime_state: FastAnimeRuntimeState):
def _add_to_list(
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""Helper function to update an anime's media_list_type
Args:
@@ -848,7 +877,9 @@ def media_actions_menu(
input("Enter to continue...")
media_actions_menu(config, fastanime_runtime_state)
def _score_anime(config: "Config", fastanime_runtime_state: FastAnimeRuntimeState):
def _score_anime(
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""Helper function to score anime on anilist from terminal or rofi
Args:
@@ -879,7 +910,7 @@ def media_actions_menu(
# FIX: For some reason this fails to delete
def _remove_from_list(
config: "Config", fastanime_runtime_state: FastAnimeRuntimeState
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""Remove an anime from your media list
@@ -1121,7 +1152,7 @@ def media_actions_menu(
# ---- ANILIST RESULTS MENU ----
#
def anilist_results_menu(
config: "Config", fastanime_runtime_state: FastAnimeRuntimeState
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""The menu that handles and displays the results of an anilist action enabling using to select anime of choice
@@ -1153,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:
@@ -1288,7 +1320,7 @@ def handle_animelist(
def fastanime_main_menu(
config: "Config", fastanime_runtime_state: FastAnimeRuntimeState
config: "Config", fastanime_runtime_state: "FastAnimeRuntimeState"
):
"""The main entry point to the anilist command

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

@@ -117,7 +117,10 @@ class MpvPlayer(object):
# always select the first
if server == "top":
selected_server = next(episode_streams)
selected_server = next(episode_streams, None)
if not selected_server:
self.mpv_player.show_text("Sth went wrong when loading the episode")
return
else:
episode_streams_dict = {
episode_stream["server"]: episode_stream
@@ -136,6 +139,7 @@ class MpvPlayer(object):
if not stream_link_:
self.mpv_player.show_text("Quality not found")
return
self.mpv_player._set_property("start", "0")
stream_link = stream_link_["link"]
return stream_link
@@ -230,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

@@ -14,7 +14,7 @@ class FastAnimeRuntimeState(dict):
self.__setitem__(attr, value)
def exit_app(*args):
def exit_app(exit_code=0, *args):
import os
import shutil
import sys
@@ -46,4 +46,4 @@ def exit_app(*args):
from rich import print
print("Have a good day :smile:", USER_NAME)
sys.exit(0)
sys.exit(exit_code)

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

@@ -1,16 +1,21 @@
import os
import sys
from pathlib import Path
from platform import system
from platformdirs import PlatformDirs
from plyer import storagepath
from . import APP_NAME, AUTHOR
from . import APP_NAME, AUTHOR, __version__
PLATFORM = system()
dirs = PlatformDirs(appname=APP_NAME, appauthor=AUTHOR, ensure_exists=True)
# ---- app deps ----
APP_DIR = os.path.abspath(os.path.dirname(__file__))
try:
APP_DIR = storagepath.get_application_dir() # pyright:ignore
except Exception:
APP_DIR = None
if not APP_DIR:
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,19 +29,78 @@ PREVIEW_IMAGE = os.path.join(ASSETS_DIR, "preview")
# ----- user configs and data -----
APP_DATA_DIR = dirs.user_config_dir
if not APP_DATA_DIR:
APP_DATA_DIR = dirs.user_data_dir
S_PLATFORM = sys.platform
try:
app_data_dir_base = None
video_dir_base = storagepath.get_videos_dir() # pyright:ignore
cache_dir_base = None
except Exception:
video_dir_base = None
cache_dir_base = None
app_data_dir_base = None
if S_PLATFORM == "win32":
# app data
if not app_data_dir_base:
app_data_dir_base = os.getenv("LOCALAPPDATA")
if not app_data_dir_base:
raise RuntimeError("Could not determine app data dir please report to devs")
APP_DATA_DIR = os.path.join(app_data_dir_base, AUTHOR, APP_NAME)
# cache dir
APP_CACHE_DIR = os.path.join(APP_DATA_DIR, "cache")
# videos dir
if not video_dir_base:
video_dir_base = os.path.expanduser("~/Videos")
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
elif S_PLATFORM == "darwin":
# app data
if not app_data_dir_base:
app_data_dir_base = os.path.expanduser("~/Library/Application Support")
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME, __version__)
# cache dir
if not cache_dir_base:
cache_dir_base = os.path.expanduser("~/Library/Caches")
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME, __version__)
# videos dir
if not video_dir_base:
video_dir_base = os.path.expanduser("~/Movies")
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
else:
# app data
if not app_data_dir_base:
app_data_dir_base = os.environ.get("XDG_CONFIG_HOME", "")
if not app_data_dir_base.strip():
app_data_dir_base = os.path.expanduser("~/.config")
APP_DATA_DIR = os.path.join(app_data_dir_base, APP_NAME)
# cache dir
if not cache_dir_base:
cache_dir_base = os.environ.get("XDG_CACHE_HOME", "")
if not cache_dir_base.strip():
cache_dir_base = os.path.expanduser("~/.cache")
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME)
# videos dir
if not video_dir_base:
video_dir_base = os.environ.get("XDG_VIDEOS_DIR", "")
if not video_dir_base.strip():
video_dir_base = os.path.expanduser("~/Videos")
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
# ensure paths exist
Path(APP_DATA_DIR).mkdir(parents=True, exist_ok=True)
Path(APP_CACHE_DIR).mkdir(parents=True, exist_ok=True)
Path(USER_VIDEOS_DIR).mkdir(parents=True, exist_ok=True)
# useful paths
USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json")
USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini")
NOTIFIER_LOG_FILE_PATH = os.path.join(APP_DATA_DIR, "notifier.log")
# cache dir
APP_CACHE_DIR = dirs.user_cache_dir
# video dir
USER_VIDEOS_DIR = os.path.join(dirs.user_videos_dir, APP_NAME)
USER_NAME = os.environ.get("USERNAME", "Anime fun")

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

@@ -1,7 +1,9 @@
import logging
import random
import re
import shutil
import subprocess
import time
from typing import TYPE_CHECKING
from yt_dlp.utils import (
@@ -68,21 +70,57 @@ class AnimePaheApi(AnimeProvider):
return {}
def get_anime(self, session_id: str, *args):
page = 1
try:
anime_result: "AnimeSearchResult" = [
anime
for anime in self.search_page["data"]
if anime["session"] == session_id
][0]
url = (
f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page=1"
data: "AnimePaheAnimePage" = {} # pyright:ignore
url = f"{ANIMEPAHE_ENDPOINT}m=release&id={session_id}&sort=episode_asc&page={page}"
def _pages_loader(
url,
page,
):
response = self.session.get(url, headers=REQUEST_HEADERS)
if response.status_code == 200:
if not data:
data.update(response.json())
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,
page,
)
response = self.session.get(url, headers=REQUEST_HEADERS)
if not response.status_code == 200:
if not data:
return {}
data: "AnimePaheAnimePage" = response.json()
self.anime = data
episodes = list(map(str, range(data["total"])))
self.anime = data # pyright:ignore
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}

53
poetry.lock generated
View File

@@ -861,20 +861,6 @@ nodeenv = ">=1.6.0"
all = ["twine (>=3.4.1)"]
dev = ["twine (>=3.4.1)"]
[[package]]
name = "pyshortcuts"
version = "1.9.0"
description = "Create desktop and Start Menu shortcuts for python scripts"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyshortcuts-1.9.0-py3-none-any.whl", hash = "sha256:54d12ed8cd29bf83ac15153ce882a77072f2032b5f979474c519a2bac5af849d"},
{file = "pyshortcuts-1.9.0.tar.gz", hash = "sha256:016e89111337f74ce1ba3f4b79b295a643bc70b3e63ce4600247aa4bafa06877"},
]
[package.dependencies]
pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "pytest"
version = "8.3.2"
@@ -897,43 +883,6 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "pywin32"
version = "306"
description = "Python for Window Extensions"
optional = false
python-versions = "*"
files = [
{file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"},
{file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"},
{file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"},
{file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"},
{file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"},
{file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"},
{file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"},
{file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"},
{file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"},
{file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"},
{file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"},
{file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"},
{file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"},
{file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"},
]
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -1407,4 +1356,4 @@ test = ["pytest (>=8.1,<9.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "83ec7de7d9466dcd1fadef4b21eec2a879cc9a7d526992ed280b6af53b49d9f1"
content-hash = "7d20e2d0c0c3c8f3a48d9160a2b4a11a5f353d23bb5d7a06ec527fe08e425b91"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "fastanime"
version = "1.0.0"
version = "1.6.2.dev1"
description = "A browser anime site experience from the terminal"
authors = ["Benextempest <benextempest@gmail.com>"]
license = "UNLICENSE"
@@ -12,12 +12,9 @@ yt-dlp = "^2024.5.27"
rich = "^13.7.1"
click = "^8.1.7"
inquirerpy = "^0.3.4"
platformdirs = "^4.2.2"
python-dotenv = "^1.0.1"
thefuzz = "^0.22.1"
requests = "^2.32.3"
plyer = "^2.1.0"
pyshortcuts = "^1.9.0"
mpv = "^1.0.7"
[tool.poetry.group.dev.dependencies]