diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c924d7..9b4c623 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,10 @@ repos: - id: black name: black language_version: python3.10 - - repo: https://github.com/PyCQA/bandit - rev: 1.7.9 # Update me! - hooks: - - id: bandit + # ------ TODO: re-add this ----- + # - repo: https://github.com/PyCQA/bandit + # rev: 1.7.9 # Update me! + # hooks: + # - id: bandit + # args: ["-c", "pyproject.toml"] + # additional_dependencies: ["bandit[toml]"] diff --git a/fa b/fa index c9d68be..46ef129 100755 --- a/fa +++ b/fa @@ -1,4 +1,5 @@ #! /usr/bin/bash +cd $HOME/code/python/kivy_apps/FastAnime poetry install clear poetry run fastanime $* diff --git a/fastanime/FastAnime/config.ini b/fastanime/FastAnime/config.ini new file mode 100644 index 0000000..dbcc5ff --- /dev/null +++ b/fastanime/FastAnime/config.ini @@ -0,0 +1,15 @@ +[DEFAULT] +server = +continue_from_history = False +quality = 0 +auto_next = True +sort_by = search match +downloads_dir = /home/benxl-85/Videos/FastAnime +translation_type = sub + +[stream] + +[general] + +[anilist] + diff --git a/fastanime/Utility/add_desktop_entry.py b/fastanime/Utility/add_desktop_entry.py new file mode 100644 index 0000000..1c65c8b --- /dev/null +++ b/fastanime/Utility/add_desktop_entry.py @@ -0,0 +1,32 @@ +import os +import shutil + +from pyshortcuts import make_shortcut + +from .. import ASSETS_DIR, PLATFORM + + +def create_desktop_shortcut(): + app = "_ -m fastanime --gui" + + logo = os.path.join(ASSETS_DIR, "logo.png") + if PLATFORM == "Windows": + logo = os.path.join(ASSETS_DIR, "logo.ico") + if fastanime := shutil.which("fastanime"): + app = f"{fastanime} --gui" + make_shortcut( + app, + name="FastAnime", + description="Download and watch anime", + terminal=False, + icon=logo, + executable=fastanime, + ) + else: + make_shortcut( + app, + name="FastAnime", + description="Download and watch anime", + terminal=False, + icon=logo, + ) diff --git a/fastanime/Utility/data.py b/fastanime/Utility/data.py index 591defe..0a46856 100644 --- a/fastanime/Utility/data.py +++ b/fastanime/Utility/data.py @@ -9,6 +9,8 @@ anime_normalizer = { } +anilist_sort_normalizer = {"search match": "SEARCH_MATCH"} + themes_available = [ "Aliceblue", "Antiquewhite", diff --git a/fastanime/Utility/downloader/downloader.py b/fastanime/Utility/downloader/downloader.py index 0857700..d10565d 100644 --- a/fastanime/Utility/downloader/downloader.py +++ b/fastanime/Utility/downloader/downloader.py @@ -3,7 +3,7 @@ from threading import Thread import yt_dlp -from ... import downloads_dir +from ... import USER_DOWNLOADS_DIR from ..show_notification import show_notification from ..utils import sanitize_filename @@ -53,7 +53,7 @@ class YtDLPDownloader: def _download_file(self, url: str, title, custom_progress_hook, silent): anime_title = sanitize_filename(title[0]) ydl_opts = { - "outtmpl": f"{downloads_dir}/{anime_title}/{anime_title}-episode {title[1]}.%(ext)s", # Specify the output path and template + "outtmpl": f"{USER_DOWNLOADS_DIR}/{anime_title}/{anime_title}-episode {title[1]}.%(ext)s", # Specify the output path and template "progress_hooks": [ main_progress_hook, custom_progress_hook, diff --git a/fastanime/__init__.py b/fastanime/__init__.py index 1295467..dcffd4f 100644 --- a/fastanime/__init__.py +++ b/fastanime/__init__.py @@ -1,49 +1,58 @@ import logging import os import sys +from platform import platform import plyer -from rich import print from rich.traceback import install -install() +install(show_locals=True) # Create a logger instance logger = logging.getLogger(__name__) -# TODO:confirm data integrity +# initiate constants +__version__ = "0.3.0" -# ----- some useful paths ----- -app_dir = os.path.abspath(os.path.dirname(__file__)) -data_folder = os.path.join(app_dir, "data") -configs_folder = os.path.join(app_dir, "configs") -if not os.path.exists(data_folder): - os.mkdir(data_folder) +PLATFORM = platform() +APP_NAME = "FastAnime" -if vid_path := plyer.storagepath.get_videos_dir(): # type: ignore - downloads_dir = os.path.join(vid_path, "FastAnime") - if not os.path.exists(downloads_dir): - os.mkdir(downloads_dir) +# ---- app deps ---- +APP_DIR = os.path.abspath(os.path.dirname(__file__)) +CONFIGS_DIR = os.path.join(APP_DIR, "configs") +ASSETS_DIR = os.path.join(APP_DIR, "assets") + +# ----- user configs and data ----- +if PLATFORM == "windows": + APP_DATA_DIR_ = os.environ.get("LOCALAPPDATA", APP_DIR) else: - # fallback - downloads_dir = os.path.join(app_dir, "videos") - if not os.path.exists(downloads_dir): - os.mkdir(downloads_dir) + APP_DATA_DIR_ = os.environ.get("XDG_DATA_HOME", APP_DIR) -user_data_path = os.path.join(data_folder, "user_data.json") -assets_folder = os.path.join(app_dir, "assets") +if not APP_DATA_DIR_: + APP_DATA_DIR = os.path.join(APP_DIR, "data") +else: + APP_DATA_DIR = os.path.join(APP_DATA_DIR_, APP_NAME) + +if not os.path.exists(APP_DATA_DIR): + os.mkdir(APP_DATA_DIR) + +USER_DATA_PATH = os.path.join(APP_DATA_DIR, "user_data.json") +USER_CONFIG_PATH = os.path.join(APP_DATA_DIR, "config.ini") -def FastAnime(gui=False, log=False): +# video dir +if vid_path := plyer.storagepath.get_videos_dir(): # type: ignore + USER_DOWNLOADS_DIR = os.path.join(vid_path, "FastAnime") +else: + USER_DOWNLOADS_DIR = os.path.join(APP_DIR, "videos") + +if not os.path.exists(USER_DOWNLOADS_DIR): + os.mkdir(USER_DOWNLOADS_DIR) + + +def FastAnime(gui=False): if "--gui" in sys.argv: gui = True sys.argv.remove("--gui") - if "--log" in sys.argv: - log = True - sys.argv.remove("--log") - if not log: - logger.propagate = False - - else: # Configure logging from rich.logging import RichHandler @@ -53,13 +62,9 @@ def FastAnime(gui=False, log=False): datefmt="[%X]", # Use a custom date format handlers=[RichHandler()], # Use RichHandler to format the logs ) - - print(f"Hello {os.environ.get('USERNAME','User')} from the fastanime team") if gui: - print(__name__) - from .gui.gui import run_gui + from .gui import run_gui - print("Run GUI") run_gui() else: from .cli import run_cli diff --git a/fastanime/cli/__init__.py b/fastanime/cli/__init__.py index e126b88..6215f45 100644 --- a/fastanime/cli/__init__.py +++ b/fastanime/cli/__init__.py @@ -1,11 +1,62 @@ import click -from rich import print -from .commands import anilist, download, search +from .. import __version__ +from ..libs.anime_provider.allanime.constants import SERVERS_AVAILABLE +from ..Utility.data import anilist_sort_normalizer +from .commands.anilist import anilist +from .commands.config import configure +from .commands.download import download +from .commands.search import search +from .config import Config -commands = {"search": search, "download": download, "anilist": anilist} +commands = { + "search": search, + "download": download, + "anilist": anilist, + "config": configure, +} -@click.group(commands=commands) -def run_cli(): - print("Yellow") +@click.group(commands=commands, invoke_without_command=True) +@click.version_option(__version__, "--version") +@click.option( + "-s", + "--server", + type=click.Choice(SERVERS_AVAILABLE, case_sensitive=False), +) +@click.option("-h", "--hist", type=bool) +@click.option("-q", "--quality", type=int) +@click.option("-t-t", "--translation_type") +@click.option("-a-n", "--auto-next", type=bool) +@click.option( + "-s-b", + "--sort-by", + type=click.Choice(anilist_sort_normalizer.keys()), # pyright: ignore +) +@click.option("-d", "--downloads-dir", type=click.Path()) +@click.pass_context +def run_cli( + ctx: click.Context, + server, + hist, + translation_type, + quality, + auto_next, + sort_by, + downloads_dir, +): + ctx.obj = Config() + if server: + ctx.obj.server = server + if hist: + ctx.obj.continue_from_history = hist + if quality: + ctx.obj.quality = quality + if auto_next: + ctx.obj.auto_next = auto_next + if sort_by: + ctx.obj.sort_by = sort_by + if downloads_dir: + ctx.obj.downloads_dir = downloads_dir + if translation_type: + ctx.obj.translation_type = translation_type diff --git a/fastanime/cli/commands/anilist/__init__.py b/fastanime/cli/commands/anilist/__init__.py index 48a8faf..5f1ae65 100644 --- a/fastanime/cli/commands/anilist/__init__.py +++ b/fastanime/cli/commands/anilist/__init__.py @@ -1,5 +1,6 @@ import click +from ...interfaces import anilist as anilist_interface from .favourites import favourites from .popular import popular from .recent import recent @@ -17,6 +18,7 @@ commands = { } -@click.group(commands=commands) -def anilist(): - pass +@click.group(commands=commands, invoke_without_command=True) +@click.pass_obj +def anilist(config): + anilist_interface(config=config) diff --git a/fastanime/cli/commands/anilist/search.py b/fastanime/cli/commands/anilist/search.py index e570281..abab9b5 100644 --- a/fastanime/cli/commands/anilist/search.py +++ b/fastanime/cli/commands/anilist/search.py @@ -1,7 +1,6 @@ import click from ....libs.anilist.anilist import AniList -from ...interfaces.anime_interface import anime_interface from .utils import get_search_result @@ -10,6 +9,4 @@ from .utils import get_search_result def search(title): success, search_results = AniList.search(title) if search_results and success: - result = get_search_result(search_results) - if result: - anime_interface(result) + get_search_result(search_results) diff --git a/fastanime/cli/commands/anilist/trending.py b/fastanime/cli/commands/anilist/trending.py index 2c633c1..f0d0138 100644 --- a/fastanime/cli/commands/anilist/trending.py +++ b/fastanime/cli/commands/anilist/trending.py @@ -1,7 +1,6 @@ import click from ....libs.anilist.anilist import AniList -from ...interfaces.anime_interface import anime_interface from .utils import get_search_result @@ -9,6 +8,4 @@ from .utils import get_search_result def trending(): success, trending = AniList.get_trending() if trending and success: - result = get_search_result(trending) - if result: - anime_interface(result) + get_search_result(trending) diff --git a/fastanime/cli/commands/config.py b/fastanime/cli/commands/config.py new file mode 100644 index 0000000..fe78f05 --- /dev/null +++ b/fastanime/cli/commands/config.py @@ -0,0 +1,8 @@ +import click + + +@click.command() +def configure(): + pass + + # create_desktop_shortcut() diff --git a/fastanime/cli/commands/search.py b/fastanime/cli/commands/search.py index 2a540ba..bd92bb6 100644 --- a/fastanime/cli/commands/search.py +++ b/fastanime/cli/commands/search.py @@ -1,6 +1,15 @@ import click +from ..interfaces import anime_provider_ + @click.command() -def search(): - print("Searching") +@click.pass_obj +def search( + config, + anime_title, +): + anime_provider_( + config, + anime_title, + ) diff --git a/fastanime/cli/config.py b/fastanime/cli/config.py new file mode 100644 index 0000000..0566005 --- /dev/null +++ b/fastanime/cli/config.py @@ -0,0 +1,67 @@ +import os +from configparser import ConfigParser + +from .. import USER_CONFIG_PATH, USER_DOWNLOADS_DIR + + +class Config(object): + def __init__(self) -> None: + self.configparser = ConfigParser( + { + "server": "", + "continue_from_history": "False", + "quality": "0", + "auto_next": "True", + "sort_by": "search match", + "downloads_dir": USER_DOWNLOADS_DIR, + "translation_type": "sub", + } + ) + self.configparser.add_section("stream") + self.configparser.add_section("general") + self.configparser.add_section("anilist") + if not os.path.exists(USER_CONFIG_PATH): + with open(USER_CONFIG_PATH, "w") as config: + self.configparser.write(config) + self.configparser.read(USER_CONFIG_PATH) + + # --- set defaults --- + self.downloads_dir = self.get_downloads_dir() + self.translation_type = self.get_translation_type() + self.sort_by = self.get_sort_by() + self.continue_from_history = self.get_continue_from_history() + self.auto_next = self.get_auto_next() + self.quality = self.get_quality() + self.server = self.get_server() + + def get_downloads_dir(self): + return self.configparser.get("general", "downloads_dir") + + 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_quality(self): + return self.configparser.getint("stream", "quality") + + def get_server(self): + return self.configparser.get("stream", "server") + + 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()})" + + def __str__(self): + return self.__repr__() diff --git a/fastanime/cli/interfaces.py b/fastanime/cli/interfaces.py new file mode 100644 index 0000000..5fb7923 --- /dev/null +++ b/fastanime/cli/interfaces.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from InquirerPy import inquirer + +from ..libs.anilist.anilist import AniList +from ..libs.anilist.anilist_data_schema import AnilistDataSchema +from ..libs.anime_provider.allanime.api import anime_provider +from .config import Config +from .utils.mpv import mpv +from .utils.utils import clear, fuzzy_inquirer, get_selected_anime, get_selected_server + + +def fetch_episode(config: Config, anime, translation_type, selected_anime): + # fetch episode + episode_number = fuzzy_inquirer( + "Select Episode:", + [*anime["show"]["availableEpisodesDetail"][translation_type], "back"], + ) + if episode_number == "back": + anime_provider_( + config, + selected_anime[0]["name"], + ) + return + print(config.translation_type) + episode = anime_provider.get_anime_episode( + selected_anime[0]["_id"], episode_number, config.translation_type + ) + + fetch_streams(config, episode, anime, translation_type, selected_anime) + + +def fetch_streams(config: Config, episode, *args): + episode_streams = list(anime_provider.get_episode_streams(episode)) + + server = fuzzy_inquirer( + "Select Server:", [episode_stream[0] for episode_stream in episode_streams] + ) + selected_server = get_selected_server(server, episode_streams) + + quality = config.quality + links = selected_server[1]["links"] + if quality > len(links) - 1: + quality = len(links) - 1 + elif quality < 0: + quality = 0 + stream_link = links[quality]["link"] + print("Now playing:", args[-1][0]["name"]) + mpv(stream_link) + clear() + player_controls(config, episode, links, *args) + + +def player_controls(config: Config, episode, links: list, *args): + def _back(): + fetch_streams(config, episode, *args) + + def _replay(): + pass + + def _next_episode(): + pass + + def _episodes(): + fetch_episode(config, *args) + + def _previous_episode(): + pass + + def _change_quality(): + options = [link["link"] for link in links] + quality = fuzzy_inquirer("Select Quality:", options) + config.quality = options.index(quality) # set quality + player_controls(config, episode, links, *args) + + def _change_translation_type(): + options = ["sub", "dub"] + translation_type = fuzzy_inquirer("Select Translation Type:", options) + config.translation_type = translation_type # set trannslation type + player_controls(config, episode, links, *args) + + options = { + "Replay": _replay, + "Next Episode": _next_episode, + "Episodes": _episodes, + "Previous Episode": _previous_episode, + "Change Quality": _change_quality, + "Change Translation Type": _change_translation_type, + "Back": _back, + } + + action = fuzzy_inquirer("Select Action:", options.keys()) + options[action]() + + +def anime_provider_(config: Config, anime_title, **kwargs): + translation_type = config.translation_type + search_results = anime_provider.search_for_anime(anime_title, translation_type) + search_results_anime_titles = [ + anime["name"] for anime in search_results["shows"]["edges"] + ] + selected_anime_title = fuzzy_inquirer( + "Select Search Result:", + [*search_results_anime_titles, "back"], + default=kwargs.get("default_anime_title", ""), + ) + if selected_anime_title == "back": + anilist(config) + return + fetch_anime_epiosode( + config, + selected_anime_title, + search_results, + ) + + +def fetch_anime_epiosode(config, selected_anime_title, search_results): + translation_type = config.translation_type + selected_anime = get_selected_anime(selected_anime_title, search_results) + anime = anime_provider.get_anime(selected_anime[0]["_id"]) + + fetch_episode(config, anime, translation_type, selected_anime) + + +def _stream(config, anilist_data: AnilistDataSchema, preferred_lang="romaji"): + anime_titles = [ + str(anime["title"][preferred_lang]) + for anime in anilist_data["data"]["Page"]["media"] + ] + selected_anime_title = fuzzy_inquirer("Select Anime:", anime_titles) + anime_provider_( + config, selected_anime_title, default_anime_title=selected_anime_title + ) + + +def anilist_options(config, anilist_data: AnilistDataSchema): + def _watch_trailer(): + pass + + def _add_to_list(): + pass + + def _remove_from_list(): + pass + + def _view_info(): + pass + + options = { + "stream": _stream, + "watch trailer": _watch_trailer, + "add to list": _add_to_list, + "remove from list": _remove_from_list, + "view info": _view_info, + "back": anilist, + } + action = fuzzy_inquirer("Select Action:", options.keys()) + options[action](config, anilist_data) + + +def anilist(config, *args, **kwargs): + def _anilist_search(): + search_term = inquirer.text( + "Search:", instruction="Enter anime to search for" + ).execute() + + return AniList.search(query=search_term) + + options = { + "trending": AniList.get_trending, + "search": _anilist_search, + "most popular anime": AniList.get_most_popular, + "most favourite anime": AniList.get_most_favourite, + "most scored anime": AniList.get_most_scored, + "upcoming anime": AniList.get_most_favourite, + "recently updated anime": AniList.get_most_recently_updated, + } + action = fuzzy_inquirer("Select Action:", options.keys()) + anilist_data = options[action]() + if anilist_data[0]: + anilist_options(config, anilist_data[1]) diff --git a/fastanime/cli/interfaces/__init__.py b/fastanime/cli/interfaces/__init__.py deleted file mode 100644 index 653cb2d..0000000 --- a/fastanime/cli/interfaces/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -def bye(): - import sys - - sys.exit() diff --git a/fastanime/cli/interfaces/anime_interface.py b/fastanime/cli/interfaces/anime_interface.py deleted file mode 100644 index 6cdea54..0000000 --- a/fastanime/cli/interfaces/anime_interface.py +++ /dev/null @@ -1,24 +0,0 @@ -from ..utils.fzf import fzf -from . import ( - binge_interface, - bye, - download_interface, - info_interface, - stream_interface, - watchlist_interface, -) - -options = { - "info": info_interface, - "stream": stream_interface, - "binge": binge_interface, - "download": download_interface, - "watchlist": watchlist_interface, - "quit": bye, -} - - -def anime_interface(anime): - command = fzf(options.keys()) - if command: - options[command](anime, options) diff --git a/fastanime/cli/interfaces/binge_interface.py b/fastanime/cli/interfaces/binge_interface.py deleted file mode 100644 index a0231bd..0000000 --- a/fastanime/cli/interfaces/binge_interface.py +++ /dev/null @@ -1,2 +0,0 @@ -def binge_interface(anime, back): - print(anime) diff --git a/fastanime/cli/interfaces/download_interface.py b/fastanime/cli/interfaces/download_interface.py deleted file mode 100644 index 568a553..0000000 --- a/fastanime/cli/interfaces/download_interface.py +++ /dev/null @@ -1,2 +0,0 @@ -def download_interface(anime, back): - print(anime) diff --git a/fastanime/cli/interfaces/info_interface.py b/fastanime/cli/interfaces/info_interface.py deleted file mode 100644 index 03966c4..0000000 --- a/fastanime/cli/interfaces/info_interface.py +++ /dev/null @@ -1,2 +0,0 @@ -def info_interface(anime, back): - print(anime) diff --git a/fastanime/cli/interfaces/quit.py b/fastanime/cli/interfaces/quit.py deleted file mode 100644 index f89635a..0000000 --- a/fastanime/cli/interfaces/quit.py +++ /dev/null @@ -1,8 +0,0 @@ -import sys - -from rich import print - - -def bye(*args): - print("Goodbye") - sys.exit() diff --git a/fastanime/cli/interfaces/stream_interface.py b/fastanime/cli/interfaces/stream_interface.py deleted file mode 100644 index 71c9b1b..0000000 --- a/fastanime/cli/interfaces/stream_interface.py +++ /dev/null @@ -1,105 +0,0 @@ -import logging - -from fuzzywuzzy import fuzz - -from ...libs.anime_provider.allanime.api import anime_provider -from ...Utility.data import anime_normalizer -from ..utils.fzf import fzf -from ..utils.mpv import mpv - -logger = logging.getLogger(__name__) - - -def back_(anime, options): - command = fzf(options.keys()) - if command: - options[command](anime, options) - - -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 - - -def get_matched_result(anime_title, _search_results): - result = max( - _search_results, - key=lambda x: anime_title_percentage_match(x, anime_title), - ) - - return result - - -def _get_result(result, compare): - return result["name"] == compare - - -def _get_server(server, server_name): - return server[0] == server_name - - -def stream_interface(_anime, back, prefered_translation="sub"): - results = anime_provider.search_for_anime(_anime["title"]["romaji"]) - if results: - _search_results = [result["name"] for result in results["shows"]["edges"]] - - anime_title = get_matched_result( - (_anime["title"]["romaji"], _anime["title"]["english"]), _search_results - ) - result = list( - filter(lambda x: _get_result(x, anime_title), results["shows"]["edges"]) - ) - if not result: - return - - anime = anime_provider.get_anime(result[0]["_id"]) - episode = fzf(anime["show"]["availableEpisodesDetail"][prefered_translation]) - - if not episode: - return - if t_type := fzf(["sub", "dub"]): - prefered_translation = t_type - _episode_streams = anime_provider.get_anime_episode( - result[0]["_id"], episode, prefered_translation - ) - if _episode_streams: - episode_streams = anime_provider.get_episode_streams(_episode_streams) - if not episode_streams: - return - servers = list(episode_streams) - - _sever = fzf([server[0] for server in servers]) - if not _sever: - return - - server = list(filter(lambda x: _get_server(x, _sever), servers)).pop() - - if not server: - return - # - stream_link = server[1]["links"][0]["link"] - mpv(stream_link) - # - # mpv_player.run_mpv(stream_link) - stream_interface(_anime, back, prefered_translation) diff --git a/fastanime/cli/interfaces/watchlist_interface.py b/fastanime/cli/interfaces/watchlist_interface.py deleted file mode 100644 index cee2edf..0000000 --- a/fastanime/cli/interfaces/watchlist_interface.py +++ /dev/null @@ -1,2 +0,0 @@ -def watchlist_interface(anime, back): - print(anime) diff --git a/fastanime/cli/utils/mpv.py b/fastanime/cli/utils/mpv.py index cae3d2e..39f15c5 100644 --- a/fastanime/cli/utils/mpv.py +++ b/fastanime/cli/utils/mpv.py @@ -1,11 +1,10 @@ import shutil import subprocess -import sys def mpv(link, *custom_args): MPV = shutil.which("mpv") if not MPV: + print("mpv not found") return subprocess.run([MPV, *custom_args, link]) - sys.stdout.flush() diff --git a/fastanime/cli/utils/utils.py b/fastanime/cli/utils/utils.py new file mode 100644 index 0000000..5428d84 --- /dev/null +++ b/fastanime/cli/utils/utils.py @@ -0,0 +1,68 @@ +import logging +import os + +from fuzzywuzzy import fuzz +from InquirerPy import inquirer + +from ... import PLATFORM +from ...Utility.data import anime_normalizer + +logger = logging.getLogger(__name__) + + +def clear(): + if PLATFORM == "Windows": + os.system("cls") + else: + os.system("clear") + + +def fuzzy_inquirer(prompt: str, choices, **kwargs): + clear() + action = inquirer.fuzzy( + prompt, choices, height="100%", border=True, **kwargs + ).execute() + return action + + +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 + + +def get_selected_anime(anime_title, results): + def _get_result(result, compare): + return result["name"] == compare + + return list( + filter(lambda x: _get_result(x, anime_title), results["shows"]["edges"]) + ) + + +def get_selected_server(_server, servers): + def _get_server(server, server_name): + return server[0] == server_name + + server = list(filter(lambda x: _get_server(x, _server), servers)).pop() + return server diff --git a/fastanime/ensure_desktop_icon.py b/fastanime/ensure_desktop_icon.py deleted file mode 100644 index 4c19e44..0000000 --- a/fastanime/ensure_desktop_icon.py +++ /dev/null @@ -1,26 +0,0 @@ -import os -import shutil - -from kivy.utils import platform -from pyshortcuts import make_shortcut - -from . import assets_folder - -app = "_ -m fastanime" - -if fastanime := shutil.which("fastanime"): - app = fastanime - - -logo = os.path.join(assets_folder, "logo.png") - -if platform == "win": - logo = os.path.join(assets_folder, "logo.ico") - -make_shortcut( - app, - name="FastAnime", - description="Download and watch anime", - terminal=False, - icon=logo, -) diff --git a/fastanime/gui/Controller/my_list_screen.py b/fastanime/gui/Controller/my_list_screen.py index 0bf1790..c0f02eb 100644 --- a/fastanime/gui/Controller/my_list_screen.py +++ b/fastanime/gui/Controller/my_list_screen.py @@ -6,8 +6,8 @@ from kivy.logger import Logger # from kivy.clock import Clock from kivy.utils import difference -from ...Utility import user_data_helper from ..Model.my_list_screen import MyListScreenModel +from ..Utility import user_data_helper from ..View.MylistScreen.my_list_screen import MyListScreenView diff --git a/fastanime/gui/Utility/media_card_loader.py b/fastanime/gui/Utility/media_card_loader.py index e83440b..90d09f3 100644 --- a/fastanime/gui/Utility/media_card_loader.py +++ b/fastanime/gui/Utility/media_card_loader.py @@ -3,7 +3,8 @@ from kivy.cache import Cache from kivy.logger import Logger from ...libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema -from ...Utility import anilist_data_helper, user_data_helper +from ...Utility import anilist_data_helper +from . import user_data_helper Cache.register("trailer_urls.anime", timeout=360) @@ -48,9 +49,9 @@ class MediaCardDataLoader(object): # TODO: switch to season and year # - media_card_data[ - "first_aired_on" - ] = f'{anilist_data_helper.format_anilist_date_object(anime_item["startDate"])}' + media_card_data["first_aired_on"] = ( + f'{anilist_data_helper.format_anilist_date_object(anime_item["startDate"])}' + ) media_card_data["studios"] = anilist_data_helper.format_list_data_with_comma( [ diff --git a/fastanime/Utility/user_data_helper.py b/fastanime/gui/Utility/user_data_helper.py similarity index 86% rename from fastanime/Utility/user_data_helper.py rename to fastanime/gui/Utility/user_data_helper.py index 1956c4f..bb2ddef 100644 --- a/fastanime/Utility/user_data_helper.py +++ b/fastanime/gui/Utility/user_data_helper.py @@ -5,15 +5,18 @@ Contains Helper functions to read and write the user data files from datetime import date, datetime from kivy.logger import Logger +from kivy.storage.jsonstore import JsonStore + +from ... import USER_DATA_PATH today = date.today() now = datetime.now() +user_data = JsonStore(USER_DATA_PATH) + # Get the user data def get_user_anime_list() -> list: - from .. import user_data - try: return user_data.get("user_anime_list")[ "user_anime_list" @@ -24,8 +27,6 @@ def get_user_anime_list() -> list: def update_user_anime_list(updated_list: list): - from .. import user_data - try: updated_list_ = list(set(updated_list)) user_data.put("user_anime_list", user_anime_list=updated_list_) diff --git a/fastanime/gui/View/MylistScreen/my_list_screen.py b/fastanime/gui/View/MylistScreen/my_list_screen.py index c7cef44..598becb 100644 --- a/fastanime/gui/View/MylistScreen/my_list_screen.py +++ b/fastanime/gui/View/MylistScreen/my_list_screen.py @@ -19,6 +19,3 @@ class MyListScreenView(BaseScreenView): def update_layout(self, widget): self.user_anime_list_container.data.append(widget) - - -__all__ = ["MyListScreenView"] diff --git a/fastanime/gui/__init__.py b/fastanime/gui/__init__.py index 69b2d8b..b6682fa 100644 --- a/fastanime/gui/__init__.py +++ b/fastanime/gui/__init__.py @@ -1,146 +1,4 @@ -import os -import random - -from kivy.config import Config -from kivy.loader import Loader -from kivy.logger import Logger -from kivy.resources import resource_add_path, resource_find -from kivy.uix.screenmanager import FadeTransition, ScreenManager -from kivy.uix.settings import Settings, SettingsWithSidebar -from kivymd.app import MDApp - -from .. import assets_folder, configs_folder, downloads_dir -from ..libs.mpv.player import mpv_player -from ..Utility import user_data_helper -from ..Utility.data import themes_available -from ..Utility.downloader.downloader import downloader -from ..Utility.show_notification import show_notification -from .View.components.media_card.components.media_popup import MediaPopup -from .View.screens import screens - - -def setup_app(): - os.environ["KIVY_VIDEO"] = "ffpyplayer" # noqa: E402 - Config.set("graphics", "width", "1000") # noqa: E402 - Config.set("graphics", "minimum_width", "1000") # noqa: E402 - Config.set("kivy", "window_icon", resource_find("logo.ico")) # noqa: E402 - Config.write() # noqa: E402 - - Loader.num_workers = 5 - Loader.max_upload_per_frame = 10 - - resource_add_path(assets_folder) - resource_add_path(configs_folder) - - -class FastAnime(MDApp): - default_anime_image = resource_find(random.choice(["default_1.jpg", "default.jpg"])) - default_banner_image = resource_find(random.choice(["banner_1.jpg", "banner.jpg"])) - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.icon = resource_find("logo.png") - - self.load_all_kv_files(self.directory) - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Lightcoral" - self.manager_screens = ScreenManager() - self.manager_screens.transition = FadeTransition() - - def build(self) -> ScreenManager: - self.settings_cls = SettingsWithSidebar - - self.generate_application_screens() - - if config := self.config: - if theme_color := config.get("Preferences", "theme_color"): - self.theme_cls.primary_palette = theme_color - if theme_style := config.get("Preferences", "theme_style"): - self.theme_cls.theme_style = theme_style - - self.anime_screen = self.manager_screens.get_screen("anime screen") - self.search_screen = self.manager_screens.get_screen("search screen") - self.download_screen = self.manager_screens.get_screen("downloads screen") - self.home_screen = self.manager_screens.get_screen("home screen") - return self.manager_screens - - def on_start(self, *args): - self.media_card_popup = MediaPopup() - - def generate_application_screens(self) -> None: - for i, name_screen in enumerate(screens.keys()): - model = screens[name_screen]["model"]() - controller = screens[name_screen]["controller"](model) - view = controller.get_view() - view.manager_screens = self.manager_screens - view.name = name_screen - self.manager_screens.add_widget(view) - - def build_config(self, config): - # General settings setup - config.setdefaults( - "Preferences", - { - "theme_color": "Cyan", - "theme_style": "Dark", - "downloads_dir": downloads_dir, - }, - ) - - def build_settings(self, settings: Settings): - settings.add_json_panel( - "Settings", self.config, resource_find("general_settings_panel.json") - ) - - def on_config_change(self, config, section, key, value): - # TODO: Change to match case - if section == "Preferences": - match key: - case "theme_color": - if value in themes_available: - self.theme_cls.primary_palette = value - else: - Logger.warning( - "AniXStream Settings: An invalid theme has been entered and will be ignored" - ) - config.set("Preferences", "theme_color", "Cyan") - config.write() - case "theme_style": - self.theme_cls.theme_style = value - - def on_stop(self): - pass - - def search_for_anime(self, search_field, **kwargs): - if self.manager_screens.current != "search screen": - self.manager_screens.current = "search screen" - self.search_screen.handle_search_for_anime(search_field, **kwargs) - - def add_anime_to_user_anime_list(self, id: int): - updated_list = user_data_helper.get_user_anime_list() - updated_list.append(id) - user_data_helper.update_user_anime_list(updated_list) - - def remove_anime_from_user_anime_list(self, id: int): - updated_list = user_data_helper.get_user_anime_list() - if updated_list.count(id): - updated_list.remove(id) - user_data_helper.update_user_anime_list(updated_list) - - def show_anime_screen(self, id: int, title, caller_screen_name: str): - self.manager_screens.current = "anime screen" - self.anime_screen.controller.update_anime_view(id, title, caller_screen_name) - - def play_on_mpv(self, anime_video_url: str): - if mpv_player.mpv_process: - mpv_player.stop_mpv() - mpv_player.run_mpv(anime_video_url) - - def download_anime_video(self, url: str, anime_title: tuple): - self.download_screen.new_download_task(anime_title) - show_notification("New Download", f"{anime_title[0]} episode: {anime_title[1]}") - progress_hook = self.download_screen.on_episode_download_progress - downloader.download_file(url, anime_title, progress_hook) +from .app import FastAnime def run_gui(): diff --git a/fastanime/gui/app.py b/fastanime/gui/app.py new file mode 100644 index 0000000..3d89f98 --- /dev/null +++ b/fastanime/gui/app.py @@ -0,0 +1,143 @@ +import os +import random + +from kivy.config import Config +from kivy.loader import Loader +from kivy.logger import Logger +from kivy.resources import resource_add_path, resource_find +from kivy.uix.screenmanager import FadeTransition, ScreenManager +from kivy.uix.settings import Settings, SettingsWithSidebar +from kivymd.app import MDApp + +from .. import ASSETS_DIR, CONFIGS_DIR, USER_DOWNLOADS_DIR +from ..libs.mpv.player import mpv_player +from ..Utility.data import themes_available +from ..Utility.downloader.downloader import downloader +from ..Utility.show_notification import show_notification +from .Utility import user_data_helper +from .View.components.media_card.components.media_popup import MediaPopup +from .View.screens import screens + + +def setup_app(): + os.environ["KIVY_VIDEO"] = "ffpyplayer" # noqa: E402 + Config.set("graphics", "width", "1000") # noqa: E402 + Config.set("graphics", "minimum_width", "1000") # noqa: E402 + Config.set("kivy", "window_icon", resource_find("logo.ico")) # noqa: E402 + Config.write() # noqa: E402 + + Loader.num_workers = 5 + Loader.max_upload_per_frame = 10 + + resource_add_path(ASSETS_DIR) + resource_add_path(CONFIGS_DIR) + + +class FastAnime(MDApp): + default_anime_image = resource_find(random.choice(["default_1.jpg", "default.jpg"])) + default_banner_image = resource_find(random.choice(["banner_1.jpg", "banner.jpg"])) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.icon = resource_find("logo.png") + + self.load_all_kv_files(self.directory) + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Lightcoral" + self.manager_screens = ScreenManager() + self.manager_screens.transition = FadeTransition() + + def build(self) -> ScreenManager: + self.settings_cls = SettingsWithSidebar + + self.generate_application_screens() + + if config := self.config: + if theme_color := config.get("Preferences", "theme_color"): + self.theme_cls.primary_palette = theme_color + if theme_style := config.get("Preferences", "theme_style"): + self.theme_cls.theme_style = theme_style + + self.anime_screen = self.manager_screens.get_screen("anime screen") + self.search_screen = self.manager_screens.get_screen("search screen") + self.download_screen = self.manager_screens.get_screen("downloads screen") + self.home_screen = self.manager_screens.get_screen("home screen") + return self.manager_screens + + def on_start(self, *args): + self.media_card_popup = MediaPopup() + + def generate_application_screens(self) -> None: + for i, name_screen in enumerate(screens.keys()): + model = screens[name_screen]["model"]() + controller = screens[name_screen]["controller"](model) + view = controller.get_view() + view.manager_screens = self.manager_screens + view.name = name_screen + self.manager_screens.add_widget(view) + + def build_config(self, config): + # General settings setup + config.setdefaults( + "Preferences", + { + "theme_color": "Cyan", + "theme_style": "Dark", + "downloads_dir": USER_DOWNLOADS_DIR, + }, + ) + + def build_settings(self, settings: Settings): + settings.add_json_panel( + "Settings", self.config, resource_find("general_settings_panel.json") + ) + + def on_config_change(self, config, section, key, value): + # TODO: Change to match case + if section == "Preferences": + match key: + case "theme_color": + if value in themes_available: + self.theme_cls.primary_palette = value + else: + Logger.warning( + "AniXStream Settings: An invalid theme has been entered and will be ignored" + ) + config.set("Preferences", "theme_color", "Cyan") + config.write() + case "theme_style": + self.theme_cls.theme_style = value + + def on_stop(self): + pass + + def search_for_anime(self, search_field, **kwargs): + if self.manager_screens.current != "search screen": + self.manager_screens.current = "search screen" + self.search_screen.handle_search_for_anime(search_field, **kwargs) + + def add_anime_to_user_anime_list(self, id: int): + updated_list = user_data_helper.get_user_anime_list() + updated_list.append(id) + user_data_helper.update_user_anime_list(updated_list) + + def remove_anime_from_user_anime_list(self, id: int): + updated_list = user_data_helper.get_user_anime_list() + if updated_list.count(id): + updated_list.remove(id) + user_data_helper.update_user_anime_list(updated_list) + + def show_anime_screen(self, id: int, title, caller_screen_name: str): + self.manager_screens.current = "anime screen" + self.anime_screen.controller.update_anime_view(id, title, caller_screen_name) + + def play_on_mpv(self, anime_video_url: str): + if mpv_player.mpv_process: + mpv_player.stop_mpv() + mpv_player.run_mpv(anime_video_url) + + def download_anime_video(self, url: str, anime_title: tuple): + self.download_screen.new_download_task(anime_title) + show_notification("New Download", f"{anime_title[0]} episode: {anime_title[1]}") + progress_hook = self.download_screen.on_episode_download_progress + downloader.download_file(url, anime_title, progress_hook) diff --git a/fastanime/libs/anilist/anilist.py b/fastanime/libs/anilist/anilist.py index c249995..9552938 100644 --- a/fastanime/libs/anilist/anilist.py +++ b/fastanime/libs/anilist/anilist.py @@ -90,6 +90,7 @@ class AniList: start_greater: int | None = None, start_lesser: int | None = None, page: int | None = None, + **kwargs, ): """ A powerful method for searching anime using the anilist api availing most of its options @@ -110,7 +111,7 @@ class AniList: return cls.get_data(anime_query, variables) @classmethod - def get_trending(cls): + def get_trending(cls, *_, **kwargs): """ Gets the currently trending anime """ @@ -118,7 +119,7 @@ class AniList: return trending @classmethod - def get_most_favourite(cls): + def get_most_favourite(cls, *_, **kwargs): """ Gets the most favoured anime on anilist """ @@ -126,7 +127,7 @@ class AniList: return most_favourite @classmethod - def get_most_scored(cls): + def get_most_scored(cls, *_, **kwargs): """ Gets most scored anime on anilist """ @@ -134,7 +135,7 @@ class AniList: return most_scored @classmethod - def get_most_recently_updated(cls): + def get_most_recently_updated(cls, *_, **kwargs): """ Gets most recently updated anime from anilist """ @@ -142,7 +143,7 @@ class AniList: return most_recently_updated @classmethod - def get_most_popular(cls): + def get_most_popular(cls, *_, **kwargs): """ Gets most popular anime on anilist """ @@ -151,30 +152,30 @@ class AniList: # FIXME:dont know why its not giving useful data @classmethod - def get_recommended_anime_for(cls, id: int): + def get_recommended_anime_for(cls, id: int, *_, **kwargs): recommended_anime = cls.get_data(recommended_query) return recommended_anime @classmethod - def get_charcters_of(cls, id: int): + def get_charcters_of(cls, id: int, *_, **kwargs): variables = {"id": id} characters = cls.get_data(anime_characters_query, variables) return characters @classmethod - def get_related_anime_for(cls, id: int): + def get_related_anime_for(cls, id: int, *_, **kwargs): variables = {"id": id} related_anime = cls.get_data(anime_relations_query, variables) return related_anime @classmethod - def get_airing_schedule_for(cls, id: int): + def get_airing_schedule_for(cls, id: int, *_, **kwargs): variables = {"id": id} airing_schedule = cls.get_data(airing_schedule_query, variables) return airing_schedule @classmethod - def get_upcoming_anime(cls, page: int): + def get_upcoming_anime(cls, page: int, *_, **kwargs): """ Gets upcoming anime from anilist """ diff --git a/fastanime/libs/anime_provider/allanime/api.py b/fastanime/libs/anime_provider/allanime/api.py index 99bf7b2..49124e1 100644 --- a/fastanime/libs/anime_provider/allanime/api.py +++ b/fastanime/libs/anime_provider/allanime/api.py @@ -36,6 +36,7 @@ class AllAnimeAPI: "query": query, }, headers={"Referer": ALLANIME_REFERER, "User-Agent": USER_AGENT}, + timeout=10, ) if response.status_code != 200: return {} @@ -121,25 +122,26 @@ class AllAnimeAPI: "Referer": ALLANIME_REFERER, "User-Agent": USER_AGENT, }, + timeout=10, ) if resp.status_code == 200: match embed["sourceName"]: case "Luf-mp4": Logger.debug("allanime:Found streams from gogoanime") - print("[yellow]gogoanime") + print("[yellow]GogoAnime Fetched") yield "gogoanime", resp.json() case "Kir": Logger.debug("allanime:Found streams from wetransfer") - print("[yellow]wetransfer") + print("[yellow]WeTransfer Fetched") yield "wetransfer", resp.json() case "S-mp4": Logger.debug("allanime:Found streams from sharepoint") - print("[yellow]sharepoint") + print("[yellow]Sharepoint Fetched") yield "sharepoint", resp.json() case "Sak": Logger.debug("allanime:Found streams from dropbox") - print("[yellow]dropbox") + print("[yellow]Dropbox Fetched") yield "dropbox", resp.json() case _: yield "Unknown", resp.json() diff --git a/fastanime/libs/anime_provider/allanime/constants.py b/fastanime/libs/anime_provider/allanime/constants.py index 185d93a..5ec3527 100644 --- a/fastanime/libs/anime_provider/allanime/constants.py +++ b/fastanime/libs/anime_provider/allanime/constants.py @@ -2,3 +2,4 @@ ALLANIME_BASE = "allanime.day" ALLANIME_REFERER = "https://allanime.to/" ALLANIME_API_ENDPOINT = "https://api.{}/api/".format(ALLANIME_BASE) USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0" +SERVERS_AVAILABLE = ["sharepoint", "dropbox", "gogoanime", "weTransfer"] diff --git a/pyproject.toml b/pyproject.toml index 8d680dd..75d7e2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,9 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] fastanime = 'fastanime:FastAnime' + +# FILE: .bandit +[tool.bandit] +#exclude = tests,path/to/file +#tests = B201,B301 +skips = ["B311","B603","B607","B404"]