diff --git a/fastanime/Model/download_screen.py b/fastanime/Model/download_screen.py index b4513c9..0ac76ad 100644 --- a/fastanime/Model/download_screen.py +++ b/fastanime/Model/download_screen.py @@ -5,3 +5,17 @@ class DownloadsScreenModel(BaseScreenModel): """ Handles the download screen logic """ + + def update_download_progress(self, d): + print( + d["filename"], + d["downloaded_bytes"], + d["total_bytes"], + d.get("total_bytes"), + d["elapsed"], + d["eta"], + d["speed"], + d.get("percent"), + ) + if d["status"] == "finished": + print("Done downloading, now converting ...") diff --git a/fastanime/Utility/downloader/downloader.py b/fastanime/Utility/downloader/downloader.py index 6abc598..6429fb1 100644 --- a/fastanime/Utility/downloader/downloader.py +++ b/fastanime/Utility/downloader/downloader.py @@ -1,4 +1,10 @@ +from threading import Thread +from queue import Queue + import yt_dlp +from ... import downloads_dir +from ..utils import sanitize_filename +from ..show_notification import show_notification class MyLogger: @@ -12,27 +18,55 @@ class MyLogger: print(msg) -def my_hook(d): - if d["status"] == "finished": - print("Done downloading, now converting ...") +def main_progress_hook(data): + match data["status"]: + case "error": + show_notification( + "Something went wrong while downloading the video", data["filename"] + ) + case "finished": + show_notification("Downloaded", data["filename"]) -# 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]) +class YtDLPDownloader: + downloads_queue = Queue() + + def _worker(self): + while True: + task, args = self.downloads_queue.get() + try: + task(*args) + except Exception as e: + show_notification("Something went wrong", f"Reason: {e}") + self.downloads_queue.task_done() + + def __init__(self): + self._thread = Thread(target=self._worker) + self._thread.daemon = True + self._thread.start() + + # Function to download the file + 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 + "progress_hooks": [ + main_progress_hook, + custom_progress_hook, + ], # Progress hook + "silent": silent, + } + + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + ydl.download([url]) + + def download_file(self, url: str, title, custom_progress_hook, silent=True): + self.downloads_queue.put( + (self._download_file, (url, title, custom_progress_hook, silent)) + ) -# Call the function -download_file(url, ydl_opts) +downloader = YtDLPDownloader() diff --git a/fastanime/Utility/utils.py b/fastanime/Utility/utils.py index 4f2c6b7..57e0f7f 100644 --- a/fastanime/Utility/utils.py +++ b/fastanime/Utility/utils.py @@ -1,7 +1,7 @@ import os import shutil from datetime import datetime - +import re # TODO: make it use color_text instead of fixed vals # from .kivy_markup_helper import color_text @@ -34,3 +34,62 @@ def move_file(source_path, dest_path): return (1, path) except Exception as e: return (0, e) + + +def sanitize_filename(filename: str): + """ + Sanitize a string to be safe for use as a file name. + + :param filename: The original filename string. + :return: A sanitized filename string. + """ + # List of characters not allowed in filenames on various operating systems + invalid_chars = r'[<>:"/\\|?*\0]' + reserved_names = { + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + } + + # Replace invalid characters with an underscore + sanitized = re.sub(invalid_chars, "_", filename) + + # Remove leading and trailing whitespace + sanitized = sanitized.strip() + + # Check for reserved filenames + name, ext = os.path.splitext(sanitized) + if name.upper() in reserved_names: + name += "_file" + sanitized = name + ext + + # Ensure the filename is not empty + if not sanitized: + sanitized = "default_filename" + + return sanitized + + +# Example usage +unsafe_filename = "CON:example?file*name.txt" +safe_filename = sanitize_filename(unsafe_filename) +print(safe_filename) # Output: 'CON_example_file_name.txt' diff --git a/fastanime/View/AnimeScreen/anime_screen.kv b/fastanime/View/AnimeScreen/anime_screen.kv index 22ccff5..3691a10 100644 --- a/fastanime/View/AnimeScreen/anime_screen.kv +++ b/fastanime/View/AnimeScreen/anime_screen.kv @@ -47,6 +47,8 @@ on_press: root.next_episode() MDIconButton: icon:"download" + on_press: + if root.current_link: app.download_anime_video(root.current_link,(root.current_title[0],root.current_episode)) MDButton: on_press: if root.current_link: app.play_on_mpv(root.current_link) diff --git a/fastanime/View/DownloadsScreen/components/task_card.kv b/fastanime/View/DownloadsScreen/components/task_card.kv index 2cbd4fc..45814d0 100644 --- a/fastanime/View/DownloadsScreen/components/task_card.kv +++ b/fastanime/View/DownloadsScreen/components/task_card.kv @@ -18,11 +18,6 @@ TaskText: size_hint_x:.8 - text:color_text(root.anime_task_name,root.theme_cls.primaryColor) - TaskText: - size_hint_x:.2 - # color:self.theme_cls.surfaceDimColor - theme_text_color:"Secondary" - text:color_text(root.episodes_to_download,root.theme_cls.secondaryColor) - MDIcon: - icon:"download" + text:"{} Episode: {}".format(root.file[0],root.file[1]) + + diff --git a/fastanime/View/DownloadsScreen/components/task_card.py b/fastanime/View/DownloadsScreen/components/task_card.py index 4c8a34c..c7ac5e5 100644 --- a/fastanime/View/DownloadsScreen/components/task_card.py +++ b/fastanime/View/DownloadsScreen/components/task_card.py @@ -1,13 +1,13 @@ -from kivy.properties import StringProperty +from kivy.properties import StringProperty, ListProperty from kivymd.uix.boxlayout import MDBoxLayout -# TODO: add a progress bar to show the individual progress of each task class TaskCard(MDBoxLayout): - anime_task_name = StringProperty() - episodes_to_download = StringProperty() + file = ListProperty(("", "")) + eta = StringProperty() - def __init__(self, anime_title: str, episodes: str, *args, **kwargs): + def __init__(self, file: str, *args, **kwargs): super().__init__(*args, **kwargs) - self.anime_task_name = f"{anime_title}" - self.episodes_to_download = f"Episodes: {episodes}" + self.file = file + # self.eta = eta + # diff --git a/fastanime/View/DownloadsScreen/download_screen.kv b/fastanime/View/DownloadsScreen/download_screen.kv index 7092a86..fe881d9 100644 --- a/fastanime/View/DownloadsScreen/download_screen.kv +++ b/fastanime/View/DownloadsScreen/download_screen.kv @@ -7,7 +7,7 @@ shorten:False markup:True font_style: "Label" - role: "large" + role: "small" bold:True md_bg_color: self.theme_cls.backgroundColor @@ -46,12 +46,12 @@ md_bg_color:self.theme_cls.secondaryContainerColor DownloadsScreenLabel: id:download_progress_label - size_hint_x: .6 + size_hint_x: .8 text:"Try Downloading sth :)" pos_hint: {'center_y': .5} MDLinearProgressIndicator: id: progress_bar - size_hint_x: .4 + size_hint_x: .2 size_hint_y:None height:"10dp" type: "determinate" diff --git a/fastanime/View/DownloadsScreen/download_screen.py b/fastanime/View/DownloadsScreen/download_screen.py index 6add860..d137166 100644 --- a/fastanime/View/DownloadsScreen/download_screen.py +++ b/fastanime/View/DownloadsScreen/download_screen.py @@ -11,21 +11,31 @@ class DownloadsScreenView(BaseScreenView): progress_bar = ObjectProperty() download_progress_label = ObjectProperty() - def on_new_download_task(self, anime_title: str, episodes: str | None): - if not episodes: - episodes = "All" + def new_download_task(self, filename): Clock.schedule_once( - lambda _: self.main_container.add_widget(TaskCard(anime_title, episodes)) + lambda _: self.main_container.add_widget(TaskCard(filename)) ) - 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) - self.download_progress_label.text = progress_text + def on_episode_download_progress(self, data): + percentage_completion = round( + (data.get("downloaded_bytes", 0) / data.get("total_bytes", 0)) * 100 + ) + speed = format_bytes_to_human(data.get("speed", 0)) if data.get("speed") else 0 + progress_text = f"Downloading: {data.get('filename', 'unknown')} ({format_bytes_to_human(data.get('downloaded_bytes',0)) if data.get('downloaded_bytes') else 0}/{format_bytes_to_human(data.get('total_bytes',0)) if data.get('total_bytes') else 0})\n Elapsed: {round(data.get('elapsed',0)) if data.get('elapsed') else 0}s ETA: {data.get('eta',0) if data.get('eta') else 0}s Speed: {speed}/s" + + self.progress_bar.value = max(min(percentage_completion, 100), 0) + self.download_progress_label.text = progress_text def update_layout(self, widget): self.user_anime_list_container.add_widget(widget) + + # + # d["filename"], + # d["downloaded_bytes"], + # d["total_bytes"], + # d.get("total_bytes"), + # d["elapsed"], + # d["eta"], + # d["speed"], + # d.get("percent"), + # ) diff --git a/fastanime/__init__.py b/fastanime/__init__.py index 31afdda..b09ba4d 100644 --- a/fastanime/__init__.py +++ b/fastanime/__init__.py @@ -15,7 +15,7 @@ if vid_path := plyer.storagepath.get_videos_dir(): # type: ignore if not os.path.exists(downloads_dir): os.mkdir(downloads_dir) else: - downloads_dir = os.path.join(".", "videos") + downloads_dir = os.path.join(app_dir, "videos") if not os.path.exists(downloads_dir): os.mkdir(downloads_dir) diff --git a/fastanime/__main__.py b/fastanime/__main__.py index c92401c..9979241 100644 --- a/fastanime/__main__.py +++ b/fastanime/__main__.py @@ -10,12 +10,15 @@ from kivy.uix.screenmanager import FadeTransition, ScreenManager from kivy.uix.settings import Settings, SettingsWithSidebar from kivymd.app import MDApp +from fastanime.Utility.show_notification import show_notification + from . import downloads_dir from .libs.mpv.player import mpv_player from .Utility import ( themes_available, user_data_helper, ) +from .Utility.downloader.downloader import downloader from .Utility.utils import write_crash from .View.components.media_card.components.media_popup import MediaPopup from .View.screens import screens @@ -141,6 +144,12 @@ class FastAnime(MDApp): 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) + def run_app(): FastAnime().run() diff --git a/mpv-shot0001.jpg b/mpv-shot0001.jpg deleted file mode 100644 index 3c8d839..0000000 Binary files a/mpv-shot0001.jpg and /dev/null differ