feat(cli):add quality and translation type selection

This commit is contained in:
Benex254
2024-08-05 09:47:00 +03:00
parent f377711c4a
commit d2a328c2c8
36 changed files with 675 additions and 403 deletions

View File

@@ -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]"]

1
fa
View File

@@ -1,4 +1,5 @@
#! /usr/bin/bash
cd $HOME/code/python/kivy_apps/FastAnime
poetry install
clear
poetry run fastanime $*

View File

@@ -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]

View File

@@ -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,
)

View File

@@ -9,6 +9,8 @@ anime_normalizer = {
}
anilist_sort_normalizer = {"search match": "SEARCH_MATCH"}
themes_available = [
"Aliceblue",
"Antiquewhite",

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -0,0 +1,8 @@
import click
@click.command()
def configure():
pass
# create_desktop_shortcut()

View File

@@ -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,
)

67
fastanime/cli/config.py Normal file
View File

@@ -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__()

181
fastanime/cli/interfaces.py Normal file
View File

@@ -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])

View File

@@ -1,4 +0,0 @@
def bye():
import sys
sys.exit()

View File

@@ -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)

View File

@@ -1,2 +0,0 @@
def binge_interface(anime, back):
print(anime)

View File

@@ -1,2 +0,0 @@
def download_interface(anime, back):
print(anime)

View File

@@ -1,2 +0,0 @@
def info_interface(anime, back):
print(anime)

View File

@@ -1,8 +0,0 @@
import sys
from rich import print
def bye(*args):
print("Goodbye")
sys.exit()

View File

@@ -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)

View File

@@ -1,2 +0,0 @@
def watchlist_interface(anime, back):
print(anime)

View File

@@ -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()

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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(
[

View File

@@ -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_)

View File

@@ -19,6 +19,3 @@ class MyListScreenView(BaseScreenView):
def update_layout(self, widget):
self.user_anime_list_container.data.append(widget)
__all__ = ["MyListScreenView"]

View File

@@ -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():

143
fastanime/gui/app.py Normal file
View File

@@ -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)

View File

@@ -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
"""

View File

@@ -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()

View File

@@ -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"]

View File

@@ -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"]