From 7621ff8a57d57fbd46c7a2123f6c63be30235ac0 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Sun, 30 Jun 2024 19:11:17 +0300 Subject: [PATCH 01/24] Minor fixes --- scripts/release.py | 16 ++++++++++++++-- senpwai/common/classes.py | 3 +++ senpwai/windows/main.py | 33 ++++++++++++++++++--------------- senpwai/windows/search.py | 2 +- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 2ccf76b..81fdfbd 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,4 +1,5 @@ import subprocess +import sys from scripts.common import ( ARGS, @@ -28,7 +29,15 @@ def publish_branch() -> None: def add_change_log_link(release_notes: str) -> str: - change_log_link = f"\n\n**Full Changelog**: {REPO_URL}/compare/v{bump_version.get_prev_version()}...v{bump_version.get_new_version()}" + prev_version = bump_version.get_prev_version() + new_version = bump_version.get_new_version() + if new_version == prev_version: + new_version = input("Failed to get prev version, manual input required\n> ") + if not new_version: + sys.exit() + change_log_link = ( + f"\n\n**Full Changelog**: {REPO_URL}/compare/v{prev_version}...v{new_version}" + ) return release_notes + change_log_link @@ -46,7 +55,10 @@ def get_release_notes() -> str: def publish_release(release_notes: str) -> None: - subprocess.run("glow", input=release_notes.encode()).check_returncode() + try: + subprocess.run("glow", input=release_notes.encode()).check_returncode() + except FileNotFoundError: + print(release_notes) subprocess.run( f'gh release create {BRANCH_NAME} --notes "{release_notes}"' ).check_returncode() diff --git a/senpwai/common/classes.py b/senpwai/common/classes.py index 2f907f6..137e9cb 100644 --- a/senpwai/common/classes.py +++ b/senpwai/common/classes.py @@ -92,6 +92,9 @@ def __init__(self) -> None: self.configure_settings() self.setup_logger() self.is_update_install = self.version != VERSION + if self.is_update_install: + self.version = VERSION + self.save_settings() def setup_logger(self) -> None: error_logs_file_path = os.path.join(self.config_dir, "errors.log") diff --git a/senpwai/windows/main.py b/senpwai/windows/main.py index 8b63ddf..25384fe 100644 --- a/senpwai/windows/main.py +++ b/senpwai/windows/main.py @@ -128,31 +128,34 @@ def center_window(self) -> None: x = (screen_geometry.width() - self.width()) // 2 self.move(x, 0) - def setup_and_switch_to_chosen_anime_window(self, anime: Anime, site: str): + def switch_to_chosen_anime_window(self, anime: Anime, site: str): # This if statement prevents error: "QThread: Destroyed while thread is still running" that happens when more than one thread is spawned - # When a user clicks more than one ResultButton quickly causing the reference to the original thread to be overwridden hence garbage collected/destroyed - if self.setup_chosen_anime_window_thread is None: - self.search_window.loading.start() - self.search_window.bottom_section_stacked_widgets.setCurrentWidget( - self.search_window.loading - ) - self.setup_chosen_anime_window_thread = MakeAnimeDetailsThread( - self, anime, site - ) - self.setup_chosen_anime_window_thread.finished.connect( - lambda anime_details: self.make_chosen_anime_window(anime_details) + # when a user clicks more than one ResultButton quickly causing the reference to the original thread to be overwridden hence garbage collected/destroyed + if self.setup_chosen_anime_window_thread is not None: + return + self.search_window.loading.start() + self.search_window.bottom_section_stacked_widgets.setCurrentWidget( + self.search_window.loading + ) + self.setup_chosen_anime_window_thread = MakeAnimeDetailsThread( + self, anime, site + ) + self.setup_chosen_anime_window_thread.finished.connect( + lambda anime_details: self.real_switch_to_chosen_anime_window( + anime_details ) - self.setup_chosen_anime_window_thread.start() + ) + self.setup_chosen_anime_window_thread.start() - def make_chosen_anime_window(self, anime_details: AnimeDetails): + def real_switch_to_chosen_anime_window(self, anime_details: AnimeDetails): self.setup_chosen_anime_window_thread = None self.search_window.bottom_section_stacked_widgets.setCurrentWidget( self.search_window.results_widget ) - self.search_window.loading.stop() chosen_anime_window = ChosenAnimeWindow(self, anime_details) self.stacked_windows.addWidget(chosen_anime_window) self.set_bckg_img(chosen_anime_window.bckg_img_path) + self.search_window.loading.stop() self.stacked_windows.setCurrentWidget(chosen_anime_window) self.setup_chosen_anime_window_thread = None diff --git a/senpwai/windows/search.py b/senpwai/windows/search.py index f956ff9..b23c6e4 100644 --- a/senpwai/windows/search.py +++ b/senpwai/windows/search.py @@ -414,7 +414,7 @@ def __init__( }}""" ) self.clicked.connect( - lambda: main_window.setup_and_switch_to_chosen_anime_window(anime, site) + lambda: main_window.switch_to_chosen_anime_window(anime, site) ) self.installEventFilter(self) From 355541a9677e5fccb2fc63ef14ba564a76e41fe2 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 2 Jul 2024 03:08:00 +0300 Subject: [PATCH 02/24] Fix failed to fetch direct download links notification on cancelling direct download links retrieval --- docs/release-notes.md | 10 ++-------- senpwai/windows/download.py | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index c78fd38..5bb1487 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,11 +1,5 @@ # New features - -- Add tips in about section -- senpcli: Stylize output 💅✨ + # Bug fixes - -- Maybe fix randomly skipped downloads, if it still happens report it -- pahe: Fix infinite loading for some anime. Issue #43 -- senpcli: Sort of fix Ctrl+C not terminating program, it still bugs out sometimes though -- Fix some linux port issues +- Fix failed to fetch direct download links notification on cancelling direct download links retrieval diff --git a/senpwai/windows/download.py b/senpwai/windows/download.py index 3233a4a..7445e3a 100644 --- a/senpwai/windows/download.py +++ b/senpwai/windows/download.py @@ -1266,10 +1266,10 @@ def run(self): lambda x: self.update_bar.emit(x), ) - if len(self.anime_details.ddls_or_segs_urls) < len(self.download_page_links): + if not obj.cancelled and len(self.anime_details.ddls_or_segs_urls) < len(self.download_page_links): self.download_window.main_window.tray_icon.make_notification( "Error", - f"Failed to find some {'hls' if self.anime_details.is_hls_download else 'direct download'} links for {self.anime_details.anime.title}", + f"Failed to retrieve some {'hls' if self.anime_details.is_hls_download else 'direct download'} links for {self.anime_details.anime.title}", False, None, ) From b62dd2086aa3d00fe07ed8a9a7a1e9953548e5f9 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 2 Jul 2024 03:17:00 +0300 Subject: [PATCH 03/24] Fix anime randomizer --- docs/release-notes.md | 1 + senpwai/windows/search.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5bb1487..a3e3dbf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,3 +3,4 @@ # Bug fixes - Fix failed to fetch direct download links notification on cancelling direct download links retrieval +- Fix anime randomizer diff --git a/senpwai/windows/search.py b/senpwai/windows/search.py index b23c6e4..53e2c19 100644 --- a/senpwai/windows/search.py +++ b/senpwai/windows/search.py @@ -299,7 +299,7 @@ def get_random_sen_favourite(self) -> str | None: favourites: list[dict["str", Any]] = response_json["data"]["User"][ "favourites" ]["anime"]["nodes"] - if favourites: + if not favourites: return None chosen_favourite = random_choice(favourites) anime_title = chosen_favourite["title"]["romaji"] From 38c029bf0628051cb25c48ae4db2d8b4727306ad Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 2 Jul 2024 04:13:24 +0300 Subject: [PATCH 04/24] Shorten long anime titles in progress bars --- docs/release-notes.md | 2 +- senpwai/common/classes.py | 11 ++++++++++- senpwai/common/widgets.py | 1 + senpwai/senpcli/main.py | 4 ++-- senpwai/windows/download.py | 11 ++++++++--- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index a3e3dbf..b3a2c50 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,5 @@ # New features - +- Shorten long anime titles in progress bars # Bug fixes - Fix failed to fetch direct download links notification on cancelling direct download links retrieval diff --git a/senpwai/common/classes.py b/senpwai/common/classes.py index 137e9cb..a442dd6 100644 --- a/senpwai/common/classes.py +++ b/senpwai/common/classes.py @@ -255,6 +255,7 @@ def __init__(self, anime: Anime, site: str) -> None: else False ) self.sanitised_title = sanitise_title(anime.title) + self.shortened_title = self.get_shortened_title() self.default_download_path = SETTINGS.download_folder_paths[0] self.anime_folder_path = self.get_anime_folder_path() self.potentially_haved_episodes = list(Path(self.anime_folder_path).glob("*")) @@ -281,9 +282,17 @@ def __init__(self, anime: Anime, site: str) -> None: else False ) + def get_shortened_title(self): + # Around 5 words i.e., 5 * 8 + max_anime_title_length = 40 + if len(self.sanitised_title) <= max_anime_title_length: + return self.sanitised_title + shortened = self.sanitised_title[: max_anime_title_length - 3] + return f"{shortened.strip()}..." + def episode_title(self, lacked_eps_idx: int) -> str: episode_number_str = str(self.lacked_episode_numbers[lacked_eps_idx]).zfill(2) - return f"{self.sanitised_title} E{episode_number_str}" + return f"{self.shortened_title} E{episode_number_str}" def validate_anime_folder_path(self) -> None: if not os.path.isdir(self.anime_folder_path): diff --git a/senpwai/common/widgets.py b/senpwai/common/widgets.py index 05224d4..3e812d4 100644 --- a/senpwai/common/widgets.py +++ b/senpwai/common/widgets.py @@ -679,6 +679,7 @@ def __init__( ): super().__init__(Icon(size_x, size_y, FOLDER_ICON_PATH), 1.3, parent) self.folder_path = path + self.setToolTip(path) self.clicked.connect(lambda: open_folder(self.folder_path)) diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index 3f9095e..e485648 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -464,9 +464,9 @@ def download_manager( ): anime_details.validate_anime_folder_path() desc = ( - f"Downloading [HLS] {anime_details.sanitised_title}" + f"Downloading [HLS] {anime_details.shortened_title}" if is_hls_download - else f"Downloading {anime_details.sanitised_title}" + else f"Downloading {anime_details.shortened_title}" ) episodes_pbar = ProgressBar(total=len(ddls_or_segs_urls), desc=desc, unit="eps") download_slot_available = Event() diff --git a/senpwai/windows/download.py b/senpwai/windows/download.py index 7445e3a..87f3341 100644 --- a/senpwai/windows/download.py +++ b/senpwai/windows/download.py @@ -639,7 +639,7 @@ def queue_download(self, anime_details: AnimeDetails): anime_progress_bar = ProgressBarWithoutButtons( self, "Downloading", - anime_details.anime.title, + anime_details.shortened_title, anime_details.total_download_size, "MB", 1, @@ -1266,10 +1266,15 @@ def run(self): lambda x: self.update_bar.emit(x), ) - if not obj.cancelled and len(self.anime_details.ddls_or_segs_urls) < len(self.download_page_links): + if not obj.cancelled and len(self.anime_details.ddls_or_segs_urls) < len( + self.download_page_links + ): + link_name = ( + "hls" if self.anime_details.is_hls_download else "direct download" + ) self.download_window.main_window.tray_icon.make_notification( "Error", - f"Failed to retrieve some {'hls' if self.anime_details.is_hls_download else 'direct download'} links for {self.anime_details.anime.title}", + f"Failed to retrieve some {link_name} links for {self.anime_details.anime.title}", False, None, ) From 2f8e52efea49648d1f41a4c73faa7d7434eec691 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 2 Jul 2024 04:52:56 +0300 Subject: [PATCH 05/24] Fix opening chosen anime window for other anime failing if previous attempt failed --- docs/release-notes.md | 3 ++- senpwai/windows/main.py | 26 ++++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index b3a2c50..8411636 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,7 @@ -# New features +# New features and Changes - Shorten long anime titles in progress bars # Bug fixes - Fix failed to fetch direct download links notification on cancelling direct download links retrieval - Fix anime randomizer +- Fix opening chosen anime window for other anime failing if previous attempt failed diff --git a/senpwai/windows/main.py b/senpwai/windows/main.py index 25384fe..51aae68 100644 --- a/senpwai/windows/main.py +++ b/senpwai/windows/main.py @@ -58,7 +58,7 @@ def __init__(self, app: QApplication): self.stacked_windows.addWidget(self.settings_window) self.stacked_windows.addWidget(self.about_window) self.setCentralWidget(self.stacked_windows) - self.setup_chosen_anime_window_thread: QThread | None = None + self.make_anime_details_thread: QThread | None = None self.download_window.start_auto_download() def closeEvent(self, a0: QCloseEvent | None) -> None: @@ -129,26 +129,24 @@ def center_window(self) -> None: self.move(x, 0) def switch_to_chosen_anime_window(self, anime: Anime, site: str): - # This if statement prevents error: "QThread: Destroyed while thread is still running" that happens when more than one thread is spawned - # when a user clicks more than one ResultButton quickly causing the reference to the original thread to be overwridden hence garbage collected/destroyed - if self.setup_chosen_anime_window_thread is not None: + if ( + self.make_anime_details_thread is not None + # Incase the thread crashed like when an exception is raised meaning the thread is not running + # but also did not emit on finish to reset to None + and self.make_anime_details_thread.isRunning() + ): return self.search_window.loading.start() self.search_window.bottom_section_stacked_widgets.setCurrentWidget( self.search_window.loading ) - self.setup_chosen_anime_window_thread = MakeAnimeDetailsThread( - self, anime, site + self.make_anime_details_thread = MakeAnimeDetailsThread(self, anime, site) + self.make_anime_details_thread.finished.connect( + lambda anime_details: self.real_switch_to_chosen_anime_window(anime_details) ) - self.setup_chosen_anime_window_thread.finished.connect( - lambda anime_details: self.real_switch_to_chosen_anime_window( - anime_details - ) - ) - self.setup_chosen_anime_window_thread.start() + self.make_anime_details_thread.start() def real_switch_to_chosen_anime_window(self, anime_details: AnimeDetails): - self.setup_chosen_anime_window_thread = None self.search_window.bottom_section_stacked_widgets.setCurrentWidget( self.search_window.results_widget ) @@ -157,7 +155,7 @@ def real_switch_to_chosen_anime_window(self, anime_details: AnimeDetails): self.set_bckg_img(chosen_anime_window.bckg_img_path) self.search_window.loading.stop() self.stacked_windows.setCurrentWidget(chosen_anime_window) - self.setup_chosen_anime_window_thread = None + self.make_anime_details_thread = None def switch_to_pahe(self, anime_title: str, initiator: QWidget): self.search_window.search_bar.setText(anime_title) From aba3dec193d08f611b90401f8dc14a91271b1853 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 2 Jul 2024 06:44:24 +0300 Subject: [PATCH 06/24] Make tooltip on folder button show folder location --- docs/release-notes.md | 1 + senpwai/common/widgets.py | 3 +++ senpwai/windows/download.py | 6 ++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8411636..11373d6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,6 @@ # New features and Changes - Shorten long anime titles in progress bars +- Hover on folder button shows folder location # Bug fixes - Fix failed to fetch direct download links notification on cancelling direct download links retrieval diff --git a/senpwai/common/widgets.py b/senpwai/common/widgets.py index 3e812d4..fce39ce 100644 --- a/senpwai/common/widgets.py +++ b/senpwai/common/widgets.py @@ -678,6 +678,9 @@ def __init__( self, path: str, size_x: int, size_y: int, parent: QWidget | None = None ): super().__init__(Icon(size_x, size_y, FOLDER_ICON_PATH), 1.3, parent) + self.set_folder_path(path) + + def set_folder_path(self, path: str): self.folder_path = path self.setToolTip(path) self.clicked.connect(lambda: open_folder(self.folder_path)) diff --git a/senpwai/windows/download.py b/senpwai/windows/download.py index 87f3341..5c0edb4 100644 --- a/senpwai/windows/download.py +++ b/senpwai/windows/download.py @@ -654,7 +654,9 @@ def queue_download(self, anime_details: AnimeDetails): ) set_minimum_size_policy(self.downloaded_episode_count) - self.folder_button = FolderButton("", 100, 100, None) + self.folder_button = FolderButton( + anime_details.anime_folder_path, 100, 100, None + ) def download_is_active() -> bool: return not ( @@ -718,7 +720,7 @@ def start_download(self): current_download_manager_thread.pause_or_resume ) self.cancel_button.cancel_callback = current_download_manager_thread.cancel - self.folder_button.folder_path = anime_details.anime_folder_path + self.folder_button.set_folder_path(anime_details.anime_folder_path) current_download_manager_thread.start() def make_episode_progress_bar( From 30e23d1a41513b9f5a1f96789e79c281dc9ec857 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 2 Jul 2024 17:30:30 +0300 Subject: [PATCH 07/24] Refactor loading settings --- senpwai/common/classes.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/senpwai/common/classes.py b/senpwai/common/classes.py index a442dd6..675053e 100644 --- a/senpwai/common/classes.py +++ b/senpwai/common/classes.py @@ -89,7 +89,8 @@ def __init__(self) -> None: self.gogo_skip_calculate = False self.version = VERSION - self.configure_settings() + self.load_settings() + self.save_settings() self.setup_logger() self.is_update_install = self.version != VERSION if self.is_update_install: @@ -114,23 +115,20 @@ def setup_config_dir(self) -> str: os.makedirs(config_dir) return config_dir - def configure_settings(self) -> None: + def load_settings(self) -> None: if not os.path.isfile(self.settings_json_path): - self.save_settings() return - with open(self.settings_json_path, "r") as f: - try: + try: + with open(self.settings_json_path, "r") as f: settings = cast(dict, json.load(f)) # TODO: DEPRECATION: Remove in version 2.1.11+ cause we use start_maximized now start_in_fullscreen = settings.get("start_in_fullscreen", None) if start_in_fullscreen is not None: self.start_maximized = start_in_fullscreen settings.pop("start_in_fullscreen") - f.close() - self.save_settings() self.__dict__.update(settings) - except json.decoder.JSONDecodeError: - self.save_settings() + except json.JSONDecodeError: + pass def setup_default_download_folder(self) -> list[str]: downloads_folder = os.path.join(Path.home(), "Downloads", "Anime") From 554cb67a768788197ee51b3b862a6ad2675b080a Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Wed, 3 Jul 2024 03:09:46 +0300 Subject: [PATCH 08/24] Minor tweaks --- senpwai/common/scraper.py | 14 ++++++++------ senpwai/common/widgets.py | 2 +- senpwai/main.py | 32 ++++++++++++++++++++------------ senpwai/scrapers/gogo/hls.py | 12 ++++++------ senpwai/scrapers/gogo/main.py | 8 ++++---- senpwai/scrapers/pahe/main.py | 10 +++++----- senpwai/scrapers/test.py | 2 +- senpwai/senpcli/main.py | 6 ++---- 8 files changed, 47 insertions(+), 39 deletions(-) diff --git a/senpwai/common/scraper.py b/senpwai/common/scraper.py index 9f65021..9ab17c7 100644 --- a/senpwai/common/scraper.py +++ b/senpwai/common/scraper.py @@ -266,9 +266,11 @@ def get_poster_bytes(self) -> bytes: return response.content -def match_quality(potential_qualities: list[str], user_quality: str) -> int: +def closest_quality_index( + potential_qualities: list[str], target_quality: str +) -> int: detected_qualities: list[tuple[int, int]] = [] - user_quality = user_quality.replace("p", "") + target_quality = target_quality.replace("p", "") for idx, potential_quality in enumerate(potential_qualities): match = QUALITY_REGEX_1.search(potential_quality) if not match: @@ -276,20 +278,20 @@ def match_quality(potential_qualities: list[str], user_quality: str) -> int: if match: quality = cast(str, match.group(1)) - if quality == user_quality: + if quality == target_quality: return idx else: detected_qualities.append((int(quality), idx)) - int_user_quality = int(user_quality) + int_target_quality = int(target_quality) if not detected_qualities: - if int_user_quality <= 480: + if int_target_quality <= 480: return 0 return -1 detected_qualities.sort(key=lambda x: x[0]) closest = detected_qualities[0] for quality in detected_qualities: - if quality[0] > int_user_quality: + if quality[0] > int_target_quality: break closest = quality return closest[1] diff --git a/senpwai/common/widgets.py b/senpwai/common/widgets.py index fce39ce..373f9c2 100644 --- a/senpwai/common/widgets.py +++ b/senpwai/common/widgets.py @@ -767,7 +767,7 @@ def __init__(self, window: QWidget | None, norm_or_hls: str, font_size: int): "Normal download functionality, similar to Animepahe but may occassionally fail" ) self.setToolTip( - "Guaranteed to work, it's like downloading a live stream as opposed to a file\nYou need to install FFmpeg for it to work but Senpwai will try to automatically install it" + "Guaranteed to work and usually downloads are faster, it's like downloading a live stream as opposed to a file\nYou need to install FFmpeg for it to work but Senpwai will try to automatically install it" ) diff --git a/senpwai/main.py b/senpwai/main.py index fb293ef..c60ec75 100644 --- a/senpwai/main.py +++ b/senpwai/main.py @@ -10,25 +10,34 @@ from senpwai.windows.main import MainWindow -def windows_app_initialisation(): +def windows_set_app_user_model_id(): # Change App ID to ensure task bar icon is Swnpwai's icon instead of Python for pip installs # StackOverflow Answer link: https://stackoverflow.com/questions/1551605/how-to-set-applications-taskbar-icon-in-windows-7/1552105#1552105 ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APP_NAME) - # Convert the app title to a null-terminated C string - app_title_c = (APP_NAME + "\0").encode("UTF-8") - # Check if the app is already running by searching for its window + + +def windows_focus_app_if_is_already_running(): + # Convert the app name to a null-terminated C string + app_title_c = (APP_NAME + "\0").encode() + # Find the app's already running window window = ctypes.windll.user32.FindWindowA(None, app_title_c) - if window != 0: - # If the app is running, bring it into focus - ctypes.windll.user32.ShowWindow(window, 9) # 9 is SW_RESTORE - ctypes.windll.user32.SetForegroundWindow(window) - sys.exit(0) + if window == 0: + return + # If the app is already running, bring it into focus then exit + ctypes.windll.user32.ShowWindow(window, 9) # 9 is SW_RESTORE + ctypes.windll.user32.SetForegroundWindow(window) + sys.exit(0) -def main(): +def init(): if OS.is_windows: - windows_app_initialisation() + windows_set_app_user_model_id() + if "--force" not in sys.argv: + windows_focus_app_if_is_already_running() + +def main(): + init() QCoreApplication.setApplicationName(APP_NAME) args = sys.argv app = QApplication(args) @@ -36,7 +45,6 @@ def main(): palette = app.palette() palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white) app.setPalette(palette) - window = MainWindow(app) app.setWindowIcon(window.senpwai_icon) window.show_with_settings(args) diff --git a/senpwai/scrapers/gogo/hls.py b/senpwai/scrapers/gogo/hls.py index 26d359d..43d6225 100644 --- a/senpwai/scrapers/gogo/hls.py +++ b/senpwai/scrapers/gogo/hls.py @@ -6,10 +6,10 @@ PARSER, CLIENT, ProgressFunction, - match_quality, + closest_quality_index, ) from bs4 import BeautifulSoup, Tag -from typing import Any, cast, Callable +from typing import cast, Callable from yarl import URL import base64 import re @@ -24,7 +24,7 @@ def get_hls_matched_quality_links( self, hls_links: list[str], quality: str, - progress_update_callback: Callable = lambda _: None, + progress_update_callback: Callable[[int], None] = lambda _: None, ) -> list[str]: matched_links: list[str] = [] for link in hls_links: @@ -34,7 +34,7 @@ def get_hls_matched_quality_links( return [] lines = response.text.split(",") qualities = [line for line in lines if "NAME=" in line] - idx = match_quality(qualities, quality) + idx = closest_quality_index(qualities, quality) resource = qualities[idx].splitlines()[1] base_url = URL(link).parent matched_links.append(f"{base_url}/{resource}") @@ -49,7 +49,7 @@ def __init__(self) -> None: def get_hls_segments_urls( self, matched_links: list[str], - progress_update_callback: Callable = lambda _: None, + progress_update_callback: Callable[[int], None] = lambda _: None, ) -> list[list[str]]: segments_urls: list[list[str]] = [] for link in matched_links: @@ -76,7 +76,7 @@ def __init__(self) -> None: def get_hls_links( self, download_page_links: list[str], - progress_update_callback: Callable[[int], Any] = lambda _: None, + progress_update_callback: Callable[[int], None] = lambda _: None, ) -> list[str]: hls_links: list[str] = [] for eps_url in download_page_links: diff --git a/senpwai/scrapers/gogo/main.py b/senpwai/scrapers/gogo/main.py index 433499c..7806db1 100644 --- a/senpwai/scrapers/gogo/main.py +++ b/senpwai/scrapers/gogo/main.py @@ -13,7 +13,7 @@ DomainNameError, ProgressFunction, get_new_home_url_from_readme, - match_quality, + closest_quality_index, sanitise_title, ) from .constants import ( @@ -83,7 +83,7 @@ def get_direct_download_links( self, download_page_links: list[str], user_quality: str, - progress_update_callback: Callable = lambda _: None, + progress_update_callback: Callable[[int], None] = lambda _: None, ) -> list[str]: direct_download_links: list[str] = [] for eps_pg_link in download_page_links: @@ -96,7 +96,7 @@ def get_direct_download_links( cast(Tag, soup.find("div", class_="cf-download")).find_all("a"), ) qualities = [a.text for a in a_tags] - idx = match_quality(qualities, user_quality) + idx = closest_quality_index(qualities, user_quality) redirect_link = cast(str, a_tags[idx]["href"]) link = CLIENT.get( redirect_link, cookies=get_session_cookies() @@ -116,7 +116,7 @@ def __init__(self): def calculate_total_download_size( self, direct_download_links: list[str], - progress_update_callback: Callable = lambda _: None, + progress_update_callback: Callable[[int], None] = lambda _: None, in_megabytes=False, ) -> int: total_size = 0 diff --git a/senpwai/scrapers/pahe/main.py b/senpwai/scrapers/pahe/main.py index a702604..6b34ce4 100644 --- a/senpwai/scrapers/pahe/main.py +++ b/senpwai/scrapers/pahe/main.py @@ -10,7 +10,7 @@ DomainNameError, ProgressFunction, get_new_home_url_from_readme, - match_quality, + closest_quality_index, ) from .constants import ( CHAR_MAP_BASE, @@ -141,7 +141,7 @@ def get_episode_page_links( first_page: dict[str, Any], anime_page_link: str, anime_id: str, - progress_update_callback: Callable = lambda _: None, + progress_update_callback: Callable[[int], None] = lambda _: None, ) -> list[str]: page_url = anime_page_link episodes_data: list[dict[str, Any]] = [] @@ -173,7 +173,7 @@ def __init__(self) -> None: def get_pahewin_page_links_and_info( self, episode_page_links: list[str], - progress_update_callback: Callable = lambda _: None, + progress_update_callback: Callable[[int], None] = lambda _: None, ) -> tuple[list[list[str]], list[list[str]]]: pahewin_links: list[list[str]] = [] download_info: list[list[str]] = [] @@ -248,7 +248,7 @@ def bind_quality_to_link_info( bound_links: list[str] = [] bound_info: list[str] = [] for links, infos in zip(pahewin_download_page_links, download_info): - index = match_quality(infos, quality) + index = closest_quality_index(infos, quality) bound_links.append(links[index]) bound_info.append(infos[index]) return (bound_links, bound_info) @@ -300,7 +300,7 @@ def __init__(self) -> None: def get_direct_download_links( self, pahewin_download_page_links: list[str], - progress_update_callback: Callable = lambda _: None, + progress_update_callback: Callable[[int], None] = lambda _: None, ) -> list[str]: direct_download_links: list[str] = [] for pahewin_link in pahewin_download_page_links: diff --git a/senpwai/scrapers/test.py b/senpwai/scrapers/test.py index 06318d5..cc1715e 100644 --- a/senpwai/scrapers/test.py +++ b/senpwai/scrapers/test.py @@ -49,7 +49,7 @@ def conditional_print(text: str): def test_start(name: str): - conditional_print(f"Running: {name} Test") + conditional_print(f"Testing: {name}") def fail_test( diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index e485648..d553324 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -159,16 +159,14 @@ def cancel_all_active(): ProgressBar.active.clear() def update_(self, added: int): - result = super().update(added) - return result + super().update(added) def close_(self, remove_from_active=True) -> None: if self not in ProgressBar.active: return if remove_from_active: ProgressBar.active.remove(self) - result = super().close() - return result + super().close() def parse_args(args: list[str]) -> tuple[Namespace, ArgumentParser]: From 329647868dcaa157c6f2213e16b8a86abbb5418c Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Sun, 7 Jul 2024 03:37:17 +0300 Subject: [PATCH 09/24] Add faster download info and emojis in tips --- senpwai/windows/about.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/senpwai/windows/about.py b/senpwai/windows/about.py index b67d04a..e9c8fe1 100644 --- a/senpwai/windows/about.py +++ b/senpwai/windows/about.py @@ -56,21 +56,21 @@ def __init__(self, main_window: "MainWindow"): tips.setMarkdown( f""" {startup_text} -- When searching use the anime's Romaji title instead of the English title +- When searching🔎 use the anime's Romaji title instead of the English title - If you don't specify end episode, Senpwai will assume you mean the last episode -- Senpwai can detect missing episodes e.g., if you have One Piece episodes 1 to 100 but are missing episode 50, specify 1 to 100, and it will only download episode 50 +- Senpwai can detect missing episodes e.g., if you have One Piece🏴‍☠️ episodes 1 to 100 but are missing episode 50, specify 1 to 100, and it will only download episode 50 - If Senpwai can't find the quality you want it will pick the closest lower one it e.g., if you choose 720p but only 1080p and 360p is available it'll pick 360p -- So long as the app is open Senpwai will try to resume ongoing downloads even if you lose an internet connection -- [Hover](https://open.spotify.com/playlist/460b5y4LB8Dixh0XajVVaL?si=fce0f0f762464e81) over something that you don't understand there's probably a tool tip for it -- If the app screen is white after you minimised it to tray and reopened it (usually on Windows), click the tray icon to fix it +- So long as the app is open Senpwai will try to resume⏯️ ongoing downloads even if you lose an internet connection +- Experiencing slow downloads? Use gogo hls mode for the fastest🚀 download speeds +- [Hover✨](https://open.spotify.com/playlist/460b5y4LB8Dixh0XajVVaL?si=fce0f0f762464e81) over something that you don't understand there's probably a tool tip for it +- If the app screen📺 is white after you minimised it to tray and reopened it (usually on Windows🗑️), click the tray icon to fix it - Open the settings folder by clicking the button with its location in the top left corner of the settings window -- To completely remove Senpwai (don't know why you would though), post-uninstallation delete the settings folder +- To completely remove Senpwai (don't know why you would though), post-uninstallation delete the settings folder, 🫵🏿®️🅰️🤡 - To use a custom font family, edit the `font_family` value in the settings file, if left empty, it will default to your OS setting -- Hate the background images? Check out the [discord]({DISCORD_INVITE_LINK}) for [senptheme](https://discord.com/channels/1131981618777702540/1211137093955362837/1211175899895038033) +- Hate the background images📸? Check out the [discord]({DISCORD_INVITE_LINK}) for [senptheme](https://discord.com/channels/1131981618777702540/1211137093955362837/1211175899895038033) """ ) tips.setMinimumHeight(200) - reviews_title = Title("Reviews") set_minimum_size_policy(reviews_title) reviews_widget = QWidget() From b3e508bc3543a21d9aa174245ea4a9f6753fd91e Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Mon, 8 Jul 2024 04:00:52 +0300 Subject: [PATCH 10/24] Minor change --- scripts/setup.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/setup.iss b/scripts/setup.iss index 79c7b90..998a0ee 100644 --- a/scripts/setup.iss +++ b/scripts/setup.iss @@ -5,7 +5,7 @@ #define MyAppVersion "2.1.10" #define MyAppPublisher "AkatsuKi Inc." #define MyAppURL "https://github.com/SenZmaKi/Senpwai" -#define MyAppExeName "Senpwai.exe" +#define MyAppExeName "senpwai.exe" #define ProjectRootDir "C:\Users\PC\Dev\Python\Senpwai" [Setup] From caaf0ccd5dcf66aae217f4344eb3f419fc63835d Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 9 Jul 2024 04:29:21 +0300 Subject: [PATCH 11/24] Minor tweak --- senpwai/windows/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/senpwai/windows/settings.py b/senpwai/windows/settings.py index 547ca96..414c002 100644 --- a/senpwai/windows/settings.py +++ b/senpwai/windows/settings.py @@ -469,6 +469,7 @@ def __init__( class GogoSkipCalculate(YesOrNoSetting): def __init__(self, settings_window: SettingsWindow): super().__init__(settings_window, "Skip calculating download size for Gogo") + self.setToolTip("Calculating total download size on gogo involves first making requests for the size of each episode") if SETTINGS.gogo_skip_calculate: self.yes_button.set_picked_status(True) else: From ec2d7d129b9cb5c0066122d185aad186b67cf6b6 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Wed, 10 Jul 2024 07:06:04 +0300 Subject: [PATCH 12/24] Minor change --- senpwai/scrapers/pahe/main.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/senpwai/scrapers/pahe/main.py b/senpwai/scrapers/pahe/main.py index 6b34ce4..f7c161b 100644 --- a/senpwai/scrapers/pahe/main.py +++ b/senpwai/scrapers/pahe/main.py @@ -191,8 +191,8 @@ def get_pahewin_page_links_and_info( return (pahewin_links, download_info) -def is_dub(anime_info: str) -> bool: - return anime_info.endswith(DUB_PATTERN) +def is_dub(episode_download_info: str) -> bool: + return episode_download_info.endswith(DUB_PATTERN) def dub_available(anime_page_link: str, anime_id: str) -> bool: @@ -202,14 +202,11 @@ def dub_available(anime_page_link: str, anime_id: str) -> bool: if episodes_data is None: return False episode_sessions = [episode["session"] for episode in episodes_data] - episode_links = [ - EPISODE_PAGE_URL.format(anime_id, episode_session) - for episode_session in episode_sessions - ] + episode_page_link = EPISODE_PAGE_URL.format(anime_id, episode_sessions[0]) ( _, download_info, - ) = GetPahewinPageLinks().get_pahewin_page_links_and_info(episode_links[:1]) + ) = GetPahewinPageLinks().get_pahewin_page_links_and_info([episode_page_link]) for info in download_info[0]: if is_dub(info): From e17fc8f988891dccdca177ea9d3086ed0cf867b1 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Fri, 12 Jul 2024 03:47:21 +0300 Subject: [PATCH 13/24] senpcli: Minor fix --- senpwai/senpcli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index d553324..ec1ecb2 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -769,7 +769,7 @@ def validate_args(parsed: Namespace) -> bool: print_error("End episode cannot be less than 1, is that your brain cell count?") return False if parsed.site != GOGO and parsed.hls: - print_error("Setting site to Gogo since HLS mode is only available for Gogo") + print_warn("Setting site to Gogo since HLS mode is only available for Gogo") parsed.site = GOGO return True From 4ed9dc30c8523417aeadbb6127c2bfbecb12490b Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Fri, 12 Jul 2024 04:23:17 +0300 Subject: [PATCH 14/24] senpcli: Always show units/time instead of time/units in progress bars --- docs/release-notes.md | 1 + senpwai/senpcli/main.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 11373d6..5d23b0d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,7 @@ # New features and Changes - Shorten long anime titles in progress bars - Hover on folder button shows folder location +- senpcli: Always show units/time instead of time/units in progress bars # Bug fixes - Fix failed to fetch direct download links notification on cancelling direct download links retrieval diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index ec1ecb2..d554148 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -141,12 +141,13 @@ def __init__( total: int, desc: str, unit: str, - unit_scale=False, + unit_scale=True, ): super().__init__( total=total, desc=desc, unit=unit, + bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_noinv_fmt}]", unit_scale=unit_scale, leave=False, ) From 009ce25cfb5717aeb005a48c3ed239fd910eb61e Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Thu, 25 Jul 2024 06:09:00 +0300 Subject: [PATCH 15/24] Improve scripts --- pyproject.toml | 6 +- scripts/announce/__main__.py | 15 +++- scripts/announce/discord.py | 133 ++++++++++++++++++++++++++++++----- scripts/announce/reddit.py | 26 +++++-- scripts/bump_version.py | 54 ++++++++------ scripts/common.py | 11 ++- scripts/crasher.py | 2 - scripts/install_pip.py | 4 +- scripts/release.py | 82 ++++++++++++++++----- scripts/ruff.py | 11 ++- scripts/run_exes.py | 54 +++++++++----- scripts/setup.py | 6 +- 12 files changed, 307 insertions(+), 97 deletions(-) delete mode 100644 scripts/crasher.py diff --git a/pyproject.toml b/pyproject.toml index 860b013..0c53026 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,9 +73,9 @@ ruff = [{ ref = "lint_fix" }, { ref = "format" }] lint = "python -m scripts.ruff" lint_fix = "python -m scripts.ruff --lint_fix" format = "python -m scripts.ruff --format" -generate_release_test = [{ref = "lint"}, { ref = "test" }, { ref = "generate_release" }] -generate_release_ddl = [{ ref = "test_ddl" }, { ref = "generate_release" }] -generate_release = [ +build_release_test = [{ref = "lint"}, { ref = "test" }, { ref = "build_release" }] +build_release_ddl = [{ ref = "test_ddl" }, { ref = "build_release" }] +build_release = [ { ref = "build_exes" }, { ref = "compile_installers" }, { ref = "install" }, diff --git a/scripts/announce/__main__.py b/scripts/announce/__main__.py index 079ba3a..7181d3a 100644 --- a/scripts/announce/__main__.py +++ b/scripts/announce/__main__.py @@ -1,5 +1,16 @@ +from argparse import ArgumentParser from scripts.announce.main import main -from scripts.common import get_piped_input, ARGS +from scripts.common import get_piped_input if __name__ == "__main__": - main(ARGS[0], get_piped_input()) + parser = ArgumentParser("Announce a new release on Discord and Reddit") + parser.add_argument("-t", "--title", type=str, help="Title of the release") + parser.add_argument( + "-r", + "--release_notes", + type=str, + help="Release notes, will use stdin if not provided", + ) + parsed = parser.parse_args() + release_notes = parsed.release_notes or get_piped_input() + main(parsed.title, release_notes) diff --git a/scripts/announce/discord.py b/scripts/announce/discord.py index c20d9ad..aa5650e 100644 --- a/scripts/announce/discord.py +++ b/scripts/announce/discord.py @@ -1,42 +1,141 @@ +from argparse import ArgumentParser +from enum import Enum import json +from typing import Any import requests import re -from scripts.common import ARGS, ROOT_DIR, get_piped_input +from scripts.common import ROOT_DIR, get_piped_input from scripts.announce.common import FailedToAnnounce -CHANNEL_URL = "https://discord.com/api/v10/channels/1142774130689720370/messages" +CHANNEL_URL = "https://discord.com/api/v10/channels/{}/messages" URL_REGEX = re.compile(r"https://\S+") +class ChannelID(Enum): + general = "1131981620975517787" + help = "1190075801815748648" + bug_reports = "1134645830163378246" + suggestions = "1134645775905861673" + github_logs = "1205730845953097779" + misc = "1211137093955362837" + announcements = "1142774130689720370" + + def __str__(self) -> str: + return self.name + + def remove_embed_url(string: str) -> str: return URL_REGEX.sub(lambda x: f"<{x.group(0)}>", string) -def main(title: str, release_notes: str) -> None: - with open(ROOT_DIR.joinpath(".credentials/discord.json")) as f: +def send_message(message: str, channel_id: str, message_reference_id: str) -> None: + with open(ROOT_DIR.joinpath(".credentials", "discord.json")) as f: token = json.load(f)["token"] headers = { "Authorization": f"Bot {token}", "Content-Type": "application/json", } - nonembed_notes = remove_embed_url(release_notes) - smaller_titles = "\n".join( - [ - line.replace("# ", "## ", 1) if line.strip().startswith("# ") else line - for line in nonembed_notes.splitlines() - ] - ) - - everyone = "@everyone\n" if "--ping_everyone" in ARGS else "" - message = f"{everyone}# {title}\n\n" + smaller_titles - payload = { + payload: dict[str, Any] = { "content": message, } - response = requests.post(url=CHANNEL_URL, headers=headers, json=payload) + if message_reference_id: + payload["message_reference"] = {"message_id": message_reference_id} + url = CHANNEL_URL.format(channel_id) + response = requests.post(url, headers=headers, json=payload) if not response.ok: raise FailedToAnnounce("discord", response.json()) +def main( + title: str, + message: str, + is_release_notes=True, + pinged_user_id="", + message_reference_id="", + channel_id=ChannelID.announcements.value, +) -> None: + if is_release_notes: + non_embed_notes = remove_embed_url(message) + message = "\n".join( + [ + line.replace("# ", "## ", 1) if line.strip().startswith("# ") else line + for line in non_embed_notes.splitlines() + ] + ) + if pinged_user_id: + ping_str = ( + "@everyone" if pinged_user_id == "everyone" else f"<@{pinged_user_id}>" + ) + message = ( + f"{ping_str}\n{message}" + if pinged_user_id == "everyone" and is_release_notes + else f"{ping_str} {message}" + ) + if title: + message = f"# {title}\n{message}" + send_message(message, channel_id, message_reference_id) + + if __name__ == "__main__": - main(ARGS[0], get_piped_input()) + parser = ArgumentParser(description="Message on Discord") + parser.add_argument("-t", "--title", help="Title of the message", default="") + parser.add_argument( + "-m", + "--message", + help="Body of the message, will use stdin if not provided", + default="", + ) + parser.add_argument( + "-irn", + "--is_release_notes", + action="store_true", + help="Whether the message is release notes", + ) + parser.add_argument( + "-pui", + "--pinged_user_id", + type=str, + help="ID of the user to ping", + default="", + ) + parser.add_argument( + "-pe", + "--ping_everyone", + action="store_true", + help="Ping everyone", + ) + parser.add_argument( + "-mri", + "--message_reference_id", + type=str, + help="ID of the message to reply to", + default="", + ) + parser.add_argument( + "-cid", + "--channel_id", + type=str, + help="ID of the channel to send the message to", + default="", + ) + parser.add_argument( + "-c", + "--channel", + type=lambda channel: ChannelID[channel], + choices=list(ChannelID), + help="Channel to send the message to", + default=ChannelID.announcements, + ) + parsed = parser.parse_args() + message = parsed.message or get_piped_input() + channel_id = parsed.channel_id or parsed.channel.value + pinged_user_id = "everyone" if parsed.ping_everyone else parsed.pinged_user_id + main( + parsed.title, + message, + parsed.is_release_notes, + pinged_user_id, + parsed.message_reference_id, + channel_id, + ) diff --git a/scripts/announce/reddit.py b/scripts/announce/reddit.py index 4dd4eba..1cfb524 100644 --- a/scripts/announce/reddit.py +++ b/scripts/announce/reddit.py @@ -1,6 +1,7 @@ +from argparse import ArgumentParser from typing import Any, cast from scripts.announce.common import FailedToAnnounce -from scripts.common import ARGS, ROOT_DIR, get_piped_input +from scripts.common import ROOT_DIR, get_piped_input import json import requests import re @@ -22,7 +23,7 @@ def validate_response(is_ok: bool, response_json: dict[str, Any]) -> None: def fetch_access_token() -> str: - with open(ROOT_DIR.joinpath(".credentials/reddit.json"), "r") as f: + with open(ROOT_DIR.joinpath(".credentials", "reddit.json"), "r") as f: credentials = json.load(f) data = { "scope": "*", @@ -41,7 +42,7 @@ def fetch_access_token() -> str: def submit_post( title: str, - release_notes: str, + message: str, access_token: str, ) -> str: headers = {"Authorization": f"Bearer {access_token}", "User-Agent": "script"} @@ -52,7 +53,7 @@ def submit_post( "sr": "Senpwai", "resubmit": "true", "send_replies": "true", - "text": release_notes, + "text": message, } response = requests.post(SUBMIT_POST_URL, headers=headers, data=data) @@ -70,14 +71,25 @@ def approve_post(post_short_id: str, access_token: str) -> None: validate_response(response.ok, response_json) -def main(title: str, release_notes: str) -> None: +def main(title: str, message: str) -> None: log_info("Fetching auth token") access_token = fetch_access_token() log_info("Submitting post") - post_short_id = submit_post(title, release_notes, access_token) + post_short_id = submit_post(title, message, access_token) log_info("Approving post") approve_post(post_short_id, access_token) if __name__ == "__main__": - main(ARGS[0], get_piped_input()) + parser = ArgumentParser(description="Post on Reddit") + parser.add_argument("-t", "--title", type=str, help="Title of the post") + parser.add_argument( + "-m", + "--message", + type=str, + default="", + help="Body of the post, will use stdin if not provided", + ) + parsed = parser.parse_args() + message = parsed.message or get_piped_input() + main(parsed.title, message) diff --git a/scripts/bump_version.py b/scripts/bump_version.py index 58014e9..c58ae5a 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -1,11 +1,12 @@ +from argparse import ArgumentParser from functools import cache +from hmac import new import subprocess import sys import re from typing import cast from .common import ( ROOT_DIR, - ARGS, get_current_branch_name, git_commit, log_error as common_log_error, @@ -23,9 +24,6 @@ ROOT_DIR.joinpath("scripts/setup_senpcli.iss"), ] -USAGE = """ -Usage: bump_version -""" ENCOUNTERED_ERROR = False VERSION_REGEX = re.compile(r"(\d+(\.\d+)*)") @@ -59,11 +57,6 @@ def get_new_version() -> str: return cast(re.Match, new_version).group(1) -def get_versions() -> tuple[str, str]: - prev_version, new_version = get_prev_version(), get_new_version() - return prev_version, new_version - - def bump_version(prev_version: str, new_version: str, ignore_same: bool): for file_path in FILES_PATHS: if not file_path.is_file(): @@ -85,20 +78,37 @@ def bump_version(prev_version: str, new_version: str, ignore_same: bool): def main(ignore_same=False) -> None: - if len(ARGS) == 1 and ARGS[0] in ("--help", "-h"): - print(USAGE) - return - if len(ARGS) == 2: - prev_version = ARGS[0] - new_version = ARGS[1] - else: - prev_version, new_version = get_versions() - if not ignore_same and prev_version == new_version: - log_error(f"Previous and New version are the same: {prev_version}", True) - log_info(f"Bumping version from {prev_version} --> {new_version}") - bump_version(prev_version, new_version, ignore_same) + parser = ArgumentParser(description="Bump version in files") + parser.add_argument( + "-is", + "--ignore_same", + action="store_true", + help="Ignore if previous and new version are the same", + ) + parser.add_argument( + "-pv", + "--prev_version", + type=str, + help="Previous version to bump from", + default=get_prev_version(), + ) + parser.add_argument( + "-nv", + "--new_version", + type=str, + help="New version to bump to", + default=get_new_version(), + ) + parsed = parser.parse_args() + ignore_same = parsed.ignore_same + previous_version = parsed.previous_version + new_version = parsed.new_version + if not ignore_same and previous_version == new_version: + log_error(f"Previous and New version are the same: {previous_version}", True) + log_info(f"Bumping version from {previous_version} --> {new_version}") + bump_version(previous_version, new_version, ignore_same) subprocess.run("git --no-pager diff").check_returncode() - git_commit(f"Bump version from {prev_version} --> {new_version}") + git_commit(f"Bump version from {previous_version} --> {new_version}") if ENCOUNTERED_ERROR: sys.exit(1) diff --git a/scripts/common.py b/scripts/common.py index eb4ee99..01623fe 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -3,11 +3,20 @@ import sys from functools import cache from io import TextIOWrapper +import os ROOT_DIR = Path(__file__).parent.parent REPO_URL = "https://github.com/SenZmaKi/Senpwai" -ARGS = sys.argv[1:] +def join_from_local_appdata(*paths: str) -> str: + return os.path.join( + os.environ["LOCALAPPDATA"], + "Programs", + *paths, + ) + +def join_from_py_scripts(*paths: str) -> str: + return join_from_local_appdata("Python", "Python311", "Scripts", *paths) def git_commit(msg: str) -> None: subprocess.run(f'git commit -am "scripts: {msg}"') diff --git a/scripts/crasher.py b/scripts/crasher.py deleted file mode 100644 index ecf4b59..0000000 --- a/scripts/crasher.py +++ /dev/null @@ -1,2 +0,0 @@ - -raise EOFError diff --git a/scripts/install_pip.py b/scripts/install_pip.py index 831d4f6..0763e98 100644 --- a/scripts/install_pip.py +++ b/scripts/install_pip.py @@ -1,11 +1,11 @@ import subprocess -from scripts.common import ROOT_DIR +from scripts.common import ROOT_DIR, join_from_py_scripts def main() -> None: # Global pip cause venv pip is weird sometimes cause poetry and stuff - pip_path = r"C:\Users\PC\AppData\Local\Programs\Python\Python311\Scripts\pip.exe" + pip_path = join_from_py_scripts("pip.exe") dist_dir = ROOT_DIR.joinpath("dist") distributable = next(dist_dir.glob("*")) subprocess.run( diff --git a/scripts/release.py b/scripts/release.py index 8eb974c..9e5d666 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,7 +1,6 @@ import subprocess from scripts.common import ( - ARGS, REPO_URL, get_current_branch_name, log_info, @@ -11,11 +10,12 @@ ) from scripts import bump_version, ruff from scripts import announce +from argparse import ArgumentParser BRANCH_NAME = get_current_branch_name() -def publish_branch() -> None: +def merge_branch() -> None: git_status_completed_process = subprocess.run( "git status", capture_output=True, text=True ) @@ -32,9 +32,9 @@ def add_change_log_link(release_notes: str) -> str: return release_notes + change_log_link -def get_release_notes() -> str: - with open(ROOT_DIR.joinpath("docs" , "release-notes.md"), "r+") as f: - if "--from_commits" not in ARGS: +def get_release_notes(from_commits: bool) -> str: + with open(ROOT_DIR.joinpath("docs", "release-notes.md"), "r+") as f: + if from_commits: return add_change_log_link(f.read()) new_commits_completed_process = subprocess.run( f"git log --oneline master..{BRANCH_NAME}", capture_output=True, text=True @@ -56,33 +56,77 @@ def publish_release(release_notes: str) -> None: def main() -> None: + parser = ArgumentParser(description="Release pipeline") + parser.add_argument( + "-fc", + "--from_commits", + action="store_true", + help="Generate release notes from commits", + ) + parser.add_argument( + "-sb", "--skip_bump", action="store_true", help="Skip bumping version" + ) + parser.add_argument( + "-sr", "--skip_ruff", action="store_true", help="Skip running ruff" + ) + parser.add_argument( + "-sbr", + "--skip_build_release", + action="store_true", + help="Skip building release", + ) + parser.add_argument( + "-smb", + "--skip_merge_branch", + action="store_true", + help="Skip merging branch", + ) + parser.add_argument( + "-spr", + "--skip_publish_release", + action="store_true", + help="Skip publishing release", + ) + parser.add_argument( + "-sp", "--skip_pypi", action="store_true", help="Skip publishing to PyPi" + ) + parser.add_argument( + "-sa", "--skip_announce", action="store_true", help="Skip announcing" + ) + parser.add_argument( + "-snb", + "--skip_new_branch", + action="store_true", + help="Skip creating new branch", + ) + parsed = parser.parse_args() if BRANCH_NAME == "master": log_error("On master branch, switch to version branch", True) - if "--skip_bump" not in ARGS: + if not parsed.skip_bumb: log_info("Bumping version") bump_version.main(True) - if "--skip_ruff" not in ARGS: + if not parsed.skip_ruff: ruff.main(True, True) - if "--skip_build" not in ARGS: - log_info("Generating release") - subprocess.run("poe generate_release_ddl").check_returncode() - release_notes = get_release_notes() - if "--skip_branch" not in ARGS: - log_info(f"Publishing branch {BRANCH_NAME}") - publish_branch() - if "--skip_release" not in ARGS: + if not parsed.skip_build_release: + log_info("Building release") + subprocess.run("poe build_release_ddl").check_returncode() + release_notes = get_release_notes(parsed.from_commits) + if not parsed.skip_merge_branch: + log_info(f"Merging branch {BRANCH_NAME}") + merge_branch() + if not parsed.skip_publish_release: log_info(f"Publishing release {BRANCH_NAME}") publish_release(release_notes) - if "--skip_pypi" not in ARGS: + if not parsed.skip_pypi: log_info("Publishing to PyPi") - subprocess.run("poetry publish") - if "--skip_announce" not in ARGS: + subprocess.run("poetry publish").check_returncode() + if not parsed.skip_announce: log_info("Announcing") announce.main( f"Version {bump_version.get_new_version()} is Out!", release_notes ) log_info(f"Finished release {BRANCH_NAME}") - if "--skip_new_branch" not in ARGS: + if not parsed.skip_new_branch: new_branch_name = input("Enter new branch name\n> ") if new_branch_name: subprocess.run(f"git checkout -b {new_branch_name}").check_returncode() diff --git a/scripts/ruff.py b/scripts/ruff.py index e4e3a94..54174a3 100644 --- a/scripts/ruff.py +++ b/scripts/ruff.py @@ -1,16 +1,21 @@ +from argparse import ArgumentParser import subprocess -from scripts.common import ARGS, ROOT_DIR, git_commit +from scripts.common import ROOT_DIR, git_commit def main( lint_fix=False, format=False, ) -> None: - if lint_fix or "--lint_fix" in ARGS: + parser = ArgumentParser("Run ruff on the project") + parser.add_argument("-f", "--format", action="store_true", help="Format the code") + parser.add_argument("-lf", "--lint_fix", action="store_true", help="Fix linting issues") + parsed = parser.parse_args() + if lint_fix or parsed.lint_fix: subprocess.run(f"ruff {ROOT_DIR} --fix").check_returncode() git_commit("Fix linting issues with ruff") return - if format or "--format" in ARGS: + if format or parsed.format: subprocess.run(f"ruff format {ROOT_DIR}").check_returncode() git_commit("Format with ruff") return diff --git a/scripts/run_exes.py b/scripts/run_exes.py index c14a002..ff63d18 100644 --- a/scripts/run_exes.py +++ b/scripts/run_exes.py @@ -1,8 +1,11 @@ import subprocess import time + from scripts.bump_version import log_error -from scripts.common import ARGS +from argparse import ArgumentParser +from scripts.common import join_from_local_appdata, join_from_py_scripts + def run_process(command: str) -> None: @@ -13,23 +16,40 @@ def run_process(command: str) -> None: log_error(f"Returncode {process.returncode} by {command}", True) + +def run_normal_install(app_name: str, args: str): + app_path = join_from_local_appdata("Senpwai", f"{app_name}.exe") + run_process(rf"{app_path} {args}") + + + + +def run_pip_install(app_name: str, args: str): + pip_path = join_from_py_scripts(app_name) + run_process(rf"{pip_path} {args}") + + def main() -> None: - all_ = "--all" in ARGS - if "--senpwai" in ARGS or all_: - run_process( - r"C:\Users\PC\AppData\Local\Programs\Senpwai\senpwai.exe --minimised_to_tray" - ) - if "--senpwai_pip" in ARGS or all_: - run_process( - r"C:\Users\PC\AppData\Local\Programs\Python\Python311\Scripts\senpwai.exe --minimised_to_tray" - ) - - if "--senpcli" in ARGS or all_: - run_process(r"C:\Users\PC\AppData\Local\Programs\Senpcli\senpcli.exe --version") - if "--senpcli_pip" in ARGS or all_: - run_process( - r"C:\Users\PC\AppData\Local\Programs\Python\Python311\Scripts\senpcli.exe --version" - ) + parser = ArgumentParser(description="Run executables") + parser.add_argument("-a", "--all", action="store_true", help="Run all executables") + parser.add_argument("-sw", "--senpwai", action="store_true", help="Run Senpwai") + parser.add_argument( + "-swp", "--senpwai_pip", action="store_true", help="Run Senpwai pip" + ) + parser.add_argument("-sc", "--senpcli", action="store_true", help="Run Senpcli") + parser.add_argument( + "-scp", "--senpcli_pip", action="store_true", help="Run Senpcli pip" + ) + parsed = parser.parse_args() + + if parsed.senpwai or parsed.all: + run_normal_install("senpwai", "--minimised_to_tray") + if parsed.senpwai_pip or parsed.all: + run_pip_install("senpwai", "--minimised_to_tray") + if parsed.senpcli or parsed.all: + run_normal_install("senpcli", "--version") + if parsed.senpcli_pip or parsed.all: + run_pip_install("senpcli", "--version") if __name__ == "__main__": diff --git a/scripts/setup.py b/scripts/setup.py index e7e8da6..838f0e4 100644 --- a/scripts/setup.py +++ b/scripts/setup.py @@ -70,9 +70,11 @@ def get_options(build_dir: str, assets_dir: str, senpcli_only: bool) -> dict: def main(): - senpcli_only = "--senpcli" in sys.argv - if senpcli_only: + try: sys.argv.remove("--senpcli") + senpcli_only = True + except ValueError: + senpcli_only = False senpwai_package_dir = ROOT_DIR.joinpath("senpwai") sys.path.append(str(senpwai_package_dir)) metadata = parse_metadata() From 4d6823c337cc11f3e38e6dc932224b5f4366fd94 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Thu, 25 Jul 2024 06:37:44 +0300 Subject: [PATCH 16/24] Minor fixes --- scripts/release.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 254d6ad..93ee294 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -32,8 +32,8 @@ def add_change_log_link(release_notes: str) -> str: prev_version = bump_version.get_prev_version() new_version = bump_version.get_new_version() if new_version == prev_version: - new_version = input("Failed to get prev version, manual input required\n> ") - if not new_version: + prev_version = input('Failed to get previous version number, manual input required (without the starting "v")\n> ') + if not prev_version: sys.exit() change_log_link = ( f"\n\n**Full Changelog**: {REPO_URL}/compare/v{prev_version}...v{new_version}" @@ -141,6 +141,7 @@ def main() -> None: if not parsed.skip_new_branch: new_branch_name = input("Enter new branch name\n> ") if new_branch_name: + subprocess.run("git checkout master").check_returncode() subprocess.run(f"git checkout -b {new_branch_name}").check_returncode() From 0caf464f81d17e9f3b6daa54e8416941a5df3a93 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Mon, 29 Jul 2024 03:59:46 +0300 Subject: [PATCH 17/24] Fix some gogo downloads randomly failing cause of improperly handled redirects --- docs/release-notes.md | 1 + senpwai/common/scraper.py | 16 ++++++++++----- senpwai/scrapers/gogo/main.py | 35 +++++++++++++++++---------------- senpwai/scrapers/pahe/main.py | 9 +++++---- senpwai/scrapers/test.py | 5 ++++- senpwai/senpcli/main.py | 19 +++++++++++------- senpwai/windows/chosen_anime.py | 6 +++--- senpwai/windows/download.py | 8 ++++++-- senpwai/windows/misc.py | 8 ++++---- 9 files changed, 64 insertions(+), 43 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5d23b0d..a7d461a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ - senpcli: Always show units/time instead of time/units in progress bars # Bug fixes +- Fix some gogo downloads randomly failing, if you still experience it be sure to report it - Fix failed to fetch direct download links notification on cancelling direct download links retrieval - Fix anime randomizer - Fix opening chosen anime window for other anime failing if previous attempt failed diff --git a/senpwai/common/scraper.py b/senpwai/common/scraper.py index 9ab17c7..61f631a 100644 --- a/senpwai/common/scraper.py +++ b/senpwai/common/scraper.py @@ -1,3 +1,4 @@ +from enum import Enum import os import re import subprocess @@ -244,20 +245,26 @@ def network_error_retry_wrapper( CLIENT = Client() +class AiringStatus(Enum): + ONGOING = "Ongoing" + UPCOMING = "Upcoming" + FINISHED = "Finished" + + class AnimeMetadata: def __init__( self, poster_url: str, summary: str, episode_count: int, - status: str, + airing_status: AiringStatus, genres: list[str], release_year: int, ): self.poster_url = poster_url self.summary = summary self.episode_count = episode_count - self.airing_status = status + self.airing_status = airing_status self.genres = genres self.release_year = release_year @@ -266,9 +273,7 @@ def get_poster_bytes(self) -> bytes: return response.content -def closest_quality_index( - potential_qualities: list[str], target_quality: str -) -> int: +def closest_quality_index(potential_qualities: list[str], target_quality: str) -> int: detected_qualities: list[tuple[int, int]] = [] target_quality = target_quality.replace("p", "") for idx, potential_quality in enumerate(potential_qualities): @@ -499,6 +504,7 @@ def hls_download(self) -> bool: def normal_download(self) -> bool: self.link_or_segment_urls = cast(str, self.link_or_segment_urls) + print(f"Downloading {self.link_or_segment_urls}") response = CLIENT.get( self.link_or_segment_urls, stream=True, timeout=30, cookies=self.cookies ) diff --git a/senpwai/scrapers/gogo/main.py b/senpwai/scrapers/gogo/main.py index 7806db1..ce15bde 100644 --- a/senpwai/scrapers/gogo/main.py +++ b/senpwai/scrapers/gogo/main.py @@ -9,6 +9,7 @@ IBYTES_TO_MBS_DIVISOR, PARSER, RESOURCE_MOVED_STATUS_CODES, + AiringStatus, AnimeMetadata, DomainNameError, ProgressFunction, @@ -97,10 +98,7 @@ def get_direct_download_links( ) qualities = [a.text for a in a_tags] idx = closest_quality_index(qualities, user_quality) - redirect_link = cast(str, a_tags[idx]["href"]) - link = CLIENT.get( - redirect_link, cookies=get_session_cookies() - ).headers.get("Location", redirect_link) + link = cast(str, a_tags[idx]["href"]) direct_download_links.append(link) self.resume.wait() if self.cancelled: @@ -118,10 +116,14 @@ def calculate_total_download_size( direct_download_links: list[str], progress_update_callback: Callable[[int], None] = lambda _: None, in_megabytes=False, - ) -> int: + ) -> tuple[int, list[str]]: total_size = 0 + redirect_ddls: list[str] = [] for link in direct_download_links: - response = CLIENT.get(link, stream=True, cookies=get_session_cookies()) + response = CLIENT.get( + link, stream=True, cookies=get_session_cookies(), allow_redirects=True + ) + redirect_ddls.append(response.url) size = response.headers.get("Content-Length", 0) if in_megabytes: total_size += round(int(size) / IBYTES_TO_MBS_DIVISOR) @@ -129,9 +131,9 @@ def calculate_total_download_size( total_size += int(size) self.resume.wait() if self.cancelled: - return 0 + return 0, [*redirect_ddls, *direct_download_links[len(redirect_ddls)-1 :]] progress_update_callback(1) - return total_size + return total_size, redirect_ddls def get_anime_page_content(anime_page_link: str) -> tuple[bytes, str]: @@ -140,17 +142,17 @@ def get_anime_page_content(anime_page_link: str) -> tuple[bytes, str]: try: response = CLIENT.get( anime_page_link, + allow_redirects=True, exceptions_to_raise=(DomainNameError, KeyboardInterrupt), ) - if response.status_code not in RESOURCE_MOVED_STATUS_CODES: + if not response.history: return response.content, anime_page_link - new_anime_page_link = response.headers.get("Location", anime_page_link) # The url in location seems to be in http instead of https but the http one doesn't work - new_anime_page_link = new_anime_page_link.replace("http://", "https://") + new_anime_page_link = response.url.replace("http://", "https://") # If the link is not different we assume they changed their domain name but didn't pass # the link with the new one in the location headers if new_anime_page_link == anime_page_link: - raise DomainNameError(Exception("Received resource moved status code")) + raise DomainNameError(Exception("Redirected but provided the same domain")) match = cast(re.Match[str], BASE_URL_REGEX.search(new_anime_page_link)) GOGO_HOME_URL = match.group(1) return get_anime_page_content(new_anime_page_link) @@ -191,14 +193,13 @@ def extract_anime_metadata(anime_page_content: bytes) -> AnimeMetadata: ) tag = soup.find("a", title="Ongoing Anime") if tag: - status = "ONGOING" + airing_status = AiringStatus.ONGOING elif episode_count == 0: - status = "UPCOMING" + airing_status = AiringStatus.UPCOMING else: - status = "FINISHED" - + airing_status = AiringStatus.FINISHED return AnimeMetadata( - poster_link, summary, episode_count, status, genres, release_year + poster_link, summary, episode_count, airing_status, genres, release_year ) diff --git a/senpwai/scrapers/pahe/main.py b/senpwai/scrapers/pahe/main.py index f7c161b..4548a3e 100644 --- a/senpwai/scrapers/pahe/main.py +++ b/senpwai/scrapers/pahe/main.py @@ -6,6 +6,7 @@ from senpwai.common.scraper import ( CLIENT, PARSER, + AiringStatus, AnimeMetadata, DomainNameError, ProgressFunction, @@ -363,11 +364,11 @@ def get_anime_metadata(anime_id: str) -> AnimeMetadata: episode_count = decoded["total"] tag = soup.find(title="Currently Airing") if tag: - status = "ONGOING" + airing_status = AiringStatus.ONGOING elif episode_count == 0: - status = "UPCOMING" + airing_status = AiringStatus.UPCOMING else: - status = "FINISHED" + airing_status = AiringStatus.FINISHED return AnimeMetadata( - poster_url, summary, episode_count, status, genres, int(release_year) + poster_url, summary, episode_count, airing_status, genres, int(release_year) ) diff --git a/senpwai/scrapers/test.py b/senpwai/scrapers/test.py index cc1715e..88fca00 100644 --- a/senpwai/scrapers/test.py +++ b/senpwai/scrapers/test.py @@ -708,7 +708,10 @@ def print_metadata(metadata: AnimeMetadata): test_name = "Download size" test_start(test_name) runtime_getter = get_run_time_later() - total_download_size = gogo.CalculateTotalDowloadSize().calculate_total_download_size( + ( + total_download_size, + direct_download_links, + ) = gogo.CalculateTotalDowloadSize().calculate_total_download_size( direct_download_links, in_megabytes=True ) rt = runtime_getter() diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index d554148..f69700c 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -19,6 +19,7 @@ ) from senpwai.common.scraper import ( IBYTES_TO_MBS_DIVISOR, + AiringStatus, Download, ffmpeg_is_installed, fuzz_str, @@ -369,13 +370,13 @@ def gogo_get_download_page_links( return gogo.get_download_page_links(start_episode, end_episode, anime_id) -def gogo_calculate_total_download_size(direct_download_links: list[str]) -> None: +def gogo_calculate_total_download_size(direct_download_links: list[str]) -> list[str]: pbar = ProgressBar( total=len(direct_download_links), desc="Calculating total download size", unit="eps", ) - total = gogo.CalculateTotalDowloadSize().calculate_total_download_size( + total, redirect_ddls = gogo.CalculateTotalDowloadSize().calculate_total_download_size( direct_download_links, pbar.update_ ) pbar.close_() @@ -384,6 +385,7 @@ def gogo_calculate_total_download_size(direct_download_links: list[str]) -> None f"{size} MB { ', go shower' if size >= 1000 else ''}", Color.MAGENTA ) print_info(f"Total download size: {size_text}") + return redirect_ddls def gogo_get_direct_download_links( @@ -416,7 +418,7 @@ def pahe_get_direct_download_links(download_page_links: list[str]) -> list[str]: def create_progress_bar( episode_title: str, link_or_segs_urls: str | list[str], is_hls_download: bool -) -> ProgressBar: +) -> tuple[ProgressBar, str | list[str]]: if is_hls_download: episode_size = len(link_or_segs_urls) pbar = ProgressBar( @@ -425,14 +427,14 @@ def create_progress_bar( desc=f"Downloading [HLS] {episode_title}", ) else: - episode_size, _ = Download.get_resource_length(cast(str, link_or_segs_urls)) + episode_size, link_or_segs_urls = Download.get_resource_length(cast(str, link_or_segs_urls)) pbar = ProgressBar( total=episode_size, unit="iB", unit_scale=True, desc=f"Downloading {episode_title}", ) - return pbar + return pbar, link_or_segs_urls def download_thread( @@ -492,7 +494,7 @@ def wait(event: Event): for idx, link in enumerate(ddls_or_segs_urls): wait(download_slot_available) episode_title = anime_details.episode_title(idx) - pbar = create_progress_bar(episode_title, link, is_hls_download) + pbar, link = create_progress_bar(episode_title, link, is_hls_download) Thread( target=download_thread, args=( @@ -649,7 +651,7 @@ def handle_gogo(parsed: Namespace, anime_details: AnimeDetails): download_page_links, parsed.quality ) if not parsed.skip_calculating: - gogo_calculate_total_download_size(direct_download_links) + direct_download_links = gogo_calculate_total_download_size(direct_download_links) download_manager( direct_download_links, anime_details, @@ -726,6 +728,9 @@ def get_anime_details(parsed) -> AnimeDetails | None: if not dub_available: return None anime_details = AnimeDetails(anime, parsed.site) + if anime_details.metadata.airing_status == AiringStatus.UPCOMING: + print_error("No episodes out yet, anime is an upcoming release") + return None if parsed.sub_or_dub == DUB: if not anime_details.dub_available: print_error("Dub not available for this anime") diff --git a/senpwai/windows/chosen_anime.py b/senpwai/windows/chosen_anime.py index 0adde6e..2241b06 100644 --- a/senpwai/windows/chosen_anime.py +++ b/senpwai/windows/chosen_anime.py @@ -11,7 +11,7 @@ QWidget, ) from senpwai.common.classes import SETTINGS, Anime, AnimeDetails -from senpwai.common.scraper import lacked_episode_numbers +from senpwai.common.scraper import AiringStatus, lacked_episode_numbers from senpwai.common.static import ( CHOSEN_ANIME_WINDOW_BCKG_IMAGE_PATH, DUB, @@ -119,7 +119,7 @@ def __init__(self, main_window: "MainWindow", anime_details: AnimeDetails): set_minimum_size_policy(release_year) bottom_top_layout.addWidget(release_year) airing_status = StyledLabel(None, 21, "blue") - airing_status.setText(anime_details.metadata.airing_status) + airing_status.setText(anime_details.metadata.airing_status.value) set_minimum_size_policy(airing_status) bottom_top_layout.addWidget(airing_status) self.episode_count = EpisodeCount(str(self.anime_details.episode_count)) @@ -234,7 +234,7 @@ def __init__(self, main_window: "MainWindow", anime_details: AnimeDetails): else "1" ) input_size = QSize(80, 40) - if anime_details.metadata.airing_status != "UPCOMING": + if anime_details.metadata.airing_status != AiringStatus.UPCOMING: self.start_episode_input = NumberInput(21) self.start_episode_input.setFixedSize(input_size) self.start_episode_input.setPlaceholderText("START") diff --git a/senpwai/windows/download.py b/senpwai/windows/download.py index 5c0edb4..a087e53 100644 --- a/senpwai/windows/download.py +++ b/senpwai/windows/download.py @@ -15,6 +15,7 @@ from senpwai.common.classes import SETTINGS, Anime, AnimeDetails from senpwai.common.scraper import ( IBYTES_TO_MBS_DIVISOR, + AiringStatus, Download, NoResourceLengthException, ProgressFunction, @@ -1307,7 +1308,10 @@ def run(self): obj = gogo.CalculateTotalDowloadSize() self.progress_bar.pause_callback = obj.pause_or_resume self.progress_bar.cancel_callback = obj.cancel - self.anime_details.total_download_size = obj.calculate_total_download_size( + ( + self.anime_details.total_download_size, + self.anime_details.ddls_or_segs_urls, + ) = obj.calculate_total_download_size( cast(list[str], self.anime_details.ddls_or_segs_urls), lambda x: self.update_bar.emit(x), True, @@ -1365,7 +1369,7 @@ def run(self): ) if not anime_details.lacked_episode_numbers: haved_end = anime_details.haved_end - if anime_details.metadata.airing_status == "FINISHED" and ( + if anime_details.metadata.airing_status == AiringStatus.FINISHED and ( haved_end and haved_end >= anime_details.episode_count ): self.download_window.main_window.settings_window.tracked_anime.remove_anime( diff --git a/senpwai/windows/misc.py b/senpwai/windows/misc.py index 4d12400..fa15273 100644 --- a/senpwai/windows/misc.py +++ b/senpwai/windows/misc.py @@ -270,10 +270,10 @@ def __init__( self.file_name = file_name def run(self): - response = CLIENT.get(self.download_url, stream=True) - if response.status_code in RESOURCE_MOVED_STATUS_CODES: - self.download_url = response.headers["Location"] - response = CLIENT.get(self.download_url, stream=True) + response = CLIENT.get( + self.download_url, stream=True, allow_redirects=True + ) + self.download_url = response.url total_size = int(response.headers["Content-Length"]) self.total_size.emit(total_size) self.update_window.progress_bar From 10471b5de7244b2b3c927eb878f4628c2d1e975d Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Mon, 29 Jul 2024 23:55:45 +0300 Subject: [PATCH 18/24] Add anime tracking functionality to senpcli --- docs/release-notes.md | 1 + senpwai/common/scraper.py | 9 +++- senpwai/common/tracker.py | 84 +++++++++++++++++++++++++++++ senpwai/main.py | 2 +- senpwai/senpcli/main.py | 104 ++++++++++++++++++++++++++++-------- senpwai/windows/download.py | 104 +++++++++--------------------------- senpwai/windows/search.py | 2 +- 7 files changed, 203 insertions(+), 103 deletions(-) create mode 100644 senpwai/common/tracker.py diff --git a/docs/release-notes.md b/docs/release-notes.md index a7d461a..4a9da42 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ - Shorten long anime titles in progress bars - Hover on folder button shows folder location - senpcli: Always show units/time instead of time/units in progress bars +- senpcli: Add anime tracking functionality # Bug fixes - Fix some gogo downloads randomly failing, if you still experience it be sure to report it diff --git a/senpwai/common/scraper.py b/senpwai/common/scraper.py index 61f631a..9530d62 100644 --- a/senpwai/common/scraper.py +++ b/senpwai/common/scraper.py @@ -250,6 +250,14 @@ class AiringStatus(Enum): UPCOMING = "Upcoming" FINISHED = "Finished" + # Stack Overflow answer link: https://stackoverflow.com/a/66575463/17193072 + # Enums in python suck so bad + def __eq__(self, other: object) -> bool: + if type(self).__qualname__ != type(other).__qualname__: + return False + other = cast(AiringStatus, other) + return self.value == other.value + class AnimeMetadata: def __init__( @@ -504,7 +512,6 @@ def hls_download(self) -> bool: def normal_download(self) -> bool: self.link_or_segment_urls = cast(str, self.link_or_segment_urls) - print(f"Downloading {self.link_or_segment_urls}") response = CLIENT.get( self.link_or_segment_urls, stream=True, timeout=30, cookies=self.cookies ) diff --git a/senpwai/common/tracker.py b/senpwai/common/tracker.py new file mode 100644 index 0000000..75bc015 --- /dev/null +++ b/senpwai/common/tracker.py @@ -0,0 +1,84 @@ +from common.scraper import AiringStatus, lacked_episode_numbers, sanitise_title +from common.static import DUB, GOGO, PAHE +from senpwai.common.classes import SETTINGS, Anime, AnimeDetails +from senpwai.scrapers import pahe, gogo +from typing import Callable + + +def check_for_new_episodes( + removed_tracked_callback: Callable[[str], None], + finished_tracking_callback: Callable[[str], None], + no_dub_callback: Callable[[str], None], + start_download_callback: Callable[[AnimeDetails], None], + queued_new_episodes_callback: Callable[[str], None], + start_downloading_immediately: bool, +) -> None: + queued: list[str] = [] + all_anime_details: list[AnimeDetails] = [] + for title in SETTINGS.tracked_anime: + anime: Anime + site = SETTINGS.auto_download_site + if site == PAHE: + result = pahe_fetch_anime_obj(title) + if not result: + result = gogo_fetch_anime_obj(title) + if not result: + continue + site = GOGO + else: + result = gogo_fetch_anime_obj(title) + if not result: + result = pahe_fetch_anime_obj(title) + if not result: + continue + site = PAHE + anime = result + anime_details = AnimeDetails(anime, site) + start_eps = anime_details.haved_end if anime_details.haved_end else 1 + anime_details.lacked_episode_numbers = lacked_episode_numbers( + start_eps, anime_details.episode_count, anime_details.haved_episodes + ) + if not anime_details.lacked_episode_numbers: + haved_end = anime_details.haved_end + if anime_details.metadata.airing_status == AiringStatus.FINISHED and ( + haved_end and (haved_end >= anime_details.episode_count) + ): + removed_tracked_callback(anime_details.anime.title) + finished_tracking_callback(anime_details.sanitised_title) + continue + if anime_details.sub_or_dub == DUB and not anime_details.dub_available: + no_dub_callback(anime_details.sanitised_title) + continue + queued.append(anime_details.sanitised_title) + if start_downloading_immediately: + start_download_callback(anime_details) + all_anime_details.append(anime_details) + if queued: + all_str = ", ".join(queued) + queued_new_episodes_callback(all_str) + if not start_downloading_immediately: + for anime_details in all_anime_details: + start_download_callback(anime_details) + + +def pahe_fetch_anime_obj(title: str) -> Anime | None: + results = pahe.search(title) + for result in results: + res_title, page_link, anime_id = pahe.extract_anime_title_page_link_and_id( + result + ) + if sanitise_title(res_title.lower(), True) == sanitise_title( + title.lower(), True + ): + return Anime(title, page_link, anime_id) + return None + + +def gogo_fetch_anime_obj(title: str) -> Anime | None: + results = gogo.search(title) + for res_title, page_link in results: + if sanitise_title(res_title.lower(), True) == sanitise_title( + title.lower(), True + ): + return Anime(title, page_link, None) + return None diff --git a/senpwai/main.py b/senpwai/main.py index c60ec75..0c0fbcb 100644 --- a/senpwai/main.py +++ b/senpwai/main.py @@ -12,7 +12,7 @@ def windows_set_app_user_model_id(): # Change App ID to ensure task bar icon is Swnpwai's icon instead of Python for pip installs - # StackOverflow Answer link: https://stackoverflow.com/questions/1551605/how-to-set-applications-taskbar-icon-in-windows-7/1552105#1552105 + # Stack Overflow answer link: https://stackoverflow.com/questions/1551605/how-to-set-applications-taskbar-icon-in-windows-7/1552105#1552105 ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APP_NAME) diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index f69700c..021486a 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -8,6 +8,7 @@ from threading import Event, Lock, Thread from typing import Callable, cast +from common.tracker import check_for_new_episodes from tqdm import tqdm from senpwai.common.classes import ( @@ -245,6 +246,22 @@ def parse_args(args: list[str]) -> tuple[Namespace, ArgumentParser]: help="Maximum number of simultaneous downloads", default=SETTINGS.max_simultaneous_downloads, ) + parser.add_argument( + "-cta", + "--check_tracked_anime", + help="Check tracked anime for new episodes then auto download", + action="store_true", + ) + parser.add_argument( + "-ata", + "--add_tracked_anime", + help="Add an anime to the tracked anime list", + ) + parser.add_argument( + "-rta", + "--remove_tracked_anime", + help="Remove an anime from the tracked anime list", + ) return parser.parse_args(args), parser @@ -376,7 +393,10 @@ def gogo_calculate_total_download_size(direct_download_links: list[str]) -> list desc="Calculating total download size", unit="eps", ) - total, redirect_ddls = gogo.CalculateTotalDowloadSize().calculate_total_download_size( + ( + total, + redirect_ddls, + ) = gogo.CalculateTotalDowloadSize().calculate_total_download_size( direct_download_links, pbar.update_ ) pbar.close_() @@ -427,7 +447,9 @@ def create_progress_bar( desc=f"Downloading [HLS] {episode_title}", ) else: - episode_size, link_or_segs_urls = Download.get_resource_length(cast(str, link_or_segs_urls)) + episode_size, link_or_segs_urls = Download.get_resource_length( + cast(str, link_or_segs_urls) + ) pbar = ProgressBar( total=episode_size, unit="iB", @@ -651,7 +673,9 @@ def handle_gogo(parsed: Namespace, anime_details: AnimeDetails): download_page_links, parsed.quality ) if not parsed.skip_calculating: - direct_download_links = gogo_calculate_total_download_size(direct_download_links) + direct_download_links = gogo_calculate_total_download_size( + direct_download_links + ) download_manager( direct_download_links, anime_details, @@ -719,6 +743,18 @@ def start_update_check_thread() -> tuple[Thread, Queue[UpdateInfo]]: return update_check_thread, update_check_result_queue +def finish_update_check( + update_check_thread: Thread, + update_check_result_queue: Queue[UpdateInfo], + print_msg=False, +) -> None: + update_check_thread.join() + update_info = update_check_result_queue.get() + if print_msg and not update_info.is_update_available: + print("No update available, already at latest version") + handle_update_check_result(update_info) + + def get_anime_details(parsed) -> AnimeDetails | None: anime = search(parsed.title, parsed.site) if anime is None: @@ -821,30 +857,54 @@ def main(): with open(SETTINGS.settings_json_path) as f: contents = f.read() print(f"{contents}\n\n{SETTINGS.settings_json_path}") - return elif parsed.update: - update_check_thread, update_check_result_queue = start_update_check_thread() - update_check_thread.join() - update_info = update_check_result_queue.get() - if not update_info.is_update_available: - print("No update available, already at latest version") - handle_update_check_result(update_info) - return + update_params = start_update_check_thread() + finish_update_check(*update_params, True) + elif parsed.check_tracked_anime: + + def start_download_callback(anime_details: AnimeDetails) -> None: + parsed.start_episode = anime_details.haved_end + parsed.end_episode = anime_details.metadata.episode_count + parsed.site = anime_details.site + initiate_download_pipeline(parsed, anime_details) + + update_params = start_update_check_thread() + check_for_new_episodes( + lambda title: SETTINGS.remove_tracked_anime(title), + lambda sanitised_title: print_info( + f"Finished tracking {sanitised_title}" + ), + lambda sanitised_title: print_error( + f"No dub available for {sanitised_title}" + ), + start_download_callback, + lambda titles: print_info(f"Queued new episodes of: {titles}"), + False, + ) + finish_update_check(*update_params) + elif parsed.remove_tracked_anime: + try: + SETTINGS.remove_tracked_anime(parsed.remove_tracked_anime) + except ValueError: + print_error("Anime not found in tracked list") + elif parsed.add_tracked_anime: + if parsed.add_tracked_anime in SETTINGS.tracked_anime: + print_error("Anime already being tracked") + return + SETTINGS.add_tracked_anime(parsed.add_tracked_anime) elif parsed.title is None: print( f"{parser.format_usage()}senpcli: error: the following arguments are required: title" ) - return - if not validate_args(parsed): - return - update_check_thread, update_check_result_queue = start_update_check_thread() - anime_details = get_anime_details(parsed) - if anime_details is None: - return - initiate_download_pipeline(parsed, anime_details) - update_check_thread.join() - update_info = update_check_result_queue.get() - handle_update_check_result(update_info) + else: + if not validate_args(parsed): + return + update_params = start_update_check_thread() + anime_details = get_anime_details(parsed) + if anime_details is None: + return + initiate_download_pipeline(parsed, anime_details) + finish_update_check(*update_params) except KeyboardInterrupt: ProgressBar.cancel_all_active() diff --git a/senpwai/windows/download.py b/senpwai/windows/download.py index a087e53..831c901 100644 --- a/senpwai/windows/download.py +++ b/senpwai/windows/download.py @@ -11,6 +11,7 @@ QVBoxLayout, QWidget, ) +from common.tracker import check_for_new_episodes from senpwai.scrapers import gogo, pahe from senpwai.common.classes import SETTINGS, Anime, AnimeDetails from senpwai.common.scraper import ( @@ -208,7 +209,7 @@ def __init__( label = StyledLabel(font_size=14) self.anime_details = anime_details self.progress_bar = progress_bar - label.setText(anime_details.anime.title) + label.setText(anime_details.sanitised_title) set_minimum_size_policy(label) self.main_layout = QHBoxLayout() self.up_button = IconButton(download_queue.up_icon, 1.1, self) @@ -620,7 +621,7 @@ def queue_download(self, anime_details: AnimeDetails): anime_progress_bar = ProgressBarWithoutButtons( self, "Downloading[HLS]", - anime_details.anime.title, + anime_details.sanitised_title, total_segments, "segs", 1, @@ -630,7 +631,7 @@ def queue_download(self, anime_details: AnimeDetails): anime_progress_bar = ProgressBarWithoutButtons( self, "Downloading", - anime_details.anime.title, + anime_details.sanitised_title, len(anime_details.ddls_or_segs_urls), "eps", 1, @@ -1277,7 +1278,7 @@ def run(self): ) self.download_window.main_window.tray_icon.make_notification( "Error", - f"Failed to retrieve some {link_name} links for {self.anime_details.anime.title}", + f"Failed to retrieve some {link_name} links for {self.anime_details.sanitised_title}", False, None, ) @@ -1343,81 +1344,28 @@ def __init__( ) def run(self): - queued: list[str] = [] - for title in self.anime_titles: - anime: Anime - site = SETTINGS.auto_download_site - if site == PAHE: - result = self.pahe_fetch_anime_obj(title) - if not result: - result = self.gogo_fetch_anime_obj(title) - if not result: - continue - site = GOGO - else: - result = self.gogo_fetch_anime_obj(title) - if not result: - result = self.pahe_fetch_anime_obj(title) - if not result: - continue - site = PAHE - anime = result - anime_details = AnimeDetails(anime, site) - start_eps = anime_details.haved_end if anime_details.haved_end else 1 - anime_details.lacked_episode_numbers = lacked_episode_numbers( - start_eps, anime_details.episode_count, anime_details.haved_episodes - ) - if not anime_details.lacked_episode_numbers: - haved_end = anime_details.haved_end - if anime_details.metadata.airing_status == AiringStatus.FINISHED and ( - haved_end and haved_end >= anime_details.episode_count - ): - self.download_window.main_window.settings_window.tracked_anime.remove_anime( - anime_details.anime.title - ) - self.download_window.main_window.tray_icon.make_notification( - "Finished Tracking", - f"You have the final episode of {title} and it has finished airing so I have removed it from your tracking list", - True, - ) - continue - if anime_details.sub_or_dub == DUB and not anime_details.dub_available: - self.download_window.main_window.tray_icon.make_notification( - "Error", - f"Failed to find dub for {anime_details.anime.title}", - False, - None, - ) - continue - queued.append(anime_details.anime.title) - self.initate_download_pipeline.emit(anime_details) - if queued: - all_str = ", ".join(queued) - self.download_window.main_window.tray_icon.make_notification( + check_for_new_episodes( + lambda title: self.download_window.main_window.settings_window.tracked_anime.remove_anime( + title + ), + lambda sanitised_title: self.download_window.main_window.tray_icon.make_notification( + "Finished Tracking", + f"You have the final episode of {sanitised_title} and it has finished airing so I have removed it from your tracking list", + True, + ), + lambda sanitised_title: self.download_window.main_window.tray_icon.make_notification( + "Error", + f"Failed to find dub for {sanitised_title}", + False, + None, + ), + lambda anime_details: self.initate_download_pipeline.emit(anime_details), + lambda queued_anime_titles: self.download_window.main_window.tray_icon.make_notification( "Queued new episodes", - all_str, + queued_anime_titles, False, self.download_window.main_window.switch_to_download_window, - ) + ), + True, + ) self.clean_out_auto_download_thread_signal.emit() - - def pahe_fetch_anime_obj(self, title: str) -> Anime | None: - results = pahe.search(title) - for result in results: - res_title, page_link, anime_id = pahe.extract_anime_title_page_link_and_id( - result - ) - if sanitise_title(res_title.lower(), True) == sanitise_title( - title.lower(), True - ): - return Anime(title, page_link, anime_id) - return None - - def gogo_fetch_anime_obj(self, title: str) -> Anime | None: - results = gogo.search(title) - for res_title, page_link in results: - if sanitise_title(res_title.lower(), True) == sanitise_title( - title.lower(), True - ): - return Anime(title, page_link, None) - return None diff --git a/senpwai/windows/search.py b/senpwai/windows/search.py index 53e2c19..c7cf15e 100644 --- a/senpwai/windows/search.py +++ b/senpwai/windows/search.py @@ -124,7 +124,7 @@ def __init__(self, main_window: "MainWindow"): self.setLayout(self.full_layout) # We use a timer instead of calling setFocus normally cause apparently Qt wont really set the widget in focus if the widget isn't shown on screen, # So we gotta wait a bit first till the UI is rendered. - # StackOverflow Comment link: https://stackoverflow.com/questions/52853701/set-focus-on-button-in-app-with-group-boxes#comment92652037_52858926 + # Stack Overflow comment link: https://stackoverflow.com/questions/52853701/set-focus-on-button-in-app-with-group-boxes#comment92652037_52858926 QTimer.singleShot(0, self.search_bar.setFocus) # Qt pushes the horizontal scroll bar to the center automatically sometimes From 5c7c6a05bc1a8b45b7eec63db728f7a644194b01 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 30 Jul 2024 10:06:53 +0300 Subject: [PATCH 19/24] senpcli: Add option to print direct download links instead of downloading #47 --- docs/release-notes.md | 1 + senpwai/senpcli/main.py | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4a9da42..c94ccf1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ - Hover on folder button shows folder location - senpcli: Always show units/time instead of time/units in progress bars - senpcli: Add anime tracking functionality +- senpcli: Add option to print direct download links instead of downloading # Bug fixes - Fix some gogo downloads randomly failing, if you still experience it be sure to report it diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index 021486a..080cb19 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -2,7 +2,7 @@ import subprocess import sys from argparse import ArgumentParser, Namespace -from os import path +import os from queue import Queue from random import choice as random_choice from threading import Event, Lock, Thread @@ -52,7 +52,7 @@ from enum import Enum APP_NAME = "Senpcli" -SENPWAI_IS_INSTALLED = path.isfile(SENPWAI_EXE_PATH) +SENPWAI_IS_INSTALLED = os.path.isfile(SENPWAI_EXE_PATH) APP_NAME_LOWER = "senpcli" DESCRIPTION = "The CLI alternative for Senpwai" ASCII_APP_NAME = r""" @@ -262,6 +262,12 @@ def parse_args(args: list[str]) -> tuple[Namespace, ArgumentParser]: "--remove_tracked_anime", help="Remove an anime from the tracked anime list", ) + parser.add_argument( + "-ddl", + "--direct_download_links", + help="Print direct download links instead of downloading", + action="store_true", + ) return parser.parse_args(args), parser @@ -630,6 +636,9 @@ def handle_pahe(parsed: Namespace, anime_details: AnimeDetails): episode_page_links, parsed.quality, parsed.sub_or_dub ) direct_download_links = pahe_get_direct_download_links(download_page_links) + if parsed.direct_download_links: + print("\n".join(direct_download_links)) + return download_manager( direct_download_links, anime_details, False, parsed.max_simultaneous_downloads ) @@ -654,6 +663,9 @@ def handle_gogo(parsed: Namespace, anime_details: AnimeDetails): matched_quality_links = gogo_get_hls_matched_quality_links( hls_links, parsed.quality ) + if parsed.direct_download_links: + print("\n".join(hls_links)) + return hls_segments_urls = gogo_get_hls_segments_urls(matched_quality_links) download_manager( hls_segments_urls, @@ -672,6 +684,9 @@ def handle_gogo(parsed: Namespace, anime_details: AnimeDetails): direct_download_links = gogo_get_direct_download_links( download_page_links, parsed.quality ) + if parsed.direct_download_links: + print("\n".join(direct_download_links)) + return if not parsed.skip_calculating: direct_download_links = gogo_calculate_total_download_size( direct_download_links @@ -702,12 +717,12 @@ def download_and_install_update( unit="iB", unit_scale=True, ) - file_name_no_ext, file_ext = path.splitext(file_name) + file_name_no_ext, file_ext = os.path.splitext(file_name) tempdir = senpwai_tempdir() download = Download(download_url, file_name_no_ext, tempdir, pbar.update_, file_ext) download.start_download() pbar.close_() - subprocess.Popen([path.join(tempdir, file_name), "/silent", "/update"]) + subprocess.Popen([os.path.join(tempdir, file_name), "/silent", "/update"]) def handle_update_check_result(update_info: UpdateInfo) -> None: @@ -791,7 +806,8 @@ def get_anime_details(parsed) -> AnimeDetails | None: def initiate_download_pipeline(parsed: Namespace, anime_details: AnimeDetails): - print_info(f"Downloading to: {anime_details.anime_folder_path}") + if not parsed.direct_download_links: + print_info(f"Downloading to: {anime_details.anime_folder_path}") if parsed.site == PAHE: handle_pahe(parsed, anime_details) else: From 06bad1c30bdd84eb7e3aa799d6e994e3d4668b78 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 30 Jul 2024 11:31:31 +0300 Subject: [PATCH 20/24] Improve tray icon menu UX #48. General QoL changes and deprecations --- senpwai/common/classes.py | 49 ++++++++++++++-------- senpwai/common/tracker.py | 2 +- senpwai/main.py | 5 +-- senpwai/senpcli/main.py | 6 +-- senpwai/windows/about.py | 2 +- senpwai/windows/download.py | 42 +++++++++---------- senpwai/windows/main.py | 34 ++++++++------- senpwai/windows/settings.py | 84 ++++++++++++++++++++++--------------- 8 files changed, 127 insertions(+), 97 deletions(-) diff --git a/senpwai/common/classes.py b/senpwai/common/classes.py index 675053e..cc8779d 100644 --- a/senpwai/common/classes.py +++ b/senpwai/common/classes.py @@ -71,9 +71,15 @@ class Settings: def __init__(self) -> None: self.config_dir = self.setup_config_dir() self.settings_json_path = os.path.join(self.config_dir, "settings.json") + # NOTE: Everytime you add a new class member that isn't a setting, make sure to add it here + self.excluded_in_save = ( + "config_dir", + "settings_json_path", + "is_update_install", + "excluded_in_save", + ) # Default settings # Only these settings will be saved to settings.json - # NOTE: Everytime you add a new class member that isn't a setting, make sure to update the Settings.dict_settings() method self.sub_or_dub = SUB self.quality = Q_720 self.download_folder_paths = self.setup_default_download_folder() @@ -82,10 +88,10 @@ def __init__(self) -> None: self.allow_notifications = True self.start_maximized = True self.run_on_startup = False - self.gogo_norm_or_hls_mode = GOGO_NORM_MODE + self.gogo_mode = GOGO_NORM_MODE self.tracked_anime: list[str] = [] - self.auto_download_site = PAHE - self.check_for_new_eps_after = 24 + self.tracking_site = PAHE + self.tracking_interval = 24 self.gogo_skip_calculate = False self.version = VERSION @@ -121,11 +127,23 @@ def load_settings(self) -> None: try: with open(self.settings_json_path, "r") as f: settings = cast(dict, json.load(f)) - # TODO: DEPRECATION: Remove in version 2.1.11+ cause we use start_maximized now + # TODO: DEPRECATIONs: Remove start_in_fullscreen = settings.get("start_in_fullscreen", None) if start_in_fullscreen is not None: self.start_maximized = start_in_fullscreen settings.pop("start_in_fullscreen") + auto_download_site = settings.get("auto_download_site", None) + if auto_download_site is not None: + self.tracking_site = auto_download_site + settings.pop("auto_download_site") + check_for_new_eps_after = settings.get("check_for_new_eps_after", None) + if check_for_new_eps_after is not None: + self.tracking_interval = check_for_new_eps_after + settings.pop("check_for_new_eps_after") + gogo_norm_or_hls_mode = settings.get("gogo_norm_or_hls_mode", None) + if gogo_norm_or_hls_mode is not None: + self.gogo_mode = gogo_norm_or_hls_mode + settings.pop("gogo_norm_or_hls_mode") self.__dict__.update(settings) except json.JSONDecodeError: pass @@ -140,10 +158,7 @@ def setup_default_download_folder(self) -> list[str]: def dict_settings(self) -> dict: return { - k: v - for k, v in self.__dict__.items() - # NOTE: Everytime you add a new class member that isn't a setting, make sure to include it here - if k not in ("config_dir", "settings_json_path", "is_update_install") + k: v for k, v in self.__dict__.items() if k not in self.excluded_in_save } def update_sub_or_dub(self, sub_or_dub: str) -> None: @@ -192,8 +207,8 @@ def update_run_on_startup(self, run_on_startup: bool) -> None: self.run_on_startup = run_on_startup self.save_settings() - def update_gogo_norm_or_hls_mode(self, gogo_norm_or_hls_mode: str) -> None: - self.gogo_norm_or_hls_mode = gogo_norm_or_hls_mode + def update_gogo_mode(self, gogo_mode: str) -> None: + self.gogo_mode = gogo_mode self.save_settings() def update_tracked_anime(self, tracked_anime: list[str]) -> None: @@ -208,12 +223,12 @@ def add_tracked_anime(self, anime_name: str) -> None: self.tracked_anime.append(anime_name) self.save_settings() - def update_auto_download_site(self, auto_download_site: str) -> None: - self.auto_download_site = auto_download_site + def update_tracking_site(self, auto_download_site: str) -> None: + self.tracking_site = auto_download_site self.save_settings() - def update_check_for_new_eps_after(self, check_for_new_eps_after: int) -> None: - self.check_for_new_eps_after = check_for_new_eps_after + def update_tracking_interval(self, tracking_interval: int) -> None: + self.tracking_interval = tracking_interval self.save_settings() def update_gogo_skip_calculate(self, gogo_skip_calculate: bool) -> None: @@ -248,9 +263,7 @@ def __init__(self, anime: Anime, site: str) -> None: self.anime = anime self.site = site self.is_hls_download = ( - True - if site == GOGO and SETTINGS.gogo_norm_or_hls_mode == GOGO_HLS_MODE - else False + True if site == GOGO and SETTINGS.gogo_mode == GOGO_HLS_MODE else False ) self.sanitised_title = sanitise_title(anime.title) self.shortened_title = self.get_shortened_title() diff --git a/senpwai/common/tracker.py b/senpwai/common/tracker.py index 75bc015..6246918 100644 --- a/senpwai/common/tracker.py +++ b/senpwai/common/tracker.py @@ -17,7 +17,7 @@ def check_for_new_episodes( all_anime_details: list[AnimeDetails] = [] for title in SETTINGS.tracked_anime: anime: Anime - site = SETTINGS.auto_download_site + site = SETTINGS.tracking_site if site == PAHE: result = pahe_fetch_anime_obj(title) if not result: diff --git a/senpwai/main.py b/senpwai/main.py index 0c0fbcb..3111b5b 100644 --- a/senpwai/main.py +++ b/senpwai/main.py @@ -39,15 +39,14 @@ def init(): def main(): init() QCoreApplication.setApplicationName(APP_NAME) - args = sys.argv - app = QApplication(args) + app = QApplication(sys.argv) app.setApplicationName(APP_NAME) palette = app.palette() palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white) app.setPalette(palette) window = MainWindow(app) app.setWindowIcon(window.senpwai_icon) - window.show_with_settings(args) + window.show_with_settings() sys.excepthook = custom_exception_handler sys.exit(app.exec()) diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index 080cb19..9d9aa0e 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -190,7 +190,7 @@ def parse_args(args: list[str]) -> tuple[Namespace, ArgumentParser]: "--site", help="Site to download from", choices=[PAHE, GOGO], - default=SETTINGS.auto_download_site, + default=SETTINGS.tracking_site, ) parser.add_argument( "-se", @@ -249,13 +249,13 @@ def parse_args(args: list[str]) -> tuple[Namespace, ArgumentParser]: parser.add_argument( "-cta", "--check_tracked_anime", - help="Check tracked anime for new episodes then auto download", + help="Check tracked anime for new episodes then download", action="store_true", ) parser.add_argument( "-ata", "--add_tracked_anime", - help="Add an anime to the tracked anime list", + help="Add an anime to the tracked anime list. Use the anime's title as it appears on your tracking site.", ) parser.add_argument( "-rta", diff --git a/senpwai/windows/about.py b/senpwai/windows/about.py index e9c8fe1..5f716e6 100644 --- a/senpwai/windows/about.py +++ b/senpwai/windows/about.py @@ -59,7 +59,7 @@ def __init__(self, main_window: "MainWindow"): - When searching🔎 use the anime's Romaji title instead of the English title - If you don't specify end episode, Senpwai will assume you mean the last episode - Senpwai can detect missing episodes e.g., if you have One Piece🏴‍☠️ episodes 1 to 100 but are missing episode 50, specify 1 to 100, and it will only download episode 50 -- If Senpwai can't find the quality you want it will pick the closest lower one it e.g., if you choose 720p but only 1080p and 360p is available it'll pick 360p +- If Senpwai can't find the quality you want it will pick the closest lower one e.g., if you choose 720p but only 1080p and 360p is available it'll pick 360p - So long as the app is open Senpwai will try to resume⏯️ ongoing downloads even if you lose an internet connection - Experiencing slow downloads? Use gogo hls mode for the fastest🚀 download speeds - [Hover✨](https://open.spotify.com/playlist/460b5y4LB8Dixh0XajVVaL?si=fce0f0f762464e81) over something that you don't understand there's probably a tool tip for it diff --git a/senpwai/windows/download.py b/senpwai/windows/download.py index 831c901..e7d0f95 100644 --- a/senpwai/windows/download.py +++ b/senpwai/windows/download.py @@ -350,10 +350,10 @@ def __init__(self, main_window: "MainWindow"): self.cancel_button: CancelAllButton self.folder_button: FolderButton self.downloaded_episode_count: DownloadedEpisodeCount - self.auto_download_timer = QTimer(self) - self.auto_download_timer.timeout.connect(self.start_auto_download) - self.setup_auto_download_timer() - self.auto_download_thread: AutoDownloadThread | None = None + self.tracked_download_timer = QTimer(self) + self.tracked_download_timer.timeout.connect(self.start_tracked_download) + self.setup_tracked_download_timer() + self.tracked_download_thread: TrackedDownloadThread | None = None def is_downloading(self) -> bool: if self.first_download_since_app_start: @@ -365,26 +365,26 @@ def is_downloading(self) -> bool: return False return True - def setup_auto_download_timer(self): - self.auto_download_timer.stop() - self.auto_download_timer.start( # Converting from hours to milliseconds - SETTINGS.check_for_new_eps_after * 1000 * 60 * 60 + def setup_tracked_download_timer(self): + self.tracked_download_timer.stop() + self.tracked_download_timer.start( # Converting from hours to milliseconds + SETTINGS.tracking_interval * 1000 * 60 * 60 ) - def clean_out_auto_download_thread(self): - self.auto_download_thread = None + def clean_out_tracked_download_thread(self): + self.tracked_download_thread = None - def start_auto_download(self): + def start_tracked_download(self): # We only spawn a new thread if one wasn't already running to avoid overwriding the reference to the previous one causing it to get garbage collected/destroyed # Cause it can cause this error "QThread: Destroyed while thread is still running" - if SETTINGS.tracked_anime and not self.auto_download_thread: - self.auto_download_thread = AutoDownloadThread( + if SETTINGS.tracked_anime and not self.tracked_download_thread: + self.tracked_download_thread = TrackedDownloadThread( self, SETTINGS.tracked_anime, self.main_window.tray_icon, - self.clean_out_auto_download_thread, + self.clean_out_tracked_download_thread, ) - self.auto_download_thread.start() + self.tracked_download_thread.start() def initiate_download_pipeline(self, anime_details: AnimeDetails): if self.first_download_since_app_start: @@ -1321,16 +1321,16 @@ def run(self): self.finished.emit(self.anime_details) -class AutoDownloadThread(QThread): +class TrackedDownloadThread(QThread): initate_download_pipeline = pyqtSignal(AnimeDetails) - clean_out_auto_download_thread_signal = pyqtSignal() + clean_out_tracked_download_thread_signal = pyqtSignal() def __init__( self, download_window: DownloadWindow, titles: list[str], tray_icon: QSystemTrayIcon, - clean_out_auto_download_thread_slot: Callable[[], None], + clean_out_tracked_download_thread_slot: Callable[[], None], ): super().__init__(download_window) self.anime_titles = titles @@ -1339,8 +1339,8 @@ def __init__( self.download_window.initiate_download_pipeline ) self.tray_icon = tray_icon - self.clean_out_auto_download_thread_signal.connect( - clean_out_auto_download_thread_slot + self.clean_out_tracked_download_thread_signal.connect( + clean_out_tracked_download_thread_slot ) def run(self): @@ -1368,4 +1368,4 @@ def run(self): ), True, ) - self.clean_out_auto_download_thread_signal.emit() + self.clean_out_tracked_download_thread_signal.emit() diff --git a/senpwai/windows/main.py b/senpwai/windows/main.py index 51aae68..c7980eb 100644 --- a/senpwai/windows/main.py +++ b/senpwai/windows/main.py @@ -59,7 +59,7 @@ def __init__(self, app: QApplication): self.stacked_windows.addWidget(self.about_window) self.setCentralWidget(self.stacked_windows) self.make_anime_details_thread: QThread | None = None - self.download_window.start_auto_download() + self.download_window.start_tracked_download() def closeEvent(self, a0: QCloseEvent | None) -> None: if not a0: @@ -80,8 +80,8 @@ def closeEvent(self, a0: QCloseEvent | None) -> None: return a0.ignore() a0.accept() - def show_with_settings(self, args: list[str]): - if MINIMISED_TO_TRAY_ARG in args: + def show_with_settings(self): + if MINIMISED_TO_TRAY_ARG in self.app.arguments(): if SETTINGS.start_maximized: self.setWindowState(self.windowState() | Qt.WindowState.WindowMaximized) self.hide() @@ -204,32 +204,34 @@ def __init__(self, main_window: MainWindow): self.setToolTip("Senpwai") self.activated.connect(self.on_tray_icon_click) self.context_menu = QMenu("Senpwai", main_window) + text = ( + "Show" + if MINIMISED_TO_TRAY_ARG in main_window.app.arguments() + else "Hide" + ) + self.toggle_show_hide_action = QAction(text, self.context_menu) + self.toggle_show_hide_action.triggered.connect(self.focus_or_hide_window) check_for_new_episodes_action = QAction( - "Check for new episodes", self.context_menu + "Check tracked anime", self.context_menu ) check_for_new_episodes_action.triggered.connect( - main_window.download_window.start_auto_download + main_window.download_window.start_tracked_download ) - search_action = QAction("Search", self.context_menu) - search_action.triggered.connect(main_window.switch_to_search_window) - search_action.triggered.connect(main_window.show_) - downloads_action = QAction("Downloads", self.context_menu) - downloads_action.triggered.connect(main_window.switch_to_download_window) - downloads_action.triggered.connect(main_window.show_) exit_action = QAction("Exit", self.context_menu) exit_action.triggered.connect(main_window.app.quit) + self.context_menu.addAction(self.toggle_show_hide_action) self.context_menu.addAction(check_for_new_episodes_action) - self.context_menu.addAction(search_action) - self.context_menu.addAction(downloads_action) self.context_menu.addAction(exit_action) self.messageClicked.connect(main_window.show_) self.setContextMenu(self.context_menu) def focus_or_hide_window(self): - if not self.main_window.isVisible(): + if self.main_window.isVisible(): + self.main_window.hide() + self.toggle_show_hide_action.setText("Show") + else: self.main_window.show_() - return - self.main_window.hide() + self.toggle_show_hide_action.setText("Hide") def on_tray_icon_click(self, reason: QSystemTrayIcon.ActivationReason): if reason == QSystemTrayIcon.ActivationReason.Context: diff --git a/senpwai/windows/settings.py b/senpwai/windows/settings.py index 414c002..87cf850 100644 --- a/senpwai/windows/settings.py +++ b/senpwai/windows/settings.py @@ -88,10 +88,11 @@ def __init__(self, main_window: "MainWindow") -> None: right_widget.setLayout(right_layout) main_layout.addWidget(left_widget) main_layout.addWidget(right_widget) - self.file_location_button = FolderButton( + self.settings_folder_button = FolderButton( self, self.font_size, SETTINGS.config_dir, SETTINGS.settings_json_path ) - set_minimum_size_policy(self.file_location_button) + self.settings_folder_button.setToolTip("Click to open the settings folder") + set_minimum_size_policy(self.settings_folder_button) self.sub_dub_setting = SubDubSetting(self) self.quality_setting = QualitySetting(self) self.max_simultaneous_downloads_setting = MaxSimultaneousDownloadsSetting(self) @@ -100,16 +101,14 @@ def __init__(self, main_window: "MainWindow") -> None: ) self.start_maximized = StartMaximized(self) self.download_folder_setting = DownloadFoldersSetting(self, main_window) - self.gogo_norm_or_hls_mode_setting = GogoNormOrHlsSetting(self) - self.tracked_anime = TrackedAnimeListSetting( - self, - ) - self.auto_download_site = AutoDownloadSite(self) - self.check_for_new_eps_after = CheckForNewEpsAfterSetting( + self.gogo_norm_or_hls_mode_setting = GogoModeSetting(self) + self.tracked_anime = TrackedAnimeListSetting(self, main_window.download_window) + self.tracking_site = TrackingSite(self) + self.check_for_new_eps_after = TrackingIntervalSetting( self, main_window.download_window ) self.gogo_skip_calculate = GogoSkipCalculate(self) - left_layout.addWidget(self.file_location_button) + left_layout.addWidget(self.settings_folder_button) left_layout.addWidget(self.sub_dub_setting) left_layout.addWidget(self.quality_setting) left_layout.addWidget(self.max_simultaneous_downloads_setting) @@ -121,7 +120,7 @@ def __init__(self, main_window: "MainWindow") -> None: self.run_on_startup = RunOnStartUp(self) left_layout.addWidget(self.run_on_startup) right_layout.addWidget(self.download_folder_setting) - right_layout.addWidget(self.auto_download_site) + right_layout.addWidget(self.tracking_site) right_layout.addWidget(self.check_for_new_eps_after) right_layout.addWidget(self.tracked_anime) self.full_layout.addWidget(main_widget) @@ -332,9 +331,21 @@ def __init__( setting_info: str, widgets_to_add: list, horizontal_layout=True, + label_is_button=False, ): super().__init__() - self.setting_label = StyledLabel(font_size=settings_window.font_size + 5) + self.setting_label = ( + StyledButton( + self, + font_size=settings_window.font_size + 5, + font_color="white", + normal_color="black", + hover_color="grey", + pressed_color="black", + ) + if label_is_button + else StyledLabel(font_size=settings_window.font_size + 5) + ) self.setting_label.setText(setting_info) set_minimum_size_policy(self.setting_label) if horizontal_layout: @@ -372,7 +383,9 @@ def __init__(self, text: str, font_size: int = 20): class TrackedAnimeListSetting(SettingWidget): - def __init__(self, settings_window: SettingsWindow): + def __init__( + self, settings_window: SettingsWindow, download_window: DownloadWindow + ): self.main_layout = QVBoxLayout() self.settings_window = settings_window main_widget = ScrollableSection(self.main_layout) @@ -384,13 +397,16 @@ def __init__(self, settings_window: SettingsWindow): line.setFixedHeight(7) super().__init__( settings_window, - "Tracked", + "Tracked anime", [line, main_widget], False, + True, ) self.setting_label.setToolTip( - "Senpwai will check for new episodes of your tracked anime when you start the app\nthen in intervals of the hours you specify so long as it is running" + "Senpwai will check for new episodes of your tracked anime when you start the app then in intervals\nof the hours you specify so long as it is running. Click to trigger a check right now" ) + self.setting_label = cast(StyledButton, self.setting_label) + self.setting_label.clicked.connect(download_window.start_tracked_download) def setup_anime_widget(self, wid: RemovableWidget): wid.remove_button.clicked.connect( @@ -415,7 +431,7 @@ def add_anime(self, title: str): SETTINGS.add_tracked_anime(title) -class AutoDownloadSite(SettingWidget): +class TrackingSite(SettingWidget): def __init__(self, settings_window: SettingsWindow): self.font_size = settings_window.font_size pahe_button = OptionButton( @@ -426,20 +442,18 @@ def __init__(self, settings_window: SettingsWindow): ) pahe_button.clicked.connect(lambda: gogo_button.set_picked_status(False)) pahe_button.clicked.connect( - lambda: SETTINGS.update_auto_download_site(cast(str, pahe_button.option)) + lambda: SETTINGS.update_tracking_site(cast(str, pahe_button.option)) ) gogo_button.clicked.connect(lambda: pahe_button.set_picked_status(False)) gogo_button.clicked.connect( - lambda: SETTINGS.update_auto_download_site(cast(str, gogo_button.option)) + lambda: SETTINGS.update_tracking_site(cast(str, gogo_button.option)) ) - if SETTINGS.auto_download_site == PAHE: + if SETTINGS.tracking_site == PAHE: pahe_button.set_picked_status(True) else: gogo_button.set_picked_status(True) - super().__init__( - settings_window, "Auto download site", [pahe_button, gogo_button] - ) + super().__init__(settings_window, "Tracking site", [pahe_button, gogo_button]) self.setting_label.setToolTip( f"If {APP_NAME} can't find the anime in the specified site it will try the other" ) @@ -469,7 +483,9 @@ def __init__( class GogoSkipCalculate(YesOrNoSetting): def __init__(self, settings_window: SettingsWindow): super().__init__(settings_window, "Skip calculating download size for Gogo") - self.setToolTip("Calculating total download size on gogo involves first making requests for the size of each episode") + self.setToolTip( + "Calculating total download size on gogo involves first making requests for the size of each episode" + ) if SETTINGS.gogo_skip_calculate: self.yes_button.set_picked_status(True) else: @@ -545,7 +561,7 @@ def __init__(self, settings_window: SettingsWindow): ) -class GogoNormOrHlsSetting(SettingWidget): +class GogoModeSetting(SettingWidget): def __init__(self, settings_window: SettingsWindow): norm_button = GogoNormOrHlsButton( settings_window, GOGO_NORM_MODE, settings_window.font_size @@ -555,20 +571,20 @@ def __init__(self, settings_window: SettingsWindow): settings_window, GOGO_HLS_MODE, settings_window.font_size ) set_minimum_size_policy(hls_button) - if SETTINGS.gogo_norm_or_hls_mode == GOGO_HLS_MODE: + if SETTINGS.gogo_mode == GOGO_HLS_MODE: hls_button.set_picked_status(True) else: norm_button.set_picked_status(True) norm_button.clicked.connect(lambda: hls_button.set_picked_status(False)) hls_button.clicked.connect(lambda: norm_button.set_picked_status(False)) norm_button.clicked.connect( - lambda: SETTINGS.update_gogo_norm_or_hls_mode(GOGO_NORM_MODE) + lambda: SETTINGS.update_gogo_mode(GOGO_NORM_MODE) ) hls_button.clicked.connect( - lambda: SETTINGS.update_gogo_norm_or_hls_mode(GOGO_HLS_MODE) + lambda: SETTINGS.update_gogo_mode(GOGO_HLS_MODE) ) super().__init__( - settings_window, "Gogo Normal or HLS mode", [norm_button, hls_button] + settings_window, "Gogo mode", [norm_button, hls_button] ) @@ -646,25 +662,25 @@ def __init__(self, settings_window: SettingsWindow): ) -class CheckForNewEpsAfterSetting(NonZeroNumberInputSetting): +class TrackingIntervalSetting(NonZeroNumberInputSetting): def __init__( self, settings_window: SettingsWindow, download_window: DownloadWindow ): self.download_window = download_window super().__init__( settings_window, - SETTINGS.check_for_new_eps_after, - SETTINGS.update_check_for_new_eps_after, - "Check for new episodes after", - "Bruh, time intervals can't be zero", + SETTINGS.tracking_interval, + SETTINGS.update_tracking_interval, + "Track anime every", + "Bruh, time intervals cannot be zero", "hours", "Senpwai will check for new episodes of your tracked anime when you start the app\nthen in intervals of the hours you specify so long as it is running", ) def text_changed(self, text: str): super().text_changed(text) - self.download_window.setup_auto_download_timer() - self.download_window.start_auto_download() + self.download_window.setup_tracked_download_timer() + self.download_window.start_tracked_download() class QualitySetting(SettingWidget): From 952225495126ea87863527be0c5ab47141e7fec2 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 30 Jul 2024 13:45:31 +0300 Subject: [PATCH 21/24] Minor changes --- docs/senpcli-guide.md | 2 +- scripts/bump_version.py | 2 +- scripts/release.py | 2 +- .../misc/{sadge-piece.gif => anime-not-found.gif} | Bin senpwai/common/static.py | 4 ++-- senpwai/windows/main.py | 6 ++---- senpwai/windows/search.py | 7 ++++--- 7 files changed, 11 insertions(+), 12 deletions(-) rename senpwai/assets/misc/{sadge-piece.gif => anime-not-found.gif} (100%) diff --git a/docs/senpcli-guide.md b/docs/senpcli-guide.md index 423c0c3..62a9b29 100644 --- a/docs/senpcli-guide.md +++ b/docs/senpcli-guide.md @@ -18,7 +18,7 @@ Senpwai ships with the Senpcli pre-installed. pip install senpwai ``` -- **Android (Using [termux](https://github.com/termux/termux-app))** +- **Android (Using [termux](https://github.com/termux/termux-app), note that the android port is currently really buggy and barely even works)** ```sh pkg update -y && curl https://raw.githubusercontent.com/SenZmaKi/Senpwai/master/termux/install.sh | bash diff --git a/scripts/bump_version.py b/scripts/bump_version.py index c58ae5a..e51fcb2 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -87,7 +87,7 @@ def main(ignore_same=False) -> None: ) parser.add_argument( "-pv", - "--prev_version", + "--previous_version", type=str, help="Previous version to bump from", default=get_prev_version(), diff --git a/scripts/release.py b/scripts/release.py index 93ee294..3a0de53 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -114,7 +114,7 @@ def main() -> None: parsed = parser.parse_args() if BRANCH_NAME == "master": log_error("On master branch, switch to version branch", True) - if not parsed.skip_bumb: + if not parsed.skip_bump: log_info("Bumping version") bump_version.main(True) if not parsed.skip_ruff: diff --git a/senpwai/assets/misc/sadge-piece.gif b/senpwai/assets/misc/anime-not-found.gif similarity index 100% rename from senpwai/assets/misc/sadge-piece.gif rename to senpwai/assets/misc/anime-not-found.gif diff --git a/senpwai/common/static.py b/senpwai/common/static.py index e5896ca..a68ee9c 100644 --- a/senpwai/common/static.py +++ b/senpwai/common/static.py @@ -175,7 +175,7 @@ def join_from_misc(file_name: str) -> str: SENPWAI_ICON_PATH = join_from_misc("senpwai-icon.ico") TASK_COMPLETE_ICON_PATH = join_from_misc("task-complete.png") LOADING_ANIMATION_PATH = join_from_misc("loading.gif") -SADGE_PIECE_PATH = join_from_misc("sadge-piece.gif") +ANIME_NOT_FOUND_PATH = join_from_misc("anime-not-found.gif") FOLDER_ICON_PATH = join_from_misc("folder.png") mascots_folder_path = join_from_assets("mascots") @@ -241,7 +241,7 @@ def join_from_audio(audio_name: str) -> str: HENTAI_ADDICT_AUDIO_PATH = join_from_audio("aqua-crying.mp3") MORBIUS_AUDIO_PATH = join_from_audio("morbin-time.mp3") SEN_FAVOURITE_AUDIO_PATH = join_from_audio("sen-favourite.wav") -SEN_FAVOURITE_AUDIO_PATH = join_from_audio( +ONE_PIECE_REAL_AUDIO_PATH = join_from_audio( f"one-piece-real-{random_choice((1, 2))}.mp3" ) KAGE_BUNSHIN_AUDIO_PATH = join_from_audio("kage-bunshin-no-jutsu.mp3") diff --git a/senpwai/windows/main.py b/senpwai/windows/main.py index c7980eb..b228153 100644 --- a/senpwai/windows/main.py +++ b/senpwai/windows/main.py @@ -67,7 +67,7 @@ def closeEvent(self, a0: QCloseEvent | None) -> None: if self.download_window.is_downloading(): message_box = QMessageBox(self) message_box.setIcon(QMessageBox.Icon.Warning) - message_box.setStyleSheet("color: black") + message_box.setStyleSheet("color: black;") message_box.setText( "You have ongoing downloads, are you sure you want to exit?" ) @@ -205,9 +205,7 @@ def __init__(self, main_window: MainWindow): self.activated.connect(self.on_tray_icon_click) self.context_menu = QMenu("Senpwai", main_window) text = ( - "Show" - if MINIMISED_TO_TRAY_ARG in main_window.app.arguments() - else "Hide" + "Show" if MINIMISED_TO_TRAY_ARG in main_window.app.arguments() else "Hide" ) self.toggle_show_hide_action = QAction(text, self.context_menu) self.toggle_show_hide_action.triggered.connect(self.focus_or_hide_window) diff --git a/senpwai/windows/search.py b/senpwai/windows/search.py index c7cf15e..dbeef33 100644 --- a/senpwai/windows/search.py +++ b/senpwai/windows/search.py @@ -35,10 +35,11 @@ PAHE_NORMAL_COLOR, PAHE_PRESSED_COLOR, RANDOM_MACOT_ICON_PATH, - SADGE_PIECE_PATH, + ANIME_NOT_FOUND_PATH, SEARCH_WINDOW_BCKG_IMAGE_PATH, SEN_ANILIST_ID, SEN_FAVOURITE_AUDIO_PATH, + ONE_PIECE_REAL_AUDIO_PATH, TOKI_WA_UGOKI_DASU_AUDIO_PATH, W_ANIME, WHAT_DA_HELL_AUDIO_PATH, @@ -110,7 +111,7 @@ def __init__(self, main_window: "MainWindow"): LOADING_ANIMATION_PATH, 250, 300, "Loading.. .", 1, 48, 50 ) self.anime_not_found = AnimationAndText( - SADGE_PIECE_PATH, 400, 300, ":( couldn't find that anime ", 1, 48, 50 + ANIME_NOT_FOUND_PATH, 400, 300, ":( couldn't find that anime ", 1, 48, 50 ) self.bottom_section_stacked_widgets = QStackedWidget() self.bottom_section_stacked_widgets.addWidget(self.results_widget) @@ -156,7 +157,7 @@ def search_anime(self, anime_title: str, site: str) -> None: anime_title_lower = anime_title.lower() is_naruto = "naruto" in anime_title_lower if "one piece" in anime_title_lower: - AudioPlayer(self, SEN_FAVOURITE_AUDIO_PATH, volume=100).play() + AudioPlayer(self, ONE_PIECE_REAL_AUDIO_PATH, volume=100).play() elif "jojo" in anime_title_lower: AudioPlayer(self, ZA_WARUDO_AUDIO_PATH, 100).play() for _ in range(180): From ea702534e7aff4f859151aa684c69b29af19c71a Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 30 Jul 2024 13:45:37 +0300 Subject: [PATCH 22/24] scripts: Bump version from 2.1.10 --> 2.1.11 --- pyproject.toml | 2 +- scripts/setup.iss | 6 +++--- scripts/setup_senpcli.iss | 6 +++--- senpwai/common/static.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index de272dd..eb25d36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "senpwai" -version = "2.1.10" +version = "2.1.11" description = "A desktop app for tracking and batch downloading anime" authors = ["SenZmaKi "] license = "GPL v3" diff --git a/scripts/setup.iss b/scripts/setup.iss index 998a0ee..41c3abd 100644 --- a/scripts/setup.iss +++ b/scripts/setup.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Senpwai" -#define MyAppVersion "2.1.10" +#define MyAppVersion "2.1.11" #define MyAppPublisher "AkatsuKi Inc." #define MyAppURL "https://github.com/SenZmaKi/Senpwai" #define MyAppExeName "senpwai.exe" @@ -11,10 +11,10 @@ [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{B1AC746D-A6F0-44EF-B812.1.10-0DF4571B51}} +AppId={{B1AC746D-A6F0-44EF-B812.1.11-0DF4571B51}} AppName={#MyAppName} AppVersion={#MyAppVersion} -VersionInfoVersion=2.1.10.0 +VersionInfoVersion=2.1.11.0 ;AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} diff --git a/scripts/setup_senpcli.iss b/scripts/setup_senpcli.iss index 3ad3542..58a30ae 100644 --- a/scripts/setup_senpcli.iss +++ b/scripts/setup_senpcli.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Senpcli" -#define MyAppVersion "2.1.10" +#define MyAppVersion "2.1.11" #define MyAppPublisher "AkatsuKi Inc." #define MyAppURL "https://github.com/SenZmaKi/Senpwai" #define MyAppExeName "Senpcli.exe" @@ -11,10 +11,10 @@ [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{7D4A0DD5-EACB-45-81FC-2.1.105FCFF05BB6}} +AppId={{7D4A0DD5-EACB-45-81FC-2.1.115FCFF05BB6}} AppName={#MyAppName} AppVersion={#MyAppVersion} -VersionInfoVersion=2.1.10.0 +VersionInfoVersion=2.1.11.0 ;AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} diff --git a/senpwai/common/static.py b/senpwai/common/static.py index a68ee9c..68c889f 100644 --- a/senpwai/common/static.py +++ b/senpwai/common/static.py @@ -11,7 +11,7 @@ APP_NAME = "Senpwai" APP_NAME_LOWER = "senpwai" -VERSION = "2.1.10" +VERSION = "2.1.11" DESCRIPTION = "A desktop app for tracking and batch downloading anime" IS_PIP_INSTALL = False From a51d5c4e029ec7bddc60e16565e77d73933b858a Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 30 Jul 2024 13:45:37 +0300 Subject: [PATCH 23/24] scripts: Fix linting issues with ruff --- scripts/bump_version.py | 1 - senpwai/scrapers/gogo/main.py | 1 - senpwai/windows/download.py | 6 +----- senpwai/windows/misc.py | 1 - 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/scripts/bump_version.py b/scripts/bump_version.py index e51fcb2..d468ce9 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -1,6 +1,5 @@ from argparse import ArgumentParser from functools import cache -from hmac import new import subprocess import sys import re diff --git a/senpwai/scrapers/gogo/main.py b/senpwai/scrapers/gogo/main.py index ce15bde..a93593f 100644 --- a/senpwai/scrapers/gogo/main.py +++ b/senpwai/scrapers/gogo/main.py @@ -8,7 +8,6 @@ CLIENT, IBYTES_TO_MBS_DIVISOR, PARSER, - RESOURCE_MOVED_STATUS_CODES, AiringStatus, AnimeMetadata, DomainNameError, diff --git a/senpwai/windows/download.py b/senpwai/windows/download.py index e7d0f95..33de031 100644 --- a/senpwai/windows/download.py +++ b/senpwai/windows/download.py @@ -13,23 +13,19 @@ ) from common.tracker import check_for_new_episodes from senpwai.scrapers import gogo, pahe -from senpwai.common.classes import SETTINGS, Anime, AnimeDetails +from senpwai.common.classes import SETTINGS, AnimeDetails from senpwai.common.scraper import ( IBYTES_TO_MBS_DIVISOR, - AiringStatus, Download, NoResourceLengthException, ProgressFunction, ffmpeg_is_installed, - lacked_episode_numbers, lacked_episodes, - sanitise_title, ) from senpwai.common.static import ( CANCEL_ICON_PATH, DOWNLOAD_WINDOW_BCKG_IMAGE_PATH, DUB, - GOGO, MOVE_DOWN_QUEUE_ICON_PATH, MOVE_UP_QUEUE_ICON_PATH, PAHE, diff --git a/senpwai/windows/misc.py b/senpwai/windows/misc.py index fa15273..eccdffb 100644 --- a/senpwai/windows/misc.py +++ b/senpwai/windows/misc.py @@ -10,7 +10,6 @@ from senpwai.common.scraper import ( CLIENT, IBYTES_TO_MBS_DIVISOR, - RESOURCE_MOVED_STATUS_CODES, Download, try_installing_ffmpeg, ) From ae617d62966b78ae647053df6f72cb642c0b5e20 Mon Sep 17 00:00:00 2001 From: Sen ZmaKi Date: Tue, 30 Jul 2024 14:45:02 +0300 Subject: [PATCH 24/24] Minor fixes --- docs/release-notes.md | 2 +- pyproject.toml | 5 ++--- scripts/common.py | 13 +++++++++---- scripts/release.py | 28 +++++++++++++++++----------- scripts/ruff.py | 6 +++++- senpwai/common/tracker.py | 4 ++-- senpwai/senpcli/main.py | 2 +- senpwai/windows/download.py | 2 +- 8 files changed, 38 insertions(+), 24 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index c94ccf1..caf0a36 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,4 +9,4 @@ - Fix some gogo downloads randomly failing, if you still experience it be sure to report it - Fix failed to fetch direct download links notification on cancelling direct download links retrieval - Fix anime randomizer -- Fix opening chosen anime window for other anime failing if previous attempt failed +- Fix opening chosen anime window for other anime failing if previous attempt failed \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index eb25d36..c4e86c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,10 +68,9 @@ bump_version = "python -m scripts.bump_version" announce = "python -m scripts.announce" announce_discord = "python scripts/announce/discord.py" announce_reddit = "python scripts/announce/reddit.py" -ruff = [{ ref = "lint_fix" }, { ref = "format" }] -lint = "python -m scripts.ruff" -lint_fix = "python -m scripts.ruff --lint_fix" format = "python -m scripts.ruff --format" +lint_fix = "python -m scripts.ruff --lint_fix" +lint = "python -m scripts.ruff --lint" build_release_test = [{ref = "lint"}, { ref = "test" }, { ref = "build_release" }] build_release_ddl = [{ ref = "test_ddl" }, { ref = "build_release" }] build_release = [ diff --git a/scripts/common.py b/scripts/common.py index 01623fe..7d0b942 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -8,6 +8,7 @@ ROOT_DIR = Path(__file__).parent.parent REPO_URL = "https://github.com/SenZmaKi/Senpwai" + def join_from_local_appdata(*paths: str) -> str: return os.path.join( os.environ["LOCALAPPDATA"], @@ -15,11 +16,13 @@ def join_from_local_appdata(*paths: str) -> str: *paths, ) + def join_from_py_scripts(*paths: str) -> str: return join_from_local_appdata("Python", "Python311", "Scripts", *paths) -def git_commit(msg: str) -> None: - subprocess.run(f'git commit -am "scripts: {msg}"') + +def git_commit(msg: str) -> subprocess.CompletedProcess[bytes]: + return subprocess.run(f'git commit -am "scripts: {msg}"') def get_piped_input() -> str: @@ -46,9 +49,11 @@ def overwrite(file: TextIOWrapper, content: str) -> None: @cache def get_current_branch_name() -> str: - return subprocess.run( + complete_subprocess = subprocess.run( "git branch --show-current", capture_output=True, text=True - ).stdout.strip() + ) + complete_subprocess.check_returncode() + return complete_subprocess.stdout.strip() def log_info(msg: str) -> None: diff --git a/scripts/release.py b/scripts/release.py index 3a0de53..d9301a4 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -4,6 +4,7 @@ from scripts.common import ( REPO_URL, get_current_branch_name, + git_commit, log_info, log_error, ROOT_DIR, @@ -17,11 +18,11 @@ def merge_branch() -> None: - git_status_completed_process = subprocess.run( + completed_process = subprocess.run( "git status", capture_output=True, text=True ) - git_status_completed_process.check_returncode() - if "Changes" in git_status_completed_process.stdout: + completed_process.check_returncode() + if "Changes" in completed_process.stdout: log_error("You have uncommited changes", True) subprocess.run(f"git push origin {BRANCH_NAME}").check_returncode() subprocess.run(f'gh pr create --title {BRANCH_NAME} --body "" ').check_returncode() @@ -32,25 +33,30 @@ def add_change_log_link(release_notes: str) -> str: prev_version = bump_version.get_prev_version() new_version = bump_version.get_new_version() if new_version == prev_version: - prev_version = input('Failed to get previous version number, manual input required (without the starting "v")\n> ') + prev_version = input( + 'Failed to get previous version number, manual input required (without the "v" prefix)\n> ' + ) if not prev_version: sys.exit() change_log_link = ( - f"\n\n**Full Changelog**: {REPO_URL}/compare/v{prev_version}...v{new_version}" + f"**Full Changelog**: {REPO_URL}/compare/v{prev_version}...v{new_version}" ) - return release_notes + change_log_link + return f"{release_notes}\n\n{change_log_link}" def get_release_notes(from_commits: bool) -> str: with open(ROOT_DIR.joinpath("docs", "release-notes.md"), "r+") as f: - if from_commits: + if not from_commits: return add_change_log_link(f.read()) - new_commits_completed_process = subprocess.run( - f"git log --oneline master..{BRANCH_NAME}", capture_output=True, text=True + completed_process = subprocess.run( + f'git log --oneline --format="%s" master..{BRANCH_NAME}', + capture_output=True, + text=True, ) - new_commits_completed_process.check_returncode() - release_notes = f"# Changes\n\n{new_commits_completed_process.stdout}" + completed_process.check_returncode() + release_notes = f"# Changes\n\n{completed_process.stdout}" overwrite(f, release_notes) + git_commit("Generate release notes from commits").check_returncode() return add_change_log_link(release_notes) diff --git a/scripts/ruff.py b/scripts/ruff.py index 54174a3..162acad 100644 --- a/scripts/ruff.py +++ b/scripts/ruff.py @@ -6,9 +6,11 @@ def main( lint_fix=False, format=False, + lint=False, ) -> None: parser = ArgumentParser("Run ruff on the project") parser.add_argument("-f", "--format", action="store_true", help="Format the code") + parser.add_argument("-l", "--lint", action="store_true", help="Lint the code") parser.add_argument("-lf", "--lint_fix", action="store_true", help="Fix linting issues") parsed = parser.parse_args() if lint_fix or parsed.lint_fix: @@ -19,7 +21,9 @@ def main( subprocess.run(f"ruff format {ROOT_DIR}").check_returncode() git_commit("Format with ruff") return - subprocess.run(f"ruff {ROOT_DIR}").check_returncode() + if lint or parsed.lint: + subprocess.run(f"ruff {ROOT_DIR}").check_returncode() + return if __name__ == "__main__": diff --git a/senpwai/common/tracker.py b/senpwai/common/tracker.py index 6246918..e9f36e5 100644 --- a/senpwai/common/tracker.py +++ b/senpwai/common/tracker.py @@ -1,5 +1,5 @@ -from common.scraper import AiringStatus, lacked_episode_numbers, sanitise_title -from common.static import DUB, GOGO, PAHE +from senpwai.common.scraper import AiringStatus, lacked_episode_numbers, sanitise_title +from senpwai.common.static import DUB, GOGO, PAHE from senpwai.common.classes import SETTINGS, Anime, AnimeDetails from senpwai.scrapers import pahe, gogo from typing import Callable diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index 9d9aa0e..48f24ce 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -8,7 +8,7 @@ from threading import Event, Lock, Thread from typing import Callable, cast -from common.tracker import check_for_new_episodes +from senpwai.common.tracker import check_for_new_episodes from tqdm import tqdm from senpwai.common.classes import ( diff --git a/senpwai/windows/download.py b/senpwai/windows/download.py index 33de031..331f5ce 100644 --- a/senpwai/windows/download.py +++ b/senpwai/windows/download.py @@ -11,7 +11,7 @@ QVBoxLayout, QWidget, ) -from common.tracker import check_for_new_episodes +from senpwai.common.tracker import check_for_new_episodes from senpwai.scrapers import gogo, pahe from senpwai.common.classes import SETTINGS, AnimeDetails from senpwai.common.scraper import (