From b94987dfd7bb89b0fd8bb86afc8e4c439763d609 Mon Sep 17 00:00:00 2001 From: Benedict Xavier Wanyonyi Date: Thu, 30 May 2024 15:43:45 +0300 Subject: [PATCH] doc and style: formatted the whole codebase to pep8 plus added documentation where necessary --- app/.vscode/settings.json | 3 +- app/Controller/anime_screen.py | 8 + app/Controller/crashlog_screen.py | 9 +- app/Controller/downloads_screen.py | 21 +-- app/Controller/help_screen.py | 9 +- app/Controller/home_screen.py | 18 +-- app/Controller/my_list_screen.py | 9 +- app/Controller/search_screen.py | 6 +- app/Model/anime_screen.py | 11 +- app/Model/download_screen.py | 14 +- app/Model/home_screen.py | 40 ++--- app/Model/search_screen.py | 10 +- app/Utility/anilist_data_helper.py | 8 +- app/Utility/data.py | 10 +- app/Utility/kivy_markup_helper.py | 15 +- app/Utility/media_card_loader.py | 2 + app/Utility/show_notification.py | 25 ++-- app/Utility/user_data_helper.py | 47 +++--- app/Utility/utils.py | 21 +-- app/View/AnimeScreen/anime_screen.py | 1 + .../components/animdl_stream_dialog.kv | 2 +- .../components/animdl_stream_dialog.py | 42 ++++-- app/View/AnimeScreen/components/characters.py | 56 ++++--- app/View/AnimeScreen/components/controls.kv | 1 + app/View/AnimeScreen/components/controls.py | 2 + .../AnimeScreen/components/description.kv | 13 -- .../AnimeScreen/components/description.py | 3 +- .../components/download_anime_dialog.py | 39 +++-- app/View/AnimeScreen/components/header.kv | 1 - app/View/AnimeScreen/components/header.py | 1 - .../AnimeScreen/components/rankings_bar.py | 7 +- app/View/AnimeScreen/components/review.py | 23 +-- app/View/AnimeScreen/components/side_bar.kv | 2 - app/View/AnimeScreen/components/side_bar.py | 66 +++++---- app/View/CrashLogScreen/crashlog_screen.py | 1 + .../DownloadsScreen/components/task_card.kv | 2 +- app/View/DownloadsScreen/download_screen.py | 26 ++-- app/View/HelpScreen/help_screen.kv | 2 - app/View/HelpScreen/help_screen.py | 2 + app/View/HomeScreen/home_screen.py | 1 + app/View/MylistScreen/my_list_screen.py | 15 +- app/View/SearchScreen/components/filters.py | 1 - app/View/base_screen.py | 7 +- app/View/components/general.kv | 6 +- .../media_card/components/media_popup.py | 48 +++--- app/View/components/navrail.kv | 5 - app/libs/animdl/animdl_api.py | 139 ++++++++++++++---- app/libs/animdl/animdl_data_helper.py | 42 +++++- app/libs/animdl/test.json | 1 - app/main.py | 52 +++++-- app/requirements.txt | 4 +- app/user_data.json | 2 +- .../Episode 5/Bleach | 0 .../Death note rewrite/Episode 1/Death note | 0 54 files changed, 555 insertions(+), 346 deletions(-) delete mode 100644 app/libs/animdl/test.json delete mode 100644 app/vids/Bleach sennen kessen-hen - ketsubetsu-tan/Episode 5/Bleach delete mode 100644 app/vids/Death note rewrite/Episode 1/Death note diff --git a/app/.vscode/settings.json b/app/.vscode/settings.json index dc3f727..3a65021 100644 --- a/app/.vscode/settings.json +++ b/app/.vscode/settings.json @@ -1,3 +1,4 @@ { - "python.analysis.typeCheckingMode": "basic" + "python.analysis.typeCheckingMode": "basic", + "python.analysis.autoImportCompletions": true } diff --git a/app/Controller/anime_screen.py b/app/Controller/anime_screen.py index 45c4e96..9164511 100644 --- a/app/Controller/anime_screen.py +++ b/app/Controller/anime_screen.py @@ -9,6 +9,8 @@ Cache.register("data.anime", limit=20, timeout=600) class AnimeScreenController: + """The controller for the anime screen + """ def __init__(self, model: AnimeScreenModel): self.model = model self.view = AnimeScreenView(controller=self, model=self.model) @@ -17,6 +19,12 @@ class AnimeScreenController: return self.view def update_anime_view(self, id: int, caller_screen_name: str): + """method called to update the anime screen when a new + + Args: + id (int): the anilst id of the anime + caller_screen_name (str): the screen thats calling this method; used internally to switch back to this screen + """ if self.model.anime_id != id: if cached_anime_data := Cache.get("data.anime", f"{id}"): data = cached_anime_data diff --git a/app/Controller/crashlog_screen.py b/app/Controller/crashlog_screen.py index a392447..52bfe7f 100644 --- a/app/Controller/crashlog_screen.py +++ b/app/Controller/crashlog_screen.py @@ -1,11 +1,12 @@ -from inspect import isgenerator from View import CrashLogScreenView from Model import CrashLogScreenModel -from View.components import MediaCardsContainer -from Utility import show_notification -from kivy.clock import Clock + + + class CrashLogScreenController: + """The crash log screen controller + """ def __init__(self, model:CrashLogScreenModel): self.model = model self.view = CrashLogScreenView(controller=self, model=self.model) diff --git a/app/Controller/downloads_screen.py b/app/Controller/downloads_screen.py index 5005c7b..e430616 100644 --- a/app/Controller/downloads_screen.py +++ b/app/Controller/downloads_screen.py @@ -1,28 +1,15 @@ -from inspect import isgenerator from View import DownloadsScreenView from Model import DownloadsScreenModel -from View.components import MediaCardsContainer -from Utility import show_notification -from kivy.clock import Clock + + class DownloadsScreenController: + """The controller for the download screen + """ def __init__(self, model:DownloadsScreenModel): self.model = model self.view = DownloadsScreenView(controller=self, model=self.model) - # self.update_anime_view() def get_view(self) -> DownloadsScreenView: return self.view - - # def requested_update_my_list_screen(self): - # user_anime_list = user_data_helper.get_user_anime_list() - # if animes_to_add:=difference(user_anime_list,self.model.already_in_user_anime_list): - # Logger.info("My List Screen:User anime list change;updating screen") - # anime_cards = self.model.update_my_anime_list_view(animes_to_add) - # self.model.already_in_user_anime_list = user_anime_list - # if isgenerator(anime_cards): - # for result_card in anime_cards: - # result_card.screen = self.view - # self.view.update_layout(result_card) - diff --git a/app/Controller/help_screen.py b/app/Controller/help_screen.py index ae0841b..dec46ef 100644 --- a/app/Controller/help_screen.py +++ b/app/Controller/help_screen.py @@ -1,15 +1,14 @@ -from inspect import isgenerator from View import HelpScreenView from Model import HelpScreenModel -from View.components import MediaCardsContainer -from Utility import show_notification -from kivy.clock import Clock + + class HelpScreenController: + """The help screen controller + """ def __init__(self, model:HelpScreenModel): self.model = model self.view = HelpScreenView(controller=self, model=self.model) - # self.update_anime_view() def get_view(self) -> HelpScreenView: return self.view diff --git a/app/Controller/home_screen.py b/app/Controller/home_screen.py index a383b9c..5144e2e 100644 --- a/app/Controller/home_screen.py +++ b/app/Controller/home_screen.py @@ -13,7 +13,7 @@ from Utility import show_notification # TODO:Move the update home screen to homescreen.py class HomeScreenController: """ - The `MainScreenController` class represents a controller implementation. + The `HomeScreenController` class represents a controller implementation. Coordinates work of the view with the model. The controller implements the strategy pattern. The controller connects to the view to control its actions. @@ -25,6 +25,7 @@ class HomeScreenController: self.view = HomeScreenView(controller=self, model=self.model) if self.view.app.config.get("Preferences","is_startup_anime_enable")=="1": # type: ignore Clock.schedule_once(lambda _:self.populate_home_screen()) + def get_view(self) -> HomeScreenView: return self.view @@ -108,16 +109,13 @@ class HomeScreenController: def populate_home_screen(self): self.populate_errors = [] - self.trending_anime() - self.highest_scored_anime() - self.popular_anime() - self.favourite_anime() - self.recently_updated_anime() - self.upcoming_anime() + Clock.schedule_once(lambda _:self.trending_anime()) + Clock.schedule_once(lambda _:self.highest_scored_anime()) + Clock.schedule_once(lambda _:self.popular_anime()) + Clock.schedule_once(lambda _: self.favourite_anime()) + Clock.schedule_once(lambda _:self.recently_updated_anime()) + Clock.schedule_once(lambda _:self.upcoming_anime()) if self.populate_errors: show_notification(f"Failed to fetch all home screen data",f"Theres probably a problem with your internet connection or anilist servers are down.\nFailed include:{', '.join(self.populate_errors)}") - def update_my_list(self,*args): - self.model.update_user_anime_list(*args) - diff --git a/app/Controller/my_list_screen.py b/app/Controller/my_list_screen.py index f5e57b2..a4c87c3 100644 --- a/app/Controller/my_list_screen.py +++ b/app/Controller/my_list_screen.py @@ -1,16 +1,16 @@ from inspect import isgenerator from kivy.logger import Logger -from kivy.clock import Clock +# from kivy.clock import Clock from kivy.utils import difference from View import MyListScreenView from Model import MyListScreenModel -from Utility import show_notification,user_data_helper +from Utility import user_data_helper class MyListScreenController: """ - The `MainScreenController` class represents a controller implementation. + The `MyListScreenController` class represents a controller implementation. Coordinates work of the view with the model. The controller implements the strategy pattern. The controller connects to the view to control its actions. @@ -20,6 +20,7 @@ class MyListScreenController: self.model = model self.view = MyListScreenView(controller=self, model=self.model) self.requested_update_my_list_screen() + def get_view(self) -> MyListScreenView: return self.view @@ -33,5 +34,3 @@ class MyListScreenController: for result_card in anime_cards: result_card.screen = self.view self.view.update_layout(result_card) - - diff --git a/app/Controller/search_screen.py b/app/Controller/search_screen.py index dbc79e4..7f26bd4 100644 --- a/app/Controller/search_screen.py +++ b/app/Controller/search_screen.py @@ -8,6 +8,8 @@ from Model import SearchScreenModel class SearchScreenController: + """The search screen controller + """ def __init__(self, model: SearchScreenModel): self.model = model @@ -17,6 +19,8 @@ class SearchScreenController: return self.view def update_trending_anime(self): + """Gets and adds the trending anime to the search screen + """ trending_cards_generator = self.model.get_trending_anime() if isgenerator(trending_cards_generator): self.view.trending_anime_sidebar.clear_widgets() @@ -37,7 +41,7 @@ class SearchScreenController: Clock.schedule_once( lambda _: self.view.update_pagination(self.model.pagination_info) ) - Clock.schedule_once(lambda _: self.update_trending_anime()) + self.update_trending_anime() else: Logger.error(f"Home Screen:Failed to search for {anime_title}") self.view.is_searching = False diff --git a/app/Model/anime_screen.py b/app/Model/anime_screen.py index d781ec4..bef5c6b 100644 --- a/app/Model/anime_screen.py +++ b/app/Model/anime_screen.py @@ -1,19 +1,12 @@ -import json -import os from Model.base_model import BaseScreenModel from libs.anilist import AniList -from Utility.media_card_loader import MediaCardLoader -from kivy.storage.jsonstore import JsonStore -user_data= JsonStore("user_data.json") class AnimeScreenModel(BaseScreenModel): + """the Anime screen model + """ data = {} anime_id = 0 - def media_card_generator(self): - for anime_item in self.data["data"]["Page"]["media"]: - yield MediaCardLoader.media_card(anime_item) - def get_anime_data(self,id:int): return AniList.get_anime(id) diff --git a/app/Model/download_screen.py b/app/Model/download_screen.py index 66b6751..43d4393 100644 --- a/app/Model/download_screen.py +++ b/app/Model/download_screen.py @@ -5,16 +5,4 @@ class DownloadsScreenModel(BaseScreenModel): """ Handles the download screen logic """ - - # already_in_user_anime_list = [] - # def update_my_anime_list_view(self,not_yet_in_user_anime_list:list,**kwargs): - # success,self.data = AniList.search(id_in=not_yet_in_user_anime_list) - # if success: - # return self.media_card_generator() - # else: - # show_notification(f"Failed to update my list screen view",self.data["Error"]) - # return None - - # def media_card_generator(self): - # for anime_item in self.data["data"]["Page"]["media"]: - # yield MediaCardLoader.media_card(anime_item) + \ No newline at end of file diff --git a/app/Model/home_screen.py b/app/Model/home_screen.py index 58f9e7f..6d35df2 100644 --- a/app/Model/home_screen.py +++ b/app/Model/home_screen.py @@ -1,73 +1,79 @@ from Model.base_model import BaseScreenModel from libs.anilist import AniList from Utility.media_card_loader import MediaCardLoader -from kivy.storage.jsonstore import JsonStore - -user_data= JsonStore("user_data.json") class HomeScreenModel(BaseScreenModel): + """The home screen model""" + def get_trending_anime(self): - success,data = AniList.get_trending() + success, data = AniList.get_trending() if success: + def _data_generator(): for anime_item in data["data"]["Page"]["media"]: yield MediaCardLoader.media_card(anime_item) + return _data_generator() else: return data def get_most_favourite_anime(self): - success,data = AniList.get_most_favourite() + success, data = AniList.get_most_favourite() if success: + def _data_generator(): for anime_item in data["data"]["Page"]["media"]: yield MediaCardLoader.media_card(anime_item) + return _data_generator() else: return data def get_most_recently_updated_anime(self): - success,data = AniList.get_most_recently_updated() + success, data = AniList.get_most_recently_updated() if success: + def _data_generator(): for anime_item in data["data"]["Page"]["media"]: yield MediaCardLoader.media_card(anime_item) + return _data_generator() else: return data + def get_most_popular_anime(self): - success,data = AniList.get_most_popular() + success, data = AniList.get_most_popular() if success: + def _data_generator(): for anime_item in data["data"]["Page"]["media"]: yield MediaCardLoader.media_card(anime_item) + return _data_generator() else: return data + def get_most_scored_anime(self): - success,data = AniList.get_most_scored() + success, data = AniList.get_most_scored() if success: + def _data_generator(): for anime_item in data["data"]["Page"]["media"]: yield MediaCardLoader.media_card(anime_item) + return _data_generator() else: return data + def get_upcoming_anime(self): - success,data = AniList.get_upcoming_anime(1) + success, data = AniList.get_upcoming_anime(1) if success: + def _data_generator(): for anime_item in data["data"]["Page"]["media"]: yield MediaCardLoader.media_card(anime_item) + return _data_generator() else: return data - def update_user_anime_list(self,anime_id,is_add): - my_list:list = user_data.get("my_list")["list"] - if is_add: - my_list.append(anime_id) - elif not(is_add) and my_list: - my_list.remove(anime_id) - user_data.put("my_list",list=my_list) - \ No newline at end of file diff --git a/app/Model/search_screen.py b/app/Model/search_screen.py index b9e3034..f3c79e4 100644 --- a/app/Model/search_screen.py +++ b/app/Model/search_screen.py @@ -1,10 +1,6 @@ -from typing import Generator - from Model.base_model import BaseScreenModel from libs.anilist import AniList -from Utility.media_card_loader import MediaCardLoader -from View.components import MediaCard -from Utility import show_notification +from Utility import MediaCardLoader, show_notification class SearchScreenModel(BaseScreenModel): @@ -31,7 +27,3 @@ class SearchScreenModel(BaseScreenModel): for anime_item in self.data["data"]["Page"]["media"]: yield MediaCardLoader.media_card(anime_item) self.pagination_info = self.data["data"]["Page"]["pageInfo"] - - # def extract_pagination_info(self): - # pagination_info = None - # return pagination_info diff --git a/app/Utility/anilist_data_helper.py b/app/Utility/anilist_data_helper.py index 01bd0f4..c96557d 100644 --- a/app/Utility/anilist_data_helper.py +++ b/app/Utility/anilist_data_helper.py @@ -1,5 +1,9 @@ from datetime import datetime -from libs.anilist.anilist_data_schema import AnilistDateObject,AnilistMediaNextAiringEpisode + +from libs.anilist.anilist_data_schema import ( + AnilistDateObject, + AnilistMediaNextAiringEpisode, +) # TODO: Add formating options for the final date @@ -24,7 +28,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"Episode: {airing_episode['episode']} on {format_anilist_timestamp(airing_episode['airingAt'])}" else: diff --git a/app/Utility/data.py b/app/Utility/data.py index 13e3a13..cb8a514 100644 --- a/app/Utility/data.py +++ b/app/Utility/data.py @@ -1,6 +1,6 @@ +""" +Just contains some useful data used across the codebase +""" + themes_available = ['Aliceblue', 'Antiquewhite', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 'Black', 'Blanchedalmond', 'Blue', 'Blueviolet', 'Brown', 'Burlywood', 'Cadetblue', 'Chartreuse', 'Chocolate', 'Coral', 'Cornflowerblue', 'Cornsilk', 'Crimson', 'Cyan', 'Darkblue', 'Darkcyan', 'Darkgoldenrod', 'Darkgray', 'Darkgrey', 'Darkgreen', 'Darkkhaki', 'Darkmagenta', 'Darkolivegreen', 'Darkorange', 'Darkorchid', 'Darkred', 'Darksalmon', 'Darkseagreen', 'Darkslateblue', 'Darkslategray', 'Darkslategrey', 'Darkturquoise', 'Darkviolet', 'Deeppink', 'Deepskyblue', 'Dimgray', 'Dimgrey', 'Dodgerblue', 'Firebrick', 'Floralwhite', 'Forestgreen', 'Fuchsia', 'Gainsboro', 'Ghostwhite', 'Gold', 'Goldenrod', 'Gray', 'Grey', 'Green', 'Greenyellow', 'Honeydew', 'Hotpink', 'Indianred', 'Indigo', 'Ivory', 'Khaki', 'Lavender', 'Lavenderblush', 'Lawngreen', 'Lemonchiffon', 'Lightblue', 'Lightcoral', 'Lightcyan', 'Lightgoldenrodyellow', 'Lightgreen', 'Lightgray', 'Lightgrey', 'Lightpink', 'Lightsalmon', 'Lightseagreen', 'Lightskyblue', 'Lightslategray', 'Lightslategrey', 'Lightsteelblue', 'Lightyellow', 'Lime', 'Limegreen', 'Linen', 'Magenta', 'Maroon', 'Mediumaquamarine', 'Mediumblue', 'Mediumorchid', 'Mediumpurple', 'Mediumseagreen', 'Mediumslateblue', 'Mediumspringgreen', 'Mediumturquoise', 'Mediumvioletred', 'Midnightblue', 'Mintcream', 'Mistyrose', 'Moccasin', 'Navajowhite', 'Navy', 'Oldlace', 'Olive', 'Olivedrab', 'Orange', 'Orangered', 'Orchid', 'Palegoldenrod', 'Palegreen', 'Paleturquoise', 'Palevioletred', 'Papayawhip', 'Peachpuff', 'Peru', 'Pink', 'Plum', 'Powderblue', 'Purple', 'Red', 'Rosybrown', 'Royalblue', 'Saddlebrown', 'Salmon', 'Sandybrown', 'Seagreen', 'Seashell', 'Sienna', 'Silver', 'Skyblue', 'Slateblue', 'Slategray', 'Slategrey', 'Snow', 'Springgreen', 'Steelblue', 'Tan', 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 'Whitesmoke', 'Yellow', -'Yellowgreen'] -# import time -# from datetime import date,datetime -# print(datetime.fromtimestamp(1716412399)) -# print(time.daylight,date.max,date.min) \ No newline at end of file +'Yellowgreen'] \ No newline at end of file diff --git a/app/Utility/kivy_markup_helper.py b/app/Utility/kivy_markup_helper.py index 7b1e95f..d32283e 100644 --- a/app/Utility/kivy_markup_helper.py +++ b/app/Utility/kivy_markup_helper.py @@ -8,38 +8,47 @@ from kivy.utils import get_hex_from_color def bolden(text: str): return f"[b]{text}[/b]" + def italicize(text: str): return f"[i]{text}[/i]" + def underline(text: str): return f"[u]{text}[/u]" + def strike_through(text: str): return f"[s]{text}[/s]" + def sub_script(text: str): return f"[sub]{text}[/sub]" + def super_script(text: str): return f"[sup]{text}[/sup]" + def color_text(text: str, color: tuple): hex_color = get_hex_from_color(color) return f"[color={hex_color}]{text}[/color]" + def font(text: str, font_name: str): return f"[font={font_name}]{text}[/font]" + def font_family(text: str, family: str): return f"[font_family={family}]{text}[/font_family]" + def font_context(text: str, context: str): return f"[font_context={context}]{text}[/font_context]" + def font_size(text: str, size: int): return f"[size={size}]{text}[/size]" - + + def text_ref(text: str, ref: str): return f"[ref={ref}]{text}[/ref]" - - diff --git a/app/Utility/media_card_loader.py b/app/Utility/media_card_loader.py index c09ae84..0e036ff 100644 --- a/app/Utility/media_card_loader.py +++ b/app/Utility/media_card_loader.py @@ -26,6 +26,8 @@ for link in yt_stream_links: # for youtube video links gotten from from pytube which is blocking class MediaCardDataLoader(object): + """this class loads an anime media card and gets the trailer url from pytube""" + def __init__(self): self._resume_cond = threading.Condition() self._num_workers = 5 diff --git a/app/Utility/show_notification.py b/app/Utility/show_notification.py index 2a2feb4..d6e6ad7 100644 --- a/app/Utility/show_notification.py +++ b/app/Utility/show_notification.py @@ -1,22 +1,29 @@ -from kivymd.uix.snackbar import MDSnackbar,MDSnackbarText,MDSnackbarSupportingText +from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText, MDSnackbarSupportingText from kivy.clock import Clock -def show_notification(title,details): + +def show_notification(title, details): + """helper function to display notifications + + Args: + title (str): the title of your message + details (str): the details of your message + """ + def _show(dt): MDSnackbar( MDSnackbarText( text=title, + adaptive_height=True, ), MDSnackbarSupportingText( - text=details, - shorten=False, - max_lines=0, - adaptive_height=True + text=details, shorten=False, max_lines=0, adaptive_height=True ), duration=5, y="10dp", - pos_hint={"bottom": 1,"right":.99}, + pos_hint={"bottom": 1, "right": 0.99}, padding=[0, 0, "8dp", "8dp"], - size_hint_x=.4 + size_hint_x=0.4, ).open() - Clock.schedule_once(_show,1) \ No newline at end of file + + Clock.schedule_once(_show, 1) diff --git a/app/Utility/user_data_helper.py b/app/Utility/user_data_helper.py index 47d61f9..da82c8e 100644 --- a/app/Utility/user_data_helper.py +++ b/app/Utility/user_data_helper.py @@ -2,9 +2,8 @@ Contains Helper functions to read and write the user data files """ - from kivy.storage.jsonstore import JsonStore -from datetime import date,datetime +from datetime import date, datetime from kivy.logger import Logger today = date.today() @@ -15,56 +14,68 @@ yt_cache = JsonStore("yt_cache.json") # Get the user data -def get_user_anime_list()->list: +def get_user_anime_list() -> list: try: - return user_data.get("user_anime_list")["user_anime_list"] # returns a list of anime ids + return user_data.get("user_anime_list")[ + "user_anime_list" + ] # returns a list of anime ids except Exception as e: Logger.warning(f"User Data:Read failure:{e}") return [] -def update_user_anime_list(updated_list:list): + +def update_user_anime_list(updated_list: list): try: updated_list_ = list(set(updated_list)) - user_data.put("user_anime_list",user_anime_list=updated_list_) + user_data.put("user_anime_list", user_anime_list=updated_list_) except Exception as e: Logger.warning(f"User Data:Update failure:{e}") + # Get the user data -def get_user_downloads()->list: +def get_user_downloads() -> list: try: - return user_data.get("user_downloads")["user_downloads"] # returns a list of anime ids + return user_data.get("user_downloads")[ + "user_downloads" + ] # returns a list of anime ids except Exception as e: Logger.warning(f"User Data:Read failure:{e}") return [] -def update_user_downloads(updated_list:list): + +def update_user_downloads(updated_list: list): try: - user_data.put("user_downloads",user_downloads=list(set(updated_list))) + user_data.put("user_downloads", user_downloads=list(set(updated_list))) except Exception as e: Logger.warning(f"User Data:Update failure:{e}") -# Yt persistent anime trailer cache +# Yt persistent anime trailer cache t = 1 -if now.hour<=6: +if now.hour <= 6: t = 1 -elif now.hour<=12: +elif now.hour <= 12: t = 2 -elif now.hour<=18: +elif now.hour <= 18: t = 3 else: t = 4 yt_anime_trailer_cache_name = f"{today}{t}" -def get_anime_trailer_cache()->list: + + +def get_anime_trailer_cache() -> list: try: return yt_cache["yt_stream_links"][f"{yt_anime_trailer_cache_name}"] except Exception as e: Logger.warning(f"User Data:Read failure:{e}") return [] - -def update_anime_trailer_cache(yt_stream_links:list): + + +def update_anime_trailer_cache(yt_stream_links: list): try: - yt_cache.put("yt_stream_links",**{f"{yt_anime_trailer_cache_name}":yt_stream_links}) + yt_cache.put( + "yt_stream_links", **{f"{yt_anime_trailer_cache_name}": yt_stream_links} + ) except Exception as e: Logger.warning(f"User Data:Update failure:{e}") diff --git a/app/Utility/utils.py b/app/Utility/utils.py index dec9726..06bb719 100644 --- a/app/Utility/utils.py +++ b/app/Utility/utils.py @@ -1,23 +1,26 @@ from datetime import datetime -# import tempfile import shutil -# import os + +# TODO: make it use color_text instead of fixed vals +# from .kivy_markup_helper import color_text + # utility functions -def write_crash(e:Exception): +def write_crash(e: Exception): index = datetime.today() error = f"[b][color=#fa0000][ {index} ]:[/color][/b]\n(\n\n{e}\n\n)\n" try: - with open("crashdump.txt","a") as file: + with open("crashdump.txt", "a") as file: file.write(error) except: - with open("crashdump.txt","w") as file: + with open("crashdump.txt", "w") as file: file.write(error) return index -def move_file(source_path,dest_path): + +def move_file(source_path, dest_path): try: - path = shutil.move(source_path,dest_path) - return (1,path) + path = shutil.move(source_path, dest_path) + return (1, path) except Exception as e: - return (0,e) \ No newline at end of file + return (0, e) diff --git a/app/View/AnimeScreen/anime_screen.py b/app/View/AnimeScreen/anime_screen.py index 0065e3e..1df2d8f 100644 --- a/app/View/AnimeScreen/anime_screen.py +++ b/app/View/AnimeScreen/anime_screen.py @@ -19,6 +19,7 @@ from .components import ( class AnimeScreenView(BaseScreenView): + """The anime screen view""" caller_screen_name = StringProperty() header: AnimeHeader = ObjectProperty() side_bar: AnimeSideBar = ObjectProperty() diff --git a/app/View/AnimeScreen/components/animdl_stream_dialog.kv b/app/View/AnimeScreen/components/animdl_stream_dialog.kv index 3c745c8..1828089 100644 --- a/app/View/AnimeScreen/components/animdl_stream_dialog.kv +++ b/app/View/AnimeScreen/components/animdl_stream_dialog.kv @@ -31,7 +31,7 @@ padding:"10dp" orientation:"vertical" StreamDialogHeaderLabel: - text:"Stream on Animdl" + text:"Stream Anime" StreamDialogLabel: text:"Title" MDTextField: diff --git a/app/View/AnimeScreen/components/animdl_stream_dialog.py b/app/View/AnimeScreen/components/animdl_stream_dialog.py index 6a0674a..1c90b20 100644 --- a/app/View/AnimeScreen/components/animdl_stream_dialog.py +++ b/app/View/AnimeScreen/components/animdl_stream_dialog.py @@ -1,20 +1,35 @@ +from kivy.clock import Clock from kivy.uix.modalview import ModalView -from kivymd.uix.behaviors import StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior +from kivymd.uix.behaviors import ( + StencilBehavior, + CommonElevationBehavior, + BackgroundColorBehavior, +) from kivymd.theming import ThemableBehavior -class AnimdlStreamDialog(ThemableBehavior,StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior,ModalView): - def __init__(self,data,mpv,**kwargs): + +class AnimdlStreamDialog( + ThemableBehavior, + StencilBehavior, + CommonElevationBehavior, + BackgroundColorBehavior, + ModalView, +): + """The anime streaming dialog""" + + def __init__(self, data, mpv, **kwargs): super().__init__(**kwargs) self.data = data - self.mpv=mpv - if title:=data["title"].get("romaji"): + self.mpv = mpv + if title := data["title"].get("romaji"): self.ids.title_field.text = title - elif title:=data["title"].get("english"): + elif title := data["title"].get("english"): self.ids.title_field.text = title self.ids.quality_field.text = "best" - def stream_anime(self,app): + + def _stream_anime(self, app): if self.mpv: streaming_cmds = {} title = self.ids.title_field.text @@ -27,7 +42,7 @@ class AnimdlStreamDialog(ThemableBehavior,StencilBehavior,CommonElevationBehavio quality = self.ids.quality_field.text if quality: streaming_cmds["quality"] = quality - else: + else: streaming_cmds["quality"] = "best" app.watch_on_animdl(streaming_cmds) @@ -38,14 +53,17 @@ class AnimdlStreamDialog(ThemableBehavior,StencilBehavior,CommonElevationBehavio episodes_range = self.ids.range_field.text if episodes_range: - cmds = [*cmds,"-r",episodes_range] + cmds = [*cmds, "-r", episodes_range] latest = self.ids.latest_field.text if latest: - cmds = [*cmds,"-s",latest] + cmds = [*cmds, "-s", latest] quality = self.ids.quality_field.text if quality: - cmds = [*cmds,"-q",quality] + cmds = [*cmds, "-q", quality] - app.watch_on_animdl(custom_options = cmds) + app.watch_on_animdl(custom_options=cmds) + + def stream_anime(self, app): + Clock.schedule_once(lambda _: self._stream_anime(app)) diff --git a/app/View/AnimeScreen/components/characters.py b/app/View/AnimeScreen/components/characters.py index 7ecb2fe..fa1fd5d 100644 --- a/app/View/AnimeScreen/components/characters.py +++ b/app/View/AnimeScreen/components/characters.py @@ -1,46 +1,56 @@ -from kivy.properties import ObjectProperty,ListProperty +from kivy.clock import Clock +from kivy.properties import ObjectProperty, ListProperty from kivymd.uix.boxlayout import MDBoxLayout class AnimeCharacter(MDBoxLayout): - voice_actors = ObjectProperty({ - "name":"", - "image":"" - }) - character = ObjectProperty({ - "name":"", - "gender":"", - "dateOfBirth":"", - "image":"", - "age":"", - "description":"" - }) + """an Anime character data""" + + voice_actors = ObjectProperty({"name": "", "image": ""}) + character = ObjectProperty( + { + "name": "", + "gender": "", + "dateOfBirth": "", + "image": "", + "age": "", + "description": "", + } + ) class AnimeCharacters(MDBoxLayout): + """The anime characters card""" + container = ObjectProperty() characters = ListProperty() - def on_characters(self,instance,characters): - format_date = lambda date_: f"{date_['day']}/{date_['month']}/{date_['year']}" if date_ else "" + + def update_characters_card(self, instance, characters): + format_date = lambda date_: ( + f"{date_['day']}/{date_['month']}/{date_['year']}" if date_ else "" + ) + self.container.clear_widgets() - for character_ in characters: # character (character,actor) + for character_ in characters: # character (character,actor) character = character_[0] actors = character_[1] anime_character = AnimeCharacter() anime_character.character = { - "name":character["name"]["full"], - "gender":character["gender"], - "dateOfBirth":format_date(character["dateOfBirth"]), - "image":character["image"]["medium"], - "age":character["age"], - "description":character["description"] + "name": character["name"]["full"], + "gender": character["gender"], + "dateOfBirth": format_date(character["dateOfBirth"]), + "image": character["image"]["medium"], + "age": character["age"], + "description": character["description"], } anime_character.voice_actors = { - "name":", ".join([actor["name"]["full"] for actor in actors]) + "name": ", ".join([actor["name"]["full"] for actor in actors]) } # anime_character.voice_actor = self.container.add_widget(anime_character) + def on_characters(self, *args): + Clock.schedule_once(lambda _: self.update_characters_card(*args)) diff --git a/app/View/AnimeScreen/components/controls.kv b/app/View/AnimeScreen/components/controls.kv index 060cebf..2b04282 100644 --- a/app/View/AnimeScreen/components/controls.kv +++ b/app/View/AnimeScreen/components/controls.kv @@ -3,6 +3,7 @@ padding:"10dp" spacing:"10dp" pos_hint: {'center_x': 0.5} + # StackLayout: MDButton: on_press: root.screen.add_to_user_anime_list() diff --git a/app/View/AnimeScreen/components/controls.py b/app/View/AnimeScreen/components/controls.py index 8638b41..2a8faa2 100644 --- a/app/View/AnimeScreen/components/controls.py +++ b/app/View/AnimeScreen/components/controls.py @@ -4,4 +4,6 @@ from kivymd.uix.boxlayout import MDBoxLayout class Controls(MDBoxLayout): + """The diferent controls available""" + screen = ObjectProperty() diff --git a/app/View/AnimeScreen/components/description.kv b/app/View/AnimeScreen/components/description.kv index 8cf546c..a4f5b02 100644 --- a/app/View/AnimeScreen/components/description.kv +++ b/app/View/AnimeScreen/components/description.kv @@ -1,16 +1,3 @@ -# -# adaptive_height:True -# md_bg_color:self.theme_cls.secondaryContainerColor -# MDLabel: -# text:root.text -# adaptive_height:True -# max_lines:0 -# shorten:False -# bold:True -# font_style: "Body" -# role: "large" -# padding:"10dp" - : adaptive_height:True md_bg_color:self.theme_cls.surfaceContainerLowColor diff --git a/app/View/AnimeScreen/components/description.py b/app/View/AnimeScreen/components/description.py index 87ad09d..ac289b5 100644 --- a/app/View/AnimeScreen/components/description.py +++ b/app/View/AnimeScreen/components/description.py @@ -4,5 +4,6 @@ from kivymd.uix.boxlayout import MDBoxLayout class AnimeDescription(MDBoxLayout): - description = StringProperty() + """The anime description""" + description = StringProperty() diff --git a/app/View/AnimeScreen/components/download_anime_dialog.py b/app/View/AnimeScreen/components/download_anime_dialog.py index 64d80b7..49ca90b 100644 --- a/app/View/AnimeScreen/components/download_anime_dialog.py +++ b/app/View/AnimeScreen/components/download_anime_dialog.py @@ -1,26 +1,41 @@ from kivy.uix.modalview import ModalView -from kivymd.uix.behaviors import StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior + +from kivymd.uix.behaviors import ( + StencilBehavior, + CommonElevationBehavior, + BackgroundColorBehavior, +) from kivymd.theming import ThemableBehavior + + # from main import AniXStreamApp -class DownloadAnimeDialog(ThemableBehavior,StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior,ModalView): - def __init__(self,data,**kwargs): - super(DownloadAnimeDialog,self).__init__(**kwargs) +class DownloadAnimeDialog( + ThemableBehavior, + StencilBehavior, + CommonElevationBehavior, + BackgroundColorBehavior, + ModalView, +): + """The download anime dialog""" + + def __init__(self, data, **kwargs): + super(DownloadAnimeDialog, self).__init__(**kwargs) self.data = data self.anime_id = self.data["id"] - if title:=data["title"].get("romaji"): + if title := data["title"].get("romaji"): self.ids.title_field.text = title - elif title:=data["title"].get("english"): + elif title := data["title"].get("english"): self.ids.title_field.text = title self.ids.quality_field.text = "best" - def download_anime(self,app): + + def download_anime(self, app): default_cmds = {} - title=self.ids.title_field.text + title = self.ids.title_field.text default_cmds["title"] = title - if episodes_range:=self.ids.range_field.text: + if episodes_range := self.ids.range_field.text: default_cmds["episodes_range"] = episodes_range - if quality:=self.ids.range_field.text: + if quality := self.ids.range_field.text: default_cmds["quality"] = quality - # print(title,episodes_range,latest,quality) - app.download_anime(self.anime_id,default_cmds) \ No newline at end of file + app.download_anime(self.anime_id, default_cmds) diff --git a/app/View/AnimeScreen/components/header.kv b/app/View/AnimeScreen/components/header.kv index 5243c3c..2e4e1c4 100644 --- a/app/View/AnimeScreen/components/header.kv +++ b/app/View/AnimeScreen/components/header.kv @@ -1,7 +1,6 @@ : adaptive_height:True orientation: 'vertical' - # padding:"10dp" MDBoxLayout: adaptive_height:True md_bg_color:self.theme_cls.secondaryContainerColor diff --git a/app/View/AnimeScreen/components/header.py b/app/View/AnimeScreen/components/header.py index 176313a..1b381c2 100644 --- a/app/View/AnimeScreen/components/header.py +++ b/app/View/AnimeScreen/components/header.py @@ -6,4 +6,3 @@ from kivymd.uix.boxlayout import MDBoxLayout class AnimeHeader(MDBoxLayout): titles = StringProperty() banner_image = StringProperty() - diff --git a/app/View/AnimeScreen/components/rankings_bar.py b/app/View/AnimeScreen/components/rankings_bar.py index 2117347..8f6b430 100644 --- a/app/View/AnimeScreen/components/rankings_bar.py +++ b/app/View/AnimeScreen/components/rankings_bar.py @@ -6,9 +6,8 @@ from kivymd.uix.boxlayout import MDBoxLayout class RankingsBar(MDBoxLayout): rankings = DictProperty( { - "Popularity":0, - "Favourites":0, - "AverageScore":0, + "Popularity": 0, + "Favourites": 0, + "AverageScore": 0, } ) - diff --git a/app/View/AnimeScreen/components/review.py b/app/View/AnimeScreen/components/review.py index bed2d40..5b81b83 100644 --- a/app/View/AnimeScreen/components/review.py +++ b/app/View/AnimeScreen/components/review.py @@ -1,26 +1,29 @@ -from kivy.properties import ObjectProperty,ListProperty +from kivy.properties import ObjectProperty, ListProperty +from kivy.clock import Clock from kivymd.uix.boxlayout import MDBoxLayout class AnimeReview(MDBoxLayout): - review = ObjectProperty({ - "username":"", - "avatar":"", - "summary":"" - }) + review = ObjectProperty({"username": "", "avatar": "", "summary": ""}) class AnimeReviews(MDBoxLayout): + """anime reviews""" + reviews = ListProperty() container = ObjectProperty() - def on_reviews(self,instance,reviews): + + def on_reviews(self, *args): + Clock.schedule_once(lambda _: self.update_reviews_card(*args)) + + def update_reviews_card(self, instance, reviews): self.container.clear_widgets() for review in reviews: review_ = AnimeReview() review_.review = { - "username":review["user"]["name"], - "avatar":review["user"]["avatar"]["medium"], - "summary":review["summary"] + "username": review["user"]["name"], + "avatar": review["user"]["avatar"]["medium"], + "summary": review["summary"], } self.container.add_widget(review_) diff --git a/app/View/AnimeScreen/components/side_bar.kv b/app/View/AnimeScreen/components/side_bar.kv index a506a27..99ea3c5 100644 --- a/app/View/AnimeScreen/components/side_bar.kv +++ b/app/View/AnimeScreen/components/side_bar.kv @@ -7,8 +7,6 @@ spacing:"10dp" orientation: 'vertical' pos_hint: {'center_x': 0.5} - - : adaptive_height:True max_lines:0 diff --git a/app/View/AnimeScreen/components/side_bar.py b/app/View/AnimeScreen/components/side_bar.py index 00b3033..9994479 100644 --- a/app/View/AnimeScreen/components/side_bar.py +++ b/app/View/AnimeScreen/components/side_bar.py @@ -1,4 +1,4 @@ -from kivy.properties import ObjectProperty,StringProperty,DictProperty,ListProperty +from kivy.properties import ObjectProperty, StringProperty, DictProperty, ListProperty from kivy.utils import get_hex_from_color from kivy.factory import Factory @@ -10,34 +10,42 @@ class HeaderLabel(MDBoxLayout): text = StringProperty() halign = StringProperty("center") + Factory.register("HeaderLabel", HeaderLabel) + + class SideBarLabel(MDLabel): pass +# TODO:Switch to using the kivy_markup_module class AnimeSideBar(MDBoxLayout): screen = ObjectProperty() image = StringProperty() - alternative_titles = DictProperty({ - "synonyms":"", - "english":"", - "japanese":"", - }) - information = DictProperty({ - "episodes":"", - "status":"", - "aired":"", - "nextAiringEpisode":"", - "premiered":"", - "broadcast":"", - "countryOfOrigin":"", - "hashtag":"", - "studios":"", # { "name": "Sunrise", "isAnimationStudio": true } - "source":"", - "genres":"", - "duration":"", - "producers":"", - }) + alternative_titles = DictProperty( + { + "synonyms": "", + "english": "", + "japanese": "", + } + ) + information = DictProperty( + { + "episodes": "", + "status": "", + "aired": "", + "nextAiringEpisode": "", + "premiered": "", + "broadcast": "", + "countryOfOrigin": "", + "hashtag": "", + "studios": "", # { "name": "Sunrise", "isAnimationStudio": true } + "source": "", + "genres": "", + "duration": "", + "producers": "", + } + ) statistics = ListProperty() statistics_container = ObjectProperty() external_links = ListProperty() @@ -45,7 +53,7 @@ class AnimeSideBar(MDBoxLayout): tags = ListProperty() tags_container = ObjectProperty() - def on_statistics(self,instance,value): + def on_statistics(self, instance, value): self.statistics_container.clear_widgets() header = HeaderLabel() header.text = "Rankings" @@ -56,10 +64,11 @@ class AnimeSideBar(MDBoxLayout): label.text = "[color={}]{}:[/color] {}".format( get_hex_from_color(label.theme_cls.primaryColor), stat[0].capitalize(), - f"{stat[1]}") + f"{stat[1]}", + ) self.statistics_container.add_widget(label) - def on_tags(self,instance,value): + def on_tags(self, instance, value): self.tags_container.clear_widgets() header = HeaderLabel() header.text = "Tags" @@ -69,11 +78,11 @@ class AnimeSideBar(MDBoxLayout): label.text = "[color={}]{}:[/color] {}".format( get_hex_from_color(label.theme_cls.primaryColor), tag[0].capitalize(), - f"{tag[1]} %") + f"{tag[1]} %", + ) self.tags_container.add_widget(label) - - def on_external_links(self,instance,value): + def on_external_links(self, instance, value): self.external_links_container.clear_widgets() header = HeaderLabel() header.text = "External Links" @@ -84,5 +93,6 @@ class AnimeSideBar(MDBoxLayout): label.text = "[color={}]{}:[/color] {}".format( get_hex_from_color(label.theme_cls.primaryColor), site[0].capitalize(), - site[1]) + site[1], + ) self.external_links_container.add_widget(label) diff --git a/app/View/CrashLogScreen/crashlog_screen.py b/app/View/CrashLogScreen/crashlog_screen.py index 4903e61..51de6cc 100644 --- a/app/View/CrashLogScreen/crashlog_screen.py +++ b/app/View/CrashLogScreen/crashlog_screen.py @@ -3,6 +3,7 @@ from View.base_screen import BaseScreenView class CrashLogScreenView(BaseScreenView): + """The crash log screen""" main_container = ObjectProperty() def model_is_changed(self) -> None: """ diff --git a/app/View/DownloadsScreen/components/task_card.kv b/app/View/DownloadsScreen/components/task_card.kv index e3a090b..6f5e4b6 100644 --- a/app/View/DownloadsScreen/components/task_card.kv +++ b/app/View/DownloadsScreen/components/task_card.kv @@ -25,4 +25,4 @@ theme_text_color:"Secondary" text:color_text(root.episodes_to_download,root.theme_cls.secondaryColor) MDIcon: - icon:"check-bold" + icon:"download" diff --git a/app/View/DownloadsScreen/download_screen.py b/app/View/DownloadsScreen/download_screen.py index a435c21..310978e 100644 --- a/app/View/DownloadsScreen/download_screen.py +++ b/app/View/DownloadsScreen/download_screen.py @@ -4,29 +4,29 @@ from kivy.logger import Logger from kivy.utils import format_bytes_to_human from View.base_screen import BaseScreenView -from .components.task_card import TaskCard +from .components.task_card import TaskCard + class DownloadsScreenView(BaseScreenView): main_container = ObjectProperty() progress_bar = ObjectProperty() download_progress_label = ObjectProperty() - def on_new_download_task(self,anime_title:str,episodes:str|None): + def on_new_download_task(self, anime_title: str, episodes: str | None): if not episodes: episodes = "All" - self.main_container.add_widget(TaskCard(anime_title,episodes)) + Clock.schedule_once( + lambda _: self.main_container.add_widget(TaskCard(anime_title, episodes)) + ) - def on_episode_download_progress(self,current_bytes_downloaded,total_bytes,episode_info): - percentage_completion = round((current_bytes_downloaded/total_bytes)*100) + def on_episode_download_progress( + self, current_bytes_downloaded, total_bytes, episode_info + ): + percentage_completion = round((current_bytes_downloaded / total_bytes) * 100) progress_text = f"Downloading: {episode_info['anime_title']} - {episode_info['episode']} ({format_bytes_to_human(current_bytes_downloaded)}/{format_bytes_to_human(total_bytes)})" - if (percentage_completion%5)==0: - self.progress_bar.value= max(min(percentage_completion,100),0) + if (percentage_completion % 5) == 0: + self.progress_bar.value = max(min(percentage_completion, 100), 0) self.download_progress_label.text = progress_text - Logger.info(f"Downloader: {progress_text}") - - # def on_enter(self): - # Clock.schedule_once(lambda _:self.controller.requested_update_my_list_screen()) - - def update_layout(self,widget): + def update_layout(self, widget): self.user_anime_list_container.add_widget(widget) diff --git a/app/View/HelpScreen/help_screen.kv b/app/View/HelpScreen/help_screen.kv index 9da6dfa..33baaa3 100644 --- a/app/View/HelpScreen/help_screen.kv +++ b/app/View/HelpScreen/help_screen.kv @@ -1,8 +1,6 @@ #:import get_color_from_hex kivy.utils.get_color_from_hex #:import StringProperty kivy.properties.StringProperty - - spacing:"10dp" orientation:"vertical" diff --git a/app/View/HelpScreen/help_screen.py b/app/View/HelpScreen/help_screen.py index 30fe07c..3fc991f 100644 --- a/app/View/HelpScreen/help_screen.py +++ b/app/View/HelpScreen/help_screen.py @@ -4,11 +4,13 @@ from View.base_screen import BaseScreenView from Utility.kivy_markup_helper import bolden, color_text, underline from Utility.data import themes_available + class HelpScreenView(BaseScreenView): main_container = ObjectProperty() animdl_help = StringProperty() installing_animdl_help = StringProperty() available_themes = StringProperty() + def __init__(self, **kw): super(HelpScreenView, self).__init__(**kw) self.animdl_help = f""" diff --git a/app/View/HomeScreen/home_screen.py b/app/View/HomeScreen/home_screen.py index 1e6f113..f870aaf 100644 --- a/app/View/HomeScreen/home_screen.py +++ b/app/View/HomeScreen/home_screen.py @@ -1,4 +1,5 @@ from kivy.properties import ObjectProperty + from View.base_screen import BaseScreenView diff --git a/app/View/MylistScreen/my_list_screen.py b/app/View/MylistScreen/my_list_screen.py index 0fbc609..a2ebb3f 100644 --- a/app/View/MylistScreen/my_list_screen.py +++ b/app/View/MylistScreen/my_list_screen.py @@ -1,20 +1,21 @@ -from kivy.properties import ObjectProperty,StringProperty,DictProperty +from kivy.properties import ObjectProperty, StringProperty, DictProperty from kivy.clock import Clock + from View.base_screen import BaseScreenView class MyListScreenView(BaseScreenView): user_anime_list_container = ObjectProperty() + def model_is_changed(self) -> None: """ Called whenever any change has occurred in the data model. The view in this method tracks these changes and updates the UI according to these changes. """ - - def on_enter(self): - Clock.schedule_once(lambda _:self.controller.requested_update_my_list_screen()) - - def update_layout(self,widget): - self.user_anime_list_container.add_widget(widget) + def on_enter(self): + Clock.schedule_once(lambda _: self.controller.requested_update_my_list_screen()) + + def update_layout(self, widget): + self.user_anime_list_container.add_widget(widget) diff --git a/app/View/SearchScreen/components/filters.py b/app/View/SearchScreen/components/filters.py index 6cd0b6a..52cffeb 100644 --- a/app/View/SearchScreen/components/filters.py +++ b/app/View/SearchScreen/components/filters.py @@ -58,7 +58,6 @@ class Filters(MDBoxLayout): "NOT_YET_RELEASED", "CANCELLED", "HIATUS", - ] case _: items = [] diff --git a/app/View/base_screen.py b/app/View/base_screen.py index 5c73c66..c706c31 100644 --- a/app/View/base_screen.py +++ b/app/View/base_screen.py @@ -2,7 +2,7 @@ from kivy.properties import ObjectProperty, StringProperty from kivymd.app import MDApp from kivymd.uix.screen import MDScreen -from kivymd.uix.navigationrail import MDNavigationRail +from kivymd.uix.navigationrail import MDNavigationRail, MDNavigationRailItem from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import MDIconButton from kivymd.uix.tooltip import MDTooltip @@ -26,6 +26,11 @@ class TooltipMDIconButton(Tooltip, MDIconButton): tooltip_text = StringProperty() +class CommonNavigationRailItem(MDNavigationRailItem): + icon = StringProperty() + text = StringProperty() + + class BaseScreenView(MDScreen, Observer): """ A base class that implements a visual representation of the model data. diff --git a/app/View/components/general.kv b/app/View/components/general.kv index 444dc32..31019bc 100644 --- a/app/View/components/general.kv +++ b/app/View/components/general.kv @@ -1,3 +1,3 @@ -# : -# allow_copy:True -# allow_selection:True \ No newline at end of file +: + allow_copy:True + allow_selection:True \ No newline at end of file diff --git a/app/View/components/media_card/components/media_popup.py b/app/View/components/media_card/components/media_popup.py index 89a6f93..0417336 100644 --- a/app/View/components/media_card/components/media_popup.py +++ b/app/View/components/media_card/components/media_popup.py @@ -4,18 +4,29 @@ from kivy.animation import Animation from kivy.uix.modalview import ModalView from kivymd.theming import ThemableBehavior -from kivymd.uix.behaviors import BackgroundColorBehavior,StencilBehavior,CommonElevationBehavior,HoverBehavior +from kivymd.uix.behaviors import ( + BackgroundColorBehavior, + StencilBehavior, + CommonElevationBehavior, + HoverBehavior, +) -class MediaPopup(ThemableBehavior,HoverBehavior,StencilBehavior,CommonElevationBehavior,BackgroundColorBehavior,ModalView): +class MediaPopup( + ThemableBehavior, + HoverBehavior, + StencilBehavior, + CommonElevationBehavior, + BackgroundColorBehavior, + ModalView, +): caller = ObjectProperty() player = ObjectProperty() - def __init__(self, caller,*args,**kwarg): + def __init__(self, caller, *args, **kwarg): self.caller = caller - super(MediaPopup,self).__init__(*args,**kwarg) + super(MediaPopup, self).__init__(*args, **kwarg) - def open(self, *_args, **kwargs): """Display the modal in the Window. @@ -26,33 +37,32 @@ class MediaPopup(ThemableBehavior,HoverBehavior,StencilBehavior,CommonElevationB """ from kivy.core.window import Window + if self._is_open: return self._window = Window self._is_open = True - self.dispatch('on_pre_open') + self.dispatch("on_pre_open") Window.add_widget(self) - Window.bind( - on_resize=self._align_center, - on_keyboard=self._handle_keyboard) + Window.bind(on_resize=self._align_center, on_keyboard=self._handle_keyboard) self.center = self.caller.to_window(*self.caller.center) - self.fbind('center', self._align_center) - self.fbind('size', self._align_center) - if kwargs.get('animation', True): - ani = Animation(_anim_alpha=1., d=self._anim_duration) - ani.bind(on_complete=lambda *_args: self.dispatch('on_open')) + self.fbind("center", self._align_center) + self.fbind("size", self._align_center) + if kwargs.get("animation", True): + ani = Animation(_anim_alpha=1.0, d=self._anim_duration) + ani.bind(on_complete=lambda *_args: self.dispatch("on_open")) ani.start(self) else: - self._anim_alpha = 1. - self.dispatch('on_open') + self._anim_alpha = 1.0 + self.dispatch("on_open") def _align_center(self, *_args): if self._is_open: self.center = self.caller.to_window(*self.caller.center) - - def on_leave(self,*args): + def on_leave(self, *args): def _leave(dt): if not self.hovering: self.dismiss() - Clock.schedule_once(_leave,2) + + Clock.schedule_once(_leave, 2) diff --git a/app/View/components/navrail.kv b/app/View/components/navrail.kv index ad783a9..b099387 100644 --- a/app/View/components/navrail.kv +++ b/app/View/components/navrail.kv @@ -1,8 +1,3 @@ - - icon:"" - text:"" - - MDNavigationRailItemIcon: icon:root.icon diff --git a/app/libs/animdl/animdl_api.py b/app/libs/animdl/animdl_api.py index abaada5..95f0f0a 100644 --- a/app/libs/animdl/animdl_api.py +++ b/app/libs/animdl/animdl_api.py @@ -1,12 +1,10 @@ import os import time -import json import re import shutil from subprocess import Popen, run, PIPE, CompletedProcess from typing import Callable - from .extras import Logger from .animdl_data_helper import ( filter_broken_streams, @@ -23,7 +21,7 @@ from .animdl_exceptions import ( NoValidAnimeStreamsException, Python310NotFoundException, ) -from .animdl_types import AnimdlAnimeUrlAndTitle, AnimdlData +from .animdl_types import AnimdlAnimeEpisode, AnimdlAnimeUrlAndTitle, AnimdlData broken_link_pattern = r"https://tools.fast4speed.rsvp/\w*" @@ -129,12 +127,25 @@ class AnimdlApi: ) return most_likely_anime_url_and_title # ("title","anime url") else: - raise AnimdlAnimeUrlNotFoundException + raise AnimdlAnimeUrlNotFoundException( + "The anime your searching for doesnt exist or animdl is broken or not in your system path" + ) @classmethod def stream_anime_by_title_on_animdl( - cls, title, episodes_range=None, quality: str = "best" + cls, title: str, episodes_range: str | None = None, quality: str = "best" ) -> Popen: + """Streams the anime title on animdl + + Args: + title (str): the anime title you want to stream + episodes_range (str, optional): the episodes you want to stream; should be a valid animdl range. Defaults to None. + quality (str, optional): the quality of the stream. Defaults to "best". + + Returns: + Popen: the stream child subprocess for mor control + """ + anime = cls.get_anime_url_by_title(title) base_cmds = ["stream", anime[1], "-q", quality] @@ -145,6 +156,17 @@ class AnimdlApi: def stream_anime_with_mpv( cls, title: str, episodes_range: str | None = None, quality: str = "best" ): + """Stream an anime directly with mpv without having to interact with animdl cli + + Args: + title (str): the anime title you want to stream + episodes_range (str | None, optional): a valid animdl episodes range you want ito watch. Defaults to None. + quality (str, optional): the quality of the stream. Defaults to "best". + + Yields: + Popen: the child subprocess you currently are watching + """ + anime_data = cls.get_all_stream_urls_by_anime_title(title, episodes_range) stream = [] for episode in anime_data.episodes: @@ -186,8 +208,18 @@ class AnimdlApi: @classmethod def get_all_anime_stream_urls_by_anime_url( - cls, anime_url: str, episodes_range=None - ): + cls, anime_url: str, episodes_range: str | None = None + ) -> list[AnimdlAnimeEpisode]: + """gets all the streams for the animdl url + + Args: + anime_url (str): an animdl url used in scraping + episodes_range (str | None, optional): a valid animdl episodes range. Defaults to None. + + Returns: + list[AnimdlAnimeEpisode]: A list of anime episodes gotten from animdl + """ + cmd = ( ["grab", anime_url, "-r", episodes_range] if episodes_range @@ -207,8 +239,9 @@ class AnimdlApi: episodes_range (str, optional): an animdl episodes range. Defaults to None. Returns: - _type_: _description_ + AnimdlData: The parsed data from animdl grab """ + possible_anime = cls.get_anime_url_by_title(title) return AnimdlData( possible_anime.anime_title, @@ -235,6 +268,23 @@ class AnimdlApi: episodes_range: str | None = None, quality: str = "best", ) -> tuple[list[int], list[int]]: + """Downloads anime either adaptive, progressive, or .m3u streams and uses mpv to achieve this + + Args: + _anime_title (str): the anime title you want to download + on_episode_download_progress (Callable): the callback when a chunk of an episode is downloaded + on_episode_download_complete (Callable): the callback when an episode has been successfully downloaded + on_complete (Callable): callback when the downloading process is complete + output_path (str): the directory | folder to download the anime + episodes_range (str | None, optional): a valid animdl episode range. Defaults to None. + quality (str, optional): the anime quality. Defaults to "best". + + Raises: + NoValidAnimeStreamsException: raised when no valid streams were found for a particular episode + + Returns: + tuple[list[int], list[int]]: a tuple containing successful, and failed downloads list + """ anime_streams_data = cls.get_all_stream_urls_by_anime_title( _anime_title, episodes_range @@ -260,6 +310,11 @@ class AnimdlApi: streams = filter_broken_streams(episode["streams"]) # raises an exception if no streams for current episodes + if not streams: + raise NoValidAnimeStreamsException( + f"No valid streams were found for episode {episode_number}" + ) + episode_stream = filter_streams_by_quality(streams, quality) # determine episode_title @@ -340,6 +395,17 @@ class AnimdlApi: @classmethod def download_with_mpv(cls, url: str, output_path: str, on_progress: Callable): + """The method used to download a remote resource with mpv + + Args: + url (str): the url of the remote resource to download + output_path (str): the location to download the resource to + on_progress (Callable): the callback when a chunk of the resource is downloaded + + Returns: + subprocess return code: the return code of the mpv subprocess + """ + mpv_child_process = run_mpv_command(url, f"--stream-dump={output_path}") progress_regex = re.compile(r"\d+/\d+") # eg Dumping 2044776/125359745 @@ -361,6 +427,18 @@ class AnimdlApi: episode_info: dict[str, str], on_progress: Callable, ): + """the progressive downloader of mpv + + Args: + video_url (str): a video url + output_path (str): download location + episode_info (dict[str, str]): the details of the episode we downloading + on_progress (Callable): the callback when a chunk is downloaded + + Raises: + Exception: exception raised when anything goes wrong + """ + episode = ( path_parser(episode_info["anime_title"]) + " - " @@ -385,6 +463,20 @@ class AnimdlApi: on_progress: Callable, episode_info: dict[str, str], ): + """the adaptive downloader + + Args: + video_url (str): url of video you want ot download + audio_url (str): url of audio file you want ot download + sub_url (str): url of sub file you want ot download + output_path (str): download location + on_progress (Callable): the callback when a chunk is downloaded + episode_info (dict[str, str]): episode details + + Raises: + Exception: incase anything goes wrong + """ + on_progress_ = lambda current_bytes, total_bytes: on_progress( current_bytes, total_bytes, episode_info ) @@ -421,6 +513,19 @@ class AnimdlApi: on_progress: Callable, episode_info: dict[str, str], ): + """only downloads video and subs + + Args: + video_url (str): url of video you want ot download + sub_url (str): url of sub you want ot download + output_path (str): the download location + on_progress (Callable): the callback for when a chunk is downloaded + episode_info (dict[str, str]): episode details + + Raises: + Exception: when anything goes wrong + """ + on_progress_ = lambda current_bytes, total_bytes: on_progress( current_bytes, total_bytes, episode_info ) @@ -441,21 +546,3 @@ class AnimdlApi: if is_video_failure: raise Exception - - -# TODO: ADD RUN_MPV_COMMAND = RAISES MPV NOT FOR ND EXCEPTION -# TODO: ADD STREAM WITH MPV -if __name__ == "__main__": - title = input("enter title: ") - e_range = input("enter range: ") - start = time.time() - # t = AnimdlApi.download_anime_by_title( - # title, lambda *u: print(u), lambda *u: print(u) - # ,lambda *u:print(u),".",episodes_range=e_range) - streamer = AnimdlApi.stream_anime_with_mpv(title, e_range, quality="720") - # with open("test.json","w") as file: - # print(json.dump(t,file)) - for stream in streamer: - print(stream.communicate()) - delta = time.time() - start - print(f"Took: {delta} secs") diff --git a/app/libs/animdl/animdl_data_helper.py b/app/libs/animdl/animdl_data_helper.py index 9654011..7621434 100644 --- a/app/libs/animdl/animdl_data_helper.py +++ b/app/libs/animdl/animdl_data_helper.py @@ -4,7 +4,11 @@ import json from fuzzywuzzy import fuzz from .extras import Logger -from .animdl_types import AnimdlAnimeUrlAndTitle,AnimdlData,AnimdlAnimeEpisode,AnimdlEpisodeStream +from .animdl_types import ( + AnimdlAnimeUrlAndTitle, + AnimdlAnimeEpisode, + AnimdlEpisodeStream, +) # Currently this links don't work so we filter it out @@ -70,6 +74,15 @@ def anime_title_percentage_match( def filter_broken_streams( streams: list[AnimdlEpisodeStream], ) -> list[AnimdlEpisodeStream]: + """filters the streams that the project has evaluated doesnt work + + Args: + streams (list[AnimdlEpisodeStream]): the streams to filter + + Returns: + list[AnimdlEpisodeStream]: the valid streams + """ + stream_filter = lambda stream: ( True if not re.match(broken_link_pattern, stream["stream_url"]) else False ) @@ -77,9 +90,19 @@ def filter_broken_streams( def filter_streams_by_quality( - anime_episode_streams: list[AnimdlEpisodeStream], quality: str|int, strict=False + anime_episode_streams: list[AnimdlEpisodeStream], quality: str | int, strict=False ) -> AnimdlEpisodeStream: - # filtered_streams = [] + """filters streams by quality + + Args: + anime_episode_streams (list[AnimdlEpisodeStream]): the streams to filter + quality (str | int): the quality you want to get + strict (bool, optional): whether to always return an episode if quality isn,t found. Defaults to False. + + Returns: + AnimdlEpisodeStream: the stream of specified quality + """ + # get the appropriate stream or default to best get_quality_func = lambda stream_: ( stream_.get("quality") if stream_.get("quality") else 0 @@ -104,8 +127,16 @@ def filter_streams_by_quality( # return AnimdlEpisodeStream({}) -# TODO: add typing to return dict def parse_stream_urls_data(raw_stream_urls_data: str) -> list[AnimdlAnimeEpisode]: + """parses the streams data gotten from animdl grab + + Args: + raw_stream_urls_data (str): the animdl grab data to parse + + Returns: + list[AnimdlAnimeEpisode]: the parsed streams for all episode + """ + try: return [ AnimdlAnimeEpisode(json.loads(episode.strip())) @@ -123,8 +154,9 @@ def search_output_parser(raw_data: str) -> list[AnimdlAnimeUrlAndTitle]: raw_data (str): valid animdl data Returns: - dict: parsed animdl data containing an anime title + AnimdlAnimeUrlAndTitle: parsed animdl data containing an animdl anime url and anime title """ + # get each line of dat and ignore those that contain unwanted data data = raw_data.split("\n")[3:] diff --git a/app/libs/animdl/test.json b/app/libs/animdl/test.json deleted file mode 100644 index 1d16412..0000000 --- a/app/libs/animdl/test.json +++ /dev/null @@ -1 +0,0 @@ -["jujutsu kaisen", [{"episode": 1, "streams": [{"stream_url": "https://tools.fast4speed.rsvp//media6/videos/CRZx43dgcfpWecx7W/sub/1"}]}, {"episode": 2, "streams": [{"stream_url": "https://tools.fast4speed.rsvp//media6/videos/CRZx43dgcfpWecx7W/sub/2"}]}, {"episode": 3, "streams": [{"stream_url": "https://tools.fast4speed.rsvp//media6/videos/CRZx43dgcfpWecx7W/sub/3"}]}]] \ No newline at end of file diff --git a/app/main.py b/app/main.py index 15509bf..d2d7418 100644 --- a/app/main.py +++ b/app/main.py @@ -42,7 +42,6 @@ if not (user_data_helper.yt_cache.exists("yt_stream_links")): # TODO: Confirm data integrity from user_data and yt_cache -# TODO: Arrange the app methods class AniXStreamApp(MDApp): queue = Queue() downloads_queue = Queue() @@ -51,13 +50,19 @@ class AniXStreamApp(MDApp): def worker(self, queue: Queue): while True: task = queue.get() # task should be a function - task() + try: + task() + except Exception as e: + show_notification("An error occured while streaming",f"{e}") self.queue.task_done() def downloads_worker(self, queue: Queue): while True: download_task = queue.get() # task should be a function - download_task() + try: + download_task() + except Exception as e: + show_notification("An error occured while downloading",f"{e}") self.downloads_queue.task_done() def __init__(self, **kwargs): @@ -111,12 +116,20 @@ class AniXStreamApp(MDApp): self.manager_screens.add_widget(view) def build_config(self, config): + if vid_path := plyer.storagepath.get_videos_dir(): # type: ignore + downloads_dir = os.path.join(vid_path, "anixstream") + if not os.path.exists(downloads_dir): + os.mkdir(downloads_dir) + else: + downloads_dir = os.path.join(".", "videos") + if not os.path.exists(downloads_dir): + os.mkdir(downloads_dir) config.setdefaults( "Preferences", { "theme_color": "Cyan", "theme_style": "Dark", - "downloads_dir": plyer.storagepath.get_videos_dir() if plyer.storagepath.get_videos_dir() else ".", # type: ignore + "downloads_dir": downloads_dir, "is_startup_anime_enable": False, }, ) @@ -204,6 +217,9 @@ class AniXStreamApp(MDApp): download_task = lambda: AnimdlApi.download_anime_by_title( default_cmds["title"], on_progress, + lambda anime_title, episode: show_notification( + "Finished installing an episode", f"{anime_title}-{episode}" + ), self.download_anime_complete, output_path, episodes_range, @@ -216,7 +232,9 @@ class AniXStreamApp(MDApp): download_task = lambda: AnimdlApi.download_anime_by_title( default_cmds["title"], on_progress, - lambda *arg:print(arg), + lambda anime_title, episode: show_notification( + "Finished installing an episode", f"{anime_title}-{episode}" + ), self.download_anime_complete, output_path, ) # ,default_cmds.get("quality") @@ -271,20 +289,20 @@ class AniXStreamApp(MDApp): ) def stream_anime_with_mpv( - self, title, episodes_range: str | None = None,quality:str="best" + self, title, episodes_range: str | None = None, quality: str = "best" ): self.stop_streaming = False - streams = AnimdlApi.stream_anime_with_mpv(title,episodes_range,quality) - # TODO: End mpv child process properly + streams = AnimdlApi.stream_anime_with_mpv(title, episodes_range, quality) + # TODO: End mpv child process properly for stream in streams: - self.animdl_streaming_subprocess= stream - for line in self.animdl_streaming_subprocess.stderr: # type: ignore + self.animdl_streaming_subprocess = stream + for line in self.animdl_streaming_subprocess.stderr: # type: ignore if self.stop_streaming: if stream: stream.terminate() stream.kill() del stream - return + return def watch_on_animdl( self, @@ -306,20 +324,24 @@ class AniXStreamApp(MDApp): self.animdl_streaming_subprocess.kill() self.stop_streaming = True - if stream_with_mpv_options: stream_func = lambda: self.stream_anime_with_mpv( - stream_with_mpv_options["title"], stream_with_mpv_options.get("episodes_range"),stream_with_mpv_options["quality"] + stream_with_mpv_options["title"], + stream_with_mpv_options.get("episodes_range"), + stream_with_mpv_options["quality"], ) self.queue.put(stream_func) - Logger.info(f"Animdl:Successfully started to stream {stream_with_mpv_options['title']}") + Logger.info( + f"Animdl:Successfully started to stream {stream_with_mpv_options['title']}" + ) else: stream_func = lambda: self.stream_anime_with_custom_input_cmds( *custom_options ) self.queue.put(stream_func) - show_notification("Streamer","Started streaming") + show_notification("Streamer", "Started streaming") + if __name__ == "__main__": AniXStreamApp().run() diff --git a/app/requirements.txt b/app/requirements.txt index bbe74b2..90501fe 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -4,4 +4,6 @@ ffpyplayer plyer https://github.com/kivymd/KivyMD/archive/master.zip fuzzywuzzy -python-Levenshtein \ No newline at end of file +python-Levenshtein +pyyaml +animdl \ No newline at end of file diff --git a/app/user_data.json b/app/user_data.json index 928268c..e4324bb 100644 --- a/app/user_data.json +++ b/app/user_data.json @@ -1 +1 @@ -{"user_anime_list": {"user_anime_list": [166531, 98437, 269, 104462, 21519, 150672, 104463, 21, 20631, 9756, 115230, 124194, 9253, 6702, 4654, 122671, 20657, 16049, 125367, 6213, 100185, 111322, 107226, 15583, 21857, 97889, 166372, 21745, 104051, 5114, 151806]}} \ No newline at end of file +{"user_anime_list": {"user_anime_list": [166531, 98437, 269, 104462, 21519, 150672, 104463, 21, 20631, 9756, 115230, 124194, 9253, 6702, 4654, 122671, 16049, 125367, 6213, 100185, 111322, 107226, 5081, 15583, 21857, 97889, 166372, 21745, 104051, 5114, 151806]}} \ No newline at end of file diff --git a/app/vids/Bleach sennen kessen-hen - ketsubetsu-tan/Episode 5/Bleach b/app/vids/Bleach sennen kessen-hen - ketsubetsu-tan/Episode 5/Bleach deleted file mode 100644 index e69de29..0000000 diff --git a/app/vids/Death note rewrite/Episode 1/Death note b/app/vids/Death note rewrite/Episode 1/Death note deleted file mode 100644 index e69de29..0000000