Compare commits

...

211 commits
1.4.2 ... main

Author SHA1 Message Date
amd64fox
aa537c63d9
Remove scheduled cron job from ms-spotify.yml
Removed scheduled cron job for the Spotify MS workflow.
2026-03-30 10:39:54 +03:00
amd64fox
3f6cba6bdd Temporarily disabled automatic sync trigger 2026-03-19 06:39:16 +03:00
GitHub Action
65dcb761bd Added version 1.2.85.519.g549a528b 2026-03-12 05:46:36 +00:00
GitHub Action
df4be06104 Added version 1.2.85.513.g45f09625 2026-03-10 06:44:25 +00:00
GitHub Actions
a6c9716521 Added deb version 1.2.84.476.ga1ff6607 2026-03-09 16:51:25 +00:00
GitHub Action
86ea359ab0 Added version 1.2.85.507.g795fff93 2026-03-07 06:44:37 +00:00
GitHub Action
2f207d2667 Added version 1.2.85.504.gbcffb495 2026-03-06 21:44:38 +00:00
GitHub Action
d26ef77ead Added version 1.2.85.436.g80477277 2026-03-05 08:58:00 +00:00
GitHub Action
a174eda2e0 Added version 1.2.85.376.g54b36020 2026-03-04 08:54:32 +00:00
GitHub Action
cc256be6ac Added version 1.2.85.339.g3ff2369a 2026-03-03 08:56:28 +00:00
GitHub Action
2892713442 Added version 1.2.85.304.gfdc80894 2026-03-02 08:57:41 +00:00
GitHub Action
acab906723 Added version 1.2.85.301.g1a6ed5dd 2026-03-01 08:42:48 +00:00
amd64fox
7728100b3b fix: use full version string (with hash) in deb commit message 2026-02-28 11:55:22 +03:00
amd64fox
6ccbafbb47 refactor: optimize async messaging, progress tracking, and task lifecycle
- Replace `std::sync::mpsc` with unbounded `crossbeam-channel` for improved async task messaging.
- Switch from Mutex-based processed counters to `AtomicU64` for lock-free, accurate progress tracking.
- Refactor `search_installers` to use a new `SearchOptions` struct.
- Improve task lifecycle handling, including better pause and cancel mechanisms.
- Adjust URL and path generation to return owned `String`s.
- Add `OnceLock` to cache compiled regular expressions.
- Implement explicit logging for HTTP 429 (Too Many Requests) and network errors.
- Simplify Start/Pause button logic in the UI.
- Update `.gitignore` to exclude crate targets.
2026-02-28 11:50:50 +03:00
GitHub Action
162fa66840 Added version 1.2.85.293.gb38c1e46 2026-02-28 08:41:24 +00:00
dependabot[bot]
7d8b10ba7d
Bump actions/download-artifact from 7 to 8 (#18)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 16:33:27 +03:00
dependabot[bot]
fcc8e2b822
Bump actions/upload-artifact from 6 to 7 (#19)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 16:33:25 +03:00
GitHub Action
fe889d1026 Added version 1.2.85.240.g6c6690ee 2026-02-27 08:58:42 +00:00
GitHub Action
8728576c0c Added version 1.2.84.476.ga1ff6607 2026-02-26 11:44:35 +00:00
GitHub Action
3a6a75235c Added version 1.2.84.477.gcfdf84e8 2026-02-26 05:44:18 +00:00
GitHub Action
c197397e58 Added version 1.2.84.474.g9f006f58 2026-02-25 05:44:31 +00:00
GitHub Action
9c21bf44bf Added version 1.2.84.472.g6eba3a1b 2026-02-24 06:45:12 +00:00
GitHub Action
f8fc5f57a0 Added version 1.2.84.465.gf71dea39 2026-02-21 06:44:34 +00:00
GitHub Action
3b72261622 Added version 1.2.84.461.g21733ba7 2026-02-20 06:45:36 +00:00
amd64fox
61c8175e2a
Change buildType from 'Release' to 'Master' 2026-02-20 06:17:55 +03:00
GitHub Action
5002fadff1 Added version 1.2.84.354.gc8499b61 2026-02-18 08:59:58 +00:00
GitHub Action
45bc01e8bd Added version 1.2.84.283.g28ba6b94 2026-02-17 09:00:17 +00:00
GitHub Action
5618c88aaf Added version 1.2.84.236.g47fd8e6c 2026-02-16 09:00:53 +00:00
GitHub Action
72054c2fec Added version 1.2.84.232.g0b0c686f 2026-02-15 08:44:34 +00:00
GitHub Action
c73b6f8c90 Added version 1.2.84.227.ge1d1fbbf 2026-02-14 08:45:07 +00:00
GitHub Action
02c04bbbda Added version 1.2.84.194.g162bfeda 2026-02-13 09:00:15 +00:00
GitHub Action
a5b9f1ff60 Added version 1.2.83.461.g4d9c3dae 2026-02-12 05:44:34 +00:00
GitHub Action
d683626a49 Added version 1.2.83.460.gb5356552 2026-02-11 05:44:28 +00:00
GitHub Action
9b82042f98 Added version 1.2.83.456.g111afea6 2026-02-10 06:44:23 +00:00
GitHub Actions
0618b8d452 Added deb version 1.2.82.428 2026-02-09 11:42:10 +00:00
GitHub Action
ad30f8389b Added version 1.2.83.453.g1fae877b 2026-02-07 06:46:04 +00:00
GitHub Action
d10399826f Added version 1.2.82.428.g0ac8be2b 2026-01-27 05:44:45 +00:00
GitHub Action
90c98c0eb8 Added version 1.2.82.423.ga54eef9e 2026-01-24 06:44:18 +00:00
GitHub Action
12e0093856 Added version 1.2.82.409.g64a0a3c3 2026-01-23 08:43:19 +00:00
GitHub Action
80e6947c87 Added version 1.2.82.344.g681cb6a8 2026-01-22 08:45:05 +00:00
GitHub Action
88e997b6dd Added version 1.2.82.286.g8d25d435 2026-01-21 08:45:52 +00:00
GitHub Action
ca7de1e166 Added version 1.2.82.158.g070b7ef8 2026-01-16 08:45:14 +00:00
GitHub Action
7e70f4f339 Added version 1.2.81.264.gb4ad4efa 2026-01-13 05:44:27 +00:00
GitHub Actions
41d837b5b0 Added deb version 1.2.79.427 2026-01-12 11:15:43 +00:00
GitHub Action
f399b5cdd6 Added version 1.2.81.259.g857fba73 2026-01-12 06:44:17 +00:00
GitHub Action
7c071de816 Added version 1.2.81.258.g6cf93e85 2026-01-10 06:45:24 +00:00
GitHub Action
be781c007e Added version 1.2.81.241.g2943f0b9 2026-01-09 06:46:22 +00:00
GitHub Action
43e10f7e0a Added version 1.2.81.198.g3726460d 2026-01-08 08:43:23 +00:00
GitHub Action
392a0235f8 Added version 1.2.81.159.g5fbc5508 2026-01-07 08:42:37 +00:00
GitHub Action
b25f0cf848 Added version 1.2.81.131.gcbfb0261 2026-01-06 08:42:41 +00:00
GitHub Action
7c537a68a0 Added version 1.2.81.107.g22b83685 2026-01-05 08:46:19 +00:00
GitHub Action
3fd9169c15 Added version 1.2.81.104.g225ec0e6 2026-01-04 08:38:57 +00:00
GitHub Action
3bb8caf576 Added version 1.2.81.102.g3fe6da12 2026-01-03 08:38:46 +00:00
GitHub Action
fda738dde1 Added version 1.2.81.88.g9670c97c 2026-01-02 08:42:46 +00:00
GitHub Action
c255e9eab8 Added version 1.2.80.358.g74e46c21 2025-12-30 06:44:28 +00:00
GitHub Action
4e61673309 Added version 1.2.80.354.gc3785978 2025-12-27 06:44:56 +00:00
GitHub Action
e179da8e48 Added version 1.2.80.349.g2efc88b5 2025-12-26 06:44:11 +00:00
GitHub Action
9e079d0968 Added version 1.2.80.334.gb3f9fc96 2025-12-25 08:39:29 +00:00
GitHub Action
5fba045c3a Added version 1.2.80.315.g30b9cfbe 2025-12-24 08:41:17 +00:00
GitHub Action
41a318e417 Added version 1.2.80.290.gabebb6aa 2025-12-23 08:41:11 +00:00
GitHub Action
80bb785d07 Added version 1.2.80.247.g4c104b06 2025-12-22 08:41:46 +00:00
GitHub Action
319354b1ac Added version 1.2.80.236.gb661a706 2025-12-21 08:36:09 +00:00
GitHub Action
501a8c9877 Added version 1.2.80.232.gcd5eb6df 2025-12-20 08:37:27 +00:00
GitHub Action
88e67df823 Added version 1.2.80.191.g39184aea 2025-12-19 08:44:22 +00:00
GitHub Action
2cdf95f4ad Added version 1.2.79.427.g80eb4a07 2025-12-19 06:44:47 +00:00
GitHub Actions
84854e9bf3 Added deb version 1.2.79.425 2025-12-17 09:22:54 +00:00
GitHub Action
59e6d4f47b Added version 1.2.79.425.g1d0fcf61 2025-12-16 06:44:13 +00:00
GitHub Action
6a1219a76e Added version 1.2.79.418.g617d91ef 2025-12-15 06:45:22 +00:00
GitHub Action
5f3103d908 Added version 1.2.79.417.g0e9f05fc 2025-12-13 06:46:39 +00:00
dependabot[bot]
18e8130d04
Bump actions/cache from 4 to 5 (#15)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 04:00:08 +03:00
dependabot[bot]
0874d7f305
Bump actions/download-artifact from 6 to 7 (#16)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 03:59:59 +03:00
dependabot[bot]
8d610ceaad
Bump actions/upload-artifact from 5 to 6 (#17)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 03:59:49 +03:00
GitHub Action
29b9b1e37e Added version 1.2.79.411.g33e8b03a 2025-12-12 06:45:08 +00:00
GitHub Action
18774fa88b Added version 1.2.79.378.gf06ca97d 2025-12-11 08:43:57 +00:00
GitHub Action
f450ca818c Added version 1.2.79.250.g72d95fb3 2025-12-07 08:34:52 +00:00
GitHub Action
97fff96bd7 Added version 1.2.79.246.g768eb550 2025-12-06 08:37:00 +00:00
GitHub Action
225d47e09e Added version 1.2.79.206.g6a84c90c 2025-12-05 08:43:48 +00:00
GitHub Action
1caa61daa7 Added version 1.2.78.418.gaeeb5ebd 2025-12-03 10:44:25 +00:00
GitHub Action
8b3fce1ec2 Added version 1.2.78.410.g8ed2b60a 2025-12-01 12:45:22 +00:00
GitHub Action
e060b29562 Added version 1.2.78.409.g6aead1f8 2025-11-29 06:44:19 +00:00
GitHub Action
47258b2b26 Added version 1.2.78.397.gc2b9a506 2025-11-28 06:44:15 +00:00
GitHub Action
ed8fbaf286 Added version 1.2.78.354.gcbda56e3 2025-11-27 13:02:43 +00:00
GitHub Actions
43ce7f9302 Added deb version 1.2.77.358 2025-11-25 11:14:16 +00:00
GitHub Action
27ecd89f27 Added version 1.2.78.174.g0ea26501 2025-11-22 08:33:46 +00:00
GitHub Action
f4200c56a6 Added version 1.2.78.145.g47f556f3 2025-11-21 08:42:07 +00:00
dependabot[bot]
ee2288360e
Bump actions/checkout from 5 to 6 (#14)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-20 22:52:55 +03:00
GitHub Action
27d16425b7 Added version 1.2.77.358.g4339a634 2025-11-19 05:44:32 +00:00
amd64fox
f7bc952f82 Reapply "Merge branch 'main' of https://github.com/LoaderSpot/LoaderSpot"
This reverts commit 07cf7fea90.
2025-11-19 02:13:21 +03:00
amd64fox
07cf7fea90 Revert "Merge branch 'main' of https://github.com/LoaderSpot/LoaderSpot"
This reverts commit e5cd965530, reversing
changes made to c77736ccc5.
2025-11-19 02:10:36 +03:00
amd64fox
e5cd965530 Merge branch 'main' of https://github.com/LoaderSpot/LoaderSpot 2025-11-19 02:09:26 +03:00
GitHub Action
43271f0004 Added version 1.2.56.259.g35fc40b3 2025-11-18 06:46:50 +00:00
GitHub Action
8d94b700f1 Added version 1.2.53.257.g47fa6c39 2025-11-18 06:44:53 +00:00
GitHub Action
ee6cc93a0b Added version 1.2.77.357.g6a28e474 2025-11-18 05:44:59 +00:00
GitHub Action
b89472d397 Added version 1.2.52.402.g4f67b282 2025-11-18 05:44:10 +00:00
GitHub Action
a505191e0e Added version 1.2.58.318.gc4769917 2025-11-18 04:46:26 +00:00
GitHub Action
387b329651 Added version 1.2.53.259.gecba76ba 2025-11-18 04:45:05 +00:00
GitHub Action
ca9b8c328b Added version 1.2.52.337.g72641da6 2025-11-18 04:44:09 +00:00
GitHub Action
9a887ca2d0 Added version 1.2.58.354.ga9e5d5dd 2025-11-18 03:46:58 +00:00
GitHub Action
947986fd6c Added version 1.2.57.301.gb804e5ce 2025-11-18 03:45:54 +00:00
GitHub Action
d6fb0f0483 Added version 1.2.52.297.g9ad95283 2025-11-18 03:45:05 +00:00
GitHub Action
1fb610a32e Added version 1.2.62.398.g54159197 2025-11-18 03:23:10 +00:00
GitHub Action
6e8aeba93d Added version 1.2.51.292.g9ae239fb 2025-11-18 03:22:03 +00:00
amd64fox
ff58033e51
Update update_version.py 2025-11-18 06:09:40 +03:00
amd64fox
1c69604346
Update version-check.yml 2025-11-18 06:08:50 +03:00
amd64fox
660715dd7b
Update version-check.yml 2025-11-18 06:07:29 +03:00
amd64fox
06ba92fc54
Update update_versions.yml 2025-11-18 06:06:50 +03:00
amd64fox
5717fb178c
Update version-check.yml 2025-11-18 06:01:23 +03:00
amd64fox
c77736ccc5 Revert "Update concurrency group for version check"
This reverts commit 1e1fbb6fcb.
2025-11-18 05:57:06 +03:00
amd64fox
549117406c
Increase MAX_RETRIES and update JSON handling 2025-11-18 05:51:09 +03:00
amd64fox
820937d353
Refactor update_version to accept JSON input directly 2025-11-18 05:50:24 +03:00
amd64fox
7af18cd8b2
Refactor JSON handling in update_versions.yml 2025-11-18 05:43:00 +03:00
amd64fox
815d92cd48
Refactor update_version to read JSON from file 2025-11-18 05:41:31 +03:00
amd64fox
4a0a7bb0de
Change JSON data passing method in workflow 2025-11-18 05:39:35 +03:00
amd64fox
d4981cf75c Revert "Added version 1.2.52.402.g4f67b282"
This reverts commit 871ebfb35f.
2025-11-18 05:32:31 +03:00
amd64fox
e61063df56 Revert "Added version 1.2.52.297.g9ad95283"
This reverts commit fff751fbcc.
2025-11-18 05:32:23 +03:00
amd64fox
a9433d2f73 Revert "Added version 1.2.57.301.gb804e5ce"
This reverts commit 39b110ff83.
2025-11-18 05:32:17 +03:00
amd64fox
5f4ac6a10c Revert "Added version 1.2.58.354.ga9e5d5dd"
This reverts commit e6b410b837.
2025-11-18 05:32:12 +03:00
amd64fox
8e49fe7dd7 Revert "Added version 1.2.51.292.g9ae239fb"
This reverts commit 259f31a1d3.
2025-11-18 05:32:02 +03:00
amd64fox
9046d7d8c2 Revert "Added version 1.2.56.259.g35fc40b3"
This reverts commit 4847fa31bd.
2025-11-18 05:31:50 +03:00
amd64fox
fe1af720e3 Revert "Added version 1.2.62.398.g54159197"
This reverts commit 0d00cac03f.
2025-11-18 05:31:37 +03:00
amd64fox
f3d0d53e20
Add retry logic for version update in workflow 2025-11-18 05:31:10 +03:00
amd64fox
0d00cac03f Added version 1.2.62.398.g54159197 2025-11-18 02:23:44 +00:00
amd64fox
4847fa31bd Added version 1.2.56.259.g35fc40b3 2025-11-18 02:23:20 +00:00
amd64fox
259f31a1d3 Added version 1.2.51.292.g9ae239fb 2025-11-18 02:22:59 +00:00
amd64fox
a2fd6913cc
Add push options to update_versions workflow 2025-11-18 05:20:30 +03:00
amd64fox
e6b410b837 Added version 1.2.58.354.ga9e5d5dd 2025-11-18 02:15:40 +00:00
amd64fox
39b110ff83 Added version 1.2.57.301.gb804e5ce 2025-11-18 02:15:21 +00:00
amd64fox
fff751fbcc Added version 1.2.52.297.g9ad95283 2025-11-18 02:15:03 +00:00
amd64fox
871ebfb35f Added version 1.2.52.402.g4f67b282 2025-11-18 02:14:45 +00:00
amd64fox
b8c01ad340
Refactor version update workflow to use auto-commit action 2025-11-18 05:11:59 +03:00
GitHub Action
dee58698c7 Added version 1.2.57.248.g99e4f91e 2025-11-18 01:57:56 +00:00
GitHub Action
f4b24453ac Added version 1.2.56.440.gb68df5d6 2025-11-18 01:57:35 +00:00
GitHub Action
d699d2ac8c Added version 1.2.52.290.gabcab511 2025-11-18 01:57:13 +00:00
GitHub Action
3f952f65bc Added version 1.2.51.251.g3824e6a9 2025-11-18 01:56:44 +00:00
amd64fox
1e1fbb6fcb
Update concurrency group for version check 2025-11-18 04:52:57 +03:00
GitHub Action
8dd85ffcfd Added version 1.2.63.212.g5da2018e 2025-11-18 01:48:09 +00:00
GitHub Action
2a7d1845eb Added version 1.2.61.199.g23513eeb 2025-11-18 01:46:41 +00:00
GitHub Action
172b066e95 Added version 1.2.51.210.ge411d4f7 2025-11-18 01:45:10 +00:00
GitHub Action
2c5e18b40c Added version 1.2.66.242.g7de5ae85 2025-11-18 01:01:49 +00:00
GitHub Action
f7ebdc8915 Added version 1.2.63.214.gb21d7c0d 2025-11-18 01:00:04 +00:00
GitHub Action
e7f647aa3c Added version 1.2.51.175.gfc0f5b10 2025-11-18 00:58:38 +00:00
amd64fox
4b936d86a4 add buildType 2025-11-16 14:54:10 +03:00
GitHub Action
26344b96b4 Added version 1.2.77.352.g26264c50 2025-11-16 06:44:44 +00:00
amd64fox
5b24d6d776 refactor search results to support multiple versions 2025-11-16 02:45:24 +03:00
GitHub Action
6cbe6ccdaf Added version 1.2.77.351.geb47b01f 2025-11-15 06:44:09 +00:00
amd64fox
6507e63825 fix buildType property assignment in version check script 2025-11-14 19:09:55 +03:00
amd64fox
d8e2291f0f refactor GAS submission logic in build version script 2025-11-14 19:00:24 +03:00
GitHub Action
3268102583 Added version 1.2.65.120.g1371365b 2025-11-14 04:15:48 +00:00
amd64fox
902b6f1223 Add buildType support to version update workflow 2025-11-14 07:01:40 +03:00
amd64fox
e6d3f0c76e remove redundant Write-Host debug messages 2025-11-14 05:40:41 +03:00
amd64fox
336fe44b5b replace Write-Output with Write-Host for messages 2025-11-14 05:33:05 +03:00
amd64fox
187fc4d439 refactor build info extraction to new script 2025-11-14 05:24:03 +03:00
amd64fox
5eee2572e2 remove debug output and minor message tweak 2025-11-14 02:19:30 +03:00
amd64fox
0eebb89e76 Combine build type detection and GAS sending steps 2025-11-14 02:15:12 +03:00
amd64fox
d15b5fbc32 update curl command to handle redirects in workflow 2025-11-14 02:04:00 +03:00
amd64fox
0398cb6ea1 Improve debug output for GAS POST request 2025-11-14 01:56:46 +03:00
amd64fox
d61a2e58db Improve GAS response handling in version-check workflow 2025-11-14 01:51:43 +03:00
amd64fox
e0cf1f94fc improve error handling for GAS response in workflow 2025-11-14 01:38:24 +03:00
amd64fox
430d5d40fc replace deprecated set-output with GITHUB_OUTPUT 2025-11-14 01:07:28 +03:00
amd64fox
ab8896961f refactor parameter handling in get-build-info.ps1 2025-11-14 00:43:49 +03:00
amd64fox
ba0af93cdc update version check output 2025-11-14 00:34:46 +03:00
amd64fox
86d02dde3c added build type processing 2025-11-14 00:17:15 +03:00
dependabot[bot]
31eeb1d86d
Bump actions/setup-python from 5 to 6 (#11)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-13 14:01:24 +03:00
dependabot[bot]
cb81917067
Bump actions/download-artifact from 4 to 6 (#12)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-13 14:01:01 +03:00
dependabot[bot]
4aeb2f150f
Bump actions/checkout from 4 to 5 (#13)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-13 14:00:44 +03:00
GitHub Action
817cefb17e Added version 1.2.77.288.g30addd9f 2025-11-13 10:32:33 +00:00
GitHub Action
90bc45dc0f Added version 1.2.77.250.g2d195c71 2025-11-12 17:49:09 +00:00
GitHub Action
51ff38d16f Added version 1.2.77.162.ga10209d6 2025-11-11 19:43:42 +00:00
GitHub Action
ade13834f9 Added version 1.2.76.298.g2316a870 2025-11-11 05:43:50 +00:00
GitHub Action
c1c0c715d1 Added version 1.2.70.253.g75b5048f 2025-11-10 12:11:45 +00:00
amd64fox
4e0009ce26 Improve error handling in update_version.py script 2025-11-10 15:10:22 +03:00
amd64fox
bffdc39cca refactor version update script and workflow 2025-11-10 15:03:30 +03:00
amd64fox
2d561118b9 refactor data input for version update workflow 2025-11-10 14:54:39 +03:00
amd64fox
70f3ac3249 fix DATA_JSON environment variable assignment 2025-11-10 14:51:50 +03:00
amd64fox
8806f679a9 fix DATA_JSON environment variable formatting 2025-11-10 14:48:00 +03:00
amd64fox
ea015c7280 read datajson input from environment variable 2025-11-10 14:43:40 +03:00
amd64fox
0b7ae2e90a Merge branch 'main' of https://github.com/LoaderSpot/LoaderSpot 2025-11-10 12:41:40 +03:00
amd64fox
e35724cf7f fix: increased web request timeout for gas 2025-11-10 12:41:38 +03:00
GitHub Action
6c882d93c6 Added version 1.2.77.159.gd5928880 2025-11-10 08:35:11 +00:00
amd64fox
4911d56f18 fix name 2025-11-08 15:17:41 +03:00
amd64fox
6a235cd0f5 take the compiled cli 2025-11-08 14:19:39 +03:00
amd64fox
9070044b8a updated name to loaderspot_ui 2025-11-08 11:43:38 +03:00
amd64fox
923b4b8d78 Update build-to-release.yml 2025-11-08 11:29:56 +03:00
amd64fox
4367d89cbe
Update build-to-release.yml 2025-11-08 11:21:35 +03:00
amd64fox
9053695b6b remove arm for Linux 2025-11-08 10:47:49 +03:00
amd64fox
467b7301ec added OpenSSL dependencies (arm64) 2025-11-08 10:42:38 +03:00
amd64fox
432ad0301e added OpenSSL dependencies 2025-11-08 10:36:21 +03:00
amd64fox
b4d199ea7a cli assembly added 2025-11-08 10:29:09 +03:00
GitHub Action
0eb2d0af45 Added version 1.2.77.158.g41a657be 2025-11-08 06:14:14 +00:00
GitHub Action
d894e361dd Added version 1.2.77.144.gb6198f81 2025-11-07 12:48:58 +00:00
GitHub Action
a9e9a984fd Added version 1.2.70.254.gc8d9cbb8 2025-11-05 22:30:32 +00:00
amd64fox
d508cb5e40 remove unused urlencoding dependency 2025-11-06 00:03:33 +03:00
amd64fox
8b44677f43 refactor GAS request 2025-11-05 23:49:42 +03:00
amd64fox
1e0a1f09ac fix url formatting in send_to_gas function 2025-11-05 22:55:15 +03:00
amd64fox
65e1f39299
Delete upd.py 2025-11-05 22:45:20 +03:00
amd64fox
f0d0a70f98 Merge branch 'main' of https://github.com/LoaderSpot/LoaderSpot 2025-11-05 22:44:43 +03:00
amd64fox
d24b63e43a added LoaderSpot_CLI 2025-11-05 22:44:41 +03:00
GitHub Action
8f67fb3aff Added version 1.2.76.297.ga70a30a3 2025-11-05 09:45:25 +00:00
GitHub Action
8aa7ad5534 Added version 1.2.76.286.g733f9d86 2025-11-01 16:46:48 +00:00
GitHub Action
cfbf7b4d69 Added version 1.2.71.363.g02160d4a 2025-10-30 10:18:41 +00:00
amd64fox
3ed340b20f
update cron schedule for Spotify MS workflow 2025-10-29 18:02:16 +03:00
GitHub Action
dc7df6292f Added version 1.2.76.274.g749ee04a 2025-10-29 09:26:04 +00:00
amd64fox
eeea3304d8
Fix URL encoding in send_request function
Updated URL construction to use urllib.parse.quote for proper encoding.
2025-10-28 17:01:58 +03:00
GitHub Action
9bfacab976 Added version 1.2.76.268.g83d1f263 2025-10-28 12:17:40 +00:00
amd64fox
91d87ed7ef
Delete LoaderSpot.py 2025-10-27 21:16:35 +03:00
amd64fox
a660738738
Refactor Rust toolchain installation and artifact handling
Updated the build workflow to install Rust toolchain using commands instead of an action. Changed artifact handling to copy the executable directly instead of archiving it.
2025-10-27 21:12:02 +03:00
amd64fox
4df6648885 Rename Main.rs to src/main.rs
Moved Main.rs to the src directory and renamed it to main.rs to follow Rust project conventions.
2025-10-27 20:58:00 +03:00
amd64fox
3185f44d21
Change Rust edition from 2025 to 2021 2025-10-27 20:54:30 +03:00
amd64fox
8cc9141369
Change build runner to use windows-latest 2025-10-27 20:54:01 +03:00
amd64fox
7ff5e1f0ee
Update GitHub Actions to use stable Rust and Windows 2022 2025-10-27 20:47:54 +03:00
amd64fox
d78141cc92
Change Rust toolchain from stable to nightly 2025-10-27 20:39:14 +03:00
20 changed files with 9785 additions and 695 deletions

View file

@ -1,10 +1,10 @@
name: Build Windows LoaderSpot UI
name: Build LoaderSpot
on:
workflow_dispatch:
jobs:
build:
build_windows:
runs-on: windows-latest
strategy:
matrix:
@ -17,79 +17,179 @@ jobs:
rust_target: aarch64-pc-windows-msvc
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install Rust toolchain and target
uses: actions-rs/toolchain@v1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
profile: minimal
override: true
targets: ${{ matrix.rust_target }}
- name: Build release binary
- 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: |
# fail on first error
Set-PSDebug -Strict
# build release for target
Write-Host "Building target: $env:RUST_TARGET"
cargo build --manifest-path LoaderSpot_UI/Cargo.toml --release --target $env:RUST_TARGET
# prepare artifacts folder
$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"
# locate produced executable in target/<target>/release
$exePath = Join-Path -Path "LoaderSpot_UI" -ChildPath (Join-Path -Path "target" -ChildPath (Join-Path -Path $env:RUST_TARGET -ChildPath "release"))
Write-Host "Searching for exe in: $exePath"
$exe = Get-ChildItem -Path $exePath -Filter "*.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $exe) {
Write-Error "Executable not found in $exePath"
exit 1
}
# copy and archive artifact
Copy-Item $exe.FullName -Destination "$out\loaderspot_ui_${env:ARCH}.exe" -Force
Compress-Archive -Path "$out\loaderspot_ui_${env:ARCH}.exe" -DestinationPath "$out\loaderspot_ui_${env:ARCH}.zip" -Force
- name: Upload artifact
uses: actions/upload-artifact@v5
- name: Upload Windows artifacts
uses: actions/upload-artifact@v7
with:
name: loaderspot_ui_${{ matrix.arch }}
path: artifacts/loaderspot_ui_${{ matrix.arch }}.zip
name: windows-artifacts-${{ matrix.arch }}
path: artifacts/*
- name: Get latest release
id: latest_release
uses: actions/github-script@v8
with:
script: |
const releases = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo
});
if (!releases.data || releases.data.length === 0) {
core.setFailed('No releases found for repository');
}
return releases.data[0].id;
result-encoding: string
build_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Upload to Release
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const releaseId = parseInt('${{ steps.latest_release.outputs.result }}', 10);
const path = `artifacts/loaderspot_ui_${{ matrix.arch }}.zip`;
const content = fs.readFileSync(path);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
name: `loaderspot_ui_${{ matrix.arch }}.zip`,
data: content
});
- 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

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

View file

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

View file

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

View file

@ -8,7 +8,11 @@ on:
required: true
version:
description: 'Another version parameter'
required: true
required: true
buildType:
description: 'Build type (e.g., Master, Release)'
required: false
default: 'false'
jobs:
update-version:
runs-on: ubuntu-latest
@ -17,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
token: ${{ secrets.PERSONAL_TOKEN }}
@ -29,7 +33,8 @@ jobs:
- name: Update version in versions.json
run: |
data="${{ github.event.inputs.datajson }}"
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)"
build_type="${{ github.event.inputs.buildType }}"
python3 update_version.py "$data" "$build_type"
- name: Commit changes
run: |

View file

@ -8,21 +8,47 @@ concurrency:
cancel-in-progress: false
jobs:
build:
version-search:
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.search.outputs.versions }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.12.2
- name: Install dependencies
- name: Download latest loaderspot_cli_linux_x64
env:
GH_TOKEN: ${{ github.token }}
run: |
pip install aiohttp beautifulsoup4 argparse
gh release download --repo ${{ github.repository }} --pattern "loaderspot-cli-linux-x64" --output loaderspot_cli
chmod +x loaderspot_cli
- name: Version search
run: python ${{ github.workspace }}/upd.py -v ${{ github.event.client_payload.v }} -s "${{ github.event.client_payload.s }}" -u "${{ secrets.GOOGLE_APPS_URL }}"
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 Normal file
View file

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

View file

@ -1,309 +0,0 @@
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 Normal file

File diff suppressed because it is too large Load diff

14
LoaderSpot_CLI/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[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"

341
LoaderSpot_CLI/src/main.rs Normal file
View file

@ -0,0 +1,341 @@
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 Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package]
name = "spotify_finder"
name = "loaderspot_ui"
version = "0.1.0"
edition = "2025"
edition = "2021"
[dependencies]
eframe = "0.29"
@ -11,4 +11,7 @@ reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
regex = "1.10"
winapi = { version = "0.3", features = ["winuser"] }
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, Ordering};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Instant;
use tokio::runtime::Runtime;
@ -31,13 +31,13 @@ impl Platform {
}
}
fn path_template(&self) -> &str {
fn generate_path(&self, version: &str, number: i32) -> String {
match self {
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",
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"),
}
}
@ -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 {
let path = platform
.path_template()
.replace("{version}", version)
.replace("{number}", &number.to_string());
format!("{}{}", Self::BASE_URL, path)
format!(
"{}{}",
Self::BASE_URL,
platform.generate_path(version, number)
)
}
}
@ -83,15 +83,12 @@ fn extract_base_version(version: &str) -> String {
#[allow(dead_code)]
fn should_use_win_x86(version: &str) -> bool {
let base_version = extract_base_version(version);
let parts: Vec<&str> = base_version.split('.').collect();
let mut parts = version.split('.');
if parts.len() >= 3 {
if let (Ok(major), Ok(minor), Ok(patch)) = (
parts[0].parse::<u32>(),
parts[1].parse::<u32>(),
parts[2].parse::<u32>(),
) {
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>())
{
return (major, minor, patch) <= (1, 2, 53);
}
}
@ -99,7 +96,8 @@ fn should_use_win_x86(version: &str) -> bool {
}
fn validate_version(version: &str) -> bool {
let re = regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+\.g[0-9a-f]{8}$").unwrap();
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());
re.is_match(version)
}
@ -117,8 +115,23 @@ 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)),
_ => None,
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
}
}
}
@ -151,7 +164,7 @@ async fn check_version_and_submit(client: &Client, version: &str) {
let version_exists = versions_json
.values()
.any(|v| v.fullversion.as_ref().map_or(false, |fv| fv == version));
.any(|v| v.fullversion.as_ref().is_some_and(|fv| fv == version));
if !version_exists {
submit_to_google_form(client, version).await;
@ -159,16 +172,15 @@ 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,
}
async fn search_installers(
client: &Client,
version: &str,
struct SearchOptions {
client: Client,
version: String,
start: i32,
end: i32,
platforms: Vec<Platform>,
@ -176,45 +188,59 @@ async fn search_installers(
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 total = ((end - start + 1) * platforms.len() as i32) as u64;
let semaphore = Arc::new(Semaphore::new(max_connections));
let semaphore = Arc::new(Semaphore::new(opts.max_connections));
let mut tasks = Vec::new();
let processed = Arc::new(Mutex::new(0u64));
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();
'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();
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;
drop(permit);
let mut p = processed_clone.lock().unwrap();
*p += 1;
let current = *p;
drop(p);
let _ = tx_clone.send(SearchMessage::Progress(current, total));
processed_clone.fetch_add(1, Ordering::Relaxed);
if let Some((url, platform)) = result {
let _ = tx_clone.send(SearchMessage::Result(url, platform));
@ -225,11 +251,17 @@ async fn search_installers(
}
}
if opts.cancel_flag.load(Ordering::Relaxed) {
for task in &tasks {
task.abort();
}
}
for task in tasks {
let _ = task.await;
}
let _ = tx.send(SearchMessage::Complete(version.to_string()));
let _ = opts.tx.send(SearchMessage::Complete(opts.version));
}
struct SpotifyFinderApp {
@ -247,7 +279,6 @@ struct SpotifyFinderApp {
report_unknown: bool,
search_results: String,
is_searching: bool,
is_paused: bool,
displayed_results: String,
@ -262,7 +293,8 @@ struct SpotifyFinderApp {
progress: f32,
progress_text: String,
total_work: u64,
processed_global: u64,
processed_global: Arc<AtomicU64>,
rx: Option<Receiver<SearchMessage>>,
found_urls: HashMap<Platform, Vec<String>>,
@ -284,7 +316,6 @@ 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(),
@ -296,7 +327,7 @@ impl Default for SpotifyFinderApp {
progress: 0.0,
progress_text: String::new(),
total_work: 0,
processed_global: 0,
processed_global: Arc::new(AtomicU64::new(0)),
rx: None,
found_urls: HashMap::new(),
pause_flag: Arc::new(AtomicBool::new(false)),
@ -309,7 +340,6 @@ 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() {
@ -436,7 +466,6 @@ 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;
@ -451,13 +480,15 @@ 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 = 0;
self.processed_global.store(0, Ordering::Relaxed);
self.current_version = None;
self.current_version_index = 0;
self.total_versions = versions.len();
let (tx, rx) = channel();
let (tx, rx): (Sender<SearchMessage>, Receiver<SearchMessage>) = unbounded();
self.rx = Some(rx);
self.pause_flag.store(false, Ordering::Relaxed);
@ -469,6 +500,7 @@ 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()
@ -502,17 +534,18 @@ impl SpotifyFinderApp {
continue;
}
search_installers(
&client,
&version,
search_installers(SearchOptions {
client: client.clone(),
version,
start,
end,
platforms_for_version,
max_conn,
tx.clone(),
pause.clone(),
cancel.clone(),
)
platforms: platforms_for_version,
max_connections: max_conn,
tx: tx.clone(),
pause_flag: pause.clone(),
cancel_flag: cancel.clone(),
processed: processed_for_spawn.clone(),
})
.await;
if cancel.load(Ordering::Relaxed) {
@ -534,7 +567,6 @@ impl SpotifyFinderApp {
}
fn clear_results(&mut self) {
self.search_results.clear();
self.displayed_results.clear();
self.reveal_queue.clear();
self.current_reveal = None;
@ -542,49 +574,49 @@ impl SpotifyFinderApp {
self.progress = 0.0;
self.progress_text.clear();
self.total_work = 0;
self.processed_global = 0;
self.processed_global.store(0, Ordering::Relaxed);
}
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_insert_with(Vec::new);
let entry = self.found_urls.entry(platform).or_default();
let first = entry.is_empty();
entry.push(url.clone());
@ -626,10 +658,14 @@ impl SpotifyFinderApp {
completed = true;
}
}
if processed_this_frame > 1000 && !completed {
break;
}
}
if !completed {
self.rx = Some(rx);
self.rx = Some(rx_owned);
}
}
}
@ -954,15 +990,13 @@ 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
@ -971,13 +1005,11 @@ 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| {

125
check-build-version.ps1 Normal file
View file

@ -0,0 +1,125 @@
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

@ -123,9 +123,9 @@ def commit_changes(new_versions):
# Формируем сообщение коммита в зависимости от количества новых версий
if new_versions:
commit_message = (
f"Added deb version {new_versions[0][0]}"
f"Added deb version {new_versions[0][1]}"
if len(new_versions) == 1
else f"Added deb versions: {', '.join(v[0] for v in new_versions)}"
else f"Added deb versions: {', '.join(v[1] for v in new_versions)}"
)
else:
commit_message = "Update Spotify versions"

163
upd.py
View file

@ -1,163 +0,0 @@
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())

63
update_version.py Normal file
View file

@ -0,0 +1,63 @@
#!/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,4 +1,44 @@
{
"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",