Compare commits

..

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

13 changed files with 154 additions and 1903 deletions

View file

@ -17,7 +17,7 @@ jobs:
rust_target: aarch64-pc-windows-msvc
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@ -25,7 +25,7 @@ jobs:
targets: ${{ matrix.rust_target }}
- name: Cache cargo dependencies
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
@ -61,7 +61,7 @@ jobs:
Write-Host "CLI executable ready: loaderspot-cli-win-$env:ARCH.exe"
- name: Upload Windows artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v5
with:
name: windows-artifacts-${{ matrix.arch }}
path: artifacts/*
@ -69,7 +69,7 @@ jobs:
build_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@ -77,7 +77,7 @@ jobs:
targets: x86_64-unknown-linux-gnu
- name: Cache cargo dependencies
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
@ -110,7 +110,7 @@ jobs:
echo "CLI executable ready: loaderspot-cli-linux-x64"
- name: Upload Linux artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v5
with:
name: linux-artifacts-x64
path: artifacts/*
@ -122,7 +122,7 @@ jobs:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v6
with:
path: artifacts

View file

@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
token: ${{ secrets.PERSONAL_TOKEN }}

View file

@ -1,6 +1,8 @@
name: check Spotify MS
on:
schedule:
- cron: '0 */5 * * *'
workflow_dispatch:
jobs:
@ -9,7 +11,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: run ms-spotify-check.ps1
shell: pwsh

View file

@ -1,15 +1,13 @@
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:
push:
branches:
- main
paths:
- 'versions.json'
- 'versions_deb.json'
workflow_dispatch:
jobs:
sync-files:
@ -19,10 +17,10 @@ jobs:
steps:
- name: Checkout source repo
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Checkout target repo
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
repository: LoaderSpot/table
path: table

View file

@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
token: ${{ secrets.PERSONAL_TOKEN }}

View file

@ -14,7 +14,7 @@ jobs:
versions: ${{ steps.search.outputs.versions }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Download latest loaderspot_cli_linux_x64
env:
@ -42,7 +42,7 @@ jobs:
if: success() && needs.version-search.outputs.versions
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Get build info and send to GAS
shell: pwsh

2
.gitignore vendored
View file

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

View file

@ -762,15 +762,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@ -1905,7 +1896,6 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
name = "loaderspot_ui"
version = "0.1.0"
dependencies = [
"crossbeam-channel",
"eframe",
"egui",
"regex",

View file

@ -11,7 +11,6 @@ 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"] }

View file

@ -1,13 +1,13 @@
// Hide console window on Windows
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
use crossbeam_channel::{unbounded, Receiver, Sender};
use eframe::egui;
use reqwest::Client;
use serde::Deserialize;
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tokio::runtime::Runtime;
@ -31,13 +31,13 @@ impl Platform {
}
}
fn generate_path(&self, version: &str, number: i32) -> String {
fn path_template(&self) -> &str {
match self {
Platform::WinX86 => format!("win32-x86/spotify_installer-{version}-{number}.exe"),
Platform::WinX64 => format!("win32-x86_64/spotify_installer-{version}-{number}.exe"),
Platform::WinArm64 => format!("win32-arm64/spotify_installer-{version}-{number}.exe"),
Platform::MacOsIntel => format!("osx-x86_64/spotify-autoupdate-{version}-{number}.tbz"),
Platform::MacOsArm64 => format!("osx-arm64/spotify-autoupdate-{version}-{number}.tbz"),
Platform::WinX86 => "win32-x86/spotify_installer-{version}-{number}.exe",
Platform::WinX64 => "win32-x86_64/spotify_installer-{version}-{number}.exe",
Platform::WinArm64 => "win32-arm64/spotify_installer-{version}-{number}.exe",
Platform::MacOsIntel => "osx-x86_64/spotify-autoupdate-{version}-{number}.tbz",
Platform::MacOsArm64 => "osx-arm64/spotify-autoupdate-{version}-{number}.tbz",
}
}
@ -63,11 +63,11 @@ impl UrlGenerator {
const BASE_URL: &'static str = "https://upgrade.scdn.co/upgrade/client/";
fn generate_url(platform: Platform, version: &str, number: i32) -> String {
format!(
"{}{}",
Self::BASE_URL,
platform.generate_path(version, number)
)
let path = platform
.path_template()
.replace("{version}", version)
.replace("{number}", &number.to_string());
format!("{}{}", Self::BASE_URL, path)
}
}
@ -83,12 +83,15 @@ fn extract_base_version(version: &str) -> String {
#[allow(dead_code)]
fn should_use_win_x86(version: &str) -> bool {
let mut parts = version.split('.');
let base_version = extract_base_version(version);
let parts: Vec<&str> = base_version.split('.').collect();
if let (Some(p1), Some(p2), Some(p3)) = (parts.next(), parts.next(), parts.next()) {
if let (Ok(major), Ok(minor), Ok(patch)) =
(p1.parse::<u32>(), p2.parse::<u32>(), p3.parse::<u32>())
{
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);
}
}
@ -96,8 +99,7 @@ fn should_use_win_x86(version: &str) -> bool {
}
fn validate_version(version: &str) -> bool {
static RE: OnceLock<regex::Regex> = OnceLock::new();
let re = RE.get_or_init(|| regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+\.g[0-9a-f]{8}$").unwrap());
let re = regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+\.g[0-9a-f]{8}$").unwrap();
re.is_match(version)
}
@ -115,23 +117,8 @@ const MAX_CONNECTION_OPTIONS: [usize; 6] = [50, 100, 150, 200, 250, 300];
async fn check_url(client: &Client, url: String, platform: Platform) -> Option<(String, Platform)> {
match client.head(&url).send().await {
Ok(response) => {
if response.status().is_success() {
Some((url, platform))
} else if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
eprintln!(
"RATE LIMITED (429)! Server blocked the request for: {}",
url
);
None
} else {
None
}
}
Err(e) => {
eprintln!("Network error: {}", e);
None
}
Ok(response) if response.status().is_success() => Some((url, platform)),
_ => None,
}
}
@ -164,7 +151,7 @@ async fn check_version_and_submit(client: &Client, version: &str) {
let version_exists = versions_json
.values()
.any(|v| v.fullversion.as_ref().is_some_and(|fv| fv == version));
.any(|v| v.fullversion.as_ref().map_or(false, |fv| fv == version));
if !version_exists {
submit_to_google_form(client, version).await;
@ -172,15 +159,16 @@ async fn check_version_and_submit(client: &Client, version: &str) {
}
enum SearchMessage {
Progress(u64, u64),
Result(String, Platform),
Complete(String),
VersionStart(String, usize, usize),
CompleteAll,
}
struct SearchOptions {
client: Client,
version: String,
async fn search_installers(
client: &Client,
version: &str,
start: i32,
end: i32,
platforms: Vec<Platform>,
@ -188,59 +176,45 @@ struct SearchOptions {
tx: Sender<SearchMessage>,
pause_flag: Arc<AtomicBool>,
cancel_flag: Arc<AtomicBool>,
processed: Arc<AtomicU64>,
}
async fn search_installers(opts: SearchOptions) {
) {
use tokio::sync::Semaphore;
let semaphore = Arc::new(Semaphore::new(opts.max_connections));
let total = ((end - start + 1) * platforms.len() as i32) as u64;
let semaphore = Arc::new(Semaphore::new(max_connections));
let mut tasks = Vec::new();
let processed = Arc::new(Mutex::new(0u64));
'outer: for platform in opts.platforms {
for number in opts.start..=opts.end {
if opts.cancel_flag.load(Ordering::Relaxed) {
break 'outer;
}
while opts.pause_flag.load(Ordering::Relaxed) {
if opts.cancel_flag.load(Ordering::Relaxed) {
break 'outer;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break 'outer,
};
while opts.pause_flag.load(Ordering::Relaxed) {
if opts.cancel_flag.load(Ordering::Relaxed) {
break 'outer;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
if opts.cancel_flag.load(Ordering::Relaxed) {
break 'outer;
}
let url = UrlGenerator::generate_url(platform, &opts.version, number);
let client = opts.client.clone();
let tx_clone = opts.tx.clone();
let processed_clone = opts.processed.clone();
let cancel_local = opts.cancel_flag.clone();
for platform in platforms {
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 tx_clone = tx.clone();
let processed_clone = processed.clone();
let pause_local = pause_flag.clone();
let cancel_local = cancel_flag.clone();
let task = tokio::spawn(async move {
let _permit = permit;
if cancel_local.load(Ordering::Relaxed) {
return;
}
while pause_local.load(Ordering::Relaxed) {
if cancel_local.load(Ordering::Relaxed) {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
let result = check_url(&client, url, platform).await;
processed_clone.fetch_add(1, Ordering::Relaxed);
drop(permit);
let mut p = processed_clone.lock().unwrap();
*p += 1;
let current = *p;
drop(p);
let _ = tx_clone.send(SearchMessage::Progress(current, total));
if let Some((url, platform)) = result {
let _ = tx_clone.send(SearchMessage::Result(url, platform));
@ -251,17 +225,11 @@ async fn search_installers(opts: SearchOptions) {
}
}
if opts.cancel_flag.load(Ordering::Relaxed) {
for task in &tasks {
task.abort();
}
}
for task in tasks {
let _ = task.await;
}
let _ = opts.tx.send(SearchMessage::Complete(opts.version));
let _ = tx.send(SearchMessage::Complete(version.to_string()));
}
struct SpotifyFinderApp {
@ -279,6 +247,7 @@ struct SpotifyFinderApp {
report_unknown: bool,
search_results: String,
is_searching: bool,
is_paused: bool,
displayed_results: String,
@ -293,8 +262,7 @@ struct SpotifyFinderApp {
progress: f32,
progress_text: String,
total_work: u64,
processed_global: Arc<AtomicU64>,
processed_global: u64,
rx: Option<Receiver<SearchMessage>>,
found_urls: HashMap<Platform, Vec<String>>,
@ -316,6 +284,7 @@ impl Default for SpotifyFinderApp {
platform_macos_intel: false,
platform_macos_arm64: false,
report_unknown: false,
search_results: String::new(),
is_searching: false,
is_paused: false,
displayed_results: String::new(),
@ -327,7 +296,7 @@ impl Default for SpotifyFinderApp {
progress: 0.0,
progress_text: String::new(),
total_work: 0,
processed_global: Arc::new(AtomicU64::new(0)),
processed_global: 0,
rx: None,
found_urls: HashMap::new(),
pause_flag: Arc::new(AtomicBool::new(false)),
@ -340,6 +309,7 @@ impl Default for SpotifyFinderApp {
}
impl SpotifyFinderApp {
// Improved reveal logic: finish current item immediately and fast-forward the queue
fn advance_reveal(&mut self) {
if self.current_reveal.is_some() && !self.reveal_queue.is_empty() {
if let Some(cur) = self.current_reveal.take() {
@ -466,6 +436,7 @@ impl SpotifyFinderApp {
self.is_searching = true;
self.progress = 0.0;
self.progress_text = "Starting...".to_string();
self.search_results.clear();
self.displayed_results.clear();
self.reveal_queue.clear();
self.current_reveal = None;
@ -480,15 +451,13 @@ impl SpotifyFinderApp {
let per_version = ((end - start + 1) * cnt as i32) as u64;
total_work_calc = total_work_calc.saturating_add(per_version);
}
self.total_work = total_work_calc;
self.processed_global.store(0, Ordering::Relaxed);
self.processed_global = 0;
self.current_version = None;
self.current_version_index = 0;
self.total_versions = versions.len();
let (tx, rx): (Sender<SearchMessage>, Receiver<SearchMessage>) = unbounded();
let (tx, rx) = channel();
self.rx = Some(rx);
self.pause_flag.store(false, Ordering::Relaxed);
@ -500,7 +469,6 @@ impl SpotifyFinderApp {
let pause = self.pause_flag.clone();
let cancel = self.cancel_flag.clone();
let base_platforms_for_spawn = base_platforms.clone();
let processed_for_spawn = self.processed_global.clone();
self.runtime.spawn(async move {
let client = Client::builder()
@ -534,18 +502,17 @@ impl SpotifyFinderApp {
continue;
}
search_installers(SearchOptions {
client: client.clone(),
version,
search_installers(
&client,
&version,
start,
end,
platforms: platforms_for_version,
max_connections: max_conn,
tx: tx.clone(),
pause_flag: pause.clone(),
cancel_flag: cancel.clone(),
processed: processed_for_spawn.clone(),
})
platforms_for_version,
max_conn,
tx.clone(),
pause.clone(),
cancel.clone(),
)
.await;
if cancel.load(Ordering::Relaxed) {
@ -567,6 +534,7 @@ impl SpotifyFinderApp {
}
fn clear_results(&mut self) {
self.search_results.clear();
self.displayed_results.clear();
self.reveal_queue.clear();
self.current_reveal = None;
@ -574,49 +542,49 @@ impl SpotifyFinderApp {
self.progress = 0.0;
self.progress_text.clear();
self.total_work = 0;
self.processed_global.store(0, Ordering::Relaxed);
self.processed_global = 0;
}
fn update_search_progress(&mut self) {
let current_processed = self.processed_global.load(Ordering::Relaxed);
let denom = if self.total_work == 0 {
1
} else {
self.total_work
};
self.progress = current_processed as f32 / denom as f32;
if let Some(v) = &self.current_version {
let short = short_version(v);
if self.total_versions > 1 {
self.progress_text = format!(
"Checking: {}/{}, Version: {}, No. {}/{}",
current_processed,
denom,
short,
self.current_version_index,
self.total_versions
);
} else {
self.progress_text = format!(
"Checking: {}/{}, Version: {}",
current_processed, denom, short
);
}
} else if self.is_searching {
self.progress_text = format!("Checking: {}/{}", current_processed, denom);
}
if let Some(rx_owned) = self.rx.take() {
let rx = rx_owned;
let mut completed = false;
let mut processed_this_frame = 0;
while let Ok(msg) = rx_owned.try_recv() {
processed_this_frame += 1;
while let Ok(msg) = rx.try_recv() {
match msg {
SearchMessage::Progress(_current, _total) => {
self.processed_global = self.processed_global.saturating_add(1);
let denom = if self.total_work == 0 {
1
} else {
self.total_work
};
self.progress = self.processed_global as f32 / denom as f32;
if let Some(v) = &self.current_version {
let short = short_version(v);
if self.total_versions > 1 {
self.progress_text = format!(
"Checking: {}/{}, Version: {}, No. {}/{}",
self.processed_global,
denom,
short,
self.current_version_index,
self.total_versions
);
} else {
self.progress_text = format!(
"Checking: {}/{}, Version: {}",
self.processed_global, denom, short
);
}
} else {
self.progress_text =
format!("Checking: {}/{}", self.processed_global, denom);
}
}
SearchMessage::Result(url, platform) => {
let entry = self.found_urls.entry(platform).or_default();
let entry = self.found_urls.entry(platform).or_insert_with(Vec::new);
let first = entry.is_empty();
entry.push(url.clone());
@ -658,14 +626,10 @@ impl SpotifyFinderApp {
completed = true;
}
}
if processed_this_frame > 1000 && !completed {
break;
}
}
if !completed {
self.rx = Some(rx_owned);
self.rx = Some(rx);
}
}
}
@ -990,13 +954,15 @@ impl eframe::App for SpotifyFinderApp {
self.is_paused = false;
self.pause_flag.store(false, Ordering::Relaxed);
}
} else if ui
.add_sized(btn_size, egui::Button::new("⏸ Pause"))
.clicked()
{
self.is_paused = true;
self.pause_flag.store(true, Ordering::Relaxed);
self.progress_text = "Paused".to_string();
} else {
if ui
.add_sized(btn_size, egui::Button::new("⏸ Pause"))
.clicked()
{
self.is_paused = true;
self.pause_flag.store(true, Ordering::Relaxed);
self.progress_text = "Paused".to_string();
}
}
if ui
@ -1005,11 +971,13 @@ impl eframe::App for SpotifyFinderApp {
{
self.stop_search();
}
} else if ui
.add_sized(btn_size, egui::Button::new("▶ Start Search"))
.clicked()
{
self.start_search();
} else {
if ui
.add_sized(btn_size, egui::Button::new("▶ Start Search"))
.clicked()
{
self.start_search();
}
}
ui.add_enabled_ui(!self.is_searching, |ui| {

View file

@ -123,9 +123,9 @@ def commit_changes(new_versions):
# Формируем сообщение коммита в зависимости от количества новых версий
if new_versions:
commit_message = (
f"Added deb version {new_versions[0][1]}"
f"Added deb version {new_versions[0][0]}"
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:
commit_message = "Update Spotify versions"

File diff suppressed because it is too large Load diff

View file

@ -1,44 +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",