diff --git a/.gitignore b/.gitignore index 556dbef..66cc76c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ marin.py setups build dist +.scraps/ crap.py diff --git a/bump_version.py b/bump_version.py new file mode 100644 index 0000000..6064b8a --- /dev/null +++ b/bump_version.py @@ -0,0 +1,56 @@ +from io import TextIOWrapper +import os +import subprocess + +PREV_VERSION = "2.1.3" +NEW_VERSION = "2.1.4" +FILES_PATHS = [ + "pyproject.toml", + "senpwai/utils/static.py", + "setup.iss", + "setup_senpcli.iss", +] + + +def log_error(msg: str) -> None: + print(f"[-] Error: {msg}") + + +def log_info(msg: str) -> None: + print(f"[+] Info: {msg}") + + +def log_warning(msg: str) -> None: + print(f"[!] Warning: {msg}") + + +def truncate(file: TextIOWrapper, content: str) -> None: + file.seek(0) + file.write(content) + file.truncate() + + +def main() -> None: + log_info(f"Bumping version from {PREV_VERSION} -> {NEW_VERSION}\n") + for file_path in FILES_PATHS: + if not os.path.isfile(file_path): + log_error(f'"{file_path}" not found') + continue + with open(file_path, "r+") as f: + content = f.read() + new_content = content.replace(PREV_VERSION, NEW_VERSION) + if new_content == content: + if NEW_VERSION in new_content: + log_warning( + f'Failed to find previous version in "{file_path}" but the new version is in it' + ) + else: + log_error(f'Failed to find previous version in "{file_path}"') + continue + truncate(f, new_content) + log_info(f'Bumped version in "{file_path}"') + subprocess.run("git diff") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 5103091..ea38039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "senpwai" -version = "2.1.3" +version = "2.1.4" description = "A desktop app for tracking and batch downloading anime" authors = ["SenZmaKi "] license = "GPL v3" @@ -9,7 +9,7 @@ include = ["senpwai/assets"] packages = [{ include = "senpwai" }, { include = "senpcli", from = "senpwai" }] exclude = ["src/**/test.py"] repository = "https://github.com/SenZmaKi/Senpwai" -documentation = "https://github.com/SenZmaKi/Senpwai/blob/master/docs" +documentation = "https://github.com/SenZmaKi/Senpwai/blob/master/README.md" keywords = [ "anime", "app", diff --git a/senpwai/main.py b/senpwai/main.py index 90d43b3..1e34c1d 100644 --- a/senpwai/main.py +++ b/senpwai/main.py @@ -6,7 +6,7 @@ from PyQt6.QtCore import QCoreApplication, Qt from PyQt6.QtGui import QPalette from PyQt6.QtWidgets import QApplication -from senpwai.utils.static import APP_NAME, custom_exception_handler +from senpwai.utils.static import APP_NAME, custom_exception_handler, OS from senpwai.windows.main import MainWindow @@ -26,7 +26,7 @@ def windows_app_initialisation(): def main(): - if sys.platform == "win32": + if OS.is_windows: windows_app_initialisation() QCoreApplication.setApplicationName(APP_NAME) diff --git a/senpwai/scrapers/gogo/__init__.py b/senpwai/scrapers/gogo/__init__.py index 0561220..b37e5b6 100644 --- a/senpwai/scrapers/gogo/__init__.py +++ b/senpwai/scrapers/gogo/__init__.py @@ -1,3 +1,3 @@ -from .hls import * # noqa F403 -from .main import * # noqa F403 -from .constants import GOGO # noqa F401 \ No newline at end of file +from .hls import * # noqa F403 +from .main import * # noqa F403 +from .constants import GOGO # noqa F401 diff --git a/senpwai/scrapers/gogo/constants.py b/senpwai/scrapers/gogo/constants.py index 4169721..35f57d3 100644 --- a/senpwai/scrapers/gogo/constants.py +++ b/senpwai/scrapers/gogo/constants.py @@ -2,9 +2,10 @@ GOGO = "gogo" GOGO_HOME_URL = "https://anitaku.to" -AJAX_SEARCH_URL = "https://ajax.gogo-load.com/site/loadAjaxSearch?keyword=" +AJAX_ENTRY_POINT = "https://ajax.gogocdn.net" +AJAX_SEARCH_URL = f"{AJAX_ENTRY_POINT}/site/loadAjaxSearch?keyword=" AJAX_LOAD_EPS_URL = ( - "https://ajax.gogo-load.com/ajax/load-list-episode?ep_start={}&ep_end={}&id={}" + f"{AJAX_ENTRY_POINT}/ajax/load-list-episode?ep_start={{}}&ep_end={{}}&id={{}}" ) FULL_SITE_NAME = "Gogoanime" DUB_EXTENSION = " (Dub)" diff --git a/senpwai/scrapers/gogo/main.py b/senpwai/scrapers/gogo/main.py index fc57c71..34ff316 100644 --- a/senpwai/scrapers/gogo/main.py +++ b/senpwai/scrapers/gogo/main.py @@ -37,9 +37,13 @@ def search(keyword: str, ignore_dub=True) -> list[tuple[str, str]]: for a in a_tags: title = a.text link = f'{GOGO_HOME_URL}/{a["href"]}' - if DUB_EXTENSION in title and ignore_dub: - continue title_and_link.append((title, link)) + for title, link in title_and_link: + if ignore_dub and DUB_EXTENSION in title: + sub_title = title.replace(DUB_EXTENSION, "") + if any([sub_title == title for title, _ in title_and_link]): + title_and_link.remove((title, link)) + return title_and_link @@ -49,6 +53,10 @@ def extract_anime_id(anime_page_content: bytes) -> int: return int(anime_id) +def title_is_dub(title: str) -> bool: + return DUB_EXTENSION in title + + def get_download_page_links( start_episode: int, end_episode: int, anime_id: int ) -> list[str]: @@ -206,6 +214,6 @@ def get_session_cookies(fresh=False) -> RequestsCookieJar: # A valid User-Agent is required during this post request hence the CLIENT is technically only necessary here response = CLIENT.post(login_url, form_data, cookies=response.cookies) SESSION_COOKIES = response.cookies - if len(SESSION_COOKIES) == 0: + if not SESSION_COOKIES: return get_session_cookies() return SESSION_COOKIES diff --git a/senpwai/scrapers/pahe/__init__.py b/senpwai/scrapers/pahe/__init__.py index 0d139f4..1d30550 100644 --- a/senpwai/scrapers/pahe/__init__.py +++ b/senpwai/scrapers/pahe/__init__.py @@ -1,2 +1,2 @@ -from .main import * # noqa: F403 -from .constants import PAHE # noqa: F401 \ No newline at end of file +from .main import * # noqa: F403 +from .constants import PAHE # noqa: F401 diff --git a/senpwai/scrapers/pahe/constants.py b/senpwai/scrapers/pahe/constants.py index f0c621f..533f916 100644 --- a/senpwai/scrapers/pahe/constants.py +++ b/senpwai/scrapers/pahe/constants.py @@ -1,4 +1,5 @@ import re + PAHE = "pahe" PAHE_DOMAIN = "animepahe.ru" PAHE_HOME_URL = f"https://{PAHE_DOMAIN}" @@ -22,9 +23,11 @@ Generates the load episodes link from the provided anime page link and page number. Example: {anime_page_link}&page={page_number} """ -KWIK_DOMAIN = "kwik.si" -KWIK_PAGE_REGEX = re.compile(rf"https?://{KWIK_DOMAIN}/f/([^\"']+)") +KWIK_PAGE_REGEX = re.compile(r"https?://kwik.si/f/([^\"']+)") DUB_PATTERN = "eng" + EPISODE_SIZE_REGEX = re.compile(r"\b(\d+)MB\b") PARAM_REGEX = re.compile(r"""\(\"(\w+)\",\d+,\"(\w+)\",(\d+),(\d+),(\d+)\)""") - +CHAR_MAP = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/" +CHAR_MAP_BASE = 10 +CHAR_MAP_DIGITS = CHAR_MAP[:CHAR_MAP_BASE] diff --git a/senpwai/scrapers/pahe/main.py b/senpwai/scrapers/pahe/main.py index 015e315..285c052 100644 --- a/senpwai/scrapers/pahe/main.py +++ b/senpwai/scrapers/pahe/main.py @@ -13,6 +13,8 @@ match_quality, ) from .constants import ( + CHAR_MAP_BASE, + CHAR_MAP_DIGITS, PAHE_HOME_URL, FULL_SITE_NAME, API_ENTRY_POINT, @@ -36,6 +38,7 @@ } """ + def site_request(url: str) -> Response: """ For requests that go specifically to the domain animepahe.ru instead of e.g., pahe.win or kwik.si @@ -252,22 +255,20 @@ def calculate_total_download_size(bound_info: list[str]) -> int: return total_size -def get_string(content: str, s1: int) -> int: - map_thing = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/" - s2 = 10 - map_string = map_thing[:s2] - acc = 0 +def get_char_code(content: str, s1: int) -> int: + j = 0 for index, c in enumerate(reversed(content)): - acc += (int(c) if c.isdigit() else 0) * int(math.pow(s1, index)) + j += (int(c) if c.isdigit() else 0) * int(math.pow(s1, index)) k = "" - while acc > 0: - k = map_string[acc % s2] + k - acc = (acc - (acc % s2)) // s2 - return int(k) if k.isdigit() else 0 + while j > 0: + k = CHAR_MAP_DIGITS[j % CHAR_MAP_BASE] + k + j = (j - (j % CHAR_MAP_BASE)) // CHAR_MAP_BASE + return int(k) if k else 0 # Courtesy of Saikou app https://github.com/saikou-app/saikou -def decrypt_token_and_post_url_page(full_key: str, key: str, v1: int, v2: int) -> str: +# RIP Saikou +def decrypt_post_form(full_key: str, key: str, v1: int, v2: int) -> str: r = "" i = 0 while i < len(full_key): @@ -275,9 +276,9 @@ def decrypt_token_and_post_url_page(full_key: str, key: str, v1: int, v2: int) - while full_key[i] != key[v2]: s += full_key[i] i += 1 - for j in range(len(key)): - s = s.replace(key[j], str(j)) - r += chr(get_string(s, v2) - v1) + for idx, c in enumerate(key): + s = s.replace(c, str(idx)) + r += chr(get_char_code(s, v2) - v1) i += 1 return r @@ -294,14 +295,15 @@ def get_direct_download_links( direct_download_links: list[str] = [] for pahewin_link in pahewin_download_page_links: # Extract kwik page links - html_page = CLIENT.get(pahewin_link).text - download_link = cast( - re.Match[str], KWIK_PAGE_REGEX.search(html_page) + pahewin_html_page = CLIENT.get(pahewin_link).text + kwik_page_link = cast( + re.Match[str], KWIK_PAGE_REGEX.search(pahewin_html_page) ).group() - # Extract direct download links from kwik page links - response = CLIENT.get(download_link) - cookies = response.cookies + # Extract direct download links from kwik html page + response = CLIENT.get(kwik_page_link) + with open("kwik.html", "wb") as f: + f.write(response.content) match = cast(re.Match, PARAM_REGEX.search(response.text)) full_key, key, v1, v2 = ( match.group(1), @@ -309,14 +311,14 @@ def get_direct_download_links( match.group(3), match.group(4), ) - decrypted = decrypt_token_and_post_url_page(full_key, key, int(v1), int(v2)) - soup = BeautifulSoup(decrypted, PARSER) + form = decrypt_post_form(full_key, key, int(v1), int(v2)) + soup = BeautifulSoup(form, PARSER) post_url = cast(str, cast(Tag, soup.form)["action"]) token_value = cast(str, cast(Tag, soup.input)["value"]) response = CLIENT.post( post_url, - headers=CLIENT.append_headers({"Referer": download_link}), - cookies=cookies, + headers=CLIENT.append_headers({"Referer": kwik_page_link}), + cookies=response.cookies, data={"_token": token_value}, allow_redirects=False, ) diff --git a/senpwai/scrapers/test.py b/senpwai/scrapers/test.py index 36185ee..f15f063 100644 --- a/senpwai/scrapers/test.py +++ b/senpwai/scrapers/test.py @@ -41,6 +41,7 @@ class FailedTest(Exception): def __init__(self, msg: str): super().__init__(msg) + def conditional_print(text: str): if not SILENT: print(text) @@ -153,9 +154,17 @@ def test_get_episode_page_links( run_time = get_run_time_later() test_variables = f"Anime ID: {anime_id}" anime_id = cast(str, anime_id) - start_page_num, end_page_num, _, first_page = pahe.get_episode_pages_info(anime_page_link, start_episode, end_episode) + start_page_num, end_page_num, _, first_page = pahe.get_episode_pages_info( + anime_page_link, start_episode, end_episode + ) episode_page_links = pahe.GetEpisodePageLinks().get_episode_page_links( - start_episode, end_episode, start_page_num, end_page_num, first_page, anime_page_link, anime_id + start_episode, + end_episode, + start_page_num, + end_page_num, + first_page, + anime_page_link, + anime_id, ) rt = run_time() fail_if_list_is_empty( @@ -181,9 +190,7 @@ def test_get_download_page_links( ( pahewin_page, pahewin_info, - ) = pahe.GetPahewinPageLinks().get_pahewin_page_links_and_info( - eps_page_links - ) + ) = pahe.GetPahewinPageLinks().get_pahewin_page_links_and_info(eps_page_links) download_page_links, download_info = pahe.bind_sub_or_dub_to_link_info( sub_or_dub, pahewin_page, pahewin_info ) @@ -238,6 +245,7 @@ def fail_if_list_is_empty( test_variables, ) + def test_getting_direct_download_links( site: str, download_page_links: list[str], quality: str ) -> list[str]: @@ -742,4 +750,3 @@ def print_metadata(metadata: AnimeMetadata): if __name__ == "__main__": args = ArgParser(sys.argv) run_tests(args) - diff --git a/senpwai/senpcli/main.py b/senpwai/senpcli/main.py index d6b929d..b2f9d4c 100644 --- a/senpwai/senpcli/main.py +++ b/senpwai/senpcli/main.py @@ -21,6 +21,7 @@ ) from senpwai.utils.static import ( IS_PIP_INSTALL, + OS, open_folder, DUB, APP_EXE_PATH as SENPWAI_EXE_PATH, @@ -609,7 +610,7 @@ def handle_update_check_result( ) if SENPWAI_IS_INSTALLED: return print("Update available, install it by updating Senpwai") - if sys.platform == "win32": + if OS.is_windows: print("Update available, would you like to download and install it? (y/n)") if input("> ").lower() == "y": try: diff --git a/senpwai/utils/classes.py b/senpwai/utils/classes.py index 4a2977b..4f5640d 100644 --- a/senpwai/utils/classes.py +++ b/senpwai/utils/classes.py @@ -38,7 +38,7 @@ def update_available( latest_version = match.group(1) download_url = "" target_asset_name = "" - # Update this logic if u ever officially release on Linux or Mac + # NOTE: Update this logic if you ever officially release on Linux or Mac target_asset_names = generate_windows_setup_file_titles(app_name) update_available = True if latest_version != curr_version else False if update_available: @@ -70,7 +70,7 @@ def __init__(self) -> None: self.settings_json_path = os.path.join(self.config_dir, "settings.json") # Default settings # Only these settings will be saved to settings.json - # Everytime you add a new class member that isn't a setting, make sure to pop it from the Settings.dict_settings method + # 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() @@ -134,10 +134,12 @@ def setup_default_download_folder(self) -> list[str]: return [downloads_folder] def dict_settings(self) -> dict: - d_settings = {k: v for k, v in self.__dict__.items()} - d_settings.pop("settings_json_path") - d_settings.pop("config_dir") - return d_settings + 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") + } def update_sub_or_dub(self, sub_or_dub: str) -> None: self.sub_or_dub = sub_or_dub @@ -237,7 +239,7 @@ def __init__(self, title: str, page_link: str, anime_id: str | None) -> None: class AnimeDetails: - def __init__(self, anime: Anime, site: str): + def __init__(self, anime: Anime, site: str) -> None: self.anime = anime self.site = site self.is_hls_download = ( @@ -295,7 +297,7 @@ def try_path(title: str) -> str | None: anime_type = "" parsed = {} - def init(title: str): + def init(title: str) -> None: nonlocal \ parsed, \ parsed_title, \ diff --git a/senpwai/utils/scraper.py b/senpwai/utils/scraper.py index 94b4790..0f16a66 100644 --- a/senpwai/utils/scraper.py +++ b/senpwai/utils/scraper.py @@ -12,7 +12,7 @@ import requests -from senpwai.utils.static import log_exception, try_deleting +from senpwai.utils.static import log_exception, try_deleting, OS PARSER = "html.parser" IBYTES_TO_MBS_DIVISOR = 1024 * 1024 @@ -285,7 +285,7 @@ def match_quality(potential_qualities: list[str], user_quality: str) -> int: def run_process_silently(args: list[str]) -> subprocess.CompletedProcess[bytes]: - if sys.platform == "win32": + if OS.is_windows: return subprocess.run(args, creationflags=subprocess.CREATE_NO_WINDOW) return subprocess.run(args) @@ -293,13 +293,13 @@ def run_process_silently(args: list[str]) -> subprocess.CompletedProcess[bytes]: def run_process_in_new_console( args: list[str] | str, ) -> subprocess.CompletedProcess[bytes]: - if sys.platform == "win32": + if OS.is_windows: return subprocess.run(args, creationflags=subprocess.CREATE_NEW_CONSOLE) return subprocess.run(args, shell=True) def try_installing_ffmpeg() -> bool: - if sys.platform == "win32": + if OS.is_windows: try: run_process_in_new_console("winget install Gyan.FFmpeg") # I should probably catch the specific exceptions but I'm too lazy to figure out all the possible exceptions diff --git a/senpwai/utils/static.py b/senpwai/utils/static.py index 7092cac..c5bb228 100644 --- a/senpwai/utils/static.py +++ b/senpwai/utils/static.py @@ -8,12 +8,20 @@ import logging APP_NAME = "Senpwai" -APP_NAME_LOWER = APP_NAME.lower() -VERSION = "2.1.3" +APP_NAME_LOWER = "senpwai" +VERSION = "2.1.4" DESCRIPTION = "A desktop app for tracking and batch downloading anime" IS_PIP_INSTALL = False APP_EXE_PATH = "" + + +class OS: + is_windows = sys.platform == "win32" + is_linux = sys.platform == "linux" + is_mac = sys.platform == "darwin" + + if getattr(sys, "frozen", False): # C:\Users\PC\AppData\Local\Programs\Senpwai ROOT_DIRECTORY = os.path.dirname(sys.executable) @@ -26,8 +34,8 @@ maybe_site_packages = os.path.dirname(ROOT_DIRECTORY) if os.path.basename(maybe_site_packages) == "site-packages": IS_PIP_INSTALL = True - # C:\Users\PC\AppData\Local\Programs\Python\Python311\Scripts\senpwai.exe - if sys.platform == "win32": + if OS.is_windows: + # C:\Users\PC\AppData\Local\Programs\Python\Python311\Scripts\senpwai.exe maybe_app_exe_path = os.path.join( os.path.dirname(os.path.dirname(maybe_site_packages)), "Scripts", @@ -41,18 +49,19 @@ date = datetime.today() IS_CHRISTMAS = True if date.month == 12 and date.day >= 20 else False -def log_exception(e: Exception): - custom_exception_handler(type(e), e, e.__traceback__) + +def log_exception(exception: Exception) -> None: + custom_exception_handler(type(exception), exception, exception.__traceback__) def custom_exception_handler( type_: type[BaseException], value: BaseException, traceback: TracebackType | None -): +) -> None: logging.error(f"Unhandled exception: {type_.__name__}: {value}") sys.__excepthook__(type_, value, traceback) -def try_deleting(path: str): +def try_deleting(path: str) -> None: if os.path.isfile(path): try: os.unlink(path) @@ -101,7 +110,7 @@ def generate_windows_setup_file_titles(app_name: str) -> tuple[str, str]: def open_folder(folder_path: str) -> None: - if sys.platform == "win32": + if OS.is_windows: os.startfile(folder_path) elif sys.platform == "linux": Popen(["xdg-open", folder_path]) @@ -109,31 +118,43 @@ def open_folder(folder_path: str) -> None: Popen(["open", folder_path]) -def requires_admin_access(folder_path): +def requires_admin_access(folder_path: str) -> bool: try: temp_file = os.path.join(folder_path, "temp.txt") open(temp_file, "w").close() - try_deleting(temp_file) + os.unlink(temp_file) return False except PermissionError: return True +def fix_qt_path_for_windows(path: str) -> str: + if OS.is_windows: + return path.replace("/", "\\") + return path + + +def fix_windows_path_for_qt(path: str) -> str: + if OS.is_windows: + return path.replace("\\", "/") + return path + + # Paths assets_path = os.path.join(ROOT_DIRECTORY, "assets") -def join_from_assets(file): - return os.path.join(assets_path, file) +def join_from_assets(dir_name: str) -> str: + return os.path.join(assets_path, dir_name) -misc_path = os.path.join(assets_path, "misc") +misc_path = join_from_assets("misc") -def join_from_misc(file): - return os.path.join(misc_path, file) +def join_from_misc(file_name: str) -> str: + return os.path.join(misc_path, file_name) SENPWAI_ICON_PATH = join_from_misc("senpwai-icon.ico") @@ -153,14 +174,13 @@ def join_from_misc(file): bckg_images_path = join_from_assets("background-images") -def join_from_bckg_images( - img_title, -): # fix windows path for Qt cause it only accepts forward slashes - return os.path.join(bckg_images_path, img_title).replace("\\", "/") +def join_from_bckg_images(img_name: str) -> str: + return fix_windows_path_for_qt(os.path.join(bckg_images_path, img_name)) -s = "christmas.jpg" if IS_CHRISTMAS else "search.jpg" -SEARCH_WINDOW_BCKG_IMAGE_PATH = join_from_bckg_images(s) +SEARCH_WINDOW_BCKG_IMAGE_PATH = join_from_bckg_images( + "christmas.jpg" if IS_CHRISTMAS else "search.jpg" +) CHOSEN_ANIME_WINDOW_BCKG_IMAGE_PATH = join_from_bckg_images("chosen-anime.jpg") SETTINGS_WINDOW_BCKG_IMAGE_PATH = join_from_bckg_images("settings.jpg") DOWNLOAD_WINDOW_BCKG_IMAGE_PATH = join_from_bckg_images("downloads.png") @@ -171,8 +191,8 @@ def join_from_bckg_images( link_icons_folder_path = join_from_assets("link-icons") -def join_from_link_icons(icon_path): - return os.path.join(link_icons_folder_path, icon_path) +def join_from_link_icons(icon_name: str) -> str: + return os.path.join(link_icons_folder_path, icon_name) GITHUB_SPONSORS_ICON_PATH = join_from_link_icons("github-sponsors.svg") @@ -184,8 +204,8 @@ def join_from_link_icons(icon_path): download_icons_folder_path = join_from_assets("download-icons") -def join_from_download_icons(icon_path): - return os.path.join(download_icons_folder_path, icon_path) +def join_from_download_icons(icon_name: str) -> str: + return os.path.join(download_icons_folder_path, icon_name) PAUSE_ICON_PATH = join_from_download_icons("pause.png") @@ -198,8 +218,8 @@ def join_from_download_icons(icon_path): audio_folder_path = join_from_assets("audio") -def join_from_audio(audio_path): - return os.path.join(audio_folder_path, audio_path) +def join_from_audio(audio_name: str) -> str: + return os.path.join(audio_folder_path, audio_name) GIGACHAD_AUDIO_PATH = join_from_audio("gigachad.mp3") @@ -219,19 +239,19 @@ def join_from_audio(audio_path): reviewer_profile_pics_folder_path = join_from_assets("reviewer-profile-pics") -def join_from_reviewer(icon_path): - return os.path.join(reviewer_profile_pics_folder_path, icon_path) +def join_from_reviewers(icon_name: str) -> str: + return os.path.join(reviewer_profile_pics_folder_path, icon_name) -SEN_ICON_PATH = join_from_reviewer("sen.png") -MORBIUS_IS_PEAK_ICON_PATH = join_from_reviewer("morbius-is-peak.png") -HENTAI_ADDICT_ICON_PATH = join_from_reviewer("hentai-addict.png") +SEN_ICON_PATH = join_from_reviewers("sen.png") +MORBIUS_IS_PEAK_ICON_PATH = join_from_reviewers("morbius-is-peak.png") +HENTAI_ADDICT_ICON_PATH = join_from_reviewers("hentai-addict.png") navigation_bar_icons_folder_path = join_from_assets("navigation-bar-icons") -def join_from_navbar(icon_path): - return os.path.join(navigation_bar_icons_folder_path, icon_path) +def join_from_navbar(icon_name: str) -> str: + return os.path.join(navigation_bar_icons_folder_path, icon_name) SEARCH_ICON_PATH = join_from_navbar("search.png") @@ -277,6 +297,7 @@ def join_from_navbar(icon_path): "fire force", "mieruko", "fumetsu", + "frieren", } L_ANIME = { "tokyo ghoul", diff --git a/senpwai/utils/widgets.py b/senpwai/utils/widgets.py index b0f745f..c7db28f 100644 --- a/senpwai/utils/widgets.py +++ b/senpwai/utils/widgets.py @@ -1,4 +1,3 @@ -import sys from time import time from typing import Callable, cast @@ -46,12 +45,6 @@ def set_minimum_size_policy(object): object.setFixedSize(object.sizeHint()) -def fix_qt_path_for_windows(path: str) -> str: - if sys.platform == "win32": - path = path.replace("/", "\\") - return path - - class BckgImg(QLabel): def __init__(self, parent: QWidget, image_path: str) -> None: super().__init__(parent) diff --git a/senpwai/windows/about.py b/senpwai/windows/about.py index 14b45a6..bf96d55 100644 --- a/senpwai/windows/about.py +++ b/senpwai/windows/about.py @@ -1,4 +1,3 @@ -from sys import platform as sysplatform from typing import TYPE_CHECKING from webbrowser import open_new_tab @@ -15,6 +14,7 @@ HENTAI_ADDICT_ICON_PATH, MORBIUS_AUDIO_PATH, MORBIUS_IS_PEAK_ICON_PATH, + OS, PATREON_ICON_PATH, REDDIT_ICON_PATH, SEN_ICON_PATH, @@ -36,8 +36,9 @@ if TYPE_CHECKING: from senpwai.windows.main import MainWindow + class AboutWindow(AbstractWindow): - def __init__(self, main_window: 'MainWindow'): + def __init__(self, main_window: "MainWindow"): super().__init__(main_window, ABOUT_BCKG_IMAGE_PATH) main_layout = QVBoxLayout() main_widget = ScrollableSection(main_layout) @@ -152,7 +153,7 @@ def __init__(self, main_window: 'MainWindow'): main_layout.addWidget(social_links_title) main_layout.addWidget(bug_reports_label) main_layout.addWidget(social_links_buttons_widget) - if sysplatform == "win32": + if OS.is_windows: main_layout.addWidget(uninstall_title) main_layout.addWidget(uninstall_info_label) main_layout.addWidget(version_title) diff --git a/senpwai/windows/abstracts.py b/senpwai/windows/abstracts.py index b3c2030..197ff49 100644 --- a/senpwai/windows/abstracts.py +++ b/senpwai/windows/abstracts.py @@ -33,7 +33,7 @@ def __init__(self, icon_path: str, switch_to_window_callable: Callable): class AbstractWindow(QWidget): - def __init__(self, main_window: 'MainWindow', bckg_img_path: str): + def __init__(self, main_window: "MainWindow", bckg_img_path: str): super().__init__(main_window) self.bckg_img_path = bckg_img_path self.full_layout = QHBoxLayout() @@ -66,7 +66,7 @@ def __init__(self, main_window: 'MainWindow', bckg_img_path: str): class AbstractTemporaryWindow(AbstractWindow): - def __init__(self, main_window: 'MainWindow', bckg_img_path: str): + def __init__(self, main_window: "MainWindow", bckg_img_path: str): super().__init__(main_window, bckg_img_path) for button in self.nav_bar_buttons: button.clicked.connect( diff --git a/senpwai/windows/chosen_anime.py b/senpwai/windows/chosen_anime.py index 0bf5162..e18348a 100644 --- a/senpwai/windows/chosen_anime.py +++ b/senpwai/windows/chosen_anime.py @@ -45,10 +45,13 @@ from senpwai.windows.download import DownloadWindow from senpwai.windows.settings import SettingsWindow from senpwai.windows.abstracts import AbstractTemporaryWindow +from senpwai.scrapers import gogo + # https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports/3957388#39757388 if TYPE_CHECKING: from senpwai.windows.main import MainWindow + class SummaryLabel(StyledLabel): def __init__(self, summary: str): super().__init__(font_size=18) @@ -83,7 +86,7 @@ def __init__( class MakeAnimeDetailsThread(QThread): finished = pyqtSignal(AnimeDetails) - def __init__(self, window: 'MainWindow', anime: Anime, site: str): + def __init__(self, window: "MainWindow", anime: Anime, site: str): super().__init__(window) self.anime = anime self.site = site @@ -94,7 +97,7 @@ def run(self): class ChosenAnimeWindow(AbstractTemporaryWindow): - def __init__(self, main_window: 'MainWindow', anime_details: AnimeDetails): + def __init__(self, main_window: "MainWindow", anime_details: AnimeDetails): super().__init__(main_window, CHOSEN_ANIME_WINDOW_BCKG_IMAGE_PATH) self.main_window = main_window self.anime_details = anime_details @@ -148,7 +151,8 @@ def __init__(self, main_window: 'MainWindow', anime_details: AnimeDetails): summary_widget.setMaximumHeight(300) right_widgets_layout.addWidget(summary_widget) - self.sub_button = SubDubButton(self, SUB, 18) + sub_or_dub = DUB if gogo.title_is_dub(self.anime_details.anime.title) else SUB + self.sub_button = SubDubButton(self, sub_or_dub, 18) set_minimum_size_policy(self.sub_button) self.dub_button = None self.sub_button.clicked.connect(lambda: self.update_sub_or_dub(SUB)) @@ -452,7 +456,8 @@ def handle_download_button_clicked(self): class EpisodeCount(StyledLabel): def __init__(self, count: str): super().__init__(None, 21, "rgba(255, 50, 0, 250)") - self.setText(f"{count} episodes") + eps_str = "episode" if count == "1" else "episodes" + self.setText(f"{count} {eps_str}") self.normal_style_sheet = self.styleSheet() self.setWordWrap(True) self.bright_stylesheet = ( diff --git a/senpwai/windows/download.py b/senpwai/windows/download.py index ed0abb5..3844197 100644 --- a/senpwai/windows/download.py +++ b/senpwai/windows/download.py @@ -50,6 +50,7 @@ ) from senpwai.windows.abstracts import AbstractWindow + # https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports/3957388#39757388 if TYPE_CHECKING: from senpwai.windows.main import MainWindow @@ -74,7 +75,7 @@ def update_count(self, added: int): class HlsEstimatedSize(CurrentAgainstTotal): - def __init__(self, download_window: 'DownloadWindow', total_episode_count: int): + def __init__(self, download_window: "DownloadWindow", total_episode_count: int): super().__init__(0, "MBs", parent=download_window) self.total_episode_count = total_episode_count self.current_episode_count = 0 @@ -97,7 +98,7 @@ def update_count(self, added: int): class DownloadedEpisodeCount(CurrentAgainstTotal): def __init__( self, - download_window: 'DownloadWindow', + download_window: "DownloadWindow", total_episodes: int, anime_title: str, anime_folder_path: str, @@ -198,7 +199,7 @@ def __init__( self, anime_details: AnimeDetails, progress_bar: ProgressBarWithoutButtons, - download_queue: 'DownloadQueue', + download_queue: "DownloadQueue", ): super().__init__() label = StyledLabel(font_size=14) @@ -313,7 +314,7 @@ def get_queued_downloads(self) -> list[QueuedDownload]: class DownloadWindow(AbstractWindow): - def __init__(self, main_window: 'MainWindow'): + def __init__(self, main_window: "MainWindow"): super().__init__(main_window, DOWNLOAD_WINDOW_BCKG_IMAGE_PATH) self.main_window = main_window self.main_layout = QVBoxLayout() @@ -349,11 +350,14 @@ def __init__(self, main_window: 'MainWindow'): self.auto_download_timer.timeout.connect(self.start_auto_download) self.setup_auto_download_timer() self.auto_download_thread: AutoDownloadThread | None = None - + def is_downloading(self) -> bool: if self.first_download_since_app_start: return False - if self.downloaded_episode_count.is_complete() or self.downloaded_episode_count.cancelled: + if ( + self.downloaded_episode_count.is_complete() + or self.downloaded_episode_count.cancelled + ): return False return True diff --git a/senpwai/windows/misc.py b/senpwai/windows/misc.py index 0877ab3..5533ea7 100644 --- a/senpwai/windows/misc.py +++ b/senpwai/windows/misc.py @@ -1,6 +1,5 @@ import os import subprocess -import sys from typing import TYPE_CHECKING, Any, Callable from webbrowser import open_new_tab @@ -26,6 +25,7 @@ GOGO_PRESSED_COLOR, IS_EXECUTABLE, IS_PIP_INSTALL, + OS, PAUSE_ICON_PATH, RED_HOVER_COLOR, RED_NORMAL_COLOR, @@ -50,13 +50,14 @@ AbstractTemporaryWindow, AbstractWindow, ) + # https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports/3957388#39757388 if TYPE_CHECKING: from senpwai.windows.main import MainWindow class MiscWindow(AbstractTemporaryWindow): - def __init__(self, main_window: 'MainWindow', misc_info_text: str): + def __init__(self, main_window: "MainWindow", misc_info_text: str): super().__init__(main_window, CHOPPER_CRYING_PATH) self.info_label = StyledLabel(font_size=25) self.info_label.setText(misc_info_text) @@ -76,7 +77,7 @@ def __init__(self, main_window: 'MainWindow', misc_info_text: str): class NewVersionInfoWindow(MiscWindow): - def __init__(self, main_window: 'MainWindow', info_text: str): + def __init__(self, main_window: "MainWindow", info_text: str): super().__init__(main_window, info_text) title = Title(f"Version {VERSION} Changes") set_minimum_size_policy(title) @@ -86,7 +87,7 @@ def __init__(self, main_window: 'MainWindow', info_text: str): class NoFFmpegWindow(MiscWindow): initiate_download_pipeline = pyqtSignal(AnimeDetails) - def __init__(self, main_window: 'MainWindow', anime_details: AnimeDetails): + def __init__(self, main_window: "MainWindow", anime_details: AnimeDetails): self.main_window = main_window info_text = "Sumanai, in order to use HLS mode you need to have\nFFmpeg installed and properly added to path" super().__init__(main_window, info_text) @@ -155,7 +156,7 @@ def run(self): class UpdateWindow(AbstractWindow): def __init__( self, - main_window: 'MainWindow', + main_window: "MainWindow", download_url: str, file_name: str, update_info: str, @@ -178,8 +179,10 @@ def __init__( before_click_label.setText( 'Run "pip install senpwai --upgrade" to update to the latest version' ) - main_layout.addWidget(before_click_label, alignment=Qt.AlignmentFlag.AlignCenter) - elif sys.platform == "win32" and IS_EXECUTABLE: + main_layout.addWidget( + before_click_label, alignment=Qt.AlignmentFlag.AlignCenter + ) + elif OS.is_windows and IS_EXECUTABLE: before_click_label.setText( "Before you click the update button, ensure you don't have any active downloads cause Senpwai will restart" ) @@ -205,9 +208,7 @@ def __init__( main_layout.addWidget(download_widget) download_widget.setLayout(self.download_layout) self.update_button.clicked.connect( - DownloadUpdateThread( - main_window, self, download_url, file_name - ).start + DownloadUpdateThread(main_window, self, download_url, file_name).start ) else: before_click_label.setText( @@ -219,9 +220,7 @@ def __init__( github_button = IconButton(Icon(300, 100, GITHUB_ICON_PATH), 1.1) github_button.clicked.connect(lambda: open_new_tab(GITHUB_REPO_URL)) github_button.setToolTip(GITHUB_REPO_URL) - main_layout.addWidget( - github_button, alignment=Qt.AlignmentFlag.AlignCenter - ) + main_layout.addWidget(github_button, alignment=Qt.AlignmentFlag.AlignCenter) set_minimum_size_policy(before_click_label) main_widget.setLayout(main_layout) @@ -258,7 +257,7 @@ class DownloadUpdateThread(QThread): def __init__( self, - main_window: 'MainWindow', + main_window: "MainWindow", update_window: UpdateWindow, download_url: str, file_name: str, @@ -304,7 +303,7 @@ class CheckIfUpdateAvailableThread(QThread): def __init__( self, - main_window: 'MainWindow', + main_window: "MainWindow", finished_callback: Callable[[tuple[bool, str, str, str]], Any], ): super().__init__(main_window) @@ -314,4 +313,3 @@ def run(self): self.finished.emit( update_available(GITHUB_API_LATEST_RELEASE_ENDPOINT, APP_NAME, VERSION) ) - diff --git a/senpwai/windows/search.py b/senpwai/windows/search.py index bf5e104..6cc7c9d 100644 --- a/senpwai/windows/search.py +++ b/senpwai/windows/search.py @@ -55,12 +55,14 @@ ) from senpwai.windows.abstracts import AbstractWindow + # https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports/3957388#39757388 if TYPE_CHECKING: from senpwai.windows.main import MainWindow + class SearchWindow(AbstractWindow): - def __init__(self, main_window: 'MainWindow'): + def __init__(self, main_window: "MainWindow"): super().__init__(main_window, SEARCH_WINDOW_BCKG_IMAGE_PATH) self.main_window = main_window main_widget = QWidget() @@ -370,7 +372,7 @@ class ResultButton(OutlinedButton): def __init__( self, anime: Anime, - main_window: 'MainWindow', + main_window: "MainWindow", search_window: SearchWindow, site: str, paint_x: int, diff --git a/senpwai/windows/settings.py b/senpwai/windows/settings.py index 7fca979..4b18dda 100644 --- a/senpwai/windows/settings.py +++ b/senpwai/windows/settings.py @@ -1,6 +1,5 @@ import os from typing import TYPE_CHECKING, Callable, cast -import sys from pylnk3 import for_file as pylnk3_for_file from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QFileDialog, QHBoxLayout, QLayoutItem, QVBoxLayout, QWidget @@ -19,6 +18,7 @@ GOGO_PRESSED_COLOR, IS_PIP_INSTALL, MINIMISED_TO_TRAY_ARG, + OS, PAHE, PAHE_HOVER_COLOR, PAHE_NORMAL_COLOR, @@ -33,6 +33,7 @@ SETTINGS_WINDOW_BCKG_IMAGE_PATH, SUB, requires_admin_access, + fix_qt_path_for_windows, ) from senpwai.utils.widgets import ( ErrorLabel, @@ -45,18 +46,19 @@ StyledButton, StyledLabel, SubDubButton, - fix_qt_path_for_windows, set_minimum_size_policy, ) from senpwai.windows.download import DownloadWindow from senpwai.windows.main import AbstractWindow + # https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports/3957388#39757388 if TYPE_CHECKING: from senpwai.windows.main import MainWindow + class SettingsWindow(AbstractWindow): - def __init__(self, main_window: 'MainWindow') -> None: + def __init__(self, main_window: "MainWindow") -> None: super().__init__(main_window, SETTINGS_WINDOW_BCKG_IMAGE_PATH) self.font_size = 15 main_layout = QHBoxLayout() @@ -94,7 +96,7 @@ def __init__(self, main_window: 'MainWindow') -> None: left_layout.addWidget(self.gogo_skip_calculate) left_layout.addWidget(self.make_download_complete_notification_setting) left_layout.addWidget(self.start_in_fullscreen) - if sys.platform == "win32" and not IS_PIP_INSTALL and APP_EXE_PATH: + if OS.is_windows and not IS_PIP_INSTALL and APP_EXE_PATH: self.run_on_startup = RunOnStartUp(self) left_layout.addWidget(self.run_on_startup) right_layout.addWidget(self.download_folder_setting) @@ -109,7 +111,7 @@ class FolderSetting(QWidget): def __init__( self, settings_window: SettingsWindow, - main_window: 'MainWindow', + main_window: "MainWindow", setting_info: str, setting_tool_tip: str | None, ): @@ -185,14 +187,16 @@ def update_widget_indices(self): cast(QLayoutItem, self.folder_widgets_layout.itemAt(idx)).widget(), ).index = idx - def change_from_folder_settings(self, new_folder_path: str, folder_widget: 'FolderWidget'): + def change_from_folder_settings( + self, new_folder_path: str, folder_widget: "FolderWidget" + ): SETTINGS.change_download_folder_path(folder_widget.index, new_folder_path) folder_widget.folder_path = new_folder_path folder_widget.folder_label.setText(new_folder_path) set_minimum_size_policy(folder_widget.folder_label) folder_widget.folder_label.update() - def remove_from_folder_settings(self, folder_widget: 'FolderWidget'): + def remove_from_folder_settings(self, folder_widget: "FolderWidget"): SETTINGS.pop_download_folder_path(folder_widget.index) folder_widget.deleteLater() self.update_widget_indices() @@ -218,7 +222,7 @@ def add_folder_to_settings(self): class DownloadFoldersSetting(FolderSetting): - def __init__(self, settings_window: SettingsWindow, main_window: 'MainWindow'): + def __init__(self, settings_window: SettingsWindow, main_window: "MainWindow"): super().__init__( settings_window, main_window, @@ -226,7 +230,7 @@ def __init__(self, settings_window: SettingsWindow, main_window: 'MainWindow'): f"{APP_NAME} will search these folders for anime episodes, in the order shown", ) - def remove_from_folder_settings(self, folder_widget: 'FolderWidget'): + def remove_from_folder_settings(self, folder_widget: "FolderWidget"): if len(SETTINGS.download_folder_paths) == 1: return self.error("Yarou!!! You must have at least one download folder") return super().remove_from_folder_settings(folder_widget) @@ -235,7 +239,7 @@ def remove_from_folder_settings(self, folder_widget: 'FolderWidget'): class FolderWidget(QWidget): def __init__( self, - main_window: 'MainWindow', + main_window: "MainWindow", folder_setting: FolderSetting, font_size: int, folder_path: str, diff --git a/setup.iss b/setup.iss index 54e0908..f81a3fb 100644 --- a/setup.iss +++ b/setup.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Senpwai" -#define MyAppVersion "2.1.3" +#define MyAppVersion "2.1.4" #define MyAppPublisher "AkatsuKi Inc." #define MyAppURL "https://github.com/SenZmaKi/Senpwai" #define MyAppExeName "Senpwai.exe" @@ -14,7 +14,7 @@ AppId={{B1AC746D-A6F0-44EF-B812-0D93F4571B51}} AppName={#MyAppName} AppVersion={#MyAppVersion} -VersionInfoVersion=2.1.3 +VersionInfoVersion=2.1.4 ;AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} diff --git a/setup.py b/setup.py index 498219f..edc1f06 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,9 @@ import sys -def duo_value_parser(file_path: str, split_str: str, ignore_if_startswith = ["#"]) -> list[tuple[str, str]]: +def duo_value_parser( + file_path: str, split_str: str, ignore_if_startswith=["#"] +) -> list[tuple[str, str]]: extracted: list[tuple[str, str]] = [] def process_str(s: str) -> str: @@ -91,4 +93,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/setup_senpcli.iss b/setup_senpcli.iss index c8fd578..2a07c16 100644 --- a/setup_senpcli.iss +++ b/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.3" +#define MyAppVersion "2.1.4" #define MyAppPublisher "AkatsuKi Inc." #define MyAppURL "https://github.com/SenZmaKi/Senpwai" #define MyAppExeName "Senpcli.exe" @@ -14,7 +14,7 @@ AppId={{7D4A0DD5-EACB-4593-81FC-325FCFF05BB6}} AppName={#MyAppName} AppVersion={#MyAppVersion} -VersionInfoVersion=2.1.3 +VersionInfoVersion=2.1.4 ;AppVerName={#MyAppName} {#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL}