mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-07 05:10:39 -08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eb28cfa3d | ||
|
|
542d39fa6a | ||
|
|
e5e328148f | ||
|
|
cea1a67d64 | ||
|
|
97c6dc7968 | ||
|
|
d97072e298 | ||
|
|
7cd246478e | ||
|
|
8afe1df3a9 | ||
|
|
452c2a3569 | ||
|
|
f738069794 | ||
|
|
d178eb976e | ||
|
|
d58dae6d6b | ||
|
|
136cf841e1 | ||
|
|
748d321f36 | ||
|
|
3e71239981 | ||
|
|
571ab488f8 | ||
|
|
62878311c6 | ||
|
|
432e9374cb | ||
|
|
a4974fbba7 | ||
|
|
b935e80928 | ||
|
|
8999d88d23 | ||
|
|
9608bace07 | ||
|
|
09b88df49a | ||
|
|
ccaacc9948 | ||
|
|
96d88b0f47 | ||
|
|
6939471e48 | ||
|
|
b1a2307c4d | ||
|
|
1ead2fb176 | ||
|
|
51e3ca004e | ||
|
|
3814db460f | ||
|
|
4102685cbc | ||
|
|
c80a8235e1 | ||
|
|
622427b748 | ||
|
|
e36413cdef | ||
|
|
7e9a510706 |
27
README.md
27
README.md
@@ -44,6 +44,7 @@ Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magi
|
||||
- [downloads subcommand](#downloads-subcommand)
|
||||
- [config subcommand](#config-subcommand)
|
||||
- [cache subcommand](#cache-subcommand)
|
||||
- [update subcommand](#update-subcommand)
|
||||
- [completions subcommand](#completions-subcommand)
|
||||
- [MPV specific commands](#mpv-specific-commands)
|
||||
- [Added keybindings](#added-keybindings)
|
||||
@@ -163,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.
|
||||
@@ -309,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
|
||||
@@ -348,6 +350,9 @@ fastanime config --path
|
||||
|
||||
# add a desktop entry
|
||||
fastanime config --desktop-entry
|
||||
|
||||
# view current contents of your configuration or can be used to get an example config
|
||||
fastanime config --view
|
||||
```
|
||||
|
||||
> [!Note]
|
||||
@@ -374,6 +379,20 @@ fastanime cache --size
|
||||
fastanime cache
|
||||
```
|
||||
|
||||
#### update subcommand
|
||||
|
||||
Easily update fastanime to latest
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# update fastanime to latest
|
||||
fastanime update
|
||||
|
||||
# check for latest release
|
||||
fastanime update --check
|
||||
```
|
||||
|
||||
#### completions subcommand
|
||||
|
||||
Helper command to setup shell completions
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""This package exist as away to expose functions and classes that my be useful to a developer using the fastanime library
|
||||
|
||||
[TODO:description]
|
||||
"""
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
|
||||
if TYPE_CHECKING:
|
||||
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
|
||||
|
||||
|
||||
# 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:
|
||||
return f"{anilist_date_object['day']}/{anilist_date_object['month']}/{anilist_date_object['year']}"
|
||||
else:
|
||||
@@ -25,7 +27,7 @@ def format_list_data_with_comma(data: list | None):
|
||||
return "None"
|
||||
|
||||
|
||||
def extract_next_airing_episode(airing_episode: AnilistMediaNextAiringEpisode):
|
||||
def extract_next_airing_episode(airing_episode: "AnilistMediaNextAiringEpisode"):
|
||||
if airing_episode:
|
||||
return f"{airing_episode['episode']} on {format_anilist_timestamp(airing_episode['airingAt'])}"
|
||||
else:
|
||||
|
||||
@@ -27,9 +27,27 @@ class YtDLPDownloader:
|
||||
|
||||
# Function to download the file
|
||||
# TODO: untpack the title to its actual values episode_title and anime_title
|
||||
def _download_file(self, url: str, download_dir, title, silent, vid_format="best"):
|
||||
anime_title = sanitize_filename(title[0])
|
||||
episode_title = sanitize_filename(title[1])
|
||||
def _download_file(
|
||||
self,
|
||||
url: str,
|
||||
anime_title: str,
|
||||
episode_title: str,
|
||||
download_dir: str,
|
||||
silent: bool,
|
||||
vid_format: str = "best",
|
||||
):
|
||||
"""Helper function that downloads anime given url and path details
|
||||
|
||||
Args:
|
||||
url: [TODO:description]
|
||||
anime_title: [TODO:description]
|
||||
episode_title: [TODO:description]
|
||||
download_dir: [TODO:description]
|
||||
silent: [TODO:description]
|
||||
vid_format: [TODO:description]
|
||||
"""
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
episode_title = sanitize_filename(episode_title)
|
||||
ydl_opts = {
|
||||
# Specify the output path and template
|
||||
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
|
||||
@@ -41,7 +59,15 @@ class YtDLPDownloader:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
# WARN: May remove this legacy functionality
|
||||
def download_file(self, url: str, title, silent=True):
|
||||
"""A helper that just does things in the background
|
||||
|
||||
Args:
|
||||
title ([TODO:parameter]): [TODO:description]
|
||||
silent ([TODO:parameter]): [TODO:description]
|
||||
url: [TODO:description]
|
||||
"""
|
||||
self.downloads_queue.put((self._download_file, (url, title, silent)))
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from ..constants import USER_DATA_PATH
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: merger this functionality with the config object
|
||||
class UserData:
|
||||
user_data = {"watch_history": {}, "animelist": [], "user": {}}
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
if os.path.isfile(USER_DATA_PATH):
|
||||
with open(USER_DATA_PATH, "r") as f:
|
||||
user_data = json.load(f)
|
||||
self.user_data.update(user_data)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def update_watch_history(self, watch_history: dict):
|
||||
self.user_data["watch_history"] = watch_history
|
||||
self._update_user_data()
|
||||
|
||||
def update_user_info(self, user: dict):
|
||||
self.user_data["user"] = user
|
||||
self._update_user_data()
|
||||
|
||||
def update_animelist(self, anime_list: list):
|
||||
self.user_data["animelist"] = list(set(anime_list))
|
||||
self._update_user_data()
|
||||
|
||||
def _update_user_data(self):
|
||||
with open(USER_DATA_PATH, "w") as f:
|
||||
json.dump(self.user_data, f)
|
||||
|
||||
|
||||
user_data_helper = UserData()
|
||||
@@ -1,79 +1,18 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from thefuzz import fuzz
|
||||
|
||||
from fastanime.libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
|
||||
from .data import anime_normalizer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def remove_html_tags(text: str):
|
||||
clean = re.compile("<.*?>")
|
||||
return re.sub(clean, "", text)
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def sanitize_filename(filename: str):
|
||||
"""
|
||||
Sanitize a string to be safe for use as a file name.
|
||||
|
||||
:param filename: The original filename string.
|
||||
:return: A sanitized filename string.
|
||||
"""
|
||||
# List of characters not allowed in filenames on various operating systems
|
||||
invalid_chars = r'[<>:"/\\|?*\0]'
|
||||
reserved_names = {
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"NUL",
|
||||
"COM1",
|
||||
"COM2",
|
||||
"COM3",
|
||||
"COM4",
|
||||
"COM5",
|
||||
"COM6",
|
||||
"COM7",
|
||||
"COM8",
|
||||
"COM9",
|
||||
"LPT1",
|
||||
"LPT2",
|
||||
"LPT3",
|
||||
"LPT4",
|
||||
"LPT5",
|
||||
"LPT6",
|
||||
"LPT7",
|
||||
"LPT8",
|
||||
"LPT9",
|
||||
}
|
||||
|
||||
# Replace invalid characters with an underscore
|
||||
sanitized = re.sub(invalid_chars, " ", filename)
|
||||
|
||||
# Remove leading and trailing whitespace
|
||||
sanitized = sanitized.strip()
|
||||
|
||||
# Check for reserved filenames
|
||||
name, ext = os.path.splitext(sanitized)
|
||||
if name.upper() in reserved_names:
|
||||
name += "_file"
|
||||
sanitized = name + ext
|
||||
|
||||
# Ensure the filename is not empty
|
||||
if not sanitized:
|
||||
sanitized = "default_filename"
|
||||
|
||||
return sanitized
|
||||
|
||||
|
||||
def anime_title_percentage_match(
|
||||
possible_user_requested_anime_title: str, anime: AnilistBaseMediaDataSchema
|
||||
possible_user_requested_anime_title: str, anime: "AnilistBaseMediaDataSchema"
|
||||
) -> float:
|
||||
"""Returns the percentage match between the possible title and user title
|
||||
|
||||
@@ -97,10 +36,3 @@ def anime_title_percentage_match(
|
||||
)
|
||||
logger.info(f"{locals()}")
|
||||
return percentage_ratio
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
unsafe_filename = "CON:example?file*name.txt"
|
||||
safe_filename = sanitize_filename(unsafe_filename)
|
||||
print(safe_filename) # Output: 'CON_example_file_name.txt'
|
||||
|
||||
@@ -6,7 +6,7 @@ if sys.version_info < (3, 10):
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v0.62.5"
|
||||
__version__ = "v1.1.6"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
|
||||
@@ -15,6 +15,7 @@ commands = {
|
||||
"downloads": "downloads.downloads",
|
||||
"cache": "cache.cache",
|
||||
"completions": "completions.completions",
|
||||
"update": "update.update",
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +43,6 @@ signal.signal(signal.SIGINT, handle_exit)
|
||||
@click.option("--log", help="Allow logging to stdout", is_flag=True)
|
||||
@click.option("--log-file", help="Allow logging to a file", is_flag=True)
|
||||
@click.option("--rich-traceback", help="Use rich to output tracebacks", is_flag=True)
|
||||
@click.option("--update", help="Update fastanime to the latest version", is_flag=True)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--provider",
|
||||
@@ -135,7 +135,6 @@ def run_cli(
|
||||
log,
|
||||
log_file,
|
||||
rich_traceback,
|
||||
update,
|
||||
provider,
|
||||
server,
|
||||
format,
|
||||
@@ -192,11 +191,6 @@ def run_cli(
|
||||
from rich.traceback import install
|
||||
|
||||
install()
|
||||
if update and None:
|
||||
from .app_updater import update_app
|
||||
|
||||
update_app()
|
||||
return
|
||||
|
||||
if provider:
|
||||
ctx.obj.provider = provider
|
||||
|
||||
@@ -2,8 +2,8 @@ import pathlib
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from subprocess import PIPE, Popen
|
||||
|
||||
import requests
|
||||
from rich import print
|
||||
@@ -34,71 +34,72 @@ def check_for_updates():
|
||||
|
||||
def is_git_repo(author, repository):
|
||||
# Check if the current directory contains a .git folder
|
||||
if not pathlib.Path("./.git").exists():
|
||||
git_dir = pathlib.Path(".git")
|
||||
if not git_dir.exists() or not git_dir.is_dir():
|
||||
return False
|
||||
|
||||
repository_qualname = f"{author}/{repository}"
|
||||
|
||||
# Read the .git/config file to find the remote repository URL
|
||||
config_path = pathlib.Path("./.git/config")
|
||||
# Check if the config file exists
|
||||
config_path = git_dir / "config"
|
||||
if not config_path.exists():
|
||||
return False
|
||||
print("here")
|
||||
|
||||
with open(config_path, "r") as git_config:
|
||||
git_config_content = git_config.read()
|
||||
|
||||
# Use regex to find the repository URL in the config file
|
||||
repo_name_pattern = r"\[remote \"origin\"\]\s+url = .*\/([^/]+\/[^/]+)\.git"
|
||||
match = re.search(repo_name_pattern, git_config_content)
|
||||
print(match)
|
||||
|
||||
if match is None:
|
||||
try:
|
||||
# Read the .git/config file to find the remote repository URL
|
||||
with config_path.open("r") as git_config:
|
||||
git_config_content = git_config.read()
|
||||
except (FileNotFoundError, PermissionError):
|
||||
return False
|
||||
|
||||
# Extract the repository name and compare with the expected repository_qualname
|
||||
config_repo_name = match.group(1)
|
||||
return config_repo_name == repository_qualname
|
||||
# Use regex to find the repository URL in the config file
|
||||
repo_name_pattern = r"url\s*=\s*.+/([^/]+/[^/]+)\.git"
|
||||
match = re.search(repo_name_pattern, git_config_content)
|
||||
|
||||
# Return True if match found and repository name matches
|
||||
return bool(match) and match.group(1) == f"{author}/{repository}"
|
||||
|
||||
|
||||
def update_app():
|
||||
is_latest, release_json = check_for_updates()
|
||||
if is_latest:
|
||||
print("[green]App is up to date[/]")
|
||||
return
|
||||
return False, release_json
|
||||
tag_name = release_json["tag_name"]
|
||||
|
||||
print("[cyan]Updating app to version %s[/]" % tag_name)
|
||||
if is_git_repo(AUTHOR, APP_NAME):
|
||||
executable = shutil.which("git")
|
||||
GIT_EXECUTABLE = shutil.which("git")
|
||||
args = [
|
||||
executable,
|
||||
GIT_EXECUTABLE,
|
||||
"pull",
|
||||
]
|
||||
|
||||
print(f"Pulling latest changes from the repository via git: {shlex.join(args)}")
|
||||
|
||||
if not executable:
|
||||
return print("[red]Cannot find git.[/]")
|
||||
if not GIT_EXECUTABLE:
|
||||
print("[red]Cannot find git please install it.[/]")
|
||||
return False, release_json
|
||||
|
||||
process = Popen(
|
||||
process = subprocess.run(
|
||||
args,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE,
|
||||
)
|
||||
|
||||
process.communicate()
|
||||
else:
|
||||
executable = sys.executable
|
||||
if PIPX_EXECUTABLE := shutil.which("pipx"):
|
||||
process = subprocess.run([PIPX_EXECUTABLE, "upgrade", APP_NAME])
|
||||
else:
|
||||
PYTHON_EXECUTABLE = sys.executable
|
||||
|
||||
args = [
|
||||
executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
APP_NAME,
|
||||
"--user",
|
||||
"--no-warn-script-location",
|
||||
]
|
||||
process = Popen(args)
|
||||
process.communicate()
|
||||
args = [
|
||||
PYTHON_EXECUTABLE,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
APP_NAME,
|
||||
"--user",
|
||||
"--no-warn-script-location",
|
||||
]
|
||||
process = subprocess.run(args)
|
||||
if process.returncode == 0:
|
||||
return True, release_json
|
||||
else:
|
||||
return False, release_json
|
||||
|
||||
@@ -2,9 +2,9 @@ import click
|
||||
|
||||
|
||||
@click.command(help="Helper command to get shell completions")
|
||||
@click.option("--fish", is_flag=True)
|
||||
@click.option("--zsh", is_flag=True)
|
||||
@click.option("--bash", is_flag=True)
|
||||
@click.option("--fish", is_flag=True, help="print fish completions")
|
||||
@click.option("--zsh", is_flag=True, help="print zsh completions")
|
||||
@click.option("--bash", is_flag=True, help="print bash completions")
|
||||
def completions(fish, zsh, bash):
|
||||
if not fish or not zsh or not bash:
|
||||
import os
|
||||
|
||||
@@ -1,47 +1,92 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..config import Config
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Opens up your fastanime config in your preferred editor",
|
||||
short_help="Edit your config",
|
||||
)
|
||||
@click.option("--path", "-p", help="Print the config location and exit", is_flag=True)
|
||||
@click.option(
|
||||
"--view", "-v", help="View the current contents of your config", is_flag=True
|
||||
)
|
||||
@click.option(
|
||||
"--desktop-entry",
|
||||
"-d",
|
||||
help="Configure the desktop entry of fastanime",
|
||||
is_flag=True,
|
||||
)
|
||||
# @click.pass_obj
|
||||
def config(path, desktop_entry):
|
||||
pass
|
||||
@click.pass_obj
|
||||
def config(config: "Config", path, view, desktop_entry):
|
||||
import sys
|
||||
|
||||
from pyshortcuts import make_shortcut
|
||||
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
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
@@ -111,7 +112,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")
|
||||
@@ -145,14 +149,16 @@ def download(config: "Config", anime_title, episode_range, highest_priority):
|
||||
|
||||
downloader._download_file(
|
||||
link,
|
||||
anime["title"],
|
||||
episode_title,
|
||||
download_dir,
|
||||
(anime["title"], episode_title),
|
||||
True,
|
||||
config.format,
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
time.sleep(1)
|
||||
print("Continuing")
|
||||
clear()
|
||||
print("Done")
|
||||
print("Done Downloading")
|
||||
exit_app()
|
||||
|
||||
@@ -130,7 +130,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")
|
||||
|
||||
37
fastanime/cli/commands/update.py
Normal file
37
fastanime/cli/commands/update.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(help="Helper command to update fastanime to latest")
|
||||
@click.option("--check", "-c", help="Check for the latest release", is_flag=True)
|
||||
def update(
|
||||
check,
|
||||
):
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
|
||||
from ..app_updater import check_for_updates, update_app
|
||||
|
||||
def _print_release(release_data):
|
||||
console = Console()
|
||||
body = Markdown(release_data["body"])
|
||||
tag = github_release_data["tag_name"]
|
||||
tag_title = release_data["name"]
|
||||
github_page_url = release_data["html_url"]
|
||||
console.print(f"Release Page: {github_page_url}")
|
||||
console.print(f"Tag: {tag}")
|
||||
console.print(f"Title: {tag_title}")
|
||||
console.print(body)
|
||||
|
||||
if check:
|
||||
is_update, github_release_data = check_for_updates()
|
||||
if is_update:
|
||||
print(
|
||||
"You are running an older version of fastanime please update to get the latest features"
|
||||
)
|
||||
_print_release(github_release_data)
|
||||
else:
|
||||
print("You are running the latest version of fastanime")
|
||||
_print_release(github_release_data)
|
||||
else:
|
||||
success, github_release_data = update_app()
|
||||
_print_release(github_release_data)
|
||||
@@ -1,39 +1,85 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from configparser import ConfigParser
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from rich import print
|
||||
|
||||
from ..constants import USER_CONFIG_PATH, USER_VIDEOS_DIR
|
||||
from ..constants import USER_CONFIG_PATH, USER_DATA_PATH, USER_VIDEOS_DIR
|
||||
from ..libs.rofi import Rofi
|
||||
from ..Utility.user_data_helper import user_data_helper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
from ..AnimeProvider import AnimeProvider
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""class that handles and manages configuration and user data throughout the clis lifespan
|
||||
|
||||
Attributes:
|
||||
anime_list: [TODO:attribute]
|
||||
watch_history: [TODO:attribute]
|
||||
fastanime_anilist_app_login_url: [TODO:attribute]
|
||||
anime_provider: [TODO:attribute]
|
||||
user_data: [TODO:attribute]
|
||||
configparser: [TODO:attribute]
|
||||
downloads_dir: [TODO:attribute]
|
||||
provider: [TODO:attribute]
|
||||
use_fzf: [TODO:attribute]
|
||||
use_rofi: [TODO:attribute]
|
||||
skip: [TODO:attribute]
|
||||
icons: [TODO:attribute]
|
||||
preview: [TODO:attribute]
|
||||
translation_type: [TODO:attribute]
|
||||
sort_by: [TODO:attribute]
|
||||
continue_from_history: [TODO:attribute]
|
||||
auto_next: [TODO:attribute]
|
||||
auto_select: [TODO:attribute]
|
||||
use_mpv_mod: [TODO:attribute]
|
||||
quality: [TODO:attribute]
|
||||
notification_duration: [TODO:attribute]
|
||||
error: [TODO:attribute]
|
||||
server: [TODO:attribute]
|
||||
format: [TODO:attribute]
|
||||
force_window: [TODO:attribute]
|
||||
preferred_language: [TODO:attribute]
|
||||
rofi_theme: [TODO:attribute]
|
||||
rofi_theme: [TODO:attribute]
|
||||
rofi_theme_input: [TODO:attribute]
|
||||
rofi_theme_input: [TODO:attribute]
|
||||
rofi_theme_confirm: [TODO:attribute]
|
||||
rofi_theme_confirm: [TODO:attribute]
|
||||
watch_history: [TODO:attribute]
|
||||
anime_list: [TODO:attribute]
|
||||
user: [TODO:attribute]
|
||||
"""
|
||||
|
||||
anime_list: list
|
||||
watch_history: dict
|
||||
fastanime_anilist_app_login_url = (
|
||||
"https://anilist.co/api/v2/oauth/authorize?client_id=20148&response_type=token"
|
||||
)
|
||||
anime_provider: "AnimeProvider"
|
||||
user_data = {"watch_history": {}, "animelist": [], "user": {}}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.initialize_user_data()
|
||||
self.load_config()
|
||||
|
||||
def load_config(self):
|
||||
self.configparser = ConfigParser(
|
||||
{
|
||||
"server": "top",
|
||||
"continue_from_history": "True",
|
||||
"quality": "1080",
|
||||
"auto_next": "False",
|
||||
"auto_select": "True",
|
||||
"sort_by": "search match",
|
||||
"downloads_dir": USER_VIDEOS_DIR,
|
||||
"translation_type": "sub",
|
||||
"server": "top",
|
||||
"continue_from_history": "True",
|
||||
"use_mpv_mod": "false",
|
||||
"force_window": "immediate",
|
||||
"preferred_language": "english",
|
||||
"use_fzf": "False",
|
||||
"preview": "False",
|
||||
@@ -47,8 +93,6 @@ class Config(object):
|
||||
"rofi_theme": "",
|
||||
"rofi_theme_input": "",
|
||||
"rofi_theme_confirm": "",
|
||||
"use_mpv_mod": "false",
|
||||
"force_window": "immediate",
|
||||
}
|
||||
)
|
||||
self.configparser.add_section("stream")
|
||||
@@ -60,7 +104,7 @@ class Config(object):
|
||||
|
||||
self.configparser.read(USER_CONFIG_PATH)
|
||||
|
||||
# --- set defaults ---
|
||||
# --- set config values from file or using defaults ---
|
||||
self.downloads_dir = self.get_downloads_dir()
|
||||
self.provider = self.get_provider()
|
||||
self.use_fzf = self.get_use_fzf()
|
||||
@@ -88,13 +132,14 @@ class Config(object):
|
||||
self.rofi_theme_confirm = self.get_rofi_theme_confirm()
|
||||
Rofi.rofi_theme_confirm = self.rofi_theme_confirm
|
||||
# ---- setup user data ------
|
||||
self.watch_history: dict = user_data_helper.user_data.get("watch_history", {})
|
||||
self.anime_list: list = user_data_helper.user_data.get("animelist", [])
|
||||
self.user: dict = user_data_helper.user_data.get("user", {})
|
||||
self.watch_history: dict = self.user_data.get("watch_history", {})
|
||||
self.anime_list: list = self.user_data.get("animelist", [])
|
||||
self.user: dict = self.user_data.get("user", {})
|
||||
|
||||
def update_user(self, user):
|
||||
self.user = user
|
||||
user_data_helper.update_user_info(user)
|
||||
self.user_data["user"] = user
|
||||
self._update_user_data()
|
||||
|
||||
def update_watch_history(
|
||||
self, anime_id: int, episode: str | None, start_time="0", total_time="0"
|
||||
@@ -108,24 +153,48 @@ class Config(object):
|
||||
}
|
||||
}
|
||||
)
|
||||
user_data_helper.update_watch_history(self.watch_history)
|
||||
self.user_data["watch_history"] = self.watch_history
|
||||
self._update_user_data()
|
||||
|
||||
def update_anime_list(self, anime_id: int, remove=False):
|
||||
if remove:
|
||||
try:
|
||||
self.anime_list.remove(anime_id)
|
||||
print("Succesfully removed :cry:")
|
||||
except Exception:
|
||||
print(anime_id, "Nothing to remove :confused:")
|
||||
else:
|
||||
self.anime_list.append(anime_id)
|
||||
user_data_helper.update_animelist(self.anime_list)
|
||||
print("Succesfully added :smile:")
|
||||
input("Enter to continue...")
|
||||
def initialize_user_data(self):
|
||||
try:
|
||||
if os.path.isfile(USER_DATA_PATH):
|
||||
with open(USER_DATA_PATH, "r") as f:
|
||||
user_data = json.load(f)
|
||||
self.user_data.update(user_data)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def _update_user_data(self):
|
||||
"""method that updates the actual user data file"""
|
||||
with open(USER_DATA_PATH, "w") as f:
|
||||
json.dump(self.user_data, f)
|
||||
|
||||
# getters for user configuration
|
||||
|
||||
# --- general section ---
|
||||
def get_provider(self):
|
||||
return self.configparser.get("general", "provider")
|
||||
|
||||
def get_preferred_language(self):
|
||||
return self.configparser.get("general", "preferred_language")
|
||||
|
||||
def get_downloads_dir(self):
|
||||
return self.configparser.get("general", "downloads_dir")
|
||||
|
||||
def get_icons(self):
|
||||
return self.configparser.getboolean("general", "icons")
|
||||
|
||||
def get_preview(self):
|
||||
return self.configparser.getboolean("general", "preview")
|
||||
|
||||
def get_use_fzf(self):
|
||||
return self.configparser.getboolean("general", "use_fzf")
|
||||
|
||||
# rofi conifiguration
|
||||
def get_use_rofi(self):
|
||||
return self.configparser.getboolean("general", "use_rofi")
|
||||
|
||||
def get_rofi_theme(self):
|
||||
return self.configparser.get("general", "rofi_theme")
|
||||
|
||||
@@ -135,47 +204,18 @@ class Config(object):
|
||||
def get_rofi_theme_confirm(self):
|
||||
return self.configparser.get("general", "rofi_theme_confirm")
|
||||
|
||||
def get_downloads_dir(self):
|
||||
return self.configparser.get("general", "downloads_dir")
|
||||
|
||||
def get_use_fzf(self):
|
||||
return self.configparser.getboolean("general", "use_fzf")
|
||||
|
||||
def get_use_rofi(self):
|
||||
return self.configparser.getboolean("general", "use_rofi")
|
||||
|
||||
# --- stream section ---
|
||||
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")
|
||||
|
||||
def get_preview(self):
|
||||
return self.configparser.getboolean("general", "preview")
|
||||
|
||||
def get_preferred_language(self):
|
||||
return self.configparser.get("general", "preferred_language")
|
||||
|
||||
def get_sort_by(self):
|
||||
return self.configparser.get("anilist", "sort_by")
|
||||
|
||||
def get_continue_from_history(self):
|
||||
return self.configparser.getboolean("stream", "continue_from_history")
|
||||
|
||||
def get_translation_type(self):
|
||||
return self.configparser.get("stream", "translation_type")
|
||||
|
||||
def get_auto_next(self):
|
||||
return self.configparser.getboolean("stream", "auto_next")
|
||||
|
||||
def get_auto_select(self):
|
||||
return self.configparser.getboolean("stream", "auto_select")
|
||||
|
||||
def get_quality(self):
|
||||
return self.configparser.get("stream", "quality")
|
||||
def get_continue_from_history(self):
|
||||
return self.configparser.getboolean("stream", "continue_from_history")
|
||||
|
||||
def get_use_mpv_mod(self):
|
||||
return self.configparser.getboolean("stream", "use_mpv_mod")
|
||||
@@ -186,19 +226,117 @@ class Config(object):
|
||||
def get_error(self):
|
||||
return self.configparser.getint("stream", "error")
|
||||
|
||||
def get_force_window(self):
|
||||
return self.configparser.get("stream", "force_window")
|
||||
|
||||
def get_translation_type(self):
|
||||
return self.configparser.get("stream", "translation_type")
|
||||
|
||||
def get_quality(self):
|
||||
return self.configparser.get("stream", "quality")
|
||||
|
||||
def get_server(self):
|
||||
return self.configparser.get("stream", "server")
|
||||
|
||||
def get_format(self):
|
||||
return self.configparser.get("stream", "format")
|
||||
|
||||
def get_sort_by(self):
|
||||
return self.configparser.get("anilist", "sort_by")
|
||||
|
||||
def update_config(self, section: str, key: str, value: str):
|
||||
self.configparser.set(section, key, value)
|
||||
with open(USER_CONFIG_PATH, "w") as config:
|
||||
self.configparser.write(config)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Config(server:{self.get_server()},quality:{self.get_quality()},auto_next:{self.get_auto_next()},continue_from_history:{self.get_continue_from_history()},sort_by:{self.get_sort_by()},downloads_dir:{self.get_downloads_dir()})"
|
||||
current_config_state = f"""
|
||||
[stream]
|
||||
# Auto continue from watch history
|
||||
continue_from_history = {self.continue_from_history}
|
||||
|
||||
# Preferred language for anime (options: dub, sub)
|
||||
translation_type = {self.translation_type}
|
||||
|
||||
# Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
||||
server = {self.server}
|
||||
|
||||
# Auto-select next episode
|
||||
auto_next = {self.auto_next}
|
||||
|
||||
# Auto select the anime provider results with fuzzy find.
|
||||
# Note this wont always be correct.But 99% of the time will be.
|
||||
auto_select = {self.auto_select}
|
||||
|
||||
# whether to skip the opening and ending theme songs
|
||||
# NOTE: requires ani-skip to be in path
|
||||
skip = {self.skip}
|
||||
|
||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||
# used in the continue from time stamp
|
||||
error = {self.error}
|
||||
|
||||
# whether to use python-mpv
|
||||
# to enable superior control over the player
|
||||
# adding more options to it
|
||||
use_mpv_mod = {self.use_mpv_mod}
|
||||
|
||||
# the format of downloaded anime and trailer
|
||||
# based on yt-dlp format and passed directly to it
|
||||
# learn more by looking it up on their site
|
||||
# only works for downloaded anime if server=gogoanime
|
||||
# since its the only one that offers different formats
|
||||
# the others tend not to
|
||||
format = {self.format}
|
||||
|
||||
[general]
|
||||
|
||||
# can be [allanime,animepahe]
|
||||
provider = {self.provider}
|
||||
|
||||
# Display language (options: english, romaji)
|
||||
preferred_language = {self.preferred_language}
|
||||
|
||||
# Download directory
|
||||
downloads_dir = {self.downloads_dir}
|
||||
|
||||
# whether to show a preview window when using fzf or rofi
|
||||
preview = {self.preview}
|
||||
|
||||
# whether to use fzf as the interface for the anilist command and others.
|
||||
use_fzf = {self.use_fzf}
|
||||
|
||||
# whether to use rofi for the ui
|
||||
use_rofi = {self.use_rofi}
|
||||
|
||||
# rofi theme to use
|
||||
rofi_theme = {self.rofi_theme}
|
||||
rofi_theme_input = {self.rofi_theme_input}
|
||||
rofi_theme_confirm = {self.rofi_theme_confirm}
|
||||
|
||||
# whether to show the icons
|
||||
icons = {self.icons}
|
||||
|
||||
# the duration in minutes a notification will stay in the screen
|
||||
# used by notifier command
|
||||
notification_duration = {self.notification_duration}
|
||||
"""
|
||||
return current_config_state
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
# WARNING: depracated and will probably be removed
|
||||
def update_anime_list(self, anime_id: int, remove=False):
|
||||
if remove:
|
||||
try:
|
||||
self.anime_list.remove(anime_id)
|
||||
print("Succesfully removed :cry:")
|
||||
except Exception:
|
||||
print(anime_id, "Nothing to remove :confused:")
|
||||
else:
|
||||
self.anime_list.append(anime_id)
|
||||
self.user_data["animelist"] = list(set(self.anime_list))
|
||||
self._update_user_data()
|
||||
print("Succesfully added :smile:")
|
||||
input("Enter to continue...")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -364,8 +365,35 @@ def provider_anime_episode_servers_menu(
|
||||
# no need to get all servers if top just works
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching top server...", total=None)
|
||||
selected_server = next(episode_streams_generator)
|
||||
server_name = "top"
|
||||
try:
|
||||
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)
|
||||
if not config.use_rofi:
|
||||
input("Enter to coninue...")
|
||||
else:
|
||||
if not Rofi.confirm(f"!!Sth went wrong!!: {e} Enter to continue"):
|
||||
exit_app(1)
|
||||
server_name = None
|
||||
selected_server = ""
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
else:
|
||||
with Progress() as progress:
|
||||
progress.add_task("Fetching servers...", total=None)
|
||||
@@ -400,9 +428,21 @@ def provider_anime_episode_servers_menu(
|
||||
|
||||
provider_anime_episodes_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
elif server_name == "top":
|
||||
elif server_name == "top" and episode_streams_dict.keys():
|
||||
selected_server = episode_streams_dict[list(episode_streams_dict.keys())[0]]
|
||||
else:
|
||||
if server_name == "top" or server_name == "back":
|
||||
if config.use_rofi:
|
||||
if not Rofi.confirm("No severs available..."):
|
||||
exit_app()
|
||||
else:
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
else:
|
||||
print("Failed to set server")
|
||||
input("Enter to continue")
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
return
|
||||
selected_server = episode_streams_dict[server_name]
|
||||
|
||||
# get the stream of the preferred quality
|
||||
@@ -478,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,
|
||||
@@ -506,7 +546,10 @@ def provider_anime_episode_servers_menu(
|
||||
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
|
||||
@@ -735,7 +778,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
|
||||
|
||||
@@ -757,7 +800,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
|
||||
|
||||
@@ -782,7 +825,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:
|
||||
@@ -824,7 +869,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:
|
||||
@@ -855,7 +902,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
|
||||
|
||||
@@ -917,9 +964,9 @@ def media_actions_menu(
|
||||
"""
|
||||
from rich.console import Console
|
||||
from rich.prompt import Confirm
|
||||
from yt_dlp.utils import clean_html
|
||||
|
||||
from ...Utility import anilist_data_helper
|
||||
from ...Utility.utils import remove_html_tags
|
||||
from ..utils.print_img import print_img
|
||||
|
||||
clear()
|
||||
@@ -970,7 +1017,7 @@ def media_actions_menu(
|
||||
)
|
||||
console.print(
|
||||
"[bold underline cyan]Description\n[/]",
|
||||
remove_html_tags(str(selected_anime_anilist["description"])),
|
||||
clean_html(str(selected_anime_anilist["description"])),
|
||||
)
|
||||
if Confirm.ask("Enter to continue...", default=True):
|
||||
media_actions_menu(config, fastanime_runtime_state)
|
||||
@@ -1097,7 +1144,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
|
||||
|
||||
@@ -1264,7 +1311,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
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import textwrap
|
||||
from threading import Thread
|
||||
|
||||
import requests
|
||||
from yt_dlp.utils import clean_html
|
||||
|
||||
from ...constants import APP_CACHE_DIR
|
||||
from ...libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from ...Utility import anilist_data_helper
|
||||
from ...Utility.utils import remove_html_tags
|
||||
from ..utils.utils import get_true_fg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -201,7 +201,7 @@ def write_search_results(
|
||||
template = textwrap.dedent(template)
|
||||
template = f"""
|
||||
{template}
|
||||
{textwrap.fill(remove_html_tags(
|
||||
{textwrap.fill(clean_html(
|
||||
str(anime['description'])), width=45)}
|
||||
"""
|
||||
future_to_task[executor.submit(save_info_from_str, template, title)] = title
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,20 +5,23 @@ import requests
|
||||
|
||||
|
||||
def print_img(url: str):
|
||||
executable = shutil.which("chafa")
|
||||
curl = shutil.which("curl")
|
||||
# curl -sL "$1" | chafa /dev/stdin
|
||||
"""helper funtion to print an image given its url
|
||||
|
||||
if executable is None or curl is None:
|
||||
print("chafa or curl not found")
|
||||
return
|
||||
Args:
|
||||
url: [TODO:description]
|
||||
"""
|
||||
if EXECUTABLE := shutil.which("icat"):
|
||||
subprocess.run([EXECUTABLE, url])
|
||||
else:
|
||||
EXECUTABLE = shutil.which("chafa")
|
||||
|
||||
res = requests.get(url)
|
||||
if res.status_code != 200:
|
||||
print("Error fetching image")
|
||||
return
|
||||
img_bytes = res.content
|
||||
if not img_bytes:
|
||||
print("No image found")
|
||||
img_bytes = subprocess.check_output([curl, "-sL", url])
|
||||
subprocess.run([executable, url, "--size=15x15"], input=img_bytes)
|
||||
if EXECUTABLE is None:
|
||||
print("chafanot found")
|
||||
return
|
||||
|
||||
res = requests.get(url)
|
||||
if res.status_code != 200:
|
||||
print("Error fetching image")
|
||||
return
|
||||
img_bytes = res.content
|
||||
subprocess.run([EXECUTABLE, url, "--size=15x15"], input=img_bytes)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# TODO: add typing
|
||||
class FastAnimeRuntimeState(dict):
|
||||
"""dot.notation access to dictionary attributes"""
|
||||
"""A class that manages fastanime runtime during anilist command runtime"""
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
@@ -13,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
|
||||
@@ -24,7 +25,8 @@ def exit_app(*args):
|
||||
try:
|
||||
shutil.get_terminal_size()
|
||||
return (
|
||||
sys.stdin.isatty()
|
||||
sys.stdin
|
||||
and sys.stdin.isatty()
|
||||
and sys.stdout.isatty()
|
||||
and os.getenv("TERM") is not None
|
||||
)
|
||||
@@ -44,17 +46,4 @@ def exit_app(*args):
|
||||
from rich import print
|
||||
|
||||
print("Have a good day :smile:", USER_NAME)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def get_formatted_str(string: str, style):
|
||||
from rich.text import Text
|
||||
|
||||
# Create a Text object with desired style
|
||||
text = Text(string, style="bold red")
|
||||
|
||||
# Convert the Text object to an ANSI string
|
||||
ansi_output = text.__rich_console__(None, None) # pyright:ignore
|
||||
|
||||
# Join the ANSI strings to form the final output
|
||||
"".join(segment.text for segment in ansi_output)
|
||||
sys.exit(exit_code)
|
||||
|
||||
@@ -2,9 +2,6 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from InquirerPy import inquirer
|
||||
from thefuzz import fuzz
|
||||
|
||||
from ...Utility.data import anime_normalizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
@@ -22,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:
|
||||
@@ -33,8 +30,20 @@ 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:
|
||||
try:
|
||||
print("Qualities were: ", stream_links)
|
||||
print("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"):
|
||||
@@ -101,30 +110,3 @@ def fuzzy_inquirer(choices: list, prompt: str, **kwargs):
|
||||
**kwargs,
|
||||
).execute()
|
||||
return action
|
||||
|
||||
|
||||
# WARNING: This function is depracated use the one from the main utility package
|
||||
def anime_title_percentage_match(
|
||||
possible_user_requested_anime_title: str, title: tuple
|
||||
) -> float:
|
||||
"""Returns the percentage match between the possible title and user title
|
||||
|
||||
Args:
|
||||
possible_user_requested_anime_title (str): an Animdl search result title
|
||||
title (str): the anime title the user wants
|
||||
|
||||
Returns:
|
||||
int: the percentage match
|
||||
"""
|
||||
if normalized_anime_title := anime_normalizer.get(
|
||||
possible_user_requested_anime_title
|
||||
):
|
||||
possible_user_requested_anime_title = normalized_anime_title
|
||||
for key, value in locals().items():
|
||||
logger.info(f"{key}: {value}")
|
||||
# compares both the romaji and english names and gets highest Score
|
||||
percentage_ratio = max(
|
||||
fuzz.ratio(title[0].lower(), possible_user_requested_anime_title.lower()),
|
||||
fuzz.ratio(title[1].lower(), possible_user_requested_anime_title.lower()),
|
||||
)
|
||||
return percentage_ratio
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from platform import system
|
||||
|
||||
from platformdirs import PlatformDirs
|
||||
|
||||
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__))
|
||||
@@ -24,19 +22,63 @@ 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
|
||||
|
||||
if S_PLATFORM == "win32":
|
||||
# app data
|
||||
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
|
||||
video_dir_base = os.path.expanduser("~/Videos")
|
||||
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
||||
|
||||
elif S_PLATFORM == "darwin":
|
||||
# app data
|
||||
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
|
||||
cache_dir_base = os.path.expanduser("~/Library/Caches")
|
||||
APP_CACHE_DIR = os.path.join(cache_dir_base, APP_NAME, __version__)
|
||||
|
||||
# videos dir
|
||||
video_dir_base = os.path.expanduser("~/Movies")
|
||||
USER_VIDEOS_DIR = os.path.join(video_dir_base, APP_NAME)
|
||||
else:
|
||||
# app data
|
||||
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
|
||||
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
|
||||
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")
|
||||
|
||||
@@ -18,7 +18,6 @@ from .constants import (
|
||||
USER_AGENT,
|
||||
)
|
||||
from .gql_queries import ALLANIME_EPISODES_GQL, ALLANIME_SEARCH_GQL, ALLANIME_SHOW_GQL
|
||||
from .normalizer import normalize_anime, normalize_search_results
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterator
|
||||
@@ -106,7 +105,23 @@ class AllAnimeAPI(AnimeProvider):
|
||||
}
|
||||
try:
|
||||
search_results = self._fetch_gql(ALLANIME_SEARCH_GQL, variables)
|
||||
return normalize_search_results(search_results) # pyright:ignore
|
||||
page_info = search_results["shows"]["pageInfo"]
|
||||
results = []
|
||||
for result in search_results["shows"]["edges"]:
|
||||
normalized_result = {
|
||||
"id": result["_id"],
|
||||
"title": result["name"],
|
||||
"type": result["__typename"],
|
||||
"availableEpisodes": result["availableEpisodes"],
|
||||
}
|
||||
results.append(normalized_result)
|
||||
|
||||
normalized_search_results = {
|
||||
"pageInfo": page_info,
|
||||
"results": results,
|
||||
}
|
||||
return normalized_search_results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"FA(AllAnime): {e}")
|
||||
return {}
|
||||
@@ -123,9 +138,19 @@ class AllAnimeAPI(AnimeProvider):
|
||||
variables = {"showId": allanime_show_id}
|
||||
try:
|
||||
anime = self._fetch_gql(ALLANIME_SHOW_GQL, variables)
|
||||
return normalize_anime(anime["show"])
|
||||
id: str = anime["show"]["_id"]
|
||||
title: str = anime["show"]["name"]
|
||||
availableEpisodesDetail = anime["show"]["availableEpisodesDetail"]
|
||||
type = anime.get("__typename")
|
||||
normalized_anime = {
|
||||
"id": id,
|
||||
"title": title,
|
||||
"availableEpisodesDetail": availableEpisodesDetail,
|
||||
"type": type,
|
||||
}
|
||||
return normalized_anime
|
||||
except Exception as e:
|
||||
logger.error(f"FA(AllAnime): {e}")
|
||||
logger.error(f"AllAnime(get_anime): {e}")
|
||||
return None
|
||||
|
||||
def _get_anime_episode(
|
||||
@@ -323,7 +348,9 @@ if __name__ == "__main__":
|
||||
print("Sth went wrong")
|
||||
break
|
||||
episode_streams_ = anime_provider.get_episode_streams(
|
||||
anime_data, episode, translation.strip()
|
||||
anime_data, # pyright: ignore
|
||||
episode,
|
||||
translation.strip(),
|
||||
)
|
||||
if episode_streams_ is None:
|
||||
raise Exception("Episode not found")
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
from ..types import Anime, AnimeEpisodeDetails, SearchResults
|
||||
from .types import AllAnimeEpisode, AllAnimeSearchResults, AllAnimeShow
|
||||
|
||||
# TODO: scrap this module and do the transformations directly from the provider class
|
||||
|
||||
|
||||
def normalize_search_results(search_results: AllAnimeSearchResults) -> SearchResults:
|
||||
page_info = search_results["shows"]["pageInfo"]
|
||||
results = []
|
||||
for result in search_results["shows"]["edges"]:
|
||||
normalized_result = {
|
||||
"id": result["_id"],
|
||||
"title": result["name"],
|
||||
"type": result["__typename"],
|
||||
"availableEpisodes": result["availableEpisodes"],
|
||||
}
|
||||
results.append(normalized_result)
|
||||
|
||||
normalized_search_results: SearchResults = {
|
||||
"pageInfo": page_info, # pyright:ignore
|
||||
"results": results,
|
||||
}
|
||||
|
||||
return normalized_search_results
|
||||
|
||||
|
||||
def normalize_anime(anime: AllAnimeShow) -> Anime:
|
||||
id: str = anime["_id"]
|
||||
title: str = anime["name"]
|
||||
availableEpisodesDetail: AnimeEpisodeDetails = anime[
|
||||
"availableEpisodesDetail"
|
||||
] # pyright:ignore
|
||||
type = anime.get("__typename")
|
||||
normalized_anime: Anime = { # pyright:ignore
|
||||
"id": id,
|
||||
"title": title,
|
||||
"availableEpisodesDetail": availableEpisodesDetail,
|
||||
"type": type,
|
||||
}
|
||||
return normalized_anime
|
||||
|
||||
|
||||
def normalize_episode(episode: AllAnimeEpisode):
|
||||
pass
|
||||
@@ -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,20 +70,55 @@ 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())
|
||||
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,
|
||||
)
|
||||
|
||||
_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
|
||||
self.anime = data # pyright:ignore
|
||||
episodes = list(map(str, range(data["total"])))
|
||||
title = ""
|
||||
return {
|
||||
@@ -121,9 +158,10 @@ class AnimePaheApi(AnimeProvider):
|
||||
for episode in self.anime["data"]
|
||||
if float(episode["episode"]) == float(episode_number)
|
||||
]
|
||||
|
||||
if not episode:
|
||||
logger.error(f"AnimePahe(streams): episode {episode_number} doesn't exist")
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
episode = episode[0]
|
||||
|
||||
anime_id = anime["id"]
|
||||
@@ -157,7 +195,7 @@ class AnimePaheApi(AnimeProvider):
|
||||
logger.warn(
|
||||
"AnimePahe: embed url not found please report to the developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
# get embed page
|
||||
embed_response = self.session.get(embed_url, headers=SERVER_HEADERS)
|
||||
embed = embed_response.text
|
||||
@@ -174,14 +212,14 @@ class AnimePaheApi(AnimeProvider):
|
||||
logger.warn(
|
||||
"AnimePahe: Encoded js not found please report to the developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
# execute the encoded js with node for now or maybe forever in odrder to get a more workable info
|
||||
NODE = shutil.which("node")
|
||||
if not NODE:
|
||||
logger.warn(
|
||||
"AnimePahe: animepahe currently requires node js to extract them juicy streams"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
result = subprocess.run(
|
||||
[NODE, "-e", encoded_js],
|
||||
text=True,
|
||||
@@ -193,14 +231,14 @@ class AnimePaheApi(AnimeProvider):
|
||||
logger.warn(
|
||||
"AnimePahe: could not decode encoded js using node please report to developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
# get that juicy stream
|
||||
match = JUICY_STREAM_REGEX.search(evaluted_js)
|
||||
if not match:
|
||||
logger.warn(
|
||||
"AnimePahe: could not find the juicy stream please report to developers"
|
||||
)
|
||||
raise Exception("Episode not found")
|
||||
return []
|
||||
# get the actual hls stream link
|
||||
juicy_stream = match.group(1)
|
||||
# add the link
|
||||
|
||||
@@ -67,5 +67,5 @@ class EpisodeStream(TypedDict):
|
||||
|
||||
class Server(TypedDict):
|
||||
server: str
|
||||
episode_title: str | None
|
||||
episode_title: str
|
||||
links: list[EpisodeStream]
|
||||
|
||||
59
poetry.lock
generated
59
poetry.lock
generated
@@ -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"
|
||||
@@ -1208,13 +1157,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "tox"
|
||||
version = "4.17.0"
|
||||
version = "4.17.1"
|
||||
description = "tox is a generic virtualenv management and test command line tool"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tox-4.17.0-py3-none-any.whl", hash = "sha256:82ef41e7e54182e2143daf0b2920d9030c2e1c4291e12091ebad66860c7be7a4"},
|
||||
{file = "tox-4.17.0.tar.gz", hash = "sha256:b1e2e1dfbfdc174d9be95ae78ec2c4d2cf4800d4c15571deddb197a2c90d2de6"},
|
||||
{file = "tox-4.17.1-py3-none-any.whl", hash = "sha256:2974597c0353577126ab014f52d1a399fb761049e165ff34427f84e8cfe6c990"},
|
||||
{file = "tox-4.17.1.tar.gz", hash = "sha256:2c41565a571e34480bd401d668a4899806169a4633e972ac296c54406d2ded8a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "fastanime"
|
||||
version = "0.62.5.dev1"
|
||||
version = "1.1.6.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]
|
||||
|
||||
@@ -55,6 +55,11 @@ def test_completions_help(runner: CliRunner):
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_update_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["update", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_anilist_help(runner: CliRunner):
|
||||
result = runner.invoke(run_cli, ["anilist", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
Reference in New Issue
Block a user