Compare commits

..

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

25 changed files with 1202 additions and 15068 deletions

View file

@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View file

@ -1,195 +0,0 @@
name: Build LoaderSpot
on:
workflow_dispatch:
jobs:
build_windows:
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
steps:
- uses: actions/checkout@v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
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
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
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
# 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));
}

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

View file

@ -1,18 +0,0 @@
name: check Spotify MS
on:
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: run ms-spotify-check.ps1
shell: pwsh
run: ./ms-spotify-check.ps1
env:
Token: ${{ secrets.Token }}

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

@ -1,45 +0,0 @@
name: Update Version
on:
workflow_dispatch:
inputs:
datajson:
description: 'Version to update'
required: true
version:
description: 'Another version parameter'
required: true
buildType:
description: 'Build type (e.g., Master, Release)'
required: false
default: 'false'
jobs:
update-version:
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: Update version in versions.json
run: |
data="${{ github.event.inputs.datajson }}"
build_type="${{ github.event.inputs.buildType }}"
python3 update_version.py "$data" "$build_type"
- name: Commit changes
run: |
git config --local user.email "action@github.com"
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

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

129
LoaderSpot.py Normal file
View file

@ -0,0 +1,129 @@
import re
import requests
from concurrent.futures import ThreadPoolExecutor
def check_url(url, platform_name):
try:
response = requests.get(url)
if response.status_code == 200:
if platform_name:
return response.url, platform_name
else:
return response.url
else:
print(f"\nInvalid link: {url}")
response.close()
except requests.exceptions.RequestException:
pass
return None
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: ")
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: ")
max_workers_req = ""
while True:
max_workers_req = input("Number of threads: ")
try:
max_workers = int(max_workers_req)
if 1 <= max_workers <= 150:
break
else:
print("Value should be in the range from 1 to 150")
except ValueError:
print("Invalid value, please enter an integer")
numbers = int(start_number)
find_url = []
win32 = "https://upgrade.scdn.co/upgrade/client/win32-x86/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("Select the link type for the search:")
print("[1] WIN32")
print("[2] WIN-ARM64")
print("[3] OSX")
print("[4] OSX-ARM64")
print("[5] ALL")
choice = None
while choice not in ["1", "2", "3", "4", "5"]:
choice = input("Enter the number: ")
if choice == "1":
url_template = win32
platform_name = "WIN32"
elif choice == "2":
url_template = win_arm64
platform_name = "WIN-ARM64"
elif choice == "3":
url_template = osx
platform_name = "OSX"
elif choice == "4":
url_template = osx_arm64
platform_name = "OSX-ARM64"
elif choice == "5":
url_templates = [win32, win_arm64, osx, osx_arm64]
platform_names = ["WIN32", "WIN-ARM64", "OSX", "OSX-ARM64"]
else:
print("Value should be in the range from 1 to 5")
print("Searching...")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
tasks = []
if choice == "5":
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(executor.submit(check_url, url, platform_name))
numbers += 1
else:
while numbers <= int(before_enter):
url = url_template.format(version_spoti=version_spoti, numbers=numbers)
tasks.append(executor.submit(check_url, url, platform_name))
numbers += 1
for task in tasks:
result = task.result()
if result is not None:
find_url.append(result)
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)
for platform_name, urls in platform_urls.items():
print(f"{platform_name}:")
for url in urls:
print(url)
print()
else:
print("Nothing found, consider increasing the search range.")
print("\n")
input("Press Enter to exit")

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,9 @@
<p align="center"> <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://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>
<a href="https://t.me/OfficialSpotifyUpdates"><img src="https://img.shields.io/badge/Telegram- -blue?logo=telegram" alt="Telegram"></a>
</p> </p>
<h1 align="center">LoaderSpot</h1> <h1 align="center">LoaderSpot</h1>
<h2 align="center">This tool finds full Spotify installers and also collects them</h2> <h2 align="center">A tool for downloading the full Spotify Desktop client.</h2>
<p align="center">
<p align="center">Thanks for the idea <a href="https://github.com/nick-botticelli/SpotifyUpgradeFinder">nick-botticelli</a></p> <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>

45
archive/LoaderSpot.ps1 Normal file
View file

@ -0,0 +1,45 @@
do { $version_spoti = Read-Host -Prompt "Enter the Spotify version, for example 1.1.68.632.g2b11de83" }
while ($version_spoti -notmatch '^\d.\d.\d{1,2}.\d{1,5}.[a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z]$')
do { $before_enter = Read-Host -Prompt "Enter the number to stop at (e.g. 99)" }
while ($before_enter -notmatch '^\d{1,4}$')
$numbers = 0
"Search..."
While ($numbers -le $before_enter) {
$_URL = "https://upgrade.scdn.co/upgrade/client/win32-x86/spotify_installer-$version_spoti-$numbers.exe"
try {
$request = [System.Net.WebRequest]::Create($_URL)
$response = $request.getResponse()
if ($response.StatusCode -eq "200") {
$response.ResponseUri.OriginalString
$find_url += [System.Environment]::NewLine + $response.ResponseUri.OriginalString
$response.Close()
}
}
catch {
$numbers
}
$numbers++
}
write-host "`n"
"Search completed"
write-host "`n"
if ($find_url) {
"Found links :"
$find_url
}
if (!($find_url)) {
"Nothing found, please increase your search range."
}
write-host "`n"
pause
exit

514
archive/LoaderSpot_Gui.ps1 Normal file
View file

@ -0,0 +1,514 @@
Add-Type -Name Window -Namespace Console -MemberDefinition '
[DllImport("Kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
'
function Hide-Console {
$consolePtr = [Console.Window]::GetConsoleWindow()
#0 hide
[Console.Window]::ShowWindow($consolePtr, 0)
}
Hide-Console
function Show-Search_psf {
[void][reflection.assembly]::Load('System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
[void][reflection.assembly]::Load('System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
try {
[ProgressBarOverlay] | Out-Null
}
catch {
if ($PSVersionTable.PSVersion.Major -ge 7) {
$Assemblies = 'System.Windows.Forms', 'System.Drawing', 'System.Drawing.Primitives', 'System.ComponentModel.Primitives', 'System.Drawing.Common', 'System.Runtime'
}
else {
$Assemblies = 'System.Windows.Forms', 'System.Drawing'
}
Add-Type -ReferencedAssemblies $Assemblies -TypeDefinition @"
using System;
using System.Windows.Forms;
using System.Drawing;
namespace SAPIENTypes
{
public class ProgressBarOverlay : System.Windows.Forms.ProgressBar
{
public ProgressBarOverlay() : base() { SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); }
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == 0x000F)// WM_PAINT
{
if (Style != System.Windows.Forms.ProgressBarStyle.Marquee || !string.IsNullOrEmpty(this.Text))
{
using (Graphics g = this.CreateGraphics())
{
using (StringFormat stringFormat = new StringFormat(StringFormatFlags.NoWrap))
{
stringFormat.Alignment = StringAlignment.Center;
stringFormat.LineAlignment = StringAlignment.Center;
if (!string.IsNullOrEmpty(this.Text))
g.DrawString(this.Text, this.Font, Brushes.Black, this.ClientRectangle, stringFormat);
else
{
int percent = (int)(((double)Value / (double)Maximum) * 100);
g.DrawString(percent.ToString() + "%", this.Font, Brushes.Black, this.ClientRectangle, stringFormat);
}
}
}
}
}
}
public string TextOverlay
{
get
{
return base.Text;
}
set
{
base.Text = value;
Invalidate();
}
}
}
}
"@ -IgnoreWarnings | Out-Null
}
[System.Windows.Forms.Application]::EnableVisualStyles()
$formFindingASpotifyClien = New-Object 'System.Windows.Forms.Form'
$progressbar1 = New-Object 'SAPIENTypes.ProgressBarOverlay'
$textbox1 = New-Object 'System.Windows.Forms.TextBox'
$linklabelFoundLinks = New-Object 'System.Windows.Forms.LinkLabel'
$groupbox2 = New-Object 'System.Windows.Forms.GroupBox'
$labelEnterSpotifyVersion = New-Object 'System.Windows.Forms.Label'
$maskedtextbox1 = New-Object 'System.Windows.Forms.MaskedTextBox'
$labelForExample1182758g8b = New-Object 'System.Windows.Forms.Label'
$groupbox1 = New-Object 'System.Windows.Forms.GroupBox'
$labelMaximumNumberFromFou = New-Object 'System.Windows.Forms.Label'
$labelEnterSearchRange = New-Object 'System.Windows.Forms.Label'
$labelTo = New-Object 'System.Windows.Forms.Label'
$maskedtextbox2 = New-Object 'System.Windows.Forms.MaskedTextBox'
$labelFrom = New-Object 'System.Windows.Forms.Label'
$maskedtextbox3 = New-Object 'System.Windows.Forms.MaskedTextBox'
$buttonStartSearch = New-Object 'System.Windows.Forms.Button'
$buttonExit = New-Object 'System.Windows.Forms.Button'
$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
function Update-ListBox {
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[System.Windows.Forms.ListBox]
$ListBox,
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Items,
[Parameter(Mandatory = $false)]
[string]
$DisplayMember,
[Parameter(Mandatory = $false)]
[string]$ValueMember,
[switch]
$Append
)
if (-not $Append) {
$listBox.Items.Clear()
}
if ($Items -is [System.Windows.Forms.ListBox+ObjectCollection]) {
$listBox.Items.AddRange($Items)
}
elseif ($Items -is [Array]) {
$listBox.BeginUpdate()
foreach ($obj in $Items) {
$listBox.Items.Add($obj)
}
$listBox.EndUpdate()
}
else {
$listBox.Items.Add($Items)
}
if ($DisplayMember) {
$listBox.DisplayMember = $DisplayMember
}
if ($ValueMember) {
$ListBox.ValueMember = $ValueMember
}
}
function Update-ComboBox {
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[System.Windows.Forms.ComboBox]
$ComboBox,
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Items,
[Parameter(Mandatory = $false)]
[string]$DisplayMember,
[Parameter(Mandatory = $false)]
[string]$ValueMember,
[switch]
$Append
)
if (-not $Append) {
$ComboBox.Items.Clear()
}
if ($Items -is [Object[]]) {
$ComboBox.Items.AddRange($Items)
}
elseif ($Items -is [System.Collections.IEnumerable]) {
$ComboBox.BeginUpdate()
foreach ($obj in $Items) {
$ComboBox.Items.Add($obj)
}
$ComboBox.EndUpdate()
}
else {
$ComboBox.Items.Add($Items)
}
if ($DisplayMember) {
$ComboBox.DisplayMember = $DisplayMember
}
if ($ValueMember) {
$ComboBox.ValueMember = $ValueMember
}
}
$buttonStartSearch_Click = {
# Search button logic
if ($maskedtextbox2.Text -notmatch "\D" -and $maskedtextbox3.Text -notmatch "\D") {
$ErrorActionPreference = 'SilentlyContinue'
$version_spoti = $maskedtextbox1.Text
$numbers = [int]$maskedtextbox2.Text
$before_enter = [int]$maskedtextbox3.Text
if ($before_enter -gt $numbers) {
if ($version_spoti -match '^\d.\d.\d{1,2}.\d{1,5}.[a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z]$') {
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText("Search...")
$numbers_bar = 0
$progressbar1.Minimum = 0
$progressbar1.Maximum = $before_enter - $numbers
While ($numbers -le $before_enter) {
$progressbar1.Value = $numbers_bar
$numbers_bar++
$_URL = "https://upgrade.scdn.co/upgrade/client/win32-x86/spotify_installer-$version_spoti-$numbers.exe"
try {
$request = [System.Net.WebRequest]::Create($_URL)
$response = $request.getResponse()
if ($response.StatusCode -eq "200") {
$response.ResponseUri.OriginalString
$find_url += [System.Environment]::NewLine + $response.ResponseUri.OriginalString
$response.Close()
}
}
catch
{ }
$numbers++
}
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText([System.Environment]::NewLine + "Search completed")
$textbox1.AppendText([System.Environment]::NewLine + "")
if ($find_url) {
$textbox1.AppendText([System.Environment]::NewLine + "Found links:")
$textbox1.AppendText([System.Environment]::NewLine + $find_url)
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
if (!($find_url)) {
$textbox1.AppendText([System.Environment]::NewLine + "The search returned no results, try increasing the range.")
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
}
else {
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText("Unsuccessfully")
$textbox1.AppendText([System.Environment]::NewLine + "Spotify version entered incorrectly")
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
}
else {
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText("Unsuccessfully")
$textbox1.AppendText([System.Environment]::NewLine + "Search range entered incorrectly")
$textbox1.AppendText([System.Environment]::NewLine + "The start range cannot be greater than the end range.")
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
}
else {
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText("Unsuccessfully")
$textbox1.AppendText([System.Environment]::NewLine + "Search range entered incorrectly")
$textbox1.AppendText([System.Environment]::NewLine + "Enter only numbers")
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
}
$buttonExit_Click = {
# Exit button logic
$formFindingASpotifyClien.hide()
}
$linklabelFoundLinks_LinkClicked = [System.Windows.Forms.LinkLabelLinkClickedEventHandler] {
#Event Argument: $_ = [System.Windows.Forms.LinkLabelLinkClickedEventArgs]
Start-Process "https://cutt.ly/8EH6NuH"
}
$Form_StateCorrection_Load =
{
#Correct the initial state of the form to prevent the .Net maximized form issue
$formFindingASpotifyClien.WindowState = $InitialFormWindowState
}
$Form_Cleanup_FormClosed =
{
#Remove all event handlers from the controls
try {
$linklabelFoundLinks.remove_LinkClicked($linklabelFoundLinks_LinkClicked)
$buttonStartSearch.remove_Click($buttonStartSearch_Click)
$buttonExit.remove_Click($buttonExit_Click)
$formFindingASpotifyClien.remove_Load($Form_StateCorrection_Load)
$formFindingASpotifyClien.remove_FormClosed($Form_Cleanup_FormClosed)
}
catch { Out-Null <# Prevent PSScriptAnalyzer warning #> }
}
$formFindingASpotifyClien.SuspendLayout()
$groupbox2.SuspendLayout()
$groupbox1.SuspendLayout()
#
# formFindingASpotifyClien
#
$formFindingASpotifyClien.Controls.Add($progressbar1)
$formFindingASpotifyClien.Controls.Add($textbox1)
$formFindingASpotifyClien.Controls.Add($linklabelFoundLinks)
$formFindingASpotifyClien.Controls.Add($groupbox2)
$formFindingASpotifyClien.Controls.Add($groupbox1)
$formFindingASpotifyClien.Controls.Add($buttonStartSearch)
$formFindingASpotifyClien.Controls.Add($buttonExit)
$formFindingASpotifyClien.AutoScaleDimensions = New-Object System.Drawing.SizeF(6, 13)
$formFindingASpotifyClien.AutoScaleMode = 'Font'
$formFindingASpotifyClien.AutoSizeMode = 'GrowAndShrink'
$formFindingASpotifyClien.ClientSize = New-Object System.Drawing.Size(444, 370)
$formFindingASpotifyClien.MaximizeBox = $False
$formFindingASpotifyClien.Name = 'formFindingASpotifyClien'
$formFindingASpotifyClien.ShowIcon = $False
$formFindingASpotifyClien.SizeGripStyle = 'Hide'
$formFindingASpotifyClien.StartPosition = 'CenterScreen'
$formFindingASpotifyClien.Text = 'Finding a Spotify Client'
#
# progressbar1
#
$progressbar1.Location = New-Object System.Drawing.Point(12, 307)
$progressbar1.Name = 'progressbar1'
$progressbar1.Size = New-Object System.Drawing.Size(420, 23)
$progressbar1.TabIndex = 18
#
# textbox1
#
$textbox1.Font = [System.Drawing.Font]::new('Tahoma', '6.75')
$textbox1.Location = New-Object System.Drawing.Point(13, 149)
$textbox1.Multiline = $True
$textbox1.Name = 'textbox1'
$textbox1.ReadOnly = $True
$textbox1.ScrollBars = 'Both'
$textbox1.Size = New-Object System.Drawing.Size(419, 151)
$textbox1.TabIndex = 17
#
# linklabelFoundLinks
#
$linklabelFoundLinks.LinkColor = [System.Drawing.SystemColors]::HotTrack
$linklabelFoundLinks.Location = New-Object System.Drawing.Point(12, 341)
$linklabelFoundLinks.Name = 'linklabelFoundLinks'
$linklabelFoundLinks.Size = New-Object System.Drawing.Size(62, 17)
$linklabelFoundLinks.TabIndex = 16
$linklabelFoundLinks.TabStop = $True
$linklabelFoundLinks.Text = 'Found links'
$linklabelFoundLinks.VisitedLinkColor = [System.Drawing.Color]::SlateBlue
$linklabelFoundLinks.add_LinkClicked($linklabelFoundLinks_LinkClicked)
#
# groupbox2
#
$groupbox2.Controls.Add($labelEnterSpotifyVersion)
$groupbox2.Controls.Add($maskedtextbox1)
$groupbox2.Controls.Add($labelForExample1182758g8b)
$groupbox2.Location = New-Object System.Drawing.Point(228, 20)
$groupbox2.Name = 'groupbox2'
$groupbox2.Size = New-Object System.Drawing.Size(204, 122)
$groupbox2.TabIndex = 13
$groupbox2.TabStop = $False
#
# labelEnterSpotifyVersion
#
$labelEnterSpotifyVersion.AutoSize = $True
$labelEnterSpotifyVersion.Font = [System.Drawing.Font]::new('Tahoma', '9', [System.Drawing.FontStyle]'Bold')
$labelEnterSpotifyVersion.Location = New-Object System.Drawing.Point(30, 16)
$labelEnterSpotifyVersion.Name = 'labelEnterSpotifyVersion'
$labelEnterSpotifyVersion.Size = New-Object System.Drawing.Size(138, 14)
$labelEnterSpotifyVersion.TabIndex = 3
$labelEnterSpotifyVersion.Text = 'Enter Spotify Version'
#
# maskedtextbox1
#
$maskedtextbox1.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$maskedtextbox1.Location = New-Object System.Drawing.Point(24, 87)
$maskedtextbox1.Name = 'maskedtextbox1'
$maskedtextbox1.Size = New-Object System.Drawing.Size(158, 21)
$maskedtextbox1.TabIndex = 2
$maskedtextbox1.TextAlign = 'Center'
#
# labelForExample1182758g8b
#
$labelForExample1182758g8b.AccessibleDescription = ''
$labelForExample1182758g8b.AccessibleName = ''
$labelForExample1182758g8b.AutoSize = $True
$labelForExample1182758g8b.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$labelForExample1182758g8b.Location = New-Object System.Drawing.Point(40, 48)
$labelForExample1182758g8b.Name = 'labelForExample1182758g8b'
$labelForExample1182758g8b.Size = New-Object System.Drawing.Size(118, 26)
$labelForExample1182758g8b.TabIndex = 4
$labelForExample1182758g8b.Text = 'For example:
1.1.82.758.g8b7b66c7'
#
# groupbox1
#
$groupbox1.Controls.Add($labelMaximumNumberFromFou)
$groupbox1.Controls.Add($labelEnterSearchRange)
$groupbox1.Controls.Add($labelTo)
$groupbox1.Controls.Add($maskedtextbox2)
$groupbox1.Controls.Add($labelFrom)
$groupbox1.Controls.Add($maskedtextbox3)
$groupbox1.Location = New-Object System.Drawing.Point(12, 20)
$groupbox1.Name = 'groupbox1'
$groupbox1.Size = New-Object System.Drawing.Size(204, 122)
$groupbox1.TabIndex = 12
$groupbox1.TabStop = $False
#
# labelMaximumNumberFromFou
#
$labelMaximumNumberFromFou.AutoSize = $True
$labelMaximumNumberFromFou.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$labelMaximumNumberFromFou.Location = New-Object System.Drawing.Point(40, 48)
$labelMaximumNumberFromFou.Name = 'labelMaximumNumberFromFou'
$labelMaximumNumberFromFou.Size = New-Object System.Drawing.Size(118, 26)
$labelMaximumNumberFromFou.TabIndex = 14
$labelMaximumNumberFromFou.Text = 'Maximum number from
found links was 490.'
#
# labelEnterSearchRange
#
$labelEnterSearchRange.AutoSize = $True
$labelEnterSearchRange.Font = [System.Drawing.Font]::new('Tahoma', '9', [System.Drawing.FontStyle]'Bold')
$labelEnterSearchRange.Location = New-Object System.Drawing.Point(40, 16)
$labelEnterSearchRange.Name = 'labelEnterSearchRange'
$labelEnterSearchRange.Size = New-Object System.Drawing.Size(122, 14)
$labelEnterSearchRange.TabIndex = 6
$labelEnterSearchRange.Text = 'Enter search range'
#
# labelTo
#
$labelTo.AutoSize = $True
$labelTo.Font = [System.Drawing.Font]::new('Lucida Console', '8.25')
$labelTo.Location = New-Object System.Drawing.Point(103, 92)
$labelTo.Name = 'labelTo'
$labelTo.Size = New-Object System.Drawing.Size(19, 11)
$labelTo.TabIndex = 11
$labelTo.Text = 'to'
#
# maskedtextbox2
#
$maskedtextbox2.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$maskedtextbox2.Location = New-Object System.Drawing.Point(45, 87)
$maskedtextbox2.Name = 'maskedtextbox2'
$maskedtextbox2.Size = New-Object System.Drawing.Size(50, 21)
$maskedtextbox2.TabIndex = 5
$maskedtextbox2.Text = '0'
$maskedtextbox2.TextAlign = 'Center'
#
# labelFrom
#
$labelFrom.AutoSize = $True
$labelFrom.Font = [System.Drawing.Font]::new('Lucida Console', '8.25')
$labelFrom.Location = New-Object System.Drawing.Point(6, 92)
$labelFrom.Name = 'labelFrom'
$labelFrom.Size = New-Object System.Drawing.Size(33, 11)
$labelFrom.TabIndex = 10
$labelFrom.Text = 'From'
#
# maskedtextbox3
#
$maskedtextbox3.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$maskedtextbox3.Location = New-Object System.Drawing.Point(128, 87)
$maskedtextbox3.Name = 'maskedtextbox3'
$maskedtextbox3.Size = New-Object System.Drawing.Size(50, 21)
$maskedtextbox3.TabIndex = 8
$maskedtextbox3.Text = '100'
$maskedtextbox3.TextAlign = 'Center'
#
# buttonStartSearch
#
$buttonStartSearch.Location = New-Object System.Drawing.Point(333, 336)
$buttonStartSearch.Name = 'buttonStartSearch'
$buttonStartSearch.Size = New-Object System.Drawing.Size(99, 23)
$buttonStartSearch.TabIndex = 1
$buttonStartSearch.Text = 'Start Search'
$buttonStartSearch.UseVisualStyleBackColor = $True
$buttonStartSearch.add_Click($buttonStartSearch_Click)
#
# buttonExit
#
$buttonExit.Location = New-Object System.Drawing.Point(252, 336)
$buttonExit.Name = 'buttonExit'
$buttonExit.Size = New-Object System.Drawing.Size(75, 23)
$buttonExit.TabIndex = 0
$buttonExit.TabStop = $False
$buttonExit.Text = 'Exit'
$buttonExit.UseVisualStyleBackColor = $True
$buttonExit.add_Click($buttonExit_Click)
$groupbox1.ResumeLayout()
$groupbox2.ResumeLayout()
$formFindingASpotifyClien.ResumeLayout()
#Save the initial state of the form
$InitialFormWindowState = $formFindingASpotifyClien.WindowState
#Init the OnLoad event to correct the initial state of the form
$formFindingASpotifyClien.add_Load($Form_StateCorrection_Load)
#Clean up the control events
$formFindingASpotifyClien.add_FormClosed($Form_Cleanup_FormClosed)
#Show the Form
return $formFindingASpotifyClien.ShowDialog()
} #End Function
#Call the form
Show-Search_psf | Out-Null

View file

@ -0,0 +1,508 @@
Add-Type -Name Window -Namespace Console -MemberDefinition '
[DllImport("Kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
'
function Hide-Console {
$consolePtr = [Console.Window]::GetConsoleWindow()
#0 hide
[Console.Window]::ShowWindow($consolePtr, 0)
}
Hide-Console
function Show-Search_psf {
[void][reflection.assembly]::Load('System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
[void][reflection.assembly]::Load('System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
try {
[ProgressBarOverlay] | Out-Null
}
catch {
if ($PSVersionTable.PSVersion.Major -ge 7) {
$Assemblies = 'System.Windows.Forms', 'System.Drawing', 'System.Drawing.Primitives', 'System.ComponentModel.Primitives', 'System.Drawing.Common', 'System.Runtime'
}
else {
$Assemblies = 'System.Windows.Forms', 'System.Drawing'
}
Add-Type -ReferencedAssemblies $Assemblies -TypeDefinition @"
using System;
using System.Windows.Forms;
using System.Drawing;
namespace SAPIENTypes
{
public class ProgressBarOverlay : System.Windows.Forms.ProgressBar
{
public ProgressBarOverlay() : base() { SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); }
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == 0x000F)// WM_PAINT
{
if (Style != System.Windows.Forms.ProgressBarStyle.Marquee || !string.IsNullOrEmpty(this.Text))
{
using (Graphics g = this.CreateGraphics())
{
using (StringFormat stringFormat = new StringFormat(StringFormatFlags.NoWrap))
{
stringFormat.Alignment = StringAlignment.Center;
stringFormat.LineAlignment = StringAlignment.Center;
if (!string.IsNullOrEmpty(this.Text))
g.DrawString(this.Text, this.Font, Brushes.Black, this.ClientRectangle, stringFormat);
else
{
int percent = (int)(((double)Value / (double)Maximum) * 100);
g.DrawString(percent.ToString() + "%", this.Font, Brushes.Black, this.ClientRectangle, stringFormat);
}
}
}
}
}
}
public string TextOverlay
{
get
{
return base.Text;
}
set
{
base.Text = value;
Invalidate();
}
}
}
}
"@ -IgnoreWarnings | Out-Null
}
[System.Windows.Forms.Application]::EnableVisualStyles()
$formПоискКлиентаSpotify = New-Object 'System.Windows.Forms.Form'
$progressbar1 = New-Object 'SAPIENTypes.ProgressBarOverlay'
$textbox1 = New-Object 'System.Windows.Forms.TextBox'
$linklabelНайденныеСсылки = New-Object 'System.Windows.Forms.LinkLabel'
$groupbox2 = New-Object 'System.Windows.Forms.GroupBox'
$labelВведитеВерсиюSpotify = New-Object 'System.Windows.Forms.Label'
$maskedtextbox1 = New-Object 'System.Windows.Forms.MaskedTextBox'
$labelНапример1182758g8b7b = New-Object 'System.Windows.Forms.Label'
$groupbox1 = New-Object 'System.Windows.Forms.GroupBox'
$labelМаксимальныйНомерИзН = New-Object 'System.Windows.Forms.Label'
$labelВведитеДиапозонПоиск = New-Object 'System.Windows.Forms.Label'
$labelДо = New-Object 'System.Windows.Forms.Label'
$maskedtextbox2 = New-Object 'System.Windows.Forms.MaskedTextBox'
$labelОт = New-Object 'System.Windows.Forms.Label'
$maskedtextbox3 = New-Object 'System.Windows.Forms.MaskedTextBox'
$buttonНачатьПоиск = New-Object 'System.Windows.Forms.Button'
$buttonВыйти = New-Object 'System.Windows.Forms.Button'
$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
function Update-ListBox {
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[System.Windows.Forms.ListBox]
$ListBox,
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Items,
[Parameter(Mandatory = $false)]
[string]
$DisplayMember,
[Parameter(Mandatory = $false)]
[string]$ValueMember,
[switch]
$Append
)
if (-not $Append) {
$listBox.Items.Clear()
}
if ($Items -is [System.Windows.Forms.ListBox+ObjectCollection]) {
$listBox.Items.AddRange($Items)
}
elseif ($Items -is [Array]) {
$listBox.BeginUpdate()
foreach ($obj in $Items) {
$listBox.Items.Add($obj)
}
$listBox.EndUpdate()
}
else {
$listBox.Items.Add($Items)
}
if ($DisplayMember) {
$listBox.DisplayMember = $DisplayMember
}
if ($ValueMember) {
$ListBox.ValueMember = $ValueMember
}
}
function Update-ComboBox {
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[System.Windows.Forms.ComboBox]
$ComboBox,
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
$Items,
[Parameter(Mandatory = $false)]
[string]$DisplayMember,
[Parameter(Mandatory = $false)]
[string]$ValueMember,
[switch]
$Append
)
if (-not $Append) {
$ComboBox.Items.Clear()
}
if ($Items -is [Object[]]) {
$ComboBox.Items.AddRange($Items)
}
elseif ($Items -is [System.Collections.IEnumerable]) {
$ComboBox.BeginUpdate()
foreach ($obj in $Items) {
$ComboBox.Items.Add($obj)
}
$ComboBox.EndUpdate()
}
else {
$ComboBox.Items.Add($Items)
}
if ($DisplayMember) {
$ComboBox.DisplayMember = $DisplayMember
}
if ($ValueMember) {
$ComboBox.ValueMember = $ValueMember
}
}
$buttonНачатьПоиск_Click = {
# Логика кнопки поиск
if ($maskedtextbox2.Text -notmatch "\D" -and $maskedtextbox3.Text -notmatch "\D") {
$ErrorActionPreference = 'SilentlyContinue'
$version_spoti = $maskedtextbox1.Text
$numbers = [int]$maskedtextbox2.Text
$before_enter = [int]$maskedtextbox3.Text
if ($before_enter -gt $numbers) {
if ($version_spoti -match '^\d.\d.\d{1,2}.\d{1,5}.[a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z]$') {
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText("Поиск...")
$numbers_bar = 0
$progressbar1.Minimum = 0
$progressbar1.Maximum = $before_enter - $numbers
While ($numbers -le $before_enter) {
$progressbar1.Value = $numbers_bar
$numbers_bar++
$_URL = "https://upgrade.scdn.co/upgrade/client/win32-x86/spotify_installer-$version_spoti-$numbers.exe"
try {
$request = [System.Net.WebRequest]::Create($_URL)
$response = $request.getResponse()
if ($response.StatusCode -eq "200") {
$response.ResponseUri.OriginalString
$find_url += [System.Environment]::NewLine + $response.ResponseUri.OriginalString
$response.Close()
}
}
catch
{ }
$numbers++
}
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText([System.Environment]::NewLine + "Поиск завершен")
$textbox1.AppendText([System.Environment]::NewLine + "")
if ($find_url) {
$textbox1.AppendText([System.Environment]::NewLine + "Найденные ссылки :")
$textbox1.AppendText([System.Environment]::NewLine + $find_url)
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
if (!($find_url)) {
$textbox1.AppendText([System.Environment]::NewLine + "Поиск не дал результатов, попробуйте увеличить диапазон.")
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
}
else {
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText("Неудачно")
$textbox1.AppendText([System.Environment]::NewLine + "Не корректно введена версия Spotify")
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
}
else {
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText("Неудачно")
$textbox1.AppendText([System.Environment]::NewLine + "Не корректно введен диапазон поиска")
$textbox1.AppendText([System.Environment]::NewLine + "Начальный диапазон не может быть больше конечного диапазона")
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
}
else {
$textbox1.AppendText([System.Environment]::NewLine + "")
$textbox1.AppendText("Неудачно")
$textbox1.AppendText([System.Environment]::NewLine + "Не корректно введен диапазон поиска")
$textbox1.AppendText([System.Environment]::NewLine + "Вводите только цифры")
$textbox1.AppendText([System.Environment]::NewLine + "`n")
}
}
$buttonВыйти_Click = {
# Логика кнопки выйти
$formПоискКлиентаSpotify.hide()
}
$linklabelНайденныеСсылки_LinkClicked = [System.Windows.Forms.LinkLabelLinkClickedEventHandler] {
Start-Process "https://cutt.ly/8EH6NuH"
}
$Form_StateCorrection_Load =
{
$formПоискКлиентаSpotify.WindowState = $InitialFormWindowState
}
$Form_Cleanup_FormClosed =
{
try {
$linklabelНайденныеСсылки.remove_LinkClicked($linklabelНайденныеСсылки_LinkClicked)
$buttonНачатьПоиск.remove_Click($buttonНачатьПоиск_Click)
$buttonВыйти.remove_Click($buttonВыйти_Click)
$formПоискКлиентаSpotify.remove_Load($Form_StateCorrection_Load)
$formПоискКлиентаSpotify.remove_FormClosed($Form_Cleanup_FormClosed)
}
catch { Out-Null <# Prevent PSScriptAnalyzer warning #> }
}
$formПоискКлиентаSpotify.SuspendLayout()
$groupbox1.SuspendLayout()
$groupbox2.SuspendLayout()
#
# formПоискКлиентаSpotify
#
$formПоискКлиентаSpotify.Controls.Add($progressbar1)
$formПоискКлиентаSpotify.Controls.Add($textbox1)
$formПоискКлиентаSpotify.Controls.Add($linklabelНайденныеСсылки)
$formПоискКлиентаSpotify.Controls.Add($groupbox2)
$formПоискКлиентаSpotify.Controls.Add($groupbox1)
$formПоискКлиентаSpotify.Controls.Add($buttonНачатьПоиск)
$formПоискКлиентаSpotify.Controls.Add($buttonВыйти)
$formПоискКлиентаSpotify.AutoScaleDimensions = New-Object System.Drawing.SizeF(6, 13)
$formПоискКлиентаSpotify.AutoScaleMode = 'Font'
$formПоискКлиентаSpotify.AutoSizeMode = 'GrowAndShrink'
$formПоискКлиентаSpotify.ClientSize = New-Object System.Drawing.Size(444, 370)
$formПоискКлиентаSpotify.MaximizeBox = $False
$formПоискКлиентаSpotify.Name = 'formПоискКлиентаSpotify'
$formПоискКлиентаSpotify.ShowIcon = $False
$formПоискКлиентаSpotify.SizeGripStyle = 'Hide'
$formПоискКлиентаSpotify.StartPosition = 'CenterScreen'
$formПоискКлиентаSpotify.Text = 'Поиск клиента Spotify'
#
# progressbar1
#
$progressbar1.Location = New-Object System.Drawing.Point(12, 307)
$progressbar1.Name = 'progressbar1'
$progressbar1.Size = New-Object System.Drawing.Size(420, 23)
$progressbar1.TabIndex = 18
#
# textbox1
#
$textbox1.Font = [System.Drawing.Font]::new('Tahoma', '6.75')
$textbox1.Location = New-Object System.Drawing.Point(13, 149)
$textbox1.Multiline = $True
$textbox1.Name = 'textbox1'
$textbox1.ReadOnly = $True
$textbox1.ScrollBars = 'Both'
$textbox1.Size = New-Object System.Drawing.Size(419, 151)
$textbox1.TabIndex = 17
#
# linklabelНайденныеСсылки
#
$linklabelНайденныеСсылки.LinkColor = [System.Drawing.SystemColors]::HotTrack
$linklabelНайденныеСсылки.Location = New-Object System.Drawing.Point(12, 341)
$linklabelНайденныеСсылки.Name = 'linklabelНайденныеСсылки'
$linklabelНайденныеСсылки.Size = New-Object System.Drawing.Size(107, 17)
$linklabelНайденныеСсылки.TabIndex = 16
$linklabelНайденныеСсылки.TabStop = $True
$linklabelНайденныеСсылки.Text = 'Найденные ссылки'
$linklabelНайденныеСсылки.VisitedLinkColor = [System.Drawing.Color]::SlateBlue
$linklabelНайденныеСсылки.add_LinkClicked($linklabelНайденныеСсылки_LinkClicked)
#
# groupbox2
#
$groupbox2.Controls.Add($labelВведитеВерсиюSpotify)
$groupbox2.Controls.Add($maskedtextbox1)
$groupbox2.Controls.Add($labelНапример1182758g8b7b)
$groupbox2.Location = New-Object System.Drawing.Point(228, 20)
$groupbox2.Name = 'groupbox2'
$groupbox2.Size = New-Object System.Drawing.Size(204, 122)
$groupbox2.TabIndex = 13
$groupbox2.TabStop = $False
#
# labelВведитеВерсиюSpotify
#
$labelВведитеВерсиюSpotify.AutoSize = $True
$labelВведитеВерсиюSpotify.Font = [System.Drawing.Font]::new('Tahoma', '9', [System.Drawing.FontStyle]'Bold')
$labelВведитеВерсиюSpotify.Location = New-Object System.Drawing.Point(30, 16)
$labelВведитеВерсиюSpotify.Name = 'labelВведитеВерсиюSpotify'
$labelВведитеВерсиюSpotify.Size = New-Object System.Drawing.Size(158, 14)
$labelВведитеВерсиюSpotify.TabIndex = 3
$labelВведитеВерсиюSpotify.Text = 'Введите версию Spotify'
#
# maskedtextbox1
#
$maskedtextbox1.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$maskedtextbox1.Location = New-Object System.Drawing.Point(30, 87)
$maskedtextbox1.Name = 'maskedtextbox1'
$maskedtextbox1.Size = New-Object System.Drawing.Size(158, 21)
$maskedtextbox1.TabIndex = 2
$maskedtextbox1.TextAlign = 'Center'
#
# labelНапример1182758g8b7b
#
$labelНапример1182758g8b7b.AccessibleDescription = ''
$labelНапример1182758g8b7b.AccessibleName = ''
$labelНапример1182758g8b7b.AutoSize = $True
$labelНапример1182758g8b7b.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$labelНапример1182758g8b7b.Location = New-Object System.Drawing.Point(51, 48)
$labelНапример1182758g8b7b.Name = 'labelНапример1182758g8b7b'
$labelНапример1182758g8b7b.Size = New-Object System.Drawing.Size(118, 26)
$labelНапример1182758g8b7b.TabIndex = 4
$labelНапример1182758g8b7b.Text = 'Например:
1.1.82.758.g8b7b66c7'
#
# groupbox1
#
$groupbox1.Controls.Add($labelМаксимальныйНомерИзН)
$groupbox1.Controls.Add($labelВведитеДиапозонПоиск)
$groupbox1.Controls.Add($labelДо)
$groupbox1.Controls.Add($maskedtextbox2)
$groupbox1.Controls.Add($labelОт)
$groupbox1.Controls.Add($maskedtextbox3)
$groupbox1.Location = New-Object System.Drawing.Point(12, 20)
$groupbox1.Name = 'groupbox1'
$groupbox1.Size = New-Object System.Drawing.Size(204, 122)
$groupbox1.TabIndex = 12
$groupbox1.TabStop = $False
#
# labelМаксимальныйНомерИзН
#
$labelМаксимальныйНомерИзН.AutoSize = $True
$labelМаксимальныйНомерИзН.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$labelМаксимальныйНомерИзН.Location = New-Object System.Drawing.Point(27, 48)
$labelМаксимальныйНомерИзН.Name = 'labelМаксимальныйНомерИзН'
$labelМаксимальныйНомерИзН.Size = New-Object System.Drawing.Size(151, 26)
$labelМаксимальныйНомерИзН.TabIndex = 14
$labelМаксимальныйНомерИзН.Text = 'Максимальный номер из
найденных ссылок был 490.
'
#
# labelВведитеДиапозонПоиск
#
$labelВведитеДиапозонПоиск.AutoSize = $True
$labelВведитеДиапозонПоиск.Font = [System.Drawing.Font]::new('Tahoma', '9', [System.Drawing.FontStyle]'Bold')
$labelВведитеДиапозонПоиск.Location = New-Object System.Drawing.Point(15, 16)
$labelВведитеДиапозонПоиск.Name = 'labelВведитеДиапозонПоиск'
$labelВведитеДиапозонПоиск.Size = New-Object System.Drawing.Size(171, 14)
$labelВведитеДиапозонПоиск.TabIndex = 6
$labelВведитеДиапозонПоиск.Text = 'Введите диапазон поиска'
#
# labelДо
#
$labelДо.AutoSize = $True
$labelДо.Font = [System.Drawing.Font]::new('Lucida Console', '8.25')
$labelДо.Location = New-Object System.Drawing.Point(103, 92)
$labelДо.Name = 'labelДо'
$labelДо.Size = New-Object System.Drawing.Size(19, 11)
$labelДо.TabIndex = 11
$labelДо.Text = 'До'
#
# maskedtextbox2
#
$maskedtextbox2.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$maskedtextbox2.Location = New-Object System.Drawing.Point(40, 87)
$maskedtextbox2.Name = 'maskedtextbox2'
$maskedtextbox2.Size = New-Object System.Drawing.Size(50, 21)
$maskedtextbox2.TabIndex = 5
$maskedtextbox2.Text = '0'
$maskedtextbox2.TextAlign = 'Center'
#
# labelОт
#
$labelОт.AutoSize = $True
$labelОт.Font = [System.Drawing.Font]::new('Lucida Console', '8.25')
$labelОт.Location = New-Object System.Drawing.Point(15, 92)
$labelОт.Name = 'labelОт'
$labelОт.Size = New-Object System.Drawing.Size(19, 11)
$labelОт.TabIndex = 10
$labelОт.Text = 'От'
#
# maskedtextbox3
#
$maskedtextbox3.Font = [System.Drawing.Font]::new('Tahoma', '8.25')
$maskedtextbox3.Location = New-Object System.Drawing.Point(128, 87)
$maskedtextbox3.Name = 'maskedtextbox3'
$maskedtextbox3.Size = New-Object System.Drawing.Size(50, 21)
$maskedtextbox3.TabIndex = 8
$maskedtextbox3.Text = '100'
$maskedtextbox3.TextAlign = 'Center'
#
# buttonНачатьПоиск
#
$buttonНачатьПоиск.Location = New-Object System.Drawing.Point(333, 336)
$buttonНачатьПоиск.Name = 'buttonНачатьПоиск'
$buttonНачатьПоиск.Size = New-Object System.Drawing.Size(99, 23)
$buttonНачатьПоиск.TabIndex = 1
$buttonНачатьПоиск.Text = 'Начать поиск'
$buttonНачатьПоиск.UseVisualStyleBackColor = $True
$buttonНачатьПоиск.add_Click($buttonНачатьПоиск_Click)
#
# buttonВыйти
#
$buttonВыйти.Location = New-Object System.Drawing.Point(252, 336)
$buttonВыйти.Name = 'buttonВыйти'
$buttonВыйти.Size = New-Object System.Drawing.Size(75, 23)
$buttonВыйти.TabIndex = 0
$buttonВыйти.TabStop = $False
$buttonВыйти.Text = 'Выйти'
$buttonВыйти.UseVisualStyleBackColor = $True
$buttonВыйти.add_Click($buttonВыйти_Click)
$groupbox2.ResumeLayout()
$groupbox1.ResumeLayout()
$formПоискКлиентаSpotify.ResumeLayout()
#Save the initial state of the form
$InitialFormWindowState = $formПоискКлиентаSpotify.WindowState
#Init the OnLoad event to correct the initial state of the form
$formПоискКлиентаSpotify.add_Load($Form_StateCorrection_Load)
#Clean up the control events
$formПоискКлиентаSpotify.add_FormClosed($Form_Cleanup_FormClosed)
#Show the Form
return $formПоискКлиентаSpotify.ShowDialog()
}
#Call the form
Show-Search_psf | Out-Null

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

@ -1,280 +0,0 @@
# Suppress verbose output
$VerbosePreference = 'SilentlyContinue'
function Write-Log {
param (
[string]$Message
)
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message"
}
function Invoke-WithRetry {
param (
[ScriptBlock]$ScriptBlock,
[int]$MaxAttempts = 6,
[int]$DelaySeconds = 10
)
$attempt = 1
$success = $false
while (-not $success -and $attempt -le $MaxAttempts) {
try {
$result = & $ScriptBlock
$success = $true
return $result
}
catch {
Write-Log "Attempt $attempt failed: $_"
if ($attempt -lt $MaxAttempts) {
Write-Log "Retrying in $DelaySeconds seconds..."
Start-Sleep -Seconds $DelaySeconds
}
$attempt++
}
}
if (-not $success) {
Write-Log "All $MaxAttempts attempts failed."
return $null
}
}
function Get-LatestSpotifyVersion {
Write-Log "Getting data from rg-adguard..."
$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"
$body = "type=url&url=https://apps.microsoft.com/detail/9ncbcszsjrsb&gl=US&ring=RP&lang=en"
$headers = @{
"authority" = "ru.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"
"priority" = "u=1, i"
"referer" = "https://ru.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"'
"sec-fetch-dest" = "empty"
"sec-fetch-mode" = "cors"
"sec-fetch-site" = "same-origin"
}
$ProgressPreference = 'SilentlyContinue'
$lastResponse = $null
$response = Invoke-WithRetry -ScriptBlock {
$resp = Invoke-WebRequest -Uri $url -Method "POST" -Body $body -WebSession $session -Headers $headers -UseBasicParsing
$html = $resp.Content
$script:lastResponse = $html # Store the last response
$trContents = [regex]::Matches($html, '(?s)<tr.*?>(.*?)</tr>') | ForEach-Object { $_.Groups[1].Value }
$results = @()
foreach ($trContent in $trContents) {
if ($trContent -match '<a href="(http://tlu\.dl\.delivery\.mp\.microsoft\.com[^"]+)"[^>]*>([^<]+)</a>') {
$url = $matches[1]
$fileName = $matches[2]
}
if ($trContent -match '>(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} GMT)<') {
$dateTime = [DateTime]::ParseExact($matches[1], "yyyy-MM-dd HH:mm:ss 'GMT'", $null)
}
if ($trContent -match '>(\d+(?:\.\d+)?)\s*MB<') {
$size = $matches[1] + " MB"
}
$results += [PSCustomObject]@{
FileName = $fileName
DateTime = $dateTime
Size = $size
Url = $url
}
}
$filteredResults = $results | Where-Object { $_.FileName -match 'SpotifyAB\.SpotifyMusic_.+x64.+\.appx' }
$latestFile = $filteredResults | Sort-Object -Property DateTime -Descending | Select-Object -First 1
if (-not $latestFile) {
throw "No matching Spotify file found"
}
return $latestFile
}
if ($response) {
Write-Log "Found: $($response.FileName)"
return $response
}
else {
Write-Log "Failed to find matching Spotify file after multiple attempts."
Write-Log "Response:"
Write-Log $script:lastResponse
return $null
}
}
function Download-SpotifyAppx {
param ($downloadUrl, $filePath)
if (-not $downloadUrl) {
Write-Log "Error: Download URL is empty or null"
return $false
}
Write-Log "Downloading appx file..."
$ProgressPreference = 'SilentlyContinue'
$success = Invoke-WithRetry -ScriptBlock {
Invoke-WebRequest -Uri $downloadUrl -OutFile $filePath
$true
}
return $success
}
function Extract-SpotifyAppx {
param ($filePath, $extractPath)
Write-Log "Extracting files from archive..."
Add-Type -AssemblyName "System.IO.Compression.FileSystem"
try {
$zip = [System.IO.Compression.ZipFile]::OpenRead($filePath)
$zip.Entries | Where-Object { $_.FullName -notlike '*/*' } | ForEach-Object {
$destPath = Join-Path $extractPath $_.Name
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $destPath, $true)
}
return $true
}
catch {
Write-Log "Error extracting files: $_"
return $false
}
finally {
if ($zip -ne $null) {
$zip.Dispose()
}
}
}
function Get-SpotifyExeVersion {
param ($spotifyExePath)
Write-Log "Getting version from $([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]
}
return $null
}
function Compare-SpotifyVersions {
param ($version, $jsonUrl)
Write-Log "Comparison of versions..."
$ProgressPreference = 'SilentlyContinue'
$jsonContent = Invoke-WithRetry -ScriptBlock {
Invoke-WebRequest -Uri $jsonUrl | ConvertFrom-Json
}
if (-not $jsonContent) {
Write-Log "Failed to fetch version information after multiple attempts."
return $null
}
foreach ($jsonVersion in $jsonContent.PSObject.Properties) {
if ($jsonVersion.Value.fullversion -eq $version) {
return $true
}
}
return $false
}
function Trigger-GitAction {
param (
[string]$v,
[string]$s
)
$apiUrl = "https://api.github.com/repos/amd64fox/LoaderSpot/dispatches"
$payload = @{
event_type = "webhook-event"
client_payload = @{
v = $v
s = $s
}
} | ConvertTo-Json
$headers = @{
"Accept" = "application/vnd.github.everest-preview+json"
"Authorization" = "Bearer " + $env:Token
"Content-Type" = "application/json"
}
$success = Invoke-WithRetry -ScriptBlock {
Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -Body $payload
$true
}
if ($success) {
Write-Log "Successfully triggered Git action"
}
else {
Write-Log "Failed to trigger Git action after multiple attempts"
}
}
function Main {
$latestFile = Get-LatestSpotifyVersion
if (-not $latestFile) {
Write-Log "Failed to get latest Spotify version"
return
}
$filePath = Join-Path $spotifyTempDir $latestFile.FileName
if (-not (Download-SpotifyAppx -downloadUrl $latestFile.Url -filePath $filePath)) {
Write-Log "Failed to download Spotify appx"
return
}
if (-not (Extract-SpotifyAppx -filePath $filePath -extractPath $spotifyTempDir)) {
Write-Log "Failed to extract Spotify appx"
return
}
$spotifyExePath = Get-ChildItem -Path $spotifyTempDir -Filter "Spotify.dll" -Recurse | Select-Object -First 1
if (-not $spotifyExePath) {
Write-Log "Spotify.exe not found"
return
}
$version = Get-SpotifyExeVersion -spotifyExePath $spotifyExePath.FullName
if (-not $version) {
Write-Log "Version not found in Spotify.exe"
return
}
$jsonUrl = "https://raw.githubusercontent.com/amd64fox/LoaderSpot/refs/heads/main/versions.json"
$versionExists = Compare-SpotifyVersions -version $version -jsonUrl $jsonUrl
switch ($versionExists) {
$false {
Write-Log "New version found"
Trigger-GitAction -v $version -s "[Microsoft Store](https://apps.microsoft.com/detail/9ncbcszsjrsb)"
Write-Log "Sent for search and processing in GAS"
}
$null {
Write-Log "Error comparing versions"
}
Default {
Write-Log "No new version found"
}
}
}
$tempPath = [System.IO.Path]::GetTempPath()
$spotifyTempDir = Join-Path $tempPath "Spotify"
New-Item -Path $spotifyTempDir -ItemType Directory -Force | Out-Null
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"
}
}
}