From 7ef4fabd6443098f5245ef24f187364c14bd9285 Mon Sep 17 00:00:00 2001 From: amd64fox <62529699+amd64fox@users.noreply.github.com> Date: Sat, 9 Nov 2024 13:58:30 +0300 Subject: [PATCH] Update LoaderSpot.py - added sending unknown versions to the form - minor corrections --- LoaderSpot.py | 324 +++++++++++++++++++++++++++++++------------------- 1 file changed, 201 insertions(+), 123 deletions(-) diff --git a/LoaderSpot.py b/LoaderSpot.py index 6e0fb2c..63b49f3 100644 --- a/LoaderSpot.py +++ b/LoaderSpot.py @@ -1,152 +1,229 @@ import re +import json import aiohttp import asyncio from tqdm import tqdm +from typing import Optional, Tuple, List, Dict +from dataclasses import dataclass +from enum import Enum +import threading -async def check_url(session, url, platform_name): +class Platform(Enum): + WIN_X86 = "Win-x86" + WIN_X64 = "Win-x64" + WIN_ARM64 = "Win-arm64" + MACOS_INTEL = "macOS-intel" + MACOS_ARM64 = "macOS-arm64" + + +@dataclass +class SpotifyVersion: + version: str + start_number: int + end_number: int + + +class UrlGenerator: + BASE_URL = "https://upgrade.scdn.co/upgrade/client/" + PLATFORM_PATHS = { + Platform.WIN_X86: "win32-x86/spotify_installer-{version}-{number}.exe", + Platform.WIN_X64: "win32-x86_64/spotify_installer-{version}-{number}.exe", + Platform.WIN_ARM64: "win32-arm64/spotify_installer-{version}-{number}.exe", + Platform.MACOS_INTEL: "osx-x86_64/spotify-autoupdate-{version}-{number}.tbz", + Platform.MACOS_ARM64: "osx-arm64/spotify-autoupdate-{version}-{number}.tbz", + } + + @staticmethod + def generate_url(platform: Platform, version: str, number: int) -> str: + return f"{UrlGenerator.BASE_URL}{UrlGenerator.PLATFORM_PATHS[platform].format(version=version, number=number)}" + + +async def check_url( + session: aiohttp.ClientSession, url: str, platform: Platform +) -> Optional[Tuple[str, Platform]]: try: async with session.get(url) as response: if response.status == 200: - if platform_name: - return response.url, platform_name - else: - return response.url + return str(response.url), platform except aiohttp.ClientError: pass return None -async def main(version_spoti=""): - if not version_spoti: - while not re.match(r"^\d+\.\d+\.\d+\.\d+\.g[0-9a-f]{8}$", version_spoti): - version_spoti = input("Spotify version, for example 1.1.68.632.g2b11de83: ") +async def fetch_versions_json() -> dict: + async with aiohttp.ClientSession() as session: + async with session.get( + "https://raw.githubusercontent.com/amd64fox/LoaderSpot/refs/heads/main/versions.json" + ) as response: + if response.status == 200: + try: + return json.loads(await response.text()) + except json.JSONDecodeError: + return {} + return {} - start_number = "" - while not start_number.isdigit() or not 0 <= int(start_number) <= 20000: - start_number = input("Start search from: ") - before_enter = "" - while not before_enter.isdigit() or not 1 <= int(before_enter) <= 20000: - before_enter = input("End search at: ") - - numbers = int(start_number) - find_url = [] - - win32 = "https://upgrade.scdn.co/upgrade/client/win32-x86/spotify_installer-{version_spoti}-{numbers}.exe" - win64 = "https://upgrade.scdn.co/upgrade/client/win32-x86_64/spotify_installer-{version_spoti}-{numbers}.exe" - win_arm64 = "https://upgrade.scdn.co/upgrade/client/win32-arm64/spotify_installer-{version_spoti}-{numbers}.exe" - osx = "https://upgrade.scdn.co/upgrade/client/osx-x86_64/spotify-autoupdate-{version_spoti}-{numbers}.tbz" - osx_arm64 = "https://upgrade.scdn.co/upgrade/client/osx-arm64/spotify-autoupdate-{version_spoti}-{numbers}.tbz" - - print("\nSelect the link type for the search:") - print("[1] WIN32") - print("[2] WIN64") - print("[3] WIN-ARM64") - print("[4] OSX") - print("[5] OSX-ARM64") - print("[6] All platforms") - - choices = [] - while not choices: - choice = input("Enter the number(s): ") - choice_list = choice.split(",") - for c in choice_list: - c = c.strip() - if c == "1": - choices.append("1") - elif c == "2": - choices.append("2") - elif c == "3": - choices.append("3") - elif c == "4": - choices.append("4") - elif c == "5": - choices.append("5") - elif c == "6": - choices = ["1", "2", "3", "4", "5"] - break - else: - print("Value should be in the range from 1 to 6") - - print("\nSearching...\n") +async def submit_to_google_form(version: str, comment: str = "from LoaderSpot") -> None: + form_url = "https://docs.google.com/forms/u/0/d/e/1FAIpQLSdqIxSjqt2PcjBlQzhvwqc4QckfWuq5qqWsrdpoTidQHsPGpw/formResponse" + data = {"entry.1104502920": version, "entry.1319854718": comment} + async with aiohttp.ClientSession() as session: + try: + await session.post(form_url, data=data) + except Exception: + pass + + +async def check_version_and_submit(version: str) -> None: + try: + versions_json = await fetch_versions_json() + version_exists = any( + ver_data.get("fullversion") == version + for ver_data in versions_json.values() + ) + + if not version_exists: + await submit_to_google_form(version) + + except Exception: + pass + + +def validate_version(version: str) -> bool: + return bool(re.match(r"^\d+\.\d+\.\d+\.\d+\.g[0-9a-f]{8}$", version)) + + +def validate_number(number: str, min_val: int, max_val: int) -> bool: + return number.isdigit() and min_val <= int(number) <= max_val + + +def get_valid_input(prompt: str, validator: callable, error_message: str) -> str: + while True: + value = input(prompt) + if validator(value): + return value + print(error_message) + + +def get_version_input() -> str: + while True: + version = input("Spotify version, for example 1.1.68.632.g2b11de83: ") + if validate_version(version): + return version + print("Invalid version format") + + +def calculate_total_urls( + start_number: int, end_number: int, selected_platforms: List[Platform] +) -> int: + return (end_number - start_number + 1) * len(selected_platforms) + + +async def search_installers( + version_info: SpotifyVersion, selected_platforms: List[Platform] +) -> Dict[Platform, List[str]]: async with aiohttp.ClientSession() as session: tasks = [] + total_urls = calculate_total_urls( + version_info.start_number, version_info.end_number, selected_platforms + ) - if "6" in choices: # Updated condition to include option 6 - url_templates = [win32, win64, win_arm64, osx, osx_arm64] - platform_names = ["WIN32", "WIN64", "WIN-ARM64", "OSX", "OSX-ARM64"] - for url_template, platform_name in zip(url_templates, platform_names): - numbers = int(start_number) - while numbers <= int(before_enter): - url = url_template.format( - version_spoti=version_spoti, numbers=numbers - ) - tasks.append(check_url(session, url, platform_name)) - numbers += 1 - else: - for choice in choices: - if choice == "1": - url_template = win32 - platform_name = "WIN32" - elif choice == "2": - url_template = win64 - platform_name = "WIN64" - elif choice == "3": - url_template = win_arm64 - platform_name = "WIN-ARM64" - elif choice == "4": - url_template = osx - platform_name = "OSX" - elif choice == "5": - url_template = osx_arm64 - platform_name = "OSX-ARM64" - numbers = int(start_number) - while numbers <= int(before_enter): - url = url_template.format(version_spoti=version_spoti, numbers=numbers) - tasks.append(check_url(session, url, platform_name)) - numbers += 1 + for platform in selected_platforms: + for number in range(version_info.start_number, version_info.end_number + 1): + url = UrlGenerator.generate_url(platform, version_info.version, number) + tasks.append(check_url(session, url, platform)) - results = [] + results: Dict[Platform, List[str]] = {platform: [] for platform in Platform} - with tqdm(total=len(tasks)) as pbar: + with tqdm(total=total_urls) as pbar: for task in asyncio.as_completed(tasks): - result = await task - if result is not None: - find_url.append(result) + if result := await task: + url, platform = result + results[platform].append(url) pbar.update(1) - print("\nSearch completed:\n") - if find_url: - platform_urls = {} - for item in find_url: - if isinstance(item, tuple): - url, platform_name = item - if platform_name not in platform_urls: - platform_urls[platform_name] = [] - platform_urls[platform_name].append(url) - else: - if "Unknown" not in platform_urls: - platform_urls["Unknown"] = [] - platform_urls["Unknown"].append(item) + return results - sorting_order = ["WIN32", "WIN64", "WIN-ARM64", "OSX", "OSX-ARM64"] - for platform_name in sorting_order: - if platform_name in platform_urls: - print(f"{platform_name}:") - for url in platform_urls[platform_name]: - print(url) - print() - else: - print("Nothing found, consider increasing the search range\n") +def display_results(results: Dict[Platform, List[str]]) -> None: + found_any = False + for platform in Platform: + if urls := results[platform]: + found_any = True + print(f"\n{platform.value}:") + for url in urls: + print(url) - choice = input("Choose an option:\n" - "[1] Perform the search with a new version\n" - "[2] Perform the search again with the same version\n" - "[3] Exit\n" - "Enter the number: ") + if not found_any: + print("\nNothing found, consider increasing the search range") + + +def get_platform_choices() -> List[Platform]: + print("\nSelect the link type for the search:") + for i, platform in enumerate(Platform, 1): + print(f"[{i}] {platform.value}") + print("[6] All platforms") + + while True: + choices = input("Enter the number(s): ").strip().split(",") + + if not all(choice.strip() in "123456" for choice in choices): + print("Invalid input. Please enter numbers between 1 and 6") + continue + + if "6" in choices: + return list(Platform) + + selected = [] + for choice in choices: + platform_index = int(choice.strip()) - 1 + if 0 <= platform_index < len(Platform): + selected.append(list(Platform)[platform_index]) + + if selected: + return selected + + print("Please select at least one valid platform") + + +async def main(version_spoti: str = "") -> None: + if not version_spoti: + version_spoti = get_version_input() + + version_check_thread = threading.Thread( + target=lambda: asyncio.run(check_version_and_submit(version_spoti)), daemon=True + ) + version_check_thread.start() + + start_number = int( + get_valid_input( + "Start search from: ", + lambda x: validate_number(x, 0, 20000), + "Number should be between 0 and 20000", + ) + ) + end_number = int( + get_valid_input( + "End search at: ", + lambda x: validate_number(x, start_number, 20000), + f"Number should be between {start_number} and 20000", + ) + ) + + version_info = SpotifyVersion(version_spoti, start_number, end_number) + selected_platforms = get_platform_choices() + + print("\nSearching...\n") + results = await search_installers(version_info, selected_platforms) + display_results(results) + + print("\nChoose an option:") + print("[1] Perform the search with a new version") + print("[2] Perform the search again with the same version") + print("[3] Exit") + + choice = input("Enter the number: ") if choice == "1": print("\n") @@ -158,7 +235,8 @@ async def main(version_spoti=""): elif choice == "3": return else: - print("Invalid choice. Exiting the code.") + print("Invalid choice. Exiting the program.") -asyncio.run(main()) +if __name__ == "__main__": + asyncio.run(main())