mirror of
https://github.com/SpotX-Official/SpotX.git
synced 2026-04-20 10:44:35 +10:00
Compare commits
12 Commits
95abca6bc4
...
dl-diag-sc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bdfec03a8 | ||
|
|
2783cbe0c1 | ||
|
|
f4279fb962 | ||
|
|
ee5881572c | ||
|
|
4f0706b010 | ||
|
|
4106903513 | ||
|
|
0abf98a36b | ||
|
|
ef6601d186 | ||
|
|
ffd4a6b897 | ||
|
|
bd9726f589 | ||
|
|
74915a1e3e | ||
|
|
68fe6b6726 |
964
js-helper/checkVersion.js
Normal file
964
js-helper/checkVersion.js
Normal 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
31
run.ps1
@@ -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
22
scripts/DL_Diag.bat
Normal 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
388
scripts/DL_Diag.ps1
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user