12 Commits

Author SHA1 Message Date
amd64fox
3bdfec03a8 Remove redacted host and user lines 2026-04-18 18:21:38 +03:00
amd64fox
2783cbe0c1 Move clipboard notice to end 2026-04-18 18:15:52 +03:00
amd64fox
f4279fb962 Add clipboard copy for diagnostics 2026-04-18 18:12:47 +03:00
amd64fox
ee5881572c Improve diagnostic output privacy 2026-04-18 18:05:50 +03:00
amd64fox
4f0706b010 Fix diagnostic launcher script path 2026-04-18 17:54:10 +03:00
amd64fox
4106903513 Add download diagnostics scripts 2026-04-18 17:50:05 +03:00
amd64fox
0abf98a36b fix(js-helper): stop sending version_unavailable to worker
log latest resolution failures in the client console instead

avoid noisy worker error events when all latest sources fail
2026-04-08 09:11:16 +03:00
amd64fox
ef6601d186 fix(js-helper): ignore empty bearer and remove local version fallback
- wait for a non-empty bearer token before starting and stop capture after 5 empty attempts or 30s
- use the worker latest endpoint as the final fallback and stop when no remote version is available
2026-04-07 08:34:55 +03:00
amd64fox
ffd4a6b897 use temporary download URL for latest x64
#836
2026-04-04 17:07:19 +03:00
amd64fox
bd9726f589 js-helper: tighten desktop-update probing and error reporting
- add scriptVersion to clientContext
- stop treating latest.json as a confirmed target version
- gate desktop-update probing through sequential success-only checks
- add timeout and one retry for request failures
- keep empty_response and request_error silent
- report parse failures with forensic raw response payloads
- remove dead metadata and obsolete error branches
2026-04-02 08:35:04 +03:00
amd64fox
74915a1e3e Added sending of new Spotify versions 2026-04-01 07:22:33 +03:00
amd64fox
68fe6b6726 rename worker 2026-04-01 03:43:25 +03:00
4 changed files with 1399 additions and 6 deletions

964
js-helper/checkVersion.js Normal file
View File

@@ -0,0 +1,964 @@
(() => {
if (window.oneTime) return;
window.oneTime = true;
const WORKER_BASE_URL = "https://spotify-ingest-admin.amd64fox1.workers.dev";
const SCRIPT_VERSION = "1.2.1";
const SOURCE_LABELS = {
REMOTE: "latest.json",
FIXED: "fixed-version"
};
const PLATFORMS = [
{
code: "Win32_x86_64",
assetPrefix: "spotify_installer",
assetSuffix: "x64",
extension: ".exe"
},
{
code: "Win32_ARM64",
assetPrefix: "spotify_installer",
assetSuffix: "arm64",
extension: ".exe"
},
{
code: "OSX",
assetPrefix: "spotify-autoupdate",
assetSuffix: "x86_64",
extension: ".tbz"
},
{
code: "OSX_ARM64",
assetPrefix: "spotify-autoupdate",
assetSuffix: "arm64",
extension: ".tbz"
}
];
const PLATFORM_CODES = PLATFORMS.map((platform) => platform.code);
const SUCCESS_REPORT_STORAGE_KEY = "spotify_ingest:last_successful_report_v1";
const ERROR_MESSAGES = {
token_missing: "Authorization token not captured",
version_unavailable: "Spotify version unavailable. Update check stopped",
inconsistent_target_version: "Inconsistent target version across platform links",
empty_response: "No update link in response",
desktop_update_parse_error: "Desktop-update response parse failed."
};
const CONFIG = {
fixedShortVersion: "",
latestUrls: Array.isArray(window.__spotifyLatestUrls)
? window.__spotifyLatestUrls.filter((url) => typeof url === "string" && url.trim()).map((url) => url.trim())
: window.__spotifyLatestUrl
? [String(window.__spotifyLatestUrl).trim()]
: [
"https://raw.githubusercontent.com/LoaderSpot/table/refs/heads/main/latest.json",
"https://raw.githack.com/LoaderSpot/table/main/latest.json",
`${WORKER_BASE_URL}/api/client/latest`
],
updateUrl: "https://spclient.wg.spotify.com/desktop-update/v2/update",
reportEndpoint: `${WORKER_BASE_URL}/api/client/report`,
errorEndpoint: `${WORKER_BASE_URL}/api/client/error`,
reportTimeoutMs: 15000,
versionTimeoutMs: 10000,
desktopUpdateTimeoutMs: 8000,
desktopUpdateMaxRetries: 1,
tokenCaptureMaxAttempts: 5,
tokenCaptureTimeoutMs: 30000
};
const originalFetch = window.fetch;
let runStarted = false;
let tokenCaptureStopped = false;
let tokenCaptureAttempts = 0;
let tokenCaptureTimeoutId = 0;
const SPOTIFY_VERSION_RE = /Spotify\/(\d+\.\d+\.\d+\.\d+)/;
function nowIso() {
return new Date().toISOString();
}
function extractShortVersion(value) {
return String(value || "").match(/(\d+\.\d+\.\d+\.\d+)/)?.[1] || "";
}
function readVersionSourceSnapshot() {
return {
clientInformationAppVersion: String(window.clientInformation?.appVersion || ""),
userAgent: String(navigator.userAgent || ""),
navigatorAppVersion: String(window.navigator?.appVersion || "")
};
}
function readClientVersionSources() {
const versionSources = readVersionSourceSnapshot();
return {
clientInformationAppVersion: versionSources.clientInformationAppVersion,
userAgent: versionSources.userAgent,
navigatorAppVersion: versionSources.navigatorAppVersion,
realVersion:
versionSources.userAgent.match(SPOTIFY_VERSION_RE)?.[1] ||
versionSources.navigatorAppVersion.match(SPOTIFY_VERSION_RE)?.[1] ||
"undefined"
};
}
function buildSpotifyAppVersion(shortVersion, sourceLabel) {
if (!shortVersion) {
console.warn(`Spotify version not found (${sourceLabel}).`);
return "";
}
const parts = shortVersion.split(".");
if (parts.length !== 4) {
console.warn(`Invalid Spotify version format (${sourceLabel}):`, shortVersion);
return "";
}
const [major, minor, patch, build] = parts;
return major + minor + patch + "0".repeat(Math.max(0, 7 - patch.length - build.length)) + build;
}
async function fetchJsonWithTimeout(url, timeoutMs) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await originalFetch(url, {
method: "GET",
cache: "no-store",
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
} finally {
clearTimeout(timeoutId);
}
}
async function resolveQueryVersion() {
const fixedShortVersion = String(CONFIG.fixedShortVersion || "").trim();
if (fixedShortVersion) {
return {
shortVersion: fixedShortVersion,
fullVersion: "",
spotifyAppVersion: buildSpotifyAppVersion(fixedShortVersion, SOURCE_LABELS.FIXED),
sourceLabel: SOURCE_LABELS.FIXED,
remoteVersionFailed: false,
remoteShortVersion: "",
remoteFullVersion: ""
};
}
for (const latestUrl of CONFIG.latestUrls) {
try {
const data = await fetchJsonWithTimeout(latestUrl, CONFIG.versionTimeoutMs);
const shortVersion = String(data?.version || "").trim();
const fullVersion = String(data?.fullversion || "").trim();
if (!shortVersion) {
throw new Error("version field is missing or empty.");
}
return {
shortVersion,
fullVersion,
spotifyAppVersion: buildSpotifyAppVersion(shortVersion, SOURCE_LABELS.REMOTE),
sourceLabel: SOURCE_LABELS.REMOTE,
remoteVersionFailed: false,
remoteShortVersion: shortVersion,
remoteFullVersion: fullVersion
};
} catch (error) {
console.warn(`Failed to fetch latest.json version from ${latestUrl}: ${error?.message || error}`);
}
}
return {
shortVersion: "",
fullVersion: "",
spotifyAppVersion: "",
sourceLabel: "",
remoteVersionFailed: true,
remoteShortVersion: "",
remoteFullVersion: ""
};
}
function createState(token) {
return {
token,
startedAtMs: Date.now(),
versionSources: readClientVersionSources(),
spotifyAppVersion: "",
sourceLabel: "",
queryShortVersion: "",
queryFullVersion: "",
remoteVersionFailed: false,
remoteShortVersion: "",
remoteFullVersion: "",
targetShortVersion: "",
targetFullVersion: "",
platforms: {},
failures: [],
desktopUpdateResponses: [],
retryCountByPlatform: {},
forensicMode: false
};
}
function readClientContext(state) {
const nav = window.navigator || {};
return {
scriptVersion: SCRIPT_VERSION,
userAgent: state.versionSources.userAgent || nav.userAgent || "",
platform: nav.platform || "",
language: nav.language || "",
languages: Array.isArray(nav.languages) ? nav.languages.slice(0, 5) : [],
clientInformationAppVersion: state.versionSources.clientInformationAppVersion,
navigatorAppVersion: state.versionSources.navigatorAppVersion,
"real-version": state.versionSources.realVersion,
spotifyAppVersion: state.spotifyAppVersion,
sourceLabel: state.sourceLabel,
latestJsonVersion: state.sourceLabel === SOURCE_LABELS.REMOTE ? state.remoteShortVersion : "",
latestJsonFullVersion: state.sourceLabel === SOURCE_LABELS.REMOTE ? state.remoteFullVersion : ""
};
}
function readRequestMeta(state, extra = {}) {
return {
source: "spotify-client-script",
timestamp: nowIso(),
hasAuthorization: Boolean(state.token),
headers: { "spotify-app-version": state.spotifyAppVersion },
...extra
};
}
function readDiagnostics(state, result, extra = {}) {
return {
result,
remoteVersionUsed: state.sourceLabel === SOURCE_LABELS.REMOTE,
remoteVersionFailed: state.remoteVersionFailed,
remoteVersion: state.remoteShortVersion || null,
remoteFullVersion: state.remoteFullVersion || null,
queryShortVersion: state.queryShortVersion || null,
queryFullVersion: state.queryFullVersion || null,
detectedShortVersion: state.targetShortVersion || null,
detectedFullVersion: state.targetFullVersion || null,
requestDurationMs: Math.max(0, Date.now() - state.startedAtMs),
checkedPlatforms: PLATFORM_CODES,
foundPlatforms: Object.keys(state.platforms),
failures: state.failures,
...extra
};
}
function getPayloadVersions(state) {
return {
shortVersion: state.targetShortVersion || "",
fullVersion: state.targetFullVersion || ""
};
}
function buildNormalizedAssetName(platform, fullVersion) {
return `${platform.assetPrefix}-${fullVersion}-${platform.assetSuffix}${platform.extension}`;
}
function parseUpgradeAsset(platform, sourceUrl) {
let normalizedUrl;
try {
normalizedUrl = new URL(sourceUrl);
} catch {
throw new Error(`Invalid upgrade link URL for ${platform.code}.`);
}
const assetName = decodeURIComponent(normalizedUrl.pathname.split("/").pop() || "");
const pattern = platform.extension === ".exe"
? /^spotify_installer-(.+?)-(?:\d+|x86|x64|arm64)\.exe$/i
: /^spotify-autoupdate-(.+?)-(?:\d+|x86_64|arm64)\.tbz$/i;
const fullVersion = assetName.match(pattern)?.[1]?.trim() || "";
const shortVersion = extractShortVersion(fullVersion);
if (!fullVersion || !shortVersion) {
throw new Error(`Unsupported upgrade asset name for ${platform.code}: ${assetName}`);
}
return {
url: normalizedUrl.toString(),
shortVersion,
fullVersion,
normalizedAssetName: buildNormalizedAssetName(platform, fullVersion)
};
}
function finalizeDetectedVersions(state) {
const assets = Object.values(state.platforms);
const shortVersions = [...new Set(assets.map((asset) => asset.shortVersion).filter(Boolean))];
const fullVersions = [...new Set(assets.map((asset) => asset.fullVersion).filter(Boolean))];
if (shortVersions.length > 1 || fullVersions.length > 1) {
return false;
}
state.targetShortVersion = shortVersions[0] || "";
state.targetFullVersion = fullVersions[0] || "";
return true;
}
function buildPlatformPayload(platforms) {
const payload = {};
for (const [code, asset] of Object.entries(platforms)) {
if (!asset) continue;
payload[code] = {
url: asset.url,
shortVersion: asset.shortVersion,
fullVersion: asset.fullVersion,
normalizedAssetName: asset.normalizedAssetName
};
}
return payload;
}
function getSuccessReportStorage() {
try {
return window.localStorage || null;
} catch {
return null;
}
}
function clearStoredSuccessReport(storage = getSuccessReportStorage()) {
if (!storage) {
return;
}
try {
storage.removeItem(SUCCESS_REPORT_STORAGE_KEY);
} catch {
// ignore storage cleanup failures
}
}
function readStoredSuccessReport() {
const storage = getSuccessReportStorage();
if (!storage) {
return null;
}
let rawValue = "";
try {
rawValue = String(storage.getItem(SUCCESS_REPORT_STORAGE_KEY) || "");
} catch {
return null;
}
if (!rawValue) {
return null;
}
try {
const parsed = JSON.parse(rawValue);
const shortVersion = String(parsed?.shortVersion || "").trim();
const fullVersion = String(parsed?.fullVersion || "").trim();
const reportedAt = String(parsed?.reportedAt || "").trim();
if (!fullVersion) {
clearStoredSuccessReport(storage);
return null;
}
return {
shortVersion,
fullVersion,
reportedAt
};
} catch {
clearStoredSuccessReport(storage);
return null;
}
}
function isAlreadyReported(fullVersion) {
const normalizedFullVersion = String(fullVersion || "").trim();
if (!normalizedFullVersion) {
return false;
}
return readStoredSuccessReport()?.fullVersion === normalizedFullVersion;
}
function writeStoredSuccessReport(state) {
const storage = getSuccessReportStorage();
if (!storage) {
return false;
}
const payload = {
shortVersion: state.targetShortVersion || "",
fullVersion: state.targetFullVersion || "",
reportedAt: nowIso()
};
if (!payload.fullVersion) {
return false;
}
try {
storage.setItem(SUCCESS_REPORT_STORAGE_KEY, JSON.stringify(payload));
return true;
} catch {
return false;
}
}
function postJsonWithTimeout(endpoint, body) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CONFIG.reportTimeoutMs);
return originalFetch(endpoint, {
method: "POST",
headers: { "Content-Type": "text/plain;charset=UTF-8" },
body,
cache: "no-store",
keepalive: true,
signal: controller.signal
}).finally(() => {
clearTimeout(timeoutId);
});
}
function sendBestEffortPayload(endpoint, payload) {
if (!endpoint) {
return;
}
const body = JSON.stringify(payload);
try {
if (navigator.sendBeacon && navigator.sendBeacon(endpoint, body)) {
return;
}
} catch {
// ignore beacon failure and fall back to fetch
}
void postJsonWithTimeout(endpoint, body).catch((error) => {
console.warn("Failed to send report:", error?.message || error);
});
}
async function postSuccessPayloadWithAck(endpoint, payload) {
if (!endpoint) {
return false;
}
const body = JSON.stringify(payload);
try {
const response = await postJsonWithTimeout(endpoint, body);
if (response.status === 200) {
return true;
}
console.warn(`Client report rejected with HTTP ${response.status}.`);
return false;
} catch (error) {
console.warn("Failed to send acknowledged report:", error?.message || error);
return false;
}
}
async function sendSuccess(state) {
const versions = getPayloadVersions(state);
return postSuccessPayloadWithAck(CONFIG.reportEndpoint, {
shortVersion: versions.shortVersion,
fullVersion: versions.fullVersion,
platforms: buildPlatformPayload(state.platforms),
clientContext: readClientContext(state),
requestMeta: readRequestMeta(state),
diagnostics: readDiagnostics(state, "success")
});
}
function sendError(state, kind, extra = {}) {
const versions = getPayloadVersions(state);
sendBestEffortPayload(CONFIG.errorEndpoint, {
kind,
phase: extra.phase || kind,
shortVersion: versions.shortVersion,
fullVersion: versions.fullVersion,
message: extra.message || ERROR_MESSAGES[kind] || "Unexpected error.",
stack: extra.stack || "",
partialPlatforms: buildPlatformPayload(state.platforms),
clientContext: readClientContext(state),
requestMeta: readRequestMeta(state, extra.requestMeta),
diagnostics: readDiagnostics(state, "error", extra.diagnostics),
rawPayload: extra.rawPayload
});
}
function decodeLatin1Buffer(buffer) {
return new TextDecoder("latin1").decode(buffer);
}
function extractUpgradeLink(bodyLatin1) {
const payload = String(bodyLatin1 || "");
const baseUrl = payload.match(
/https:\/\/upgrade\.scdn\.co\/upgrade\/client\/(?:win32-(?:x86_64|arm64)|osx-(?:x86_64|arm64))\/[A-Za-z0-9._-]+\.(?:exe|tbz)/i
)?.[0];
const authQuery = payload.match(/\?fauth=[A-Za-z0-9._~-]+/)?.[0];
return baseUrl && authQuery ? `${baseUrl}${authQuery}` : "";
}
function readResponseHeaders(headers) {
const result = {};
if (!headers || typeof headers.forEach !== "function") {
return result;
}
headers.forEach((value, key) => {
result[String(key || "").toLowerCase()] = String(value || "");
});
return result;
}
function formatDesktopUpdateError(platform, error) {
if (error?.name === "AbortError") {
return `${platform.code} request timeout after ${CONFIG.desktopUpdateTimeoutMs}ms`;
}
return error?.message || String(error);
}
function buildRequestErrorResult(base, errorMessage) {
return {
outcome: "request_error",
finalUrl: base.finalUrl || CONFIG.updateUrl,
status: Number.isFinite(Number(base.status)) ? Number(base.status) : null,
headers: base.headers || {},
contentType: base.contentType || null,
contentLength: base.contentLength || null,
byteLength: null,
bodyLatin1: null,
extractedUpgradeLink: "",
parseErrorMessage: null,
errorMessage: errorMessage || null
};
}
async function fetchDesktopUpdateAttempt(token, spotifyAppVersion, platform) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CONFIG.desktopUpdateTimeoutMs);
let response;
try {
response = await originalFetch(CONFIG.updateUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Spotify-App-Version": spotifyAppVersion,
"App-Platform": platform.code
},
signal: controller.signal
});
} catch (error) {
return buildRequestErrorResult({
finalUrl: CONFIG.updateUrl,
status: null,
headers: {},
contentType: null,
contentLength: null
}, formatDesktopUpdateError(platform, error));
} finally {
clearTimeout(timeoutId);
}
const finalUrl = response.url || CONFIG.updateUrl;
const headers = readResponseHeaders(response.headers);
const contentType = response.headers?.get?.("content-type") || null;
const contentLength = response.headers?.get?.("content-length") || null;
if (!response.ok) {
return buildRequestErrorResult({
finalUrl,
status: response.status,
headers,
contentType,
contentLength
}, `${platform.code} HTTP error: ${response.status}`);
}
let buffer;
try {
buffer = await response.arrayBuffer();
} catch (error) {
return buildRequestErrorResult({
finalUrl,
status: response.status,
headers,
contentType,
contentLength
}, formatDesktopUpdateError(platform, error));
}
const bodyLatin1 = decodeLatin1Buffer(buffer);
const extractedUpgradeLink = extractUpgradeLink(bodyLatin1);
const baseResult = {
finalUrl,
status: response.status,
headers,
contentType,
contentLength,
byteLength: buffer.byteLength,
bodyLatin1,
extractedUpgradeLink
};
if (!extractedUpgradeLink) {
return {
outcome: "empty_response",
...baseResult,
parseErrorMessage: null,
errorMessage: null
};
}
try {
const asset = parseUpgradeAsset(platform, extractedUpgradeLink);
return {
outcome: "success",
...baseResult,
parseErrorMessage: null,
errorMessage: null,
asset
};
} catch (error) {
return {
outcome: "parse_error",
...baseResult,
parseErrorMessage: error?.message || String(error),
errorMessage: null
};
}
}
function buildAttemptMetadata(attemptNumber, result) {
return {
attempt: attemptNumber,
outcome: result.outcome,
status: Number.isFinite(Number(result.status)) ? Number(result.status) : null,
finalUrl: result.finalUrl || null,
contentType: result.contentType || null,
contentLength: result.contentLength || null,
byteLength: Number.isFinite(Number(result.byteLength)) ? Number(result.byteLength) : null,
errorMessage: result.errorMessage || null
};
}
function buildDesktopUpdateResponseRecord(platformCode, attempts, result, requestErrors) {
return {
platform: platformCode,
attempts,
finalOutcome: result.outcome,
finalUrl: result.finalUrl || null,
status: Number.isFinite(Number(result.status)) ? Number(result.status) : null,
headers: result.headers || {},
contentType: result.contentType || null,
contentLength: result.contentLength || null,
byteLength: Number.isFinite(Number(result.byteLength)) ? Number(result.byteLength) : null,
bodyLatin1: result.bodyLatin1 || null,
extractedUpgradeLink: result.extractedUpgradeLink || "",
parseErrorMessage: result.parseErrorMessage || null,
requestErrors
};
}
function buildForensicDiagnostics(state) {
const retryCountByPlatform = {};
for (const platform of PLATFORMS) {
retryCountByPlatform[platform.code] = Number(state.retryCountByPlatform[platform.code] || 0);
}
const successfulPlatforms = [];
const parseErrorPlatforms = [];
const requestErrorPlatforms = [];
const emptyResponsePlatforms = [];
for (const item of state.desktopUpdateResponses) {
if (!item?.platform) {
continue;
}
if (item.finalOutcome === "success") successfulPlatforms.push(item.platform);
if (item.finalOutcome === "parse_error") parseErrorPlatforms.push(item.platform);
if (item.finalOutcome === "request_error") requestErrorPlatforms.push(item.platform);
if (item.finalOutcome === "empty_response") emptyResponsePlatforms.push(item.platform);
}
return {
successfulPlatforms,
parseErrorPlatforms,
requestErrorPlatforms,
emptyResponsePlatforms,
retryCountByPlatform
};
}
function buildForensicRawPayload(state) {
return {
desktopUpdateResponses: state.desktopUpdateResponses.map((item) => ({
platform: item.platform,
attempts: Array.isArray(item.attempts) ? item.attempts.map((attempt) => ({ ...attempt })) : [],
finalOutcome: item.finalOutcome,
finalUrl: item.finalUrl || null,
status: item.status ?? null,
headers: item.headers && typeof item.headers === "object" ? { ...item.headers } : {},
contentType: item.contentType || null,
contentLength: item.contentLength || null,
byteLength: item.byteLength ?? null,
bodyLatin1: item.bodyLatin1 || null,
extractedUpgradeLink: item.extractedUpgradeLink || "",
parseErrorMessage: item.parseErrorMessage || null,
requestErrors: Array.isArray(item.requestErrors) ? item.requestErrors.slice() : []
}))
};
}
async function collectPlatformResult(state, platform) {
const attempts = [];
const requestErrors = [];
const maxAttempts = 1 + Number(CONFIG.desktopUpdateMaxRetries || 0);
let finalResult = null;
for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex += 1) {
const result = await fetchDesktopUpdateAttempt(state.token, state.spotifyAppVersion, platform);
finalResult = result;
attempts.push(buildAttemptMetadata(attemptIndex + 1, result));
if (result.outcome === "request_error" && result.errorMessage) {
requestErrors.push(result.errorMessage);
}
if (result.outcome !== "request_error" || attemptIndex === maxAttempts - 1) {
break;
}
}
state.retryCountByPlatform[platform.code] = Math.max(0, attempts.length - 1);
state.desktopUpdateResponses.push(
buildDesktopUpdateResponseRecord(platform.code, attempts, finalResult, requestErrors)
);
if (finalResult.outcome === "success") {
state.platforms[platform.code] = finalResult.asset;
return finalResult.outcome;
}
if (finalResult.outcome === "parse_error") {
state.forensicMode = true;
state.failures.push({
platform: platform.code,
kind: "parse_error",
message: finalResult.parseErrorMessage || `Failed to parse ${platform.code} upgrade response`
});
return finalResult.outcome;
}
if (finalResult.outcome === "empty_response") {
state.failures.push({
platform: platform.code,
kind: "empty_response",
message: ERROR_MESSAGES.empty_response
});
return finalResult.outcome;
}
state.failures.push({
platform: platform.code,
kind: "request_error",
message: finalResult.errorMessage || `Failed to request ${platform.code} update metadata`
});
return finalResult.outcome;
}
async function collectPlatforms(state) {
for (const platform of PLATFORMS) {
const outcome = await collectPlatformResult(state, platform);
if (!state.forensicMode && outcome !== "success") {
return { aborted: true };
}
}
return { aborted: false };
}
function sendDesktopUpdateParseError(state) {
sendError(state, "desktop_update_parse_error", {
phase: "desktop_update_parse_error",
message: ERROR_MESSAGES.desktop_update_parse_error,
diagnostics: buildForensicDiagnostics(state),
rawPayload: buildForensicRawPayload(state)
});
}
function logVersionUnavailable(state) {
console.error(ERROR_MESSAGES.version_unavailable, {
scriptVersion: SCRIPT_VERSION,
remoteVersionFailed: state.remoteVersionFailed,
realVersion: state.versionSources.realVersion || "",
latestJsonVersion: state.remoteShortVersion || "",
latestJsonFullVersion: state.remoteFullVersion || ""
});
}
async function runOnce(token) {
const state = createState(token);
if (!token) {
sendError(state, "token_missing");
return;
}
const version = await resolveQueryVersion();
state.queryShortVersion = version.shortVersion;
state.queryFullVersion = version.fullVersion;
state.spotifyAppVersion = version.spotifyAppVersion;
state.sourceLabel = version.sourceLabel;
state.remoteVersionFailed = version.remoteVersionFailed;
state.remoteShortVersion = version.remoteShortVersion;
state.remoteFullVersion = version.remoteFullVersion;
if (!state.spotifyAppVersion) {
logVersionUnavailable(state);
return;
}
const collection = await collectPlatforms(state);
if (state.forensicMode) {
sendDesktopUpdateParseError(state);
return;
}
if (collection.aborted) {
return;
}
const foundCount = Object.keys(state.platforms).length;
if (!finalizeDetectedVersions(state)) {
sendError(state, "inconsistent_target_version", {
diagnostics: {
detectedShortVersions: [...new Set(Object.values(state.platforms).map((asset) => asset.shortVersion))],
detectedFullVersions: [...new Set(Object.values(state.platforms).map((asset) => asset.fullVersion))]
}
});
return;
}
if (isAlreadyReported(state.targetFullVersion)) {
return;
}
if (await sendSuccess(state)) {
writeStoredSuccessReport(state);
}
}
function getHeaderValue(headers, name) {
const target = String(name).toLowerCase();
if (!headers) return null;
if (headers instanceof Headers) {
return headers.get(target);
}
if (Array.isArray(headers)) {
return headers.find(([key]) => String(key).toLowerCase() === target)?.[1] || null;
}
if (typeof headers === "object") {
for (const key of Object.keys(headers)) {
if (key.toLowerCase() === target) {
return headers[key];
}
}
}
return null;
}
function getRequestUrl(input) {
if (typeof input === "string") return input;
if (input instanceof URL) return input.toString();
if (input instanceof Request) return input.url;
return "";
}
function isSpotifyAuthorizedRequest(url, authorization) {
return Boolean(
authorization &&
/^Bearer\s+/i.test(String(authorization)) &&
/spclient\.wg\.spotify\.com/i.test(String(url || ""))
);
}
function extractBearerToken(authorization) {
const match = String(authorization || "").match(/^Bearer\s+(.+)$/i);
if (!match) {
return "";
}
return String(match[1] || "").trim();
}
function stopTokenCapture(reason) {
if (tokenCaptureStopped) {
return;
}
tokenCaptureStopped = true;
window.fetch = originalFetch;
if (tokenCaptureTimeoutId) {
clearTimeout(tokenCaptureTimeoutId);
tokenCaptureTimeoutId = 0;
}
if (reason === "max_attempts") {
console.warn(`Spotify token capture stopped after ${CONFIG.tokenCaptureMaxAttempts} empty bearer attempts.`);
} else if (reason === "timeout") {
console.warn(`Spotify token capture stopped after ${CONFIG.tokenCaptureTimeoutMs}ms timeout.`);
}
}
tokenCaptureTimeoutId = setTimeout(() => {
if (!runStarted) {
stopTokenCapture("timeout");
}
}, CONFIG.tokenCaptureTimeoutMs);
window.fetch = async function (...args) {
const [input, init] = args;
const headers = init?.headers || (input instanceof Request ? input.headers : null);
const authorization = getHeaderValue(headers, "authorization");
if (!runStarted && !tokenCaptureStopped && isSpotifyAuthorizedRequest(getRequestUrl(input), authorization)) {
tokenCaptureAttempts += 1;
const token = extractBearerToken(authorization);
if (token) {
runStarted = true;
stopTokenCapture("success");
void runOnce(token).catch((error) => {
const state = createState(token);
sendError(state, "uncaught", {
phase: "uncaught_runOnce",
message: error?.message || String(error),
stack: error?.stack || ""
});
});
} else if (tokenCaptureAttempts >= CONFIG.tokenCaptureMaxAttempts) {
stopTokenCapture("max_attempts");
}
}
return originalFetch.apply(this, args);
};
})();

31
run.ps1
View File

@@ -116,6 +116,9 @@ param
[Parameter(HelpMessage = 'Do not create desktop shortcut.')]
[switch]$no_shortcut,
[Parameter(HelpMessage = 'Disable sending new versions')]
[switch]$sendversion_off,
[Parameter(HelpMessage = 'Static color for lyrics.')]
[ArgumentCompleter({ param($cmd, $param, $wordToComplete)
[array] $validValues = @('blue', 'blueberry', 'discord', 'drot', 'default', 'forest', 'fresh', 'github', 'lavender', 'orange', 'postlight', 'pumpkin', 'purple', 'radium', 'relish', 'red', 'sandbar', 'spotify', 'spotify#2', 'strawberry', 'turquoise', 'yellow', 'zing', 'pinkle', 'krux', 'royal', 'oceano')
@@ -474,7 +477,8 @@ function Get-SpotifyInstallerArchitecture {
}
}
$spotifyDownloadBaseUrl = "https://broad-pine-bbc0.amd64fox1.workers.dev/download"
$spotifyDownloadBaseUrl = "https://loadspot.amd64fox1.workers.dev/download"
$spotifyTemporaryDownloadBaseUrl = "https://loadspot.amd64fox1.workers.dev/temporary-download"
$systemArchitecture = Get-SystemArchitecture
$match_v = "^(?<version>\d+\.\d+\.\d+\.\d+\.g[0-9a-f]{8})(?:-\d+)?$"
@@ -960,8 +964,8 @@ function Invoke-DownloadMethodWithRetries {
return [PSCustomObject]@{
Success = $true
Error = $null
Method = $DownloadMethod
Error = $null
Method = $DownloadMethod
}
}
catch {
@@ -979,8 +983,8 @@ function Invoke-DownloadMethodWithRetries {
return [PSCustomObject]@{
Success = $false
Error = $lastError
Method = $DownloadMethod
Error = $lastError
Method = $DownloadMethod
}
}
@@ -1229,7 +1233,13 @@ function downloadSp([string]$DownloadFolder) {
-SpotifyVersion $spotifyVersion `
-LastX86SupportedVersion $last_x86
$web_Url = "$spotifyDownloadBaseUrl/spotify_installer-$onlineFull-$arch.exe"
$downloadBaseUrl = $spotifyDownloadBaseUrl
if ($onlineFull -eq $latest_full -and $arch -eq 'x64') {
# Temporary route for the latest x64 build while Cloudflare rechecks the file
$downloadBaseUrl = $spotifyTemporaryDownloadBaseUrl
}
$web_Url = "$downloadBaseUrl/spotify_installer-$onlineFull-$arch.exe"
$local_Url = Join-Path $DownloadFolder 'SpotifySetup.exe'
$web_name_file = "SpotifySetup.exe"
try {
@@ -2900,6 +2910,15 @@ if ($test_spa) {
# Forced exp
extract -counts 'one' -method 'zip' -name 'xpui.js' -helper 'ForcedExp' -add $webjson.others.byspotx.add
# Send new versions
if (!($sendversion_off)) {
$checkVersion = Get -Url (Get-Link -e "/js-helper/checkVersion.js")
if ($checkVersion -ne $null) {
injection -p $xpui_spa_patch -f "spotx-helper" -n "checkVersion.js" -c $checkVersion
}
}
# Hiding Ad-like sections or turn off podcasts from the homepage
if ($podcast_off -or $adsections_off -or $canvashome_off) {

22
scripts/DL_Diag.bat Normal file
View File

@@ -0,0 +1,22 @@
@echo off
setlocal
set "script_name=DL_Diag.ps1"
set "branch_name=dl-diag-scripts"
set "script_dir=%~dp0"
set "local_script=%script_dir%%script_name%"
set "branch_url=https://raw.githubusercontent.com/SpotX-Official/SpotX/refs/heads/%branch_name%/scripts/%script_name%"
set "main_url=https://raw.githubusercontent.com/SpotX-Official/SpotX/refs/heads/main/scripts/%script_name%"
set "temp_script=%TEMP%\SpotX-%script_name%"
set "ps=%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe"
if exist "%local_script%" (
%ps% -NoProfile -ExecutionPolicy Bypass -File "%local_script%"
) else (
%ps% -NoProfile -ExecutionPolicy Bypass ^
-Command "[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; $out='%temp_script%'; $urls=@('%branch_url%','%main_url%'); $downloaded=$false; foreach ($url in $urls) { try { Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $out -ErrorAction Stop; $downloaded=$true; break } catch {} }; if (-not $downloaded) { throw 'Failed to download diagnostic script' }; & $out"
)
pause
endlocal
exit /b

388
scripts/DL_Diag.ps1 Normal file
View File

@@ -0,0 +1,388 @@
$rawScriptUrl = 'https://raw.githubusercontent.com/SpotX-Official/SpotX/refs/heads/main/run.ps1'
$mirrorScriptUrl = 'https://spotx-official.github.io/SpotX/run.ps1'
$downloadHost = 'loadspot.amd64fox1.workers.dev'
$defaultLatestFull = '1.2.86.502.g8cd7fb22'
$stableFull = '1.2.13.661.ga588f749'
$reportLines = New-Object 'System.Collections.Generic.List[string]'
$sensitivePatterns = New-Object 'System.Collections.Generic.List[object]'
function Add-SensitivePattern {
param(
[string]$Pattern,
[string]$Replacement = '[redacted]'
)
if (-not [string]::IsNullOrWhiteSpace($Pattern)) {
$script:sensitivePatterns.Add([PSCustomObject]@{
Pattern = $Pattern
Replacement = $Replacement
}) | Out-Null
}
}
function Protect-DiagnosticText {
param(
[AllowNull()]
[string]$Text
)
if ($null -eq $Text) {
return $null
}
$value = [string]$Text
foreach ($item in $script:sensitivePatterns) {
$value = [regex]::Replace($value, $item.Pattern, $item.Replacement)
}
$value
}
function Add-ReportLine {
param(
[AllowEmptyString()]
[string]$Line = ''
)
[void]$script:reportLines.Add((Protect-DiagnosticText -Text $Line))
}
function Add-ReportSection {
param(
[Parameter(Mandatory = $true)]
[string]$Title
)
if ($script:reportLines.Count -gt 0) {
Add-ReportLine
}
Add-ReportLine ("== {0} ==" -f $Title)
}
function Add-CommandOutput {
param(
[string[]]$Lines
)
foreach ($line in $Lines) {
Add-ReportLine ([string]$line)
}
}
function Invoke-WebRequestCompat {
param(
[Parameter(Mandatory = $true)]
[string]$Uri
)
$params = @{
Uri = $Uri
ErrorAction = 'Stop'
}
if ($PSVersionTable.PSVersion.Major -lt 6) {
$params.UseBasicParsing = $true
}
Invoke-WebRequest @params
}
function Invoke-WebClientStep {
param(
[Parameter(Mandatory = $true)]
[string]$Title,
[Parameter(Mandatory = $true)]
[string]$Uri
)
Invoke-PowerShellStep -Title $Title -Action {
$wc = New-Object System.Net.WebClient
$stream = $null
try {
$stream = $wc.OpenRead($Uri)
$buffer = New-Object byte[] 1
[void]$stream.Read($buffer, 0, 1)
$lines = New-Object 'System.Collections.Generic.List[string]'
$lines.Add('WEBCLIENT_OK') | Out-Null
$lines.Add(("Url: {0}" -f $Uri)) | Out-Null
if ($wc.ResponseHeaders) {
foreach ($headerName in $wc.ResponseHeaders.AllKeys) {
$lines.Add(("{0}: {1}" -f $headerName, $wc.ResponseHeaders[$headerName])) | Out-Null
}
}
$lines
}
finally {
if ($stream) {
$stream.Dispose()
}
$wc.Dispose()
}
}
}
function Copy-TextToClipboard {
param(
[Parameter(Mandatory = $true)]
[string]$Text
)
try {
if (Get-Command Set-Clipboard -ErrorAction SilentlyContinue) {
Set-Clipboard -Value $Text -ErrorAction Stop
return $true
}
if (Get-Command clip.exe -ErrorAction SilentlyContinue) {
$Text | clip.exe
return ($LASTEXITCODE -eq 0)
}
}
catch {
return $false
}
return $false
}
function Get-DiagnosticArchitecture {
$arch = $env:PROCESSOR_ARCHITEW6432
if ([string]::IsNullOrWhiteSpace($arch)) {
$arch = $env:PROCESSOR_ARCHITECTURE
}
switch -Regex ($arch) {
'ARM64' { return 'arm64' }
'64' { return 'x64' }
default { return 'x86' }
}
}
if (-not [string]::IsNullOrWhiteSpace($env:COMPUTERNAME)) {
Add-SensitivePattern -Pattern ([regex]::Escape($env:COMPUTERNAME)) -Replacement '[redacted-host]'
}
if (-not [string]::IsNullOrWhiteSpace($env:USERNAME)) {
Add-SensitivePattern -Pattern ([regex]::Escape($env:USERNAME)) -Replacement '[redacted-user]'
}
if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) {
Add-SensitivePattern -Pattern ([regex]::Escape($env:USERPROFILE)) -Replacement '[redacted-profile]'
}
Add-SensitivePattern -Pattern '(?<![\d.])(?:\d{1,3}\.){3}\d{1,3}(?![\d.])' -Replacement '[redacted-ipv4]'
Add-SensitivePattern -Pattern '(?i)(?<![0-9a-f:])((?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,7}:|(?:[0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}|(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}|(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}|(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:(?:(?::[0-9a-f]{1,4}){1,6})|:(?:(?::[0-9a-f]{1,4}){1,7}|:))(?![0-9a-f:])' -Replacement '[redacted-ipv6]'
function Format-ExceptionDetails {
param(
[Parameter(Mandatory = $true)]
[System.Exception]$Exception
)
$details = New-Object 'System.Collections.Generic.List[string]'
$current = $Exception
while ($null -ne $current) {
[void]$details.Add($current.Message)
$current = $current.InnerException
}
$details
}
function Invoke-CurlStep {
param(
[Parameter(Mandatory = $true)]
[string]$Title,
[Parameter(Mandatory = $true)]
[string[]]$Arguments
)
Add-ReportSection -Title $Title
if (-not (Get-Command curl.exe -ErrorAction SilentlyContinue)) {
Add-ReportLine 'curl.exe not found'
return
}
try {
$output = & curl.exe '-sS' @Arguments 2>&1
Add-CommandOutput -Lines ($output | ForEach-Object { [string]$_ })
Add-ReportLine ("ExitCode: {0}" -f $LASTEXITCODE)
}
catch {
Add-CommandOutput -Lines (Format-ExceptionDetails -Exception $_.Exception)
}
}
function Invoke-PowerShellStep {
param(
[Parameter(Mandatory = $true)]
[string]$Title,
[Parameter(Mandatory = $true)]
[scriptblock]$Action
)
Add-ReportSection -Title $Title
try {
$result = & $Action
if ($null -eq $result) {
Add-ReportLine
return
}
if ($result -is [System.Array]) {
Add-CommandOutput -Lines ($result | ForEach-Object { [string]$_ })
return
}
Add-ReportLine ([string]$result)
}
catch {
Add-CommandOutput -Lines (Format-ExceptionDetails -Exception $_.Exception)
}
}
$architecture = Get-DiagnosticArchitecture
$latestFull = $defaultLatestFull
$bootstrapResults = New-Object 'System.Collections.Generic.List[object]'
foreach ($source in @(
[PSCustomObject]@{ Name = 'raw'; Url = $rawScriptUrl },
[PSCustomObject]@{ Name = 'mirror'; Url = $mirrorScriptUrl }
)) {
try {
$response = Invoke-WebRequestCompat -Uri $source.Url
$content = [string]$response.Content
if ($latestFull -eq $defaultLatestFull) {
$match = [regex]::Match($content, '\[string\]\$latest_full\s*=\s*"([^"]+)"')
if ($match.Success) {
$latestFull = $match.Groups[1].Value
}
}
$bootstrapResults.Add([PSCustomObject]@{
Name = $source.Name
Url = $source.Url
Success = $true
StatusCode = [int]$response.StatusCode
Length = $content.Length
}) | Out-Null
}
catch {
$bootstrapResults.Add([PSCustomObject]@{
Name = $source.Name
Url = $source.Url
Success = $false
StatusCode = $null
Length = $null
Error = $_.Exception.Message
}) | Out-Null
}
}
$tempUrl = if ($architecture -eq 'x64') {
'https://{0}/temporary-download/spotify_installer-{1}-x64.exe' -f $downloadHost, $latestFull
}
else {
$null
}
$directUrl = 'https://{0}/download/spotify_installer-{1}-{2}.exe' -f $downloadHost, $stableFull, $architecture
Add-ReportSection -Title 'Environment'
Add-ReportLine ("Date: {0}" -f (Get-Date).ToString('yyyy-MM-dd HH:mm:ss zzz'))
Add-ReportLine ("PowerShell: {0}" -f $PSVersionTable.PSVersion)
Add-ReportLine ("Architecture: {0}" -f $architecture)
Add-ReportLine ("LatestFull: {0}" -f $latestFull)
Add-ReportLine ("StableFull: {0}" -f $stableFull)
Add-ReportLine ("TempUrl: {0}" -f $(if ($tempUrl) { $tempUrl } else { 'skipped for non-x64 system' }))
Add-ReportLine ("DirectUrl: {0}" -f $directUrl)
Add-ReportSection -Title 'Bootstrap check'
foreach ($item in $bootstrapResults) {
Add-ReportLine ("Source: {0}" -f $item.Name)
Add-ReportLine ("Url: {0}" -f $item.Url)
Add-ReportLine ("Success: {0}" -f $item.Success)
if ($item.Success) {
Add-ReportLine ("StatusCode: {0}" -f $item.StatusCode)
Add-ReportLine ("ContentLength: {0}" -f $item.Length)
}
else {
Add-ReportLine ("Error: {0}" -f $item.Error)
}
Add-ReportLine
}
Invoke-CurlStep -Title 'curl version' -Arguments @('-V')
Invoke-PowerShellStep -Title 'DNS' -Action {
Resolve-DnsName $downloadHost | Format-Table -AutoSize | Out-String -Width 4096
}
if ($tempUrl) {
Invoke-CurlStep -Title 'HEAD temp' -Arguments @('-I', '-L', '-k', '--ssl-no-revoke', $tempUrl)
Invoke-CurlStep -Title '1MB temp' -Arguments @('-L', '-k', '--ssl-no-revoke', '--fail-with-body', '--connect-timeout', '15', '-r', '0-1048575', '-o', 'NUL', '-D', '-', '-w', "`nHTTP_STATUS:%{http_code}`n", $tempUrl)
}
else {
Add-ReportSection -Title 'HEAD temp'
Add-ReportLine 'Skipped because temporary route is only used for x64 latest build'
Add-ReportSection -Title '1MB temp'
Add-ReportLine 'Skipped because temporary route is only used for x64 latest build'
}
Invoke-CurlStep -Title 'HEAD direct stable' -Arguments @('-I', '-L', '-k', '--ssl-no-revoke', $directUrl)
Invoke-CurlStep -Title '1MB direct stable' -Arguments @('-L', '-k', '--ssl-no-revoke', '--fail-with-body', '--connect-timeout', '15', '-r', '0-1048575', '-o', 'NUL', '-D', '-', '-w', "`nHTTP_STATUS:%{http_code}`n", $directUrl)
if ($tempUrl) {
Invoke-WebClientStep -Title 'WebClient temp' -Uri $tempUrl
}
else {
Add-ReportSection -Title 'WebClient temp'
Add-ReportLine 'Skipped because temporary route is only used for x64 latest build'
}
Invoke-WebClientStep -Title 'WebClient direct stable' -Uri $directUrl
$finalOutputLines = New-Object 'System.Collections.Generic.List[string]'
$finalOutputLines.Add('----- BEGIN DIAGNOSTICS -----') | Out-Null
foreach ($line in $reportLines) {
$finalOutputLines.Add($line) | Out-Null
}
$finalOutputLines.Add('----- END DIAGNOSTICS -----') | Out-Null
$finalOutputText = [string]::Join([Environment]::NewLine, $finalOutputLines)
$clipboardCopied = Copy-TextToClipboard -Text $finalOutputText
Write-Host
Write-Host '----- BEGIN DIAGNOSTICS -----' -ForegroundColor Cyan
foreach ($line in $reportLines) {
Write-Output $line
}
Write-Host '----- END DIAGNOSTICS -----' -ForegroundColor Cyan
if ($clipboardCopied) {
Write-Host
Write-Host 'Diagnostics copied to clipboard' -ForegroundColor Green
}
else {
Write-Host
Write-Host 'Clipboard copy failed, copy the text above manually' -ForegroundColor Yellow
}