Compare commits
No commits in common. "main" and "1.4.1" have entirely different histories.
21 changed files with 562 additions and 11476 deletions
233
.github/workflows/build-to-release.yml
vendored
233
.github/workflows/build-to-release.yml
vendored
|
|
@ -1,195 +1,64 @@
|
||||||
name: Build LoaderSpot
|
name: Build Windows LoaderSpot
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_windows:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
architecture: [x86, x64]
|
||||||
- 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:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Set up Python
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.rust_target }}
|
python-version: '3.13'
|
||||||
|
architecture: ${{ matrix.architecture }}
|
||||||
- name: Cache cargo dependencies
|
|
||||||
uses: actions/cache@v5
|
- name: Install dependencies
|
||||||
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: |
|
run: |
|
||||||
$ErrorActionPreference = "Stop"
|
python -m pip install --upgrade pip
|
||||||
|
pip install aiohttp
|
||||||
$out = "artifacts"
|
pip install tqdm
|
||||||
if (Test-Path $out) { Remove-Item $out -Recurse -Force }
|
pip install pyinstaller
|
||||||
New-Item -ItemType Directory -Path $out | Out-Null
|
|
||||||
|
- name: Build executable
|
||||||
Write-Host "Building UI for $env:RUST_TARGET"
|
run: |
|
||||||
cargo build --manifest-path LoaderSpot_UI/Cargo.toml --release --target $env:RUST_TARGET
|
pyinstaller --onefile --clean --name LoaderSpot_${{ matrix.architecture }} LoaderSpot.py
|
||||||
$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"
|
- name: Upload artifact
|
||||||
Write-Host "UI executable ready: loaderspot-ui-win-$env:ARCH.exe"
|
uses: actions/upload-artifact@v4
|
||||||
|
|
||||||
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:
|
with:
|
||||||
name: windows-artifacts-${{ matrix.arch }}
|
name: LoaderSpot_${{ matrix.architecture }}
|
||||||
path: artifacts/*
|
path: dist/LoaderSpot_${{ matrix.architecture }}.exe
|
||||||
|
|
||||||
build_linux:
|
- name: Get latest release
|
||||||
runs-on: ubuntu-latest
|
id: latest_release
|
||||||
steps:
|
uses: actions/github-script@v7
|
||||||
- uses: actions/checkout@v6
|
with:
|
||||||
|
script: |
|
||||||
- name: Install Rust toolchain
|
const releases = await github.rest.repos.listReleases({
|
||||||
uses: dtolnay/rust-toolchain@stable
|
owner: context.repo.owner,
|
||||||
with:
|
repo: context.repo.repo
|
||||||
targets: x86_64-unknown-linux-gnu
|
});
|
||||||
|
return releases.data[0].id;
|
||||||
- name: Cache cargo dependencies
|
result-encoding: string
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
- name: Upload to Release
|
||||||
path: |
|
uses: actions/github-script@v7
|
||||||
~/.cargo/registry
|
with:
|
||||||
~/.cargo/git
|
script: |
|
||||||
LoaderSpot_UI/target
|
const fs = require('fs');
|
||||||
LoaderSpot_CLI/target
|
const releaseId = ${{ steps.latest_release.outputs.result }};
|
||||||
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
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
echo "Building UI for x86_64-unknown-linux-gnu"
|
owner: context.repo.owner,
|
||||||
cargo build --manifest-path LoaderSpot_UI/Cargo.toml --release --target x86_64-unknown-linux-gnu
|
repo: context.repo.repo,
|
||||||
cp "LoaderSpot_UI/target/x86_64-unknown-linux-gnu/release/loaderspot_ui" "artifacts/loaderspot-ui-linux-x64"
|
release_id: releaseId,
|
||||||
echo "UI executable ready: loaderspot-ui-linux-x64"
|
name: `LoaderSpot_${{ matrix.architecture }}.exe`,
|
||||||
|
data: fs.readFileSync(`dist/LoaderSpot_${{ matrix.architecture }}.exe`)
|
||||||
# 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));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
4
.github/workflows/check-deb-versions.yml
vendored
4
.github/workflows/check-deb-versions.yml
vendored
|
|
@ -13,12 +13,12 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PERSONAL_TOKEN }}
|
token: ${{ secrets.PERSONAL_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.12.2
|
python-version: 3.12.2
|
||||||
|
|
||||||
|
|
|
||||||
6
.github/workflows/ms-spotify.yml
vendored
6
.github/workflows/ms-spotify.yml
vendored
|
|
@ -1,15 +1,17 @@
|
||||||
name: check Spotify MS
|
name: check Spotify MS
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '25 * * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: run ms-spotify-check.ps1
|
- name: run ms-spotify-check.ps1
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
|
||||||
20
.github/workflows/sync-files.yml
vendored
20
.github/workflows/sync-files.yml
vendored
|
|
@ -1,15 +1,13 @@
|
||||||
name: Sync files to table
|
name: Sync files to table
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Temporarily disabled automatic sync trigger.
|
push:
|
||||||
# Uncomment this block to restore automatic runs on push to main.
|
branches:
|
||||||
# push:
|
- main
|
||||||
# branches:
|
paths:
|
||||||
# - main
|
- 'versions.json'
|
||||||
# paths:
|
- 'versions_deb.json'
|
||||||
# - 'versions.json'
|
workflow_dispatch:
|
||||||
# - 'versions_deb.json'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync-files:
|
sync-files:
|
||||||
|
|
@ -19,10 +17,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source repo
|
- name: Checkout source repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Checkout target repo
|
- name: Checkout target repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: LoaderSpot/table
|
repository: LoaderSpot/table
|
||||||
path: table
|
path: table
|
||||||
|
|
|
||||||
13
.github/workflows/update_versions.yml
vendored
13
.github/workflows/update_versions.yml
vendored
|
|
@ -8,11 +8,7 @@ on:
|
||||||
required: true
|
required: true
|
||||||
version:
|
version:
|
||||||
description: 'Another version parameter'
|
description: 'Another version parameter'
|
||||||
required: true
|
required: true
|
||||||
buildType:
|
|
||||||
description: 'Build type (e.g., Master, Release)'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
jobs:
|
jobs:
|
||||||
update-version:
|
update-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -21,20 +17,19 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PERSONAL_TOKEN }}
|
token: ${{ secrets.PERSONAL_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.12.2
|
python-version: 3.12.2
|
||||||
|
|
||||||
- name: Update version in versions.json
|
- name: Update version in versions.json
|
||||||
run: |
|
run: |
|
||||||
data="${{ github.event.inputs.datajson }}"
|
data="${{ github.event.inputs.datajson }}"
|
||||||
build_type="${{ github.event.inputs.buildType }}"
|
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)"
|
||||||
python3 update_version.py "$data" "$build_type"
|
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
48
.github/workflows/version-check.yml
vendored
48
.github/workflows/version-check.yml
vendored
|
|
@ -8,47 +8,21 @@ concurrency:
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version-search:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
versions: ${{ steps.search.outputs.versions }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download latest loaderspot_cli_linux_x64
|
- name: Set up Python
|
||||||
env:
|
uses: actions/setup-python@v5
|
||||||
GH_TOKEN: ${{ github.token }}
|
with:
|
||||||
|
python-version: 3.12.2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
gh release download --repo ${{ github.repository }} --pattern "loaderspot-cli-linux-x64" --output loaderspot_cli
|
pip install aiohttp beautifulsoup4 argparse
|
||||||
chmod +x loaderspot_cli
|
|
||||||
|
|
||||||
- name: Version search
|
- name: Version search
|
||||||
id: search
|
run: python ${{ github.workspace }}/upd.py -v ${{ github.event.client_payload.v }} -s "${{ github.event.client_payload.s }}" -u "${{ secrets.GOOGLE_APPS_URL }}"
|
||||||
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
2
.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
/LoaderSpot_CLI/target
|
|
||||||
/LoaderSpot_UI/target
|
|
||||||
309
LoaderSpot.py
Normal file
309
LoaderSpot.py
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def extract_base_version(version: str) -> str:
|
||||||
|
parts = version.split('.')
|
||||||
|
if len(parts) >= 3:
|
||||||
|
return f"{parts[0]}.{parts[1]}.{parts[2]}"
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def should_use_win_x86(version: str) -> bool:
|
||||||
|
base_version = extract_base_version(version)
|
||||||
|
try:
|
||||||
|
# Split by dot and compare parts
|
||||||
|
parts = base_version.split('.')
|
||||||
|
version_tuple = (int(parts[0]), int(parts[1]), int(parts[2]))
|
||||||
|
|
||||||
|
# Compare with version 1.2.53 using tuple comparison
|
||||||
|
return version_tuple <= (1, 2, 53)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
# If there's any error in parsing, default to include
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
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.head(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
return url, platform
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_versions_json(session: aiohttp.ClientSession) -> dict:
|
||||||
|
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(session: aiohttp.ClientSession, 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}
|
||||||
|
|
||||||
|
try:
|
||||||
|
await session.post(form_url, data=data)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def check_version_and_submit(session: aiohttp.ClientSession, version: str) -> None:
|
||||||
|
try:
|
||||||
|
versions_json = await fetch_versions_json(session)
|
||||||
|
version_exists = any(
|
||||||
|
ver_data.get("fullversion") == version
|
||||||
|
for ver_data in versions_json.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not version_exists:
|
||||||
|
await submit_to_google_form(session, 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) -> bool:
|
||||||
|
return number.isdigit()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_range(start: int, end: int) -> bool:
|
||||||
|
return end >= start and (end - start) <= 20000
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
session: aiohttp.ClientSession, version_info: SpotifyVersion, selected_platforms: List[Platform]
|
||||||
|
) -> Dict[Platform, List[str]]:
|
||||||
|
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}
|
||||||
|
|
||||||
|
avg_speed = [0, 0]
|
||||||
|
|
||||||
|
custom_bar_format = "{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}"
|
||||||
|
|
||||||
|
with tqdm(total=total_urls, desc="Checking URLs",
|
||||||
|
bar_format=custom_bar_format + ", ? urls/sec]") as pbar:
|
||||||
|
|
||||||
|
def update_speed(rate):
|
||||||
|
nonlocal custom_bar_format
|
||||||
|
speed_str = f", {int(rate)} urls/sec]"
|
||||||
|
pbar.bar_format = custom_bar_format + speed_str
|
||||||
|
|
||||||
|
for task in asyncio.as_completed(tasks):
|
||||||
|
if result := await task:
|
||||||
|
url, platform = result
|
||||||
|
results[platform].append(url)
|
||||||
|
|
||||||
|
pbar.update(1)
|
||||||
|
|
||||||
|
rate = pbar.format_dict.get('rate', 0)
|
||||||
|
if rate > 0:
|
||||||
|
avg_speed[0] += rate
|
||||||
|
avg_speed[1] += 1
|
||||||
|
update_speed(rate)
|
||||||
|
|
||||||
|
if avg_speed[1] > 0:
|
||||||
|
avg_rate = avg_speed[0] / avg_speed[1]
|
||||||
|
final_speed_str = f", {int(avg_rate)} urls/sec (avg)]"
|
||||||
|
pbar.bar_format = custom_bar_format + final_speed_str
|
||||||
|
pbar.refresh()
|
||||||
|
|
||||||
|
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(version_spoti: str = "") -> List[Platform]:
|
||||||
|
print("\nSelect the link type for the search:")
|
||||||
|
|
||||||
|
# Filter out WIN_X86 platform for versions > 1.2.53
|
||||||
|
available_platforms = list(Platform)
|
||||||
|
if version_spoti and not should_use_win_x86(version_spoti):
|
||||||
|
available_platforms = [p for p in Platform if p != Platform.WIN_X86]
|
||||||
|
|
||||||
|
for i, platform in enumerate(available_platforms, 1):
|
||||||
|
print(f"[{i}] {platform.value}")
|
||||||
|
print(f"[{len(available_platforms) + 1}] All platforms")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
choices = input("Enter the number(s): ").strip().split(",")
|
||||||
|
|
||||||
|
if not all(choice.strip().isdigit() and 1 <= int(choice.strip()) <= len(available_platforms) + 1 for choice in choices):
|
||||||
|
print(f"Invalid input. Please enter numbers between 1 and {len(available_platforms) + 1}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if str(len(available_platforms) + 1) in choices:
|
||||||
|
return available_platforms
|
||||||
|
|
||||||
|
selected = []
|
||||||
|
for choice in choices:
|
||||||
|
platform_index = int(choice.strip()) - 1
|
||||||
|
if 0 <= platform_index < len(available_platforms):
|
||||||
|
selected.append(available_platforms[platform_index])
|
||||||
|
|
||||||
|
if selected:
|
||||||
|
return selected
|
||||||
|
|
||||||
|
print("Please select at least one valid platform")
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_connections() -> int:
|
||||||
|
while True:
|
||||||
|
max_connections = input("Maximum number of concurrent connections (default 100): ").strip()
|
||||||
|
if not max_connections:
|
||||||
|
return 100
|
||||||
|
if max_connections.isdigit() and int(max_connections) > 0:
|
||||||
|
return int(max_connections)
|
||||||
|
print("Please enter a valid positive number")
|
||||||
|
|
||||||
|
|
||||||
|
async def main(version_spoti: str = "") -> None:
|
||||||
|
# Increase connection limit
|
||||||
|
max_connections = get_max_connections()
|
||||||
|
connector = aiohttp.TCPConnector(limit=max_connections)
|
||||||
|
async with aiohttp.ClientSession(connector=connector) as session:
|
||||||
|
if not version_spoti:
|
||||||
|
version_spoti = get_version_input()
|
||||||
|
|
||||||
|
version_check_task = asyncio.create_task(check_version_and_submit(session, version_spoti))
|
||||||
|
|
||||||
|
start_number = int(
|
||||||
|
get_valid_input(
|
||||||
|
"Start search from: ",
|
||||||
|
lambda x: validate_number(x),
|
||||||
|
"Please enter a valid number",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end_number = int(
|
||||||
|
get_valid_input(
|
||||||
|
"End search at: ",
|
||||||
|
lambda x: validate_number(x) and validate_range(start_number, int(x)),
|
||||||
|
f"Please enter a valid number that is at least {start_number} and no more than {start_number + 20000}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
version_info = SpotifyVersion(version_spoti, start_number, end_number)
|
||||||
|
selected_platforms = get_platform_choices(version_spoti)
|
||||||
|
|
||||||
|
print("\nSearching...\n")
|
||||||
|
results = await search_installers(session, version_info, selected_platforms)
|
||||||
|
display_results(results)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(version_check_task, timeout=1.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
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
2118
LoaderSpot_CLI/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
|
||||||
|
|
@ -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
4803
LoaderSpot_UI/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
|
@ -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: $_"
|
|
||||||
}
|
|
||||||
|
|
@ -123,9 +123,9 @@ def commit_changes(new_versions):
|
||||||
# Формируем сообщение коммита в зависимости от количества новых версий
|
# Формируем сообщение коммита в зависимости от количества новых версий
|
||||||
if new_versions:
|
if new_versions:
|
||||||
commit_message = (
|
commit_message = (
|
||||||
f"Added deb version {new_versions[0][1]}"
|
f"Added deb version {new_versions[0][0]}"
|
||||||
if len(new_versions) == 1
|
if len(new_versions) == 1
|
||||||
else f"Added deb versions: {', '.join(v[1] for v in new_versions)}"
|
else f"Added deb versions: {', '.join(v[0] for v in new_versions)}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
commit_message = "Update Spotify versions"
|
commit_message = "Update Spotify versions"
|
||||||
|
|
|
||||||
|
|
@ -46,19 +46,19 @@ function Get-LatestSpotifyVersion {
|
||||||
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
|
$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"
|
$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"
|
$body = "type=url&url=https://apps.microsoft.com/detail/9ncbcszsjrsb&gl=US&ring=RP&lang=en"
|
||||||
$headers = @{
|
$headers = @{
|
||||||
"authority" = "ru.store.rg-adguard.net"
|
"authority" = "store.rg-adguard.net"
|
||||||
"method" = "POST"
|
"method" = "POST"
|
||||||
"path" = "/api/GetFiles"
|
"path" = "/api/GetFiles"
|
||||||
"scheme" = "https"
|
"scheme" = "https"
|
||||||
"accept" = "*/*"
|
"accept" = "*/*"
|
||||||
"accept-language" = "ru,ru-RU;q=0.9,en;q=0.8"
|
"accept-language" = "ru,ru-RU;q=0.9,en;q=0.8"
|
||||||
"dnt" = "1"
|
"dnt" = "1"
|
||||||
"origin" = "https://ru.store.rg-adguard.net"
|
"origin" = "https://store.rg-adguard.net"
|
||||||
"priority" = "u=1, i"
|
"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" = '"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"'
|
||||||
"sec-ch-ua-mobile" = "?0"
|
"sec-ch-ua-mobile" = "?0"
|
||||||
"sec-ch-ua-platform" = '"Windows"'
|
"sec-ch-ua-platform" = '"Windows"'
|
||||||
|
|
@ -158,13 +158,14 @@ function Extract-SpotifyAppx {
|
||||||
|
|
||||||
function Get-SpotifyExeVersion {
|
function Get-SpotifyExeVersion {
|
||||||
param ($spotifyExePath)
|
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
|
$exeContent = Get-Content -Path $spotifyExePath -Raw
|
||||||
$regex = '(?<![\w\-])(\d+)\.(\d+)\.(\d+)\.(\d+)(\.g[0-9a-f]{8})(?![\w\-])'
|
$regex = '(?<![\w\-])(\d+)\.(\d+)\.(\d+)\.(\d+)(\.g[0-9a-f]{8})(?![\w\-])'
|
||||||
if ($exeContent -match $regex) {
|
if ($exeContent -match $regex) {
|
||||||
Write-Log "Version received successfully: $($matches[0])"
|
Write-Log "Version received successfully: $($matches[0])"
|
||||||
return $matches[0]
|
return $matches[0]
|
||||||
}
|
}
|
||||||
|
Write-Log "Version not found in file $([System.IO.Path]::GetFileName($spotifyExePath))"
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,7 +244,7 @@ function Main {
|
||||||
return
|
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) {
|
if (-not $spotifyExePath) {
|
||||||
Write-Log "Spotify.exe not found"
|
Write-Log "Spotify.exe not found"
|
||||||
return
|
return
|
||||||
|
|
|
||||||
163
upd.py
Normal file
163
upd.py
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
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()
|
||||||
|
if "<div" in data:
|
||||||
|
soup = BeautifulSoup(data, "html.parser")
|
||||||
|
div_element = soup.find(
|
||||||
|
"div",
|
||||||
|
style="text-align:center;font-family:monospace;margin:50px auto 0;max-width:600px",
|
||||||
|
)
|
||||||
|
if div_element:
|
||||||
|
system_response = div_element.text
|
||||||
|
else:
|
||||||
|
system_response = "Не удалось извлечь ответ из HTML"
|
||||||
|
else:
|
||||||
|
system_response = data.strip()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version(ver_str):
|
||||||
|
parts = ver_str.split(".")[:4]
|
||||||
|
return tuple(int(x) for x in parts)
|
||||||
|
|
||||||
|
|
||||||
|
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 # Размер шага
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
# не ищем архитектуру WIN32 если версия >= 1.2.54.304
|
||||||
|
if parse_version(version) >= (1, 2, 54, 304):
|
||||||
|
platform_templates.pop("WIN32", None)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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):
|
||||||
|
latest_urls = get_urls(find_url)
|
||||||
|
if len(latest_urls) < len(platform_names):
|
||||||
|
start_number = before_enter + 1
|
||||||
|
before_enter += increment
|
||||||
|
tasks = []
|
||||||
|
for platform_name in platform_names:
|
||||||
|
if platform_name not in latest_urls:
|
||||||
|
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
|
||||||
|
|
||||||
|
if tasks:
|
||||||
|
for task in asyncio.as_completed(tasks):
|
||||||
|
result = await task
|
||||||
|
if result is not None:
|
||||||
|
find_url.append(result)
|
||||||
|
|
||||||
|
if find_url:
|
||||||
|
latest_urls = get_urls(find_url)
|
||||||
|
await pre_version(latest_urls)
|
||||||
|
else:
|
||||||
|
await pre_version(False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
|
@ -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()
|
|
||||||
2590
versions.json
2590
versions.json
File diff suppressed because it is too large
Load diff
|
|
@ -1,68 +1,4 @@
|
||||||
{
|
{
|
||||||
"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": {
|
"1.2.59.514": {
|
||||||
"fullversion": "1.2.59.514.g834e17d4",
|
"fullversion": "1.2.59.514.g834e17d4",
|
||||||
"size": "143491584",
|
"size": "143491584",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue