diff --git a/fastanime/Controller/home_screen.py b/fastanime/Controller/home_screen.py index dd5f20c..16af637 100644 --- a/fastanime/Controller/home_screen.py +++ b/fastanime/Controller/home_screen.py @@ -35,8 +35,9 @@ class HomeScreenController: most_popular_cards_generator = self.model.get_most_popular_anime() if isgenerator(most_popular_cards_generator): for card in most_popular_cards_generator: - card.screen = self.view - most_popular_cards_container.container.add_widget(card) + card["screen"] = self.view + card["viewclass"] = "MediaCard" + most_popular_cards_container.container.data.append(card) self.view.main_container.add_widget(most_popular_cards_container) else: Logger.error("Home Screen:Failed to load most popular anime") @@ -48,8 +49,9 @@ class HomeScreenController: most_favourite_cards_generator = self.model.get_most_favourite_anime() if isgenerator(most_favourite_cards_generator): for card in most_favourite_cards_generator: - card.screen = self.view - most_favourite_cards_container.container.add_widget(card) + card["screen"] = self.view + card["viewclass"] = "MediaCard" + most_favourite_cards_container.container.data.append(card) self.view.main_container.add_widget(most_favourite_cards_container) else: Logger.error("Home Screen:Failed to load most favourite anime") @@ -61,8 +63,9 @@ class HomeScreenController: trending_cards_generator = self.model.get_trending_anime() if isgenerator(trending_cards_generator): for card in trending_cards_generator: - card.screen = self.view - trending_cards_container.container.add_widget(card) + card["screen"] = self.view + card["viewclass"] = "MediaCard" + trending_cards_container.container.data.append(card) self.view.main_container.add_widget(trending_cards_container) else: Logger.error("Home Screen:Failed to load trending anime") @@ -74,8 +77,9 @@ class HomeScreenController: most_scored_cards_generator = self.model.get_most_scored_anime() if isgenerator(most_scored_cards_generator): for card in most_scored_cards_generator: - card.screen = self.view - most_scored_cards_container.container.add_widget(card) + card["screen"] = self.view + card["viewclass"] = "MediaCard" + most_scored_cards_container.container.data.append(card) self.view.main_container.add_widget(most_scored_cards_container) else: Logger.error("Home Screen:Failed to load highest scored anime") @@ -89,8 +93,9 @@ class HomeScreenController: ) if isgenerator(most_recently_updated_cards_generator): for card in most_recently_updated_cards_generator: - card.screen = self.view - most_recently_updated_cards_container.container.add_widget(card) + card["screen"] = self.view + card["viewclass"] = "MediaCard" + most_recently_updated_cards_container.container.data.append(card) self.view.main_container.add_widget(most_recently_updated_cards_container) else: Logger.error("Home Screen:Failed to load recently updated anime") @@ -102,8 +107,9 @@ class HomeScreenController: upcoming_cards_generator = self.model.get_upcoming_anime() if isgenerator(upcoming_cards_generator): for card in upcoming_cards_generator: - card.screen = self.view - upcoming_cards_container.container.add_widget(card) + card["screen"] = self.view + card["viewclass"] = "MediaCard" + upcoming_cards_container.container.data.append(card) self.view.main_container.add_widget(upcoming_cards_container) else: Logger.error("Home Screen:Failed to load upcoming anime") diff --git a/fastanime/Utility/downloader/downloader.py b/fastanime/Utility/downloader/downloader.py new file mode 100644 index 0000000..6abc598 --- /dev/null +++ b/fastanime/Utility/downloader/downloader.py @@ -0,0 +1,38 @@ +import yt_dlp + + +class MyLogger: + def debug(self, msg): + print(msg) + + def warning(self, msg): + print(msg) + + def error(self, msg): + print(msg) + + +def my_hook(d): + if d["status"] == "finished": + print("Done downloading, now converting ...") + + +# URL of the file you want to download +url = "http://example.com/path/to/file.mp4" + +# Options for yt-dlp +ydl_opts = { + "outtmpl": "/path/to/downloaded/file.%(ext)s", # Specify the output path and template + "logger": MyLogger(), # Custom logger + "progress_hooks": [my_hook], # Progress hook +} + + +# Function to download the file +def download_file(url, options): + with yt_dlp.YoutubeDL(options) as ydl: + ydl.download([url]) + + +# Call the function +download_file(url, ydl_opts) diff --git a/fastanime/Utility/downloader/hsl.py b/fastanime/Utility/downloader/hsl.py new file mode 100644 index 0000000..63107c8 --- /dev/null +++ b/fastanime/Utility/downloader/hsl.py @@ -0,0 +1,39 @@ +import yt_dlp + + +class MyLogger: + def debug(self, msg): + print(msg) + + def warning(self, msg): + print(msg) + + def error(self, msg): + print(msg) + + +def my_hook(d): + if d["status"] == "finished": + print("Done downloading, now converting ...") + + +# URL of the HLS stream +url = "https://example.com/path/to/stream.m3u8" + +# Options for yt-dlp +ydl_opts = { + "format": "best", # Choose the best quality available + "outtmpl": "/path/to/downloaded/video.%(ext)s", # Specify the output path and template + "logger": MyLogger(), # Custom logger + "progress_hooks": [my_hook], # Progress hook +} + + +# Function to download the HLS video +def download_hls_video(url, options): + with yt_dlp.YoutubeDL(options) as ydl: + ydl.download([url]) + + +# Call the function +download_hls_video(url, ydl_opts) diff --git a/fastanime/Utility/media_card_loader.py b/fastanime/Utility/media_card_loader.py index bf168a1..c0cded2 100644 --- a/fastanime/Utility/media_card_loader.py +++ b/fastanime/Utility/media_card_loader.py @@ -4,7 +4,6 @@ from pytube import YouTube from ..libs.anilist.anilist_data_schema import AnilistBaseMediaDataSchema from ..Utility import anilist_data_helper, user_data_helper -from ..View.components import MediaCard Cache.register("trailer_urls.anime", timeout=360) @@ -16,34 +15,35 @@ class MediaCardDataLoader(object): self, anime_item: AnilistBaseMediaDataSchema, ): - media_card = MediaCard() - media_card.anime_id = anime_id = anime_item["id"] + media_card_data = {} + media_card_data["anime_id"] = anime_id = anime_item["id"] # TODO: ADD language preference if anime_item["title"].get("english"): - media_card.title = anime_item["title"]["english"] + media_card_data["title"] = anime_item["title"]["english"] else: - media_card.title = anime_item["title"]["romaji"] + media_card_data["title"] = anime_item["title"]["romaji"] - media_card.cover_image_url = anime_item["coverImage"]["medium"] + media_card_data["cover_image_url"] = anime_item["coverImage"]["medium"] - media_card.popularity = str(anime_item["popularity"]) + media_card_data["popularity"] = str(anime_item["popularity"]) - media_card.favourites = str(anime_item["favourites"]) + media_card_data["favourites"] = str(anime_item["favourites"]) - media_card.episodes = str(anime_item["episodes"]) + media_card_data["episodes"] = str(anime_item["episodes"]) if anime_item.get("description"): - media_card.description = anime_item["description"] + media_card_data["description"] = anime_item["description"] else: - media_card.description = "None" + media_card_data["description"] = "None" # TODO: switch to season and year - media_card.first_aired_on = ( + # + media_card_data["first_aired_on"] = ( f'{anilist_data_helper.format_anilist_date_object(anime_item["startDate"])}' ) - media_card.studios = anilist_data_helper.format_list_data_with_comma( + media_card_data["studios"] = anilist_data_helper.format_list_data_with_comma( [ studio["name"] for studio in anime_item["studios"]["nodes"] @@ -51,7 +51,7 @@ class MediaCardDataLoader(object): ] ) - media_card.producers = anilist_data_helper.format_list_data_with_comma( + media_card_data["producers"] = anilist_data_helper.format_list_data_with_comma( [ studio["name"] for studio in anime_item["studios"]["nodes"] @@ -59,36 +59,39 @@ class MediaCardDataLoader(object): ] ) - media_card.next_airing_episode = "{}".format( + media_card_data["next_airing_episode"] = "{}".format( anilist_data_helper.extract_next_airing_episode( anime_item["nextAiringEpisode"] ) ) if anime_item.get("tags"): - media_card.tags = anilist_data_helper.format_list_data_with_comma( + media_card_data["tags"] = anilist_data_helper.format_list_data_with_comma( [tag["name"] for tag in anime_item["tags"]] ) - media_card.media_status = anime_item["status"] + media_card_data["media_status"] = anime_item["status"] if anime_item.get("genres"): - media_card.genres = anilist_data_helper.format_list_data_with_comma( + media_card_data["genres"] = anilist_data_helper.format_list_data_with_comma( anime_item["genres"] ) if anime_id in user_data_helper.get_user_anime_list(): - media_card.is_in_my_list = True + media_card_data["is_in_my_list"] = True if anime_item["averageScore"]: stars = int(anime_item["averageScore"] / 100 * 6) + media_card_data["stars"] = [0, 0, 0, 0, 0, 0] if stars: for i in range(stars): - media_card.stars[i] = 1 + media_card_data["stars"][i] = 1 if trailer := anime_item.get("trailer"): trailer_url = "https://youtube.com/watch/v=" + trailer["id"] - media_card._trailer_url = trailer_url - return media_card + media_card_data["_trailer_url"] = trailer_url + else: + media_card_data["_trailer_url"] = "" + return media_card_data def get_trailer_from_pytube(self, trailer_url, anime): if trailer := Cache.get("trailer_urls.anime", trailer_url): diff --git a/fastanime/View/components/media_card/components/media_cards_container.kv b/fastanime/View/components/media_card/components/media_cards_container.kv index ceddd0b..80880c6 100644 --- a/fastanime/View/components/media_card/components/media_cards_container.kv +++ b/fastanime/View/components/media_card/components/media_cards_container.kv @@ -1,19 +1,23 @@ size_hint:1,None - height:max(self.minimum_height,dp(350),container.minimum_height) + height: dp(250) container:container orientation: 'vertical' padding:"10dp" spacing:"5dp" MDLabel: + bold:True + adaptive_height:True text:root.list_name MDScrollView: - size_hint:1,None - height:container.minimum_height - MDBoxLayout: + MDRecycleView: id:container - spacing:"10dp" - padding:"0dp","10dp","100dp","10dp" - size_hint:None,None - height:self.minimum_height - width:self.minimum_width + key_viewclass:"viewclass" + key_size:"width" + RecycleBoxLayout: + size_hint:None,1 + width:self.minimum_width + default_size_hint:None, None + default_size:dp(150),dp(100) + spacing:"10dp" + padding:"0dp","10dp","100dp","10dp" diff --git a/fastanime/View/components/media_card/components/media_player.py b/fastanime/View/components/media_card/components/media_player.py index 28b8100..f737cc1 100644 --- a/fastanime/View/components/media_card/components/media_player.py +++ b/fastanime/View/components/media_card/components/media_player.py @@ -1,10 +1,12 @@ from kivy.uix.videoplayer import VideoPlayer -# TODO: make fullscreen exp better class MediaPopupVideoPlayer(VideoPlayer): def __init__(self, **kwargs): super().__init__(**kwargs) + # FIXME: find way to make fullscreen stable + # + self.allow_fullscreen = False def on_fullscreen(self, instance, value): super().on_fullscreen(instance, value) diff --git a/fastanime/View/components/media_card/components/media_popup.kv b/fastanime/View/components/media_card/components/media_popup.kv index c06193c..3ca4b8f 100644 --- a/fastanime/View/components/media_card/components/media_popup.kv +++ b/fastanime/View/components/media_card/components/media_popup.kv @@ -27,9 +27,10 @@ height: dp(280) line_color:root.caller.has_trailer_color line_width:1 + # TODO: Remove the test source MediaPopupVideoPlayer: id:player - source: root.caller.trailer_url + source: root.caller.trailer_url if root.caller.trailer_url else 'https://www088.vipanicdn.net/streamhls/abae70787c7bd2fcd4fab986c2a5aeba/ep.7.1703900604.m3u8' thumbnail:app.default_anime_image state:"play" if root.caller.trailer_url else "stop" # fit_mode:"fill" diff --git a/fastanime/View/components/media_card/components/media_popup.py b/fastanime/View/components/media_card/components/media_popup.py index 3ce2794..a5b5272 100644 --- a/fastanime/View/components/media_card/components/media_popup.py +++ b/fastanime/View/components/media_card/components/media_popup.py @@ -59,7 +59,7 @@ class MediaPopup( self.caller = caller def on_caller(self, *args): - Clock.schedule_once(lambda _: self.apply_class_lang_rules(), -1) + self.apply_class_lang_rules() def open(self, *_args, **kwargs): """Display the modal in the Window. diff --git a/fastanime/View/components/media_card/media_card.kv b/fastanime/View/components/media_card/media_card.kv index e2091a8..ef2ba9c 100644 --- a/fastanime/View/components/media_card/media_card.kv +++ b/fastanime/View/components/media_card/media_card.kv @@ -1,11 +1,8 @@ adaptive_height:True spacing:"5dp" - image:"https://s4.anilist.co/file/anilistcdn/media/anime/cover/small/bx163270-oxwgbe43Cpog.jpg" size_hint_x: None width:dp(100) - on_release: - self.open() FitImage: source:root.cover_image_url fit_mode:"fill" @@ -14,6 +11,8 @@ height: dp(150) MDDivider: color:root.theme_cls.primaryColor if root._trailer_url else [0.5, 0.5, 0.5, 0.5] + size_hint: None, 1 + width: dp(100) SingleLineLabel: font_style:"Label" role:"medium" @@ -21,4 +20,7 @@ max_lines:2 halign:"center" color:self.theme_cls.secondaryColor + size_hint_x: None + shorten:True + width: dp(100) diff --git a/fastanime/View/components/media_card/media_card.py b/fastanime/View/components/media_card/media_card.py index 752d266..4cb5b88 100644 --- a/fastanime/View/components/media_card/media_card.py +++ b/fastanime/View/components/media_card/media_card.py @@ -1,4 +1,5 @@ from kivy.clock import Clock +from kivy.factory import Factory from kivy.properties import ( BooleanProperty, ListProperty, @@ -6,7 +7,6 @@ from kivy.properties import ( ObjectProperty, StringProperty, ) -from kivy.uix.behaviors import ButtonBehavior from kivymd.app import MDApp from kivymd.uix.behaviors import HoverBehavior from kivymd.uix.boxlayout import MDBoxLayout @@ -14,7 +14,7 @@ from kivymd.uix.boxlayout import MDBoxLayout from .components.media_popup import MediaPopup -class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout): +class MediaCard(HoverBehavior, MDBoxLayout): screen = ObjectProperty() anime_id = NumericProperty() title = StringProperty() @@ -38,6 +38,7 @@ class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout): cover_image_url = StringProperty() preview_image = StringProperty() has_trailer_color = ListProperty([0.5, 0.5, 0.5, 0.5]) + _popup_opened = False def __init__(self, trailer_url=None, **kwargs): super().__init__(**kwargs) @@ -49,6 +50,38 @@ class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout): self.trailer_url = trailer_url self.adaptive_size = True + def on_touch_down(self, touch): + if touch.is_mouse_scrolling: + return False + if not self.collide_point(touch.x, touch.y): + return False + if self in touch.ud: + return False + + if not self.app: + return False + disabled = True + # FIXME: double tap not working + # + if not disabled and touch.is_double_tap: + self.app.show_anime_screen(self.anime_id, self.screen.name) + touch.grab(self) + touch.ud[self] = True + self.last_touch = touch + + elif self.collide_point(*touch.pos): + if not self._popup_opened: + Clock.schedule_once(self.open) + self._popup_opened = True + touch.grab(self) + touch.ud[self] = True + self.last_touch = touch + return True + else: + super().on_touch_down(touch) + + # FIXME: Figure a good way implement this + # for now its debugy so scraping it till fix def on_enter(self): def _open_popup(dt): if self.hovering: @@ -59,15 +92,16 @@ class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout): return self.open() - Clock.schedule_once(_open_popup, 5) + # Clock.schedule_once(_open_popup, 5) def on_popup_open(self, popup: MediaPopup): popup.center = self.center def on_dismiss(self, popup: MediaPopup): popup.player.state = "stop" - if popup.player._video: - popup.player._video.unload() + self._popup_opened = False + # if popup.player._video: + # popup.player._video.unload() def set_preview_image(self, image): self.preview_image = image @@ -84,20 +118,19 @@ class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout): popup.bind(on_dismiss=self.on_dismiss, on_open=self.on_popup_open) popup.open(self) - # trailer stuff - from ....Utility.media_card_loader import media_card_loader + def _get_trailer(dt): + if trailer := self._trailer_url: + # trailer stuff + from ....Utility.media_card_loader import media_card_loader - if trailer := self._trailer_url: - # from ....Utility import show_notification + if trailer_url := media_card_loader.get_trailer_from_pytube( + trailer, self.title + ): + self.trailer_url = trailer_url + else: + self._trailer_url = "" - # TODO: show an indefinate progress while traile is still not available - # show_notification("Pytube", "Please wait for trailer to load") - if trailer_url := media_card_loader.get_trailer_from_pytube( - trailer, self.title - ): - self.trailer_url = trailer_url - else: - self._trailer_url = "" + Clock.schedule_once(_get_trailer, 1) # ---------------respond to user actions and call appropriate model------------------------- def on_is_in_my_list(self, instance, in_user_anime_list): @@ -111,6 +144,9 @@ class MediaCard(ButtonBehavior, HoverBehavior, MDBoxLayout): pass +Factory.register("MediaCard", MediaCard) + + class MediaCardsContainer(MDBoxLayout): container = ObjectProperty() list_name = StringProperty()