Compare commits

..

No commits in common. "main" and "1.4" have entirely different histories.
main ... 1.4

24 changed files with 493 additions and 12386 deletions

View file

@ -1,195 +1,64 @@
name: Build LoaderSpot
name: Build Windows LoaderSpot
on:
workflow_dispatch:
jobs:
build_windows:
build:
runs-on: windows-latest
strategy:
matrix:
include:
- arch: x86
rust_target: i686-pc-windows-msvc
- arch: x64
rust_target: x86_64-pc-windows-msvc
- arch: arm64
rust_target: aarch64-pc-windows-msvc
architecture: [x86, x64]
steps:
- uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
targets: ${{ matrix.rust_target }}
- name: Cache cargo dependencies
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
LoaderSpot_UI/target
LoaderSpot_CLI/target
key: ${{ runner.os }}-cargo-${{ matrix.rust_target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ matrix.rust_target }}-
- name: Build release binaries
env:
RUST_TARGET: ${{ matrix.rust_target }}
ARCH: ${{ matrix.arch }}
shell: pwsh
python-version: '3.13'
architecture: ${{ matrix.architecture }}
- name: Install dependencies
run: |
$ErrorActionPreference = "Stop"
$out = "artifacts"
if (Test-Path $out) { Remove-Item $out -Recurse -Force }
New-Item -ItemType Directory -Path $out | Out-Null
Write-Host "Building UI for $env:RUST_TARGET"
cargo build --manifest-path LoaderSpot_UI/Cargo.toml --release --target $env:RUST_TARGET
$exeUI = Get-ChildItem "LoaderSpot_UI/target/$env:RUST_TARGET/release/*.exe" | Select-Object -First 1
Copy-Item $exeUI.FullName "$out\loaderspot-ui-win-$env:ARCH.exe"
Write-Host "UI executable ready: loaderspot-ui-win-$env:ARCH.exe"
Write-Host "Building CLI for $env:RUST_TARGET"
cargo build --manifest-path LoaderSpot_CLI/Cargo.toml --release --target $env:RUST_TARGET
$exeCLI = Get-ChildItem "LoaderSpot_CLI/target/$env:RUST_TARGET/release/*.exe" | Select-Object -First 1
Copy-Item $exeCLI.FullName "$out\loaderspot-cli-win-$env:ARCH.exe"
Write-Host "CLI executable ready: loaderspot-cli-win-$env:ARCH.exe"
- name: Upload Windows artifacts
uses: actions/upload-artifact@v7
python -m pip install --upgrade pip
pip install aiohttp
pip install tqdm
pip install pyinstaller
- name: Build executable
run: |
pyinstaller --onefile --clean --name LoaderSpot_${{ matrix.architecture }} LoaderSpot.py
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: windows-artifacts-${{ matrix.arch }}
path: artifacts/*
build_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: Cache cargo dependencies
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
LoaderSpot_UI/target
LoaderSpot_CLI/target
key: ${{ runner.os }}-cargo-x86_64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-x86_64-
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libssl-dev
- name: Build release binaries
run: |
mkdir -p artifacts
name: LoaderSpot_${{ matrix.architecture }}
path: dist/LoaderSpot_${{ matrix.architecture }}.exe
- name: Get latest release
id: latest_release
uses: actions/github-script@v7
with:
script: |
const releases = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo
});
return releases.data[0].id;
result-encoding: string
- name: Upload to Release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const releaseId = ${{ steps.latest_release.outputs.result }};
# Build UI for Linux
echo "Building UI for x86_64-unknown-linux-gnu"
cargo build --manifest-path LoaderSpot_UI/Cargo.toml --release --target x86_64-unknown-linux-gnu
cp "LoaderSpot_UI/target/x86_64-unknown-linux-gnu/release/loaderspot_ui" "artifacts/loaderspot-ui-linux-x64"
echo "UI executable ready: loaderspot-ui-linux-x64"
# Build CLI for Linux
echo "Building CLI for x86_64-unknown-linux-gnu"
cargo build --manifest-path LoaderSpot_CLI/Cargo.toml --release --target x86_64-unknown-linux-gnu
cp "LoaderSpot_CLI/target/x86_64-unknown-linux-gnu/release/loaderspot_cli" "artifacts/loaderspot-cli-linux-x64"
echo "CLI executable ready: loaderspot-cli-linux-x64"
- name: Upload Linux artifacts
uses: actions/upload-artifact@v7
with:
name: linux-artifacts-x64
path: artifacts/*
release:
needs: [build_windows, build_linux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
- name: Get latest release
id: latest_release
uses: actions/github-script@v8
with:
script: |
try {
const release = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo
});
return release.data.id;
} catch (error) {
core.setFailed(`No releases found: ${error.message}`);
}
- name: Upload assets to Release
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const path = require('path');
const releaseId = parseInt('${{ steps.latest_release.outputs.result }}', 10);
async function uploadAsset(filePath) {
const fileName = path.basename(filePath);
console.log(`Processing ${fileName}`);
const assets = await github.rest.repos.listReleaseAssets({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId
});
const existingAsset = assets.data.find(a => a.name === fileName);
if (existingAsset) {
console.log(`Deleting existing asset: ${fileName}`);
await github.rest.repos.deleteReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
asset_id: existingAsset.id
});
}
console.log(`Uploading: ${fileName}`);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
name: fileName,
data: fs.readFileSync(filePath)
});
}
const files = [];
const artifactDirs = fs.readdirSync('artifacts');
for (const dir of artifactDirs) {
const dirPath = path.join('artifacts', dir);
const dirFiles = fs.readdirSync(dirPath);
for (const file of dirFiles) {
files.push(path.join(dirPath, file));
}
}
for (let i = 0; i < files.length; i += 3) {
const batch = files.slice(i, i + 3);
await Promise.all(batch.map(uploadAsset));
}
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
name: `LoaderSpot_${{ matrix.architecture }}.exe`,
data: fs.readFileSync(`dist/LoaderSpot_${{ matrix.architecture }}.exe`)
});

View file

@ -1,34 +0,0 @@
name: Checking deb versions
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
jobs:
check-and-update:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
token: ${{ secrets.PERSONAL_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.12.2
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests packaging
- name: Run version checker
env:
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: python check_deb_versions.py

23
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: Version Check
on:
repository_dispatch:
types: [webhook-event]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12.2
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run Python script
run: python ${{ github.workspace }}/upd.py -v ${{ github.event.client_payload.v }} -s "${{ github.event.client_payload.s }}" -u "${{ secrets.GOOGLE_APPS_URL }}"

View file

@ -1,15 +1,17 @@
name: check Spotify MS
on:
schedule:
- cron: '25 * * * *'
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: run ms-spotify-check.ps1
shell: pwsh

View file

@ -1,51 +0,0 @@
name: Sync files to table
on:
# Temporarily disabled automatic sync trigger.
# Uncomment this block to restore automatic runs on push to main.
# push:
# branches:
# - main
# paths:
# - 'versions.json'
# - 'versions_deb.json'
workflow_dispatch:
jobs:
sync-files:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout source repo
uses: actions/checkout@v6
- name: Checkout target repo
uses: actions/checkout@v6
with:
repository: LoaderSpot/table
path: table
token: ${{ secrets.PERSONAL_TOKEN }}
- name: Copy files
run: |
mkdir -p table/table
cp versions.json table/table/
cp versions_deb.json table/table/
- name: Commit and push changes
env:
TARGET_REPO: "https://${{ secrets.PERSONAL_TOKEN }}@github.com/LoaderSpot/table.git"
run: |
cd table
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
if ! git diff --exit-code; then
git add table/
git commit -m "Auto-sync from LoaderSpot"
git push "$TARGET_REPO" main
else
echo "No changes to commit"
fi

View file

@ -8,33 +8,24 @@ on:
required: true
version:
description: 'Another version parameter'
required: true
buildType:
description: 'Build type (e.g., Master, Release)'
required: false
default: 'false'
required: true
jobs:
update-version:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
token: ${{ secrets.PERSONAL_TOKEN }}
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v5
with:
python-version: 3.12.2
- name: Update version in versions.json
run: |
data="${{ github.event.inputs.datajson }}"
build_type="${{ github.event.inputs.buildType }}"
python3 update_version.py "$data" "$build_type"
python3 -c "import json, re; data=json.loads('$data'); file_data=json.load(open('versions.json')); file_data[list(data.keys())[0]] = data[list(data.keys())[0]]; file_data = dict(sorted(file_data.items(), key=lambda x: [int(part) if part.isdigit() else part for part in re.split(r'(\d+)', x[0])], reverse=True)); json.dump(file_data, open('versions.json', 'w'), indent=2)"
- name: Commit changes
run: |
@ -42,4 +33,4 @@ jobs:
git config --local user.name "GitHub Action"
git add versions.json
git commit -m "Added version ${{ github.event.inputs.version }}"
git push "https://${{ secrets.PERSONAL_TOKEN }}@github.com/${{ github.repository }}.git" HEAD:main
git push

View file

@ -1,54 +0,0 @@
name: Version Check
on:
repository_dispatch:
types: [webhook-event]
concurrency:
group: delayed-version-check
cancel-in-progress: false
jobs:
version-search:
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.search.outputs.versions }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download latest loaderspot_cli_linux_x64
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download --repo ${{ github.repository }} --pattern "loaderspot-cli-linux-x64" --output loaderspot_cli
chmod +x loaderspot_cli
- name: Version search
id: search
if: github.event.client_payload.v && github.event.client_payload.s
env:
v: ${{ github.event.client_payload.v }}
s: ${{ github.event.client_payload.s }}
run: |
{
echo 'versions<<EOF'
./loaderspot_cli --version "$v" --connections 300 --ladder-search
echo 'EOF'
} >> "$GITHUB_OUTPUT"
get-build-and-send:
runs-on: windows-latest
needs: version-search
if: success() && needs.version-search.outputs.versions
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Get build info and send to GAS
shell: pwsh
env:
GOOGLE_APPS_URL: ${{ secrets.GOOGLE_APPS_URL }}
VERSIONS: ${{ needs.version-search.outputs.versions }}
SOURCE: ${{ github.event.client_payload.s }}
run: |
./check-build-version.ps1 -versions "$env:VERSIONS" -source "$env:SOURCE" -googleAppsUrl "$env:GOOGLE_APPS_URL"

2
.gitignore vendored
View file

@ -1,2 +0,0 @@
/LoaderSpot_CLI/target
/LoaderSpot_UI/target

242
LoaderSpot.py Normal file
View file

@ -0,0 +1,242 @@
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
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:
return str(response.url), platform
except aiohttp.ClientError:
pass
return None
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 {}
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
)
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: Dict[Platform, List[str]] = {platform: [] for platform in Platform}
with tqdm(total=total_urls) as pbar:
for task in asyncio.as_completed(tasks):
if result := await task:
url, platform = result
results[platform].append(url)
pbar.update(1)
return results
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)
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")
await main()
elif choice == "2":
print("\n")
print(f"Search version: {version_spoti}")
await main(version_spoti)
elif choice == "3":
return
else:
print("Invalid choice. Exiting the program.")
if __name__ == "__main__":
asyncio.run(main())

2118
LoaderSpot_CLI/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +0,0 @@
[package]
name = "loaderspot_cli"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
clap = { version = "4.0", features = ["derive"] }
indicatif = "0.17.7"
regex = "1"
scraper = "0.19.0"

View file

@ -1,341 +0,0 @@
use clap::Parser;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use serde::Serialize;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Semaphore;
use regex::Regex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
enum PlatformArch {
WinX86,
WinX64,
WinArm64,
MacOsIntel,
MacOsArm64,
}
impl PlatformArch {
fn path_template(&self) -> &str {
match self {
PlatformArch::WinX86 => "win32-x86/spotify_installer-{version}-{number}.exe",
PlatformArch::WinX64 => "win32-x86_64/spotify_installer-{version}-{number}.exe",
PlatformArch::WinArm64 => "win32-arm64/spotify_installer-{version}-{number}.exe",
PlatformArch::MacOsIntel => "osx-x86_64/spotify-autoupdate-{version}-{number}.tbz",
PlatformArch::MacOsArm64 => "osx-arm64/spotify-autoupdate-{version}-{number}.tbz",
}
}
fn to_string(&self) -> &str {
match self {
PlatformArch::WinX86 => "WIN32",
PlatformArch::WinX64 => "WIN64",
PlatformArch::WinArm64 => "WIN-ARM64",
PlatformArch::MacOsIntel => "OSX",
PlatformArch::MacOsArm64 => "OSX-ARM64",
}
}
}
struct UrlGenerator;
impl UrlGenerator {
const BASE_URL: &'static str = "https://upgrade.scdn.co/upgrade/client/";
fn generate_url(platform: PlatformArch, version: &str, number: i32) -> String {
let path = platform
.path_template()
.replace("{version}", version)
.replace("{number}", &number.to_string());
format!("{}{}", Self::BASE_URL, path)
}
}
fn extract_base_version(version: &str) -> String {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() >= 3 {
format!("{}.{}.{}", parts[0], parts[1], parts[2])
} else {
version.to_string()
}
}
fn should_use_win_x86(version: &str) -> bool {
let base_version = extract_base_version(version);
let parts: Vec<&str> = base_version.split('.').collect();
if parts.len() >= 3 {
if let (Ok(major), Ok(minor), Ok(patch)) = (
parts[0].parse::<u32>(),
parts[1].parse::<u32>(),
parts[2].parse::<u32>(),
) {
return (major, minor, patch) <= (1, 2, 53);
}
}
true
}
async fn check_url(client: &Client, url: String, platform: PlatformArch) -> Option<(String, PlatformArch)> {
match client.head(&url).send().await {
Ok(response) if response.status().is_success() => Some((url, platform)),
_ => None,
}
}
async fn search_installers(
client: &Client,
version: &str,
start: i32,
end: i32,
platform: PlatformArch,
max_connections: usize,
) -> Vec<(String, PlatformArch)> {
let semaphore = Arc::new(Semaphore::new(max_connections));
let mut tasks = Vec::new();
let found_urls = Arc::new(tokio::sync::Mutex::new(Vec::new()));
for number in start..=end {
let url = UrlGenerator::generate_url(platform, version, number);
let client = client.clone();
let permit = semaphore.clone().acquire_owned().await.unwrap();
let found_urls_clone = found_urls.clone();
let task = tokio::spawn(async move {
let result = check_url(&client, url, platform).await;
drop(permit);
if let Some(found_data) = result {
let mut guard = found_urls_clone.lock().await;
guard.push(found_data);
}
});
tasks.push(task);
}
for task in tasks {
let _ = task.await;
}
let guard = found_urls.lock().await;
guard.clone()
}
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None, disable_version_flag = true)]
struct Cli {
/// Spotify version(s) to search for
#[clap(long, required = true, use_value_delimiter = true, value_delimiter = ',')]
version: Vec<String>,
/// Range of build numbers to check (e.g., 0-5000)
#[clap(long, default_value = "0-5000")]
range: String,
/// Architecture(s)
#[clap(long, use_value_delimiter = true, value_delimiter = ',', value_parser = ["x86", "x64", "arm64", "intel", "all"], default_value = "all")]
arch: Vec<String>,
/// Platform(s)
#[clap(long, name = "os", use_value_delimiter = true, value_delimiter = ',', value_parser = ["win", "mac", "all"], default_value = "all")]
platform: Vec<String>,
/// Number of concurrent connections
#[clap(long, default_value_t = 100)]
connections: usize,
/// Use ladder search algorithm
#[clap(long)]
ladder_search: bool,
}
fn parse_range(range_str: &str) -> (i32, i32) {
let parts: Vec<&str> = range_str.split('-').collect();
if parts.len() == 2 {
let start = parts[0].parse().unwrap_or(0);
let end = parts[1].parse().unwrap_or(5000);
(start, end)
} else {
(0, 5000)
}
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let platforms = if cli.platform.contains(&"all".to_string()) {
vec!["win", "mac"]
} else {
cli.platform.iter().map(|s| s.as_str()).collect()
};
let arches = if cli.arch.contains(&"all".to_string()) {
if platforms.contains(&"win") && platforms.contains(&"mac") {
vec!["x86", "x64", "arm64", "intel", "arm64"]
} else if platforms.contains(&"win") {
vec!["x86", "x64", "arm64"]
} else if platforms.contains(&"mac") {
vec!["intel", "arm64"]
} else {
vec![]
}
} else {
cli.arch.iter().map(|s| s.as_str()).collect()
};
let mut platform_arches = Vec::new();
for platform in &platforms {
for arch in &arches {
let platform_arch = match (*platform, *arch) {
("win", "x86") => Some(PlatformArch::WinX86),
("win", "x64") => Some(PlatformArch::WinX64),
("win", "arm64") => Some(PlatformArch::WinArm64),
("mac", "intel") => Some(PlatformArch::MacOsIntel),
("mac", "arm64") => Some(PlatformArch::MacOsArm64),
_ => None,
};
if let Some(pa) = platform_arch {
if !platform_arches.contains(&pa) {
platform_arches.push(pa);
}
}
}
}
if platform_arches.is_empty() {
eprintln!("Error: No valid platform and architecture combinations provided.");
std::process::exit(1);
}
let connections = cli.connections.clamp(50, 300);
let range = cli.range.clone();
let client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap();
let spinner_style = ProgressStyle::with_template("{spinner}")
.unwrap()
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"); // A classic rotating spinner
let pb = ProgressBar::new_spinner();
pb.set_style(spinner_style);
pb.enable_steady_tick(Duration::from_millis(80));
let versions = cli.version.clone();
let client_clone = client.clone();
let ladder_search = cli.ladder_search;
let versions_clone_for_task = versions.clone();
let search_task = tokio::spawn(async move {
let mut results = Vec::new();
for version in &versions_clone_for_task {
let mut all_found_urls_for_version = Vec::new();
let mut arches_to_search = platform_arches.clone();
if !should_use_win_x86(version) && arches_to_search.contains(&PlatformArch::WinX86) {
if versions_clone_for_task.len() == 1 && arches_to_search.len() == 1 {
eprintln!("Warning: x86 architecture for Windows is no longer supported for versions newer than 1.2.53.");
continue;
}
arches_to_search.retain(|&p| p != PlatformArch::WinX86);
}
if ladder_search {
let mut start_number = 0;
let mut before_enter = 1000;
let additional_searches = 15;
let increment = 1000;
for &platform_arch in &arches_to_search {
let found = search_installers(&client_clone, version, start_number, before_enter, platform_arch, connections).await;
all_found_urls_for_version.extend(found);
}
for _ in 0..additional_searches {
let latest_urls = get_latest_urls(&all_found_urls_for_version);
let target_len = arches_to_search.iter().filter(|&&p| p != PlatformArch::WinX86 || should_use_win_x86(version)).count();
if latest_urls.len() >= target_len {
break;
}
start_number = before_enter + 1;
before_enter += increment;
let mut missing_arches = Vec::new();
for &platform_arch in &arches_to_search {
if !latest_urls.contains_key(platform_arch.to_string()) {
missing_arches.push(platform_arch);
}
}
for &platform_arch in &missing_arches {
let found = search_installers(&client_clone, version, start_number, before_enter, platform_arch, connections).await;
all_found_urls_for_version.extend(found);
}
}
} else {
let (start, end) = parse_range(&range);
for &platform_arch in &arches_to_search {
let found = search_installers(&client_clone, version, start, end, platform_arch, connections).await;
all_found_urls_for_version.extend(found);
}
}
let mut latest_urls = get_latest_urls(&all_found_urls_for_version);
latest_urls.insert("version".to_string(), version.clone());
results.push(latest_urls);
}
results
});
let results = search_task.await.unwrap();
pb.finish_and_clear();
if versions.len() == 1 {
let json_output = serde_json::to_string_pretty(&results[0]).unwrap();
println!("{}", json_output);
} else {
let json_output = serde_json::to_string_pretty(&results).unwrap();
println!("{}", json_output);
}
}
fn get_latest_urls(found_urls: &[(String, PlatformArch)]) -> HashMap<String, String> {
let mut platform_urls = HashMap::new();
let version_pattern = Regex::new(r"-(\d+)\.(exe|tbz)$").unwrap();
for (url, platform) in found_urls {
if let Some(captures) = version_pattern.captures(url) {
if let Some(version_number_match) = captures.get(1) {
let version_number = version_number_match.as_str().parse::<u32>().unwrap();
let platform_key = platform.to_string().to_string();
let entry = platform_urls.entry(platform_key).or_insert_with(|| (url.clone(), version_number));
if version_number > entry.1 {
*entry = (url.clone(), version_number);
}
}
}
}
let latest_urls: HashMap<String, String> = platform_urls
.into_iter()
.map(|(k, (v, _))| (k, v))
.collect();
if latest_urls.is_empty() {
let mut empty_map = HashMap::new();
empty_map.insert("unknown".to_string(), "unknown".to_string());
return empty_map;
}
latest_urls
}

4803
LoaderSpot_UI/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +0,0 @@
[package]
name = "loaderspot_ui"
version = "0.1.0"
edition = "2021"
[dependencies]
eframe = "0.29"
egui = "0.29"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
regex = "1.10"
crossbeam-channel = "0.5"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,11 @@
<p align="center">
<a href="https://loadspot.pages.dev"><img src="https://img.shields.io/badge/Excel%20table--brightgreen.svg?style=flat&logo=microsoftexcel&label=Table"></a>
<a href="https://t.me/OfficialSpotifyUpdates"><img src="https://img.shields.io/badge/Telegram- -blue?logo=telegram" alt="Telegram"></a>
<a href="https://docs.google.com/spreadsheets/d/1wztO1L4zvNykBRw7X4jxP8pvo11oQjT0O5DvZ_-S4Ok/edit#gid=0"><img src="https://img.shields.io/badge/Excel%20table--brightgreen.svg?style=flat&logo=microsoftexcel&label=Excel table"></a>
</p>
<h1 align="center">LoaderSpot</h1>
<h2 align="center">This tool finds full Spotify installers and also collects them</h2>
<h2 align="center">Tool for finding full Spotify installers.</h2>
<p align="center">
<h3 align="center">I also created an updatable document, with all the links I found. You can see it <a href="https://docs.google.com/spreadsheets/d/1wztO1L4zvNykBRw7X4jxP8pvo11oQjT0O5DvZ_-S4Ok/edit#gid=0">here</a></h3>
</p>
<p align="center">Thanks for the idea <a href="https://github.com/nick-botticelli/SpotifyUpgradeFinder">nick-botticelli</a></p>

View file

@ -1,125 +0,0 @@
param (
[string]$versions,
[string]$source,
[string]$googleAppsUrl
)
function Find-BuildInfo {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)]
[string]$Path
)
process {
try {
$fileContent = Get-Content -Path $Path -ErrorAction Stop
$found = $false
$regex = '(Master|Release|PR|Local) Build.+(?:cef_)?(\d+\.\d+\.\d+\+g[0-9a-f]+\+chromium-\d+\.\d+\.\d+\.\d+)'
foreach ($line in $fileContent) {
$match = [regex]::Match($line, $regex)
if ($match.Success) {
$buildType = $match.Groups[1].Value
Write-Host "build type: $buildType"
return $buildType
}
}
if (-not $found) {
Write-Host "Билд не найден"
return $false
}
}
catch {
Write-Error "Ошибка при чтении файла: $_"
return $false
}
}
}
function Get-BuildTypeFromUrl {
[CmdletBinding()]
param(
[string]$Url
)
$tempPath = Join-Path ([System.IO.Path]::GetTempPath()) "spotify"
$destinationPath = Join-Path $tempPath "unpacked"
$exePath = Join-Path $tempPath "SpotifySetup.exe"
if (-not (Test-Path -Path $tempPath)) {
New-Item -ItemType Directory -Path $tempPath | Out-Null
}
if (-not (Test-Path -Path $destinationPath)) {
New-Item -ItemType Directory -Path $destinationPath | Out-Null
}
try {
Write-Host "Скачивание файла из $Url..."
Invoke-WebRequest -Uri $Url -OutFile $exePath
Write-Host "Файл сохранен в $exePath"
Write-Host "Распаковка файла в $destinationPath"
Start-Process -Wait -FilePath $exePath -ArgumentList "/extract `"$destinationPath`""
$dllPath = Join-Path $destinationPath "Spotify.dll"
$exePathForAnalysis = Join-Path $destinationPath "Spotify.exe"
if (Test-Path $dllPath) {
return Find-BuildInfo -Path $dllPath
} elseif (Test-Path $exePathForAnalysis) {
return Find-BuildInfo -Path $exePathForAnalysis
}
else {
Write-Error "Файлы Spotify.dll и Spotify.exe не найдены в $destinationPath"
return $false
}
}
catch {
Write-Error "Произошла ошибка при скачивании или распаковке: $_"
return $false
}
}
if ([string]::IsNullOrEmpty($versions) -or [string]::IsNullOrEmpty($source) -or [string]::IsNullOrEmpty($googleAppsUrl)) {
Write-Error "Один или несколько обязательных параметров (versions, source, googleAppsUrl) не предоставлены."
exit 1
}
$versionsObj = $versions | ConvertFrom-Json
$win64Url = $versionsObj.WIN64
$buildType = $false
if ($win64Url) {
$buildType = Get-BuildTypeFromUrl -Url $win64Url
}
if ($buildType -eq $false) {
$versionsObj | Add-Member -NotePropertyName "buildType" -NotePropertyValue $false -Force
} else {
$versionsObj | Add-Member -NotePropertyName "buildType" -NotePropertyValue $buildType -Force
}
$versionsObj | Add-Member -NotePropertyName "source" -NotePropertyValue $source -Force
$finalJson = $versionsObj | ConvertTo-Json -Compress
Write-Host "Отправка данных на GAS..."
try {
$response = Invoke-WebRequest -Uri $googleAppsUrl `
-Method POST `
-ContentType "application/json" `
-Body $finalJson `
-UseBasicParsing -ErrorAction Stop
if ($response.StatusCode -eq 200) {
Write-Host "Ответ от GAS: $($response.Content)"
} else {
Write-Error "Ошибка при отправке в GAS. Статус: $($response.StatusCode). Ответ: $($response.Content)"
}
} catch {
Write-Error "Критическая ошибка при отправке в GAS: $_"
}

View file

@ -1,282 +0,0 @@
import re
import json
import requests
from packaging import version
import sys
from datetime import datetime
import os
import subprocess
import tempfile
from requests.exceptions import RequestException
import time
VERSIONS_JSON_FILE = "versions_deb.json"
SPOTIFY_REPO_URL = (
"https://repository-origin.spotify.com/pool/non-free/s/spotify-client/"
)
REPO_UPLOAD = "LoaderSpot/deb-builds"
def get_spotify_versions_from_repo():
urls = [
"https://repository.spotify.com/dists/testing/non-free/binary-amd64/Packages",
SPOTIFY_REPO_URL,
]
version_pattern = r"(\d+\.\d+\.\d+\.\d+\.g[0-9a-f]{8})"
versions = set()
for url in urls:
try:
response = requests.get(url)
response.raise_for_status()
found_versions = re.findall(version_pattern, response.text)
versions.update(found_versions)
except requests.exceptions.RequestException as e:
print(f"Ошибка при запросе {url}: {e}")
min_version = version.parse("1.1.58")
return [
v for v in versions if version.parse(".".join(v.split(".")[:3])) > min_version
]
def get_existing_versions():
try:
with open(VERSIONS_JSON_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Ошибка при чтении файла {VERSIONS_JSON_FILE}: {e}")
return {}
def parse_version_key(full_version):
match = re.match(r"(\d+\.\d+\.\d+\.\d+)\..*", full_version)
return match.group(1) if match else None
def find_new_versions(repo_versions, existing_data):
new_versions = []
for full_version in repo_versions:
version_key = parse_version_key(full_version)
if version_key and version_key not in existing_data:
new_versions.append((version_key, full_version))
return new_versions
def get_file_info(full_version):
url = f"{SPOTIFY_REPO_URL}spotify-client_{full_version}_amd64.deb"
try:
response = requests.head(url, timeout=10)
if response.status_code == 200:
size = response.headers.get("Content-Length", "0")
last_modified = response.headers.get("Last-Modified", "")
if last_modified:
date_obj = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z")
formatted_date = date_obj.strftime("%d.%m.%Y")
else:
formatted_date = ""
return {"size": size, "data": formatted_date}
else:
print(f"Ошибка HEAD запроса к {url}: {response.status_code}")
except Exception as e:
print(f"Ошибка при запросе информации о файле: {e}")
return {"size": "0", "data": ""}
def sort_versions(versions_dict):
def version_key(item):
return tuple(map(int, item[0].split(".")))
return dict(sorted(versions_dict.items(), key=version_key, reverse=True))
def update_json_file(sorted_data):
try:
with open(VERSIONS_JSON_FILE, "w", encoding="utf-8") as f:
json.dump(sorted_data, f, indent=2)
print(f"Файл {VERSIONS_JSON_FILE} успешно обновлен")
return True
except Exception as e:
print(f"Ошибка при обновлении файла {VERSIONS_JSON_FILE}: {e}")
return False
def commit_changes(new_versions):
try:
github_actions = os.environ.get("GITHUB_ACTIONS") == "true"
if github_actions:
subprocess.run(["git", "config", "--global", "user.name", "GitHub Actions"])
subprocess.run(
["git", "config", "--global", "user.email", "actions@github.com"]
)
subprocess.run(["git", "add", VERSIONS_JSON_FILE])
# Формируем сообщение коммита в зависимости от количества новых версий
if new_versions:
commit_message = (
f"Added deb version {new_versions[0][1]}"
if len(new_versions) == 1
else f"Added deb versions: {', '.join(v[1] for v in new_versions)}"
)
else:
commit_message = "Update Spotify versions"
# Создаем коммит
subprocess.run(["git", "commit", "-m", commit_message])
if github_actions:
token = get_github_token()
if token:
repo_url = f"https://{token}@github.com/{os.environ.get('GITHUB_REPOSITORY')}.git"
subprocess.run(["git", "push", repo_url, "HEAD:main"])
else:
subprocess.run(["git", "push"])
else:
subprocess.run(["git", "push"])
print("Изменения успешно отправлены в репозиторий")
return True
except Exception as e:
print(f"Ошибка при отправке изменений в репозиторий: {e}")
return False
def download_deb_file(full_version):
url = f"{SPOTIFY_REPO_URL}spotify-client_{full_version}_amd64.deb"
filename = f"spotify-client_{full_version}_amd64.deb"
try:
print(f"Скачивание {filename}...")
with tempfile.NamedTemporaryFile(delete=False, suffix=".deb") as temp_file:
response = requests.get(url, stream=True, timeout=60)
response.raise_for_status()
for chunk in response.iter_content(chunk_size=8192):
if chunk:
temp_file.write(chunk)
return temp_file.name, filename
except RequestException as e:
print(f"Ошибка при скачивании файла: {e}")
return None, None
def get_github_token():
token = os.environ.get("PERSONAL_TOKEN")
if not token:
print("ОШИБКА: Не найден PERSONAL_TOKEN в переменных окружения")
return token
def get_latest_release(token):
headers = {
"Authorization": f"token {token}",
"Accept": "application/vnd.github.v3+json",
}
url = f"https://api.github.com/repos/{REPO_UPLOAD}/releases/latest"
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
except RequestException as e:
print(f"Ошибка при получении последнего релиза: {e}")
return None
def upload_file_to_release(token, release_id, file_path, file_name):
headers = {
"Authorization": f"token {token}",
"Accept": "application/vnd.github.v3+json",
"Content-Type": "application/octet-stream",
}
url = f"https://uploads.github.com/repos/{REPO_UPLOAD}/releases/{release_id}/assets?name={file_name}"
try:
with open(file_path, "rb") as file:
print(f"Загрузка {file_name} в релиз...")
response = requests.post(url, headers=headers, data=file)
response.raise_for_status()
print(f"Файл {file_name} успешно загружен в релиз")
return True
except RequestException as e:
print(f"Ошибка при загрузке файла в релиз: {e}")
return False
finally:
# Удаляем временный файл
try:
os.remove(file_path)
except Exception as e:
print(f"Не удалось удалить временный файл {file_path}: {e}")
def main():
# Получаем версии из репозитория Spotify
spotify_versions = get_spotify_versions_from_repo()
# Получаем существующие версии из JSON
existing_data = get_existing_versions()
# Находим новые версии
new_versions = find_new_versions(spotify_versions, existing_data)
if not new_versions:
print("Новых версий не найдено")
return False
print(f"Найдено новых версий: {len(new_versions)}")
# Если есть новые версии, получаем токен GitHub и загружаем файлы
github_token = get_github_token()
if github_token:
latest_release = get_latest_release(github_token)
if latest_release:
release_id = latest_release["id"]
# Скачиваем и загружаем каждую новую версию
for version_key, full_version in new_versions:
temp_file_path, file_name = download_deb_file(full_version)
if temp_file_path:
time.sleep(2)
upload_file_to_release(
github_token, release_id, temp_file_path, file_name
)
# Добавляем новые версии в существующие данные
for version_key, full_version in new_versions:
file_info = get_file_info(full_version)
existing_data[version_key] = {
"fullversion": full_version,
"size": file_info["size"],
"data": file_info["data"],
"links": {
"amd64": f"https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_{full_version}_amd64.deb"
},
}
# Сортируем и обновляем данные
sorted_data = sort_versions(existing_data)
if update_json_file(sorted_data):
# Если мы находимся в GitHub Actions, выполняем коммит
if os.environ.get("GITHUB_ACTIONS") == "true":
commit_changes(new_versions)
print("Обновление выполнено успешно")
return True
if __name__ == "__main__":
main()
sys.exit(0)

View file

@ -46,19 +46,19 @@ function Get-LatestSpotifyVersion {
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
$url = "https://ru.store.rg-adguard.net/api/GetFiles"
$url = "https://store.rg-adguard.net/api/GetFiles"
$body = "type=url&url=https://apps.microsoft.com/detail/9ncbcszsjrsb&gl=US&ring=RP&lang=en"
$headers = @{
"authority" = "ru.store.rg-adguard.net"
"authority" = "store.rg-adguard.net"
"method" = "POST"
"path" = "/api/GetFiles"
"scheme" = "https"
"accept" = "*/*"
"accept-language" = "ru,ru-RU;q=0.9,en;q=0.8"
"dnt" = "1"
"origin" = "https://ru.store.rg-adguard.net"
"origin" = "https://store.rg-adguard.net"
"priority" = "u=1, i"
"referer" = "https://ru.store.rg-adguard.net/"
"referer" = "https://store.rg-adguard.net/"
"sec-ch-ua" = '"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"'
"sec-ch-ua-mobile" = "?0"
"sec-ch-ua-platform" = '"Windows"'
@ -94,7 +94,7 @@ function Get-LatestSpotifyVersion {
Url = $url
}
}
$filteredResults = $results | Where-Object { $_.FileName -match 'SpotifyAB\.SpotifyMusic_.+x64.+\.appx' }
$filteredResults = $results | Where-Object { $_.FileName -match 'SpotifyAB\.SpotifyMusic_.+x86.+\.appx' }
$latestFile = $filteredResults | Sort-Object -Property DateTime -Descending | Select-Object -First 1
if (-not $latestFile) {
@ -158,13 +158,14 @@ function Extract-SpotifyAppx {
function Get-SpotifyExeVersion {
param ($spotifyExePath)
Write-Log "Getting version from $([System.IO.Path]::GetFileName($spotifyExePath))..."
Write-Log "Getting version from file $([System.IO.Path]::GetFileName($spotifyExePath))..."
$exeContent = Get-Content -Path $spotifyExePath -Raw
$regex = '(?<![\w\-])(\d+)\.(\d+)\.(\d+)\.(\d+)(\.g[0-9a-f]{8})(?![\w\-])'
if ($exeContent -match $regex) {
Write-Log "Version received successfully: $($matches[0])"
return $matches[0]
}
Write-Log "Version not found in file $([System.IO.Path]::GetFileName($spotifyExePath))"
return $null
}
@ -243,7 +244,7 @@ function Main {
return
}
$spotifyExePath = Get-ChildItem -Path $spotifyTempDir -Filter "Spotify.dll" -Recurse | Select-Object -First 1
$spotifyExePath = Get-ChildItem -Path $spotifyTempDir -Filter "Spotify.exe" -Recurse | Select-Object -First 1
if (-not $spotifyExePath) {
Write-Log "Spotify.exe not found"
return

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
aiohttp
asyncio
beautifulsoup4
argparse

151
upd.py Normal file
View file

@ -0,0 +1,151 @@
import aiohttp
import asyncio
import json
import re
import argparse
from bs4 import BeautifulSoup
parser = argparse.ArgumentParser()
parser.add_argument("-v", required=True)
parser.add_argument("-s", required=True)
parser.add_argument("-u", required=True)
args = parser.parse_args()
version = args.v
source = args.s
u = args.u
async def send_request(json_data):
url = u + json_data
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
data = await response.text()
soup = BeautifulSoup(data, "html.parser")
system_response = soup.find(
"div",
style="text-align:center;font-family:monospace;margin:50px auto 0;max-width:600px",
).text
print(f"Ответ от GAS: {system_response}")
async def check_url(session, url, platform_name):
try:
async with session.get(url) as response:
if response.status == 200:
if platform_name:
return response.url, platform_name
else:
return response.url
except aiohttp.ClientError:
pass
return None
async def pre_version(latest_urls):
message = f'Версия {version} {"отправлена" if latest_urls else "не найдена"}'
print(message)
if not latest_urls:
latest_urls = {"unknown": "unknown", "version": version}
latest_urls["version"] = version
latest_urls["source"] = source
req_ver = json.dumps(latest_urls, ensure_ascii=False, indent=2)
await send_request(req_ver)
def get_urls(find_url):
platform_urls = {}
version_pattern = re.compile(r"-([0-9]+)\.(exe|tbz)")
for url, platform in find_url:
match = version_pattern.search(str(url))
if match:
version_number = int(match.group(1))
if platform in platform_urls:
# Если найдено больше одного url для платформы, то выбираем с самой большой ревизией
current_version = int(
version_pattern.search(platform_urls[platform]).group(1)
)
if version_number > current_version:
platform_urls[platform] = str(url)
else:
platform_urls[platform] = str(url)
return platform_urls
async def main():
start_number = 0
before_enter = 1000
additional_searches = 10 # Максимальное кол-во шагов для дополнительного поиска
increment = 1000 # Размер шага
numbers = start_number
find_url = []
root_url = "https://upgrade.scdn.co/upgrade/client/"
platform_templates = {
"WIN32": "win32-x86/spotify_installer-{version}-{numbers}.exe",
"WIN64": "win32-x86_64/spotify_installer-{version}-{numbers}.exe",
"WIN-ARM64": "win32-arm64/spotify_installer-{version}-{numbers}.exe",
"OSX": "osx-x86_64/spotify-autoupdate-{version}-{numbers}.tbz",
"OSX-ARM64": "osx-arm64/spotify-autoupdate-{version}-{numbers}.tbz",
}
async with aiohttp.ClientSession() as session:
tasks = []
platform_names = list(platform_templates.keys())
for platform_name in platform_names:
numbers = start_number
while numbers <= before_enter:
url = root_url + platform_templates[platform_name].format(
version=version, numbers=numbers
)
tasks.append(check_url(session, url, platform_name))
numbers += 1
results = []
for task in asyncio.as_completed(tasks):
result = await task
if result is not None:
find_url.append(result)
# Дополнительный поиск
for i in range(additional_searches):
if len(get_urls(find_url)) < len(
platform_names
): # Если не найдены все платформы
start_number = before_enter + 1
before_enter += increment
tasks = []
for platform_name in platform_names:
numbers = start_number
while numbers <= before_enter:
url = root_url + platform_templates[platform_name].format(
version=version, numbers=numbers
)
tasks.append(check_url(session, url, platform_name))
numbers += 1
for task in asyncio.as_completed(tasks):
result = await task
if result is not None:
find_url.append(result)
latest_urls = get_urls(find_url)
if len(latest_urls) == len(platform_names):
await pre_version(latest_urls)
return
if find_url:
latest_urls = get_urls(find_url)
await pre_version(latest_urls)
else:
await pre_version(False)
asyncio.run(main())

View file

@ -1,63 +0,0 @@
#!/usr/bin/env python3
import json
import re
import sys
def update_version(data_json: str, build_type: str = None, version_file: str = "versions.json") -> bool:
try:
data = json.loads(data_json)
version_key = list(data.keys())[0]
version_data = data[version_key]
if build_type and build_type.lower() != 'false':
version_data = {'buildType': build_type, **version_data}
with open(version_file, 'r', encoding='utf-8') as f:
file_data = json.load(f)
file_data[version_key] = version_data
sorted_data = dict(
sorted(
file_data.items(),
key=lambda x: [
int(part) if part.isdigit() else part
for part in re.split(r'(\d+)', x[0])
],
reverse=True
)
)
with open(version_file, 'w', encoding='utf-8') as f:
json.dump(sorted_data, f, indent=2)
print(f"Version {version_key} added to {version_file}")
return True
except json.JSONDecodeError as e:
print(f"JSON parse error: {e}", file=sys.stderr)
return False
except FileNotFoundError:
print(f"File {version_file} not found", file=sys.stderr)
return False
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return False
def main():
if len(sys.argv) < 2:
print("Usage: python update_version.py '<json_data>' [build_type] [version_file]", file=sys.stderr)
sys.exit(1)
data_json = sys.argv[1]
build_type = sys.argv[2] if len(sys.argv) > 2 else None
version_file = sys.argv[3] if len(sys.argv) > 3 else "versions.json"
success = update_version(data_json, build_type, version_file)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load diff

View file

@ -1,274 +0,0 @@
{
"1.2.84.476": {
"fullversion": "1.2.84.476.ga1ff6607",
"size": "157643240",
"data": "09.03.2026",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.84.476.ga1ff6607_amd64.deb"
}
},
"1.2.82.428": {
"fullversion": "1.2.82.428.g0ac8be2b",
"size": "156375346",
"data": "09.02.2026",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.82.428.g0ac8be2b_amd64.deb"
}
},
"1.2.79.427": {
"fullversion": "1.2.79.427.g80eb4a07",
"size": "155456648",
"data": "12.01.2026",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.79.427.g80eb4a07_amd64.deb"
}
},
"1.2.79.425": {
"fullversion": "1.2.79.425.g1d0fcf61",
"size": "155381166",
"data": "17.12.2025",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.79.425.g1d0fcf61_amd64.deb"
}
},
"1.2.77.358": {
"fullversion": "1.2.77.358.g4339a634",
"size": "155510286",
"data": "25.11.2025",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.77.358.g4339a634_amd64.deb"
}
},
"1.2.74.477": {
"fullversion": "1.2.74.477.g3be53afe",
"size": "154653044",
"data": "20.10.2025",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.74.477.g3be53afe_amd64.deb"
}
},
"1.2.63.394": {
"fullversion": "1.2.63.394.g126b0d89",
"size": "147984434",
"data": "19.05.2025",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.63.394.g126b0d89_amd64.deb"
}
},
"1.2.60.564": {
"fullversion": "1.2.60.564.gcc6305cb",
"size": "144421884",
"data": "07.04.2025",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.60.564.gcc6305cb_amd64.deb"
}
},
"1.2.59.514": {
"fullversion": "1.2.59.514.g834e17d4",
"size": "143491584",
"data": "13.03.2025",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.59.514.g834e17d4_amd64.deb"
}
},
"1.2.59.513": {
"fullversion": "1.2.59.513.g9e00425b",
"size": "143470202",
"data": "12.03.2025",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.59.513.g9e00425b_amd64.deb"
}
},
"1.2.56.502": {
"fullversion": "1.2.56.502.ga68d2d4f",
"size": "142990750",
"data": "28.01.2025",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.56.502.ga68d2d4f_amd64.deb"
}
},
"1.2.53.440": {
"fullversion": "1.2.53.440.g7b2f582a",
"size": "143507202",
"data": "02.01.2025",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.53.440.g7b2f582a_amd64.deb"
}
},
"1.2.52.442": {
"fullversion": "1.2.52.442.g01893f92",
"size": "143523470",
"data": "02.12.2024",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.52.442.g01893f92_amd64.deb"
}
},
"1.2.50.335": {
"fullversion": "1.2.50.335.g5e2860a8",
"size": "143617148",
"data": "11.11.2024",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.50.335.g5e2860a8_amd64.deb"
}
},
"1.2.48.405": {
"fullversion": "1.2.48.405.gf2c48e6f",
"size": "143394450",
"data": "08.10.2024",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.48.405.gf2c48e6f_amd64.deb"
}
},
"1.2.47.364": {
"fullversion": "1.2.47.364.gf06e5cee",
"size": "109100472",
"data": "20.09.2024",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.47.364.gf06e5cee_amd64.deb"
}
},
"1.2.45.454": {
"fullversion": "1.2.45.454.gc16ec9f6",
"size": "141143192",
"data": "26.08.2024",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.45.454.gc16ec9f6_amd64.deb"
}
},
"1.2.42.290": {
"fullversion": "1.2.42.290.g242057a2",
"size": "140926882",
"data": "12.07.2024",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.42.290.g242057a2_amd64.deb"
}
},
"1.2.40.599": {
"fullversion": "1.2.40.599.g606b7f29",
"size": "140956866",
"data": "17.06.2024",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.40.599.g606b7f29_amd64.deb"
}
},
"1.2.37.701": {
"fullversion": "1.2.37.701.ge66eb7bc",
"size": "137805232",
"data": "03.05.2024",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.37.701.ge66eb7bc_amd64.deb"
}
},
"1.2.31.1205": {
"fullversion": "1.2.31.1205.g4d59ad7c",
"size": "135223836",
"data": "09.02.2024",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.31.1205.g4d59ad7c_amd64.deb"
}
},
"1.2.26.1187": {
"fullversion": "1.2.26.1187.g36b715a1",
"size": "133729904",
"data": "04.12.2023",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.26.1187.g36b715a1_amd64.deb"
}
},
"1.2.25.1011": {
"fullversion": "1.2.25.1011.g0348b2ea",
"size": "132587766",
"data": "20.11.2023",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.25.1011.g0348b2ea_amd64.deb"
}
},
"1.2.25.1009": {
"fullversion": "1.2.25.1009.g075ce884",
"size": "132533054",
"data": "17.11.2023",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.25.1009.g075ce884_amd64.deb"
}
},
"1.2.22.982": {
"fullversion": "1.2.22.982.g794acc0a",
"size": "124729466",
"data": "06.10.2023",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.22.982.g794acc0a_amd64.deb"
}
},
"1.2.20.1210": {
"fullversion": "1.2.20.1210.g2a8a8a57",
"size": "124183006",
"data": "08.09.2023",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.20.1210.g2a8a8a57_amd64.deb"
}
},
"1.2.18.999": {
"fullversion": "1.2.18.999.g9b38fc27",
"size": "122391766",
"data": "14.08.2023",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.18.999.g9b38fc27_amd64.deb"
}
},
"1.2.13.661": {
"fullversion": "1.2.13.661.ga588f749",
"size": "121793588",
"data": "05.06.2023",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.13.661.ga588f749_amd64.deb"
}
},
"1.2.11.916": {
"fullversion": "1.2.11.916.geb595a67",
"size": "121053308",
"data": "11.05.2023",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.11.916.geb595a67_amd64.deb"
}
},
"1.2.8.923": {
"fullversion": "1.2.8.923.g4f94bf0d",
"size": "119841576",
"data": "27.03.2023",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.2.8.923.g4f94bf0d_amd64.deb"
}
},
"1.1.84.716": {
"fullversion": "1.1.84.716.gc5f8b819",
"size": "119770140",
"data": "22.04.2022",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.1.84.716.gc5f8b819_amd64.deb"
}
},
"1.1.72.439": {
"fullversion": "1.1.72.439.gc253025e",
"size": "117803268",
"data": "10.11.2021",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.1.72.439.gc253025e_amd64.deb"
}
},
"1.1.68.632": {
"fullversion": "1.1.68.632.g2b11de83",
"size": "112586002",
"data": "20.09.2021",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.1.68.632.g2b11de83_amd64.deb"
}
},
"1.1.67.586": {
"fullversion": "1.1.67.586.gbb5ef64e",
"size": "118938508",
"data": "31.08.2021",
"links": {
"amd64": "https://github.com/LoaderSpot/deb-builds/releases/download/builds/spotify-client_1.1.67.586.gbb5ef64e_amd64.deb"
}
}
}