diff --git a/js-helper/checkVersion.js b/js-helper/checkVersion.js
index 43e01fd..5f2b792 100644
--- a/js-helper/checkVersion.js
+++ b/js-helper/checkVersion.js
@@ -138,7 +138,7 @@
throw new Error(`HTTP error: ${response.status}`);
}
- return response.json();
+ return await response.json();
} finally {
clearTimeout(timeoutId);
}
@@ -555,96 +555,98 @@
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));
+ 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));
+ }
+
+ 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
+ };
+ }
} 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) {
diff --git a/run.ps1 b/run.ps1
index dcabe37..321ec3a 100644
--- a/run.ps1
+++ b/run.ps1
@@ -2,7 +2,7 @@
param
(
[Parameter(HelpMessage = 'Latest recommended Spotify version for Windows 10+.')]
- [string]$latest_full = "1.3.0",
+ [string]$latest_full = "1.3.1",
[Parameter(HelpMessage = 'Latest supported Spotify version for Windows 7-8.1')]
[string]$last_win7_full = "1.2.5.1006.g22820f93",
@@ -349,6 +349,9 @@ $xpui_spa_patch = Join-Path (Join-Path $spotifyDirectory 'Apps') 'xpui.spa'
$upgrade_client = $false
$downgrading = $false
+$uninstallSpotify = $false
+$uninstallStoreSpotify = $false
+$installedVersion = $null
$ru = $false
$podcast_off = $false
$css = $null
@@ -1774,9 +1777,7 @@ function Kill-Spotify {
}
-Kill-Spotify
-
-# Remove Spotify Windows Store If Any
+# Defer removal until the replacement is ready
if ($win10 -or $win11 -or $win8_1 -or $win8 -or $win12) {
if (Get-AppxPackage -Name SpotifyAB.SpotifyMusic) {
@@ -1795,16 +1796,7 @@ if ($win10 -or $win11 -or $win8_1 -or $win8 -or $win12) {
}
if ($confirm_uninstall_ms_spoti) { $ch = 'y' }
if ($ch -eq 'y') {
- $previousProgressPreference = $ProgressPreference
- try {
- $ProgressPreference = 'SilentlyContinue' # Hiding Progress Bars
- if ($confirm_uninstall_ms_spoti) { Write-Host ($lang).MsSpoti3`n }
- if (!($confirm_uninstall_ms_spoti)) { Write-Host ($lang).MsSpoti4`n }
- Get-AppxPackage -Name SpotifyAB.SpotifyMusic | Remove-AppxPackage
- }
- finally {
- $ProgressPreference = $previousProgressPreference
- }
+ $uninstallStoreSpotify = $true
}
if ($ch -eq 'n') {
Stop-Script
@@ -1827,6 +1819,7 @@ if ($spotifyInstalled) {
# Check version Spotify offline
$offline = (Get-Item $spotifyExecutable).VersionInfo.FileVersion
+ $installedVersion = $offline
# Version comparison
# converting strings to arrays of numbers using the -split operator and a foreach loop
@@ -1880,9 +1873,7 @@ if ($spotifyInstalled) {
if ($confirm_spoti_recomended_uninstall) { $ch = 'y' }
if ($confirm_spoti_recomended_over) { $ch = 'n' }
if ($ch -eq 'y') {
- Write-Host ($lang).DelSpotify`n
- $null = Unlock-Folder
- Invoke-SpotifyUninstall -InstalledVersion $offline
+ $uninstallSpotify = $true
}
if ($ch -eq 'n') { $ch = $null }
}
@@ -1939,9 +1930,7 @@ if ($spotifyInstalled) {
if ($confirm_spoti_recomended_over) { $ch = 'n' }
if ($autoVersionDowngradeUninstall) { $ch = 'y' }
if ($ch -eq 'y') {
- Write-Host ($lang).DelSpotify`n
- $null = Unlock-Folder
- Invoke-SpotifyUninstall -InstalledVersion $offline
+ $uninstallSpotify = $true
}
if ($ch -eq 'n') { $ch = $null }
}
@@ -1953,70 +1942,12 @@ if ($spotifyInstalled) {
}
}
}
-# If there is no client or it is outdated, then install (skip if custom path is used)
-if (-not $SpotifyPath -and (-not $spotifyInstalled -or $upgrade_client)) {
-
- Write-Host ($lang).DownSpoti"" -NoNewline
- Write-Host $online -ForegroundColor Green
- Write-Host ($lang).DownSpoti2`n
-
- # Delete old version files of Spotify before installing, leave only profile files
- $ErrorActionPreference = 'SilentlyContinue'
- Kill-Spotify
- Start-Sleep -Milliseconds 600
- $null = Unlock-Folder
- Start-Sleep -Milliseconds 200
- Get-ChildItem $spotifyDirectory -Exclude 'Users', 'prefs' | Remove-Item -Recurse -Force
- Start-Sleep -Milliseconds 200
-
- $tempDirName = "SpotX_Temp-$(Get-Date -UFormat '%Y-%m-%d_%H-%M-%S')"
- $tempDirectory = Join-Path ([System.IO.Path]::GetTempPath()) $tempDirName
- if (-not (Test-Path -LiteralPath $tempDirectory)) { New-Item -ItemType Directory -Path $tempDirectory | Out-Null }
-
- # Client download
- downloadSp -DownloadFolder $tempDirectory
- Write-Host
-
- Start-Sleep -Milliseconds 200
-
- # Client installation
- $setupExe = Join-Path $tempDirectory 'SpotifySetup.exe'
- Start-Process -FilePath explorer.exe -ArgumentList $setupExe
- while (-not (get-process | Where-Object { $_.ProcessName -eq 'SpotifySetup' })) {}
- wait-process -name SpotifySetup
- Kill-Spotify
-
- # Upgrade check version Spotify offline
- $offline = (Get-Item $spotifyExecutable).VersionInfo.FileVersion
-
- # Upgrade check version Spotify.bak
- $offline_bak = (Get-Item $exe_bak).VersionInfo.FileVersion
-}
-
-
-
-# Delete Spotify shortcut if it is on desktop
-if ($no_shortcut) {
- $ErrorActionPreference = 'SilentlyContinue'
- $desktop_folder = DesktopFolder
- Start-Sleep -Milliseconds 1000
- remove-item "$desktop_folder\Spotify.lnk" -Recurse -Force
-}
+$installSpotify = -not $SpotifyPath -and (-not $spotifyInstalled -or $upgrade_client)
+if ($installSpotify) { $offline = $online }
$ch = $null
-# updated Russian translation
-if ($langCode -eq 'ru' -and [version]$offline -ge [version]"1.1.92.644") {
-
- $webjsonru = Get -Url (Get-Link -e "/patches/Augmented%20translation/ru.json")
-
- if ($webjsonru -ne $null) {
-
- $ru = $true
- }
-}
-
if ($podcasts_off) {
Write-Host ($lang).PodcatsOff`n
$ch = 'y'
@@ -2068,24 +1999,10 @@ if (!($new_theme) -and [version]$offline -ge [version]"1.2.14.1141") {
if ($ch -eq 'n') {
$not_block_update = $true
- $ErrorActionPreference = 'SilentlyContinue'
- if ((Test-Path -LiteralPath $exe_bak) -and $offline -eq $offline_bak) {
- Remove-Item $spotifyExecutable -Recurse -Force
- Rename-Item $exe_bak $spotifyExecutable
- }
}
$ch = $null
-$webjson = Get-PatchesJson -LocalPath $CustomPatchesPath
-
-if ($webjson -eq $null) {
- Write-Host
- Write-Host "Failed to load patches.json" -ForegroundColor Red
- Remove-TempDirectory -Directory $tempDirectory
- Stop-Script
-}
-
function Get-JsonValue {
param (
[AllowNull()]
@@ -2571,36 +2488,26 @@ function Helper($paramname, [switch]$CheckOnly) {
$matchPatterns = @($matchValue)
$replacements = @($replaceValue)
- if ($matchPatterns.Count -gt 1) {
-
- $count = $matchPatterns.Count - 1
- $numbers = 0
-
- While ($numbers -le $count) {
-
- if ($paramdata -match $matchPatterns[$numbers]) {
- $paramdata = $paramdata -replace $matchPatterns[$numbers], $replacements[$numbers]
- }
- else {
- $notlog = "MinJs", "MinJson", "Cssmin"
- if ($paramname -notin $notlog) {
-
- Write-Host $novariable -ForegroundColor red -NoNewline
- Write-Host "$name$contentName $numbers"'in'$n
- }
- }
- $numbers++
- }
+ $isMultiple = $matchPatterns.Count -gt 1
+ $logMissing = if ($isMultiple) {
+ $paramname -notin @("MinJs", "MinJson", "Cssmin")
}
- if ($matchPatterns.Count -eq 1) {
- if ($paramdata -match $matchPatterns[0]) {
- $paramdata = $paramdata -replace $matchPatterns[0], $replacements[0]
+ else {
+ !$translate -or $err_ru
+ }
+
+ for ($i = 0; $i -lt $matchPatterns.Count; $i++) {
+ if ($paramdata -match $matchPatterns[$i]) {
+ $paramdata = $paramdata -replace $matchPatterns[$i], $replacements[$i]
}
- else {
- if (!($translate) -or $err_ru) {
- Write-Host $novariable -ForegroundColor red -NoNewline
- Write-Host "$name$contentName"'in'$n
+ elseif ($logMissing) {
+ $patchLabel = "$name$contentName"
+ if ($isMultiple) {
+ $patchLabel += " $i"
}
+
+ Write-Host $novariable -ForegroundColor red -NoNewline
+ Write-Host $patchLabel 'in' $n
}
}
}
@@ -2727,6 +2634,7 @@ function injection {
$stream = $entry.Open()
}
+ $stream.SetLength(0)
$writer = [System.IO.StreamWriter]::new($stream)
$writer.Write($fileContent)
@@ -2749,13 +2657,33 @@ function injection {
$filesToInject = if ($FilesToInject) { $FilesToInject } else { $FileNames }
foreach ($fileName in $filesToInject) {
+ $resourcePath = "/$FolderInArchive/$fileName"
if ($fileName.EndsWith(".js")) {
- $modifiedIndexContent = $indexContent.Insert($scriptTagIndex, "")
- $indexContent = $modifiedIndexContent
+ $tagName = 'script'
+ $attribute = 'src'
+ $insertIndex = $scriptTagIndex
+ $tag = ""
}
elseif ($fileName.EndsWith(".css")) {
- $modifiedIndexContent = $indexContent.Insert($headTagIndex, "")
- $indexContent = $modifiedIndexContent
+ $tagName = 'link'
+ $attribute = 'href'
+ $insertIndex = $headTagIndex
+ $tag = ""
+ }
+ else { continue }
+
+ $connectionPattern = '(?i)<' + $tagName + '(?=\s)(?:[^>"'']|"[^"]*"|''[^'']*'')*?\s' +
+ $attribute + '\s*=\s*(["''])(?-i:' + [regex]::Escape($resourcePath) + ')\1'
+ if ([regex]::IsMatch($indexContent, $connectionPattern)) { continue }
+
+ $indexContent = $indexContent.Insert($insertIndex, $tag)
+
+ # Shift the other insertion point without reordering same-type files
+ if ($tagName -eq 'script') {
+ if ($headTagIndex -ge $insertIndex) { $headTagIndex += $tag.Length }
+ }
+ elseif ($scriptTagIndex -ge $insertIndex) {
+ $scriptTagIndex += $tag.Length
}
}
@@ -2822,29 +2750,9 @@ function Extract-WebpackModules {
$searchStartMarker = Encode-UTF16LE -Bytes $StartMarker
$searchEndMarker = Encode-UTF16LE -Bytes $EndMarker
- function IndexOfBytes($haystack, $needle, [int]$startIndex = 0) {
- if ($startIndex -lt 0) { $startIndex = 0 }
- $haystackLength = $haystack.Length
- $needleLength = $needle.Length
- $searchLimit = $haystackLength - $needleLength
- if ($searchLimit -lt $startIndex) { return -1 }
- $firstNeedleByte = $needle[0]
- for ($i = $startIndex; $i -le $searchLimit; $i++) {
- if ($haystack[$i] -eq $firstNeedleByte) {
- $found = $true
- for ($j = 1; $j -lt $needleLength; $j++) {
- if ($haystack[$i + $j] -ne $needle[$j]) {
- $found = $false
- break
- }
- }
- if ($found) { return $i }
- }
- }
- return -1
- }
+ Initialize-BinaryScanner
- $startIdx = IndexOfBytes $fileContent $searchStartMarker 2
+ $startIdx = [BinaryScannerV3]::FindBytes($fileContent, $searchStartMarker, 2)
if ($startIdx -eq -1) {
Write-Error "Start marker not found"
exit 1
@@ -2852,7 +2760,7 @@ function Extract-WebpackModules {
Write-Debug "Start marker found at index $startIdx"
$endMarkerSearchOffset = $startIdx + $searchStartMarker.Length
- $endIdx = IndexOfBytes $fileContent $searchEndMarker $endMarkerSearchOffset
+ $endIdx = [BinaryScannerV3]::FindBytes($fileContent, $searchEndMarker, $endMarkerSearchOffset)
if ($endIdx -eq -1) {
Write-Error "End marker not found after index $endMarkerSearchOffset"
exit 1
@@ -4573,39 +4481,50 @@ function Invoke-VerifiedBinaryPatch {
$bytes = [System.IO.File]::ReadAllBytes($FilePath)
$peInfo = Get-PEFileInfo -Bytes $bytes
- $location = & $Locator $bytes $peInfo
+ $locations = @(& $Locator $bytes $peInfo | Sort-Object PatchOffset)
+ if ($locations.Count -eq 0) { throw "No $PatchName patch locations were found" }
$requiredProperties = @('Architecture', 'State', 'PatchOffset', 'OriginalBytes', 'PatchedBytes')
- $missingProperties = @($requiredProperties | Where-Object {
- $null -eq $location -or $null -eq $location.PSObject.Properties[$_]
- })
- if ($missingProperties.Count -ne 0 -or $location.Architecture -ne $peInfo.Architecture) {
- throw "Unexpected $PatchName locator result"
- }
- if ($null -eq $location.PatchedBytes -or $location.PatchedBytes.Length -eq 0 -or
- $location.PatchOffset -lt 0 -or $location.PatchOffset + $location.PatchedBytes.Length -gt $bytes.Length) {
- throw "$PatchName patch range is invalid"
- }
- if ($DescribeLocation) {
- & $DescribeLocation $location
+ $previousEnd = -1L
+ foreach ($location in $locations) {
+ $missingProperties = @($requiredProperties | Where-Object {
+ $null -eq $location -or $null -eq $location.PSObject.Properties[$_]
+ })
+ if ($missingProperties.Count -ne 0 -or $location.Architecture -ne $peInfo.Architecture) {
+ throw "Unexpected $PatchName locator result"
+ }
+ if ($null -eq $location.PatchedBytes -or $location.PatchedBytes.Length -eq 0 -or
+ $location.PatchOffset -lt 0 -or $location.PatchOffset + $location.PatchedBytes.Length -gt $bytes.Length) {
+ throw "$PatchName patch range is invalid"
+ }
+ if ($DescribeLocation) {
+ & $DescribeLocation $location
+ }
+
+ if ($location.State -eq 'Patched') {
+ if (-not [BinaryScannerV3]::MatchBytes($bytes, [int]$location.PatchOffset, $location.PatchedBytes)) {
+ throw "Unexpected $PatchName patched state"
+ }
+ }
+ elseif ($location.State -ne 'Original' -or
+ $null -eq $location.OriginalBytes -or $location.OriginalBytes.Length -eq 0 -or
+ $location.OriginalBytes.Length -ne $location.PatchedBytes.Length -or
+ -not [BinaryScannerV3]::MatchBytes($bytes, [int]$location.PatchOffset, $location.OriginalBytes)) {
+ throw "Unexpected $PatchName patch state"
+ }
+ if ($location.PatchOffset -lt $previousEnd) { throw "$PatchName patch locations overlap" }
+ $previousEnd = $location.PatchOffset + $location.PatchedBytes.Length
}
- if ($location.State -eq 'Patched') {
- if (-not [BinaryScannerV3]::MatchBytes($bytes, [int]$location.PatchOffset, $location.PatchedBytes)) {
- throw "Unexpected $PatchName patched state"
- }
- Write-Verbose ("{0} already patched at offset 0x{1:X}" -f $PatchName, $location.PatchOffset)
+ if (@($locations | Where-Object State -ne 'Patched').Count -eq 0) {
+ Write-Verbose ("{0} already patched at {1} location(s)" -f $PatchName, $locations.Count)
return $true
}
- if ($location.State -ne 'Original' -or
- $null -eq $location.OriginalBytes -or $location.OriginalBytes.Length -eq 0 -or
- $location.OriginalBytes.Length -ne $location.PatchedBytes.Length -or
- -not [BinaryScannerV3]::MatchBytes($bytes, [int]$location.PatchOffset, $location.OriginalBytes)) {
- throw "Unexpected $PatchName patch state"
- }
$patchedFileBytes = [byte[]]$bytes.Clone()
- for ($i = 0; $i -lt $location.PatchedBytes.Length; $i++) {
- $patchedFileBytes[[int]$location.PatchOffset + $i] = $location.PatchedBytes[$i]
+ foreach ($location in $locations) {
+ for ($i = 0; $i -lt $location.PatchedBytes.Length; $i++) {
+ $patchedFileBytes[[int]$location.PatchOffset + $i] = $location.PatchedBytes[$i]
+ }
}
try {
@@ -4617,14 +4536,6 @@ function Invoke-VerifiedBinaryPatch {
if (-not [BinaryScannerV3]::MatchBytes($writtenBytes, 0, $patchedFileBytes)) {
throw "$PatchName patch changed unexpected bytes"
}
- $writtenPeInfo = Get-PEFileInfo -Bytes $writtenBytes
- $writtenLocation = & $Locator $writtenBytes $writtenPeInfo
- if ($writtenPeInfo.Architecture -ne $peInfo.Architecture -or
- $writtenLocation.Architecture -ne $location.Architecture -or
- $writtenLocation.State -ne 'Patched' -or $writtenLocation.PatchOffset -ne $location.PatchOffset -or
- -not [BinaryScannerV3]::MatchBytes($writtenBytes, [int]$location.PatchOffset, $location.PatchedBytes)) {
- throw "$PatchName patch verification failed"
- }
}
catch {
$patchError = $_.Exception.Message
@@ -4645,10 +4556,13 @@ function Invoke-VerifiedBinaryPatch {
throw $patchError
}
- Write-Verbose ("{0} patched at offset 0x{1:X} with {2}" -f
- $PatchName,
- $location.PatchOffset,
- (($location.PatchedBytes | ForEach-Object { $_.ToString('X2') }) -join ' '))
+ foreach ($location in $locations) {
+ $locationName = if ($location.PatchName) { $location.PatchName } else { $PatchName }
+ Write-Verbose ("{0} patched at offset 0x{1:X} with {2}" -f
+ $locationName,
+ $location.PatchOffset,
+ (($location.PatchedBytes | ForEach-Object { $_.ToString('X2') }) -join ' '))
+ }
return $true
}
catch {
@@ -4790,6 +4704,8 @@ function Find-X64CrossfadeEnabledBinaryPatchLocation {
}
}
+
+
function Find-Arm64CrossfadeEnabledBinaryPatchLocation {
param(
[byte[]]$Bytes,
@@ -4902,10 +4818,225 @@ function Find-CrossfadeEnabledBinaryPatchLocation {
}
}
+function Find-X64ListPlayerBinaryPatchLocation {
+ param([byte[]]$Bytes, [object]$PeInfo)
+
+ if ($PeInfo.Architecture -ne 'x64') { throw 'ListPlayer locator requires x64' }
+ $context = Get-BinaryPatchContext -PeInfo $PeInfo
+ $anchor = Get-UniqueBinaryAnchor -Bytes $Bytes -PeInfo $PeInfo -Text 'enable_list_player' -NullTerminated
+ $ownerBytes = [Text.Encoding]::ASCII.GetBytes("core-list-player`0")
+ $anchorRefs = @([BinaryScannerV3]::FindRipLeaRefs(
+ $Bytes, [int]$context.Text.RawPtr, [int]$context.Text.RawSize, $anchor.Rva,
+ $context.RawPtrs, $context.RawSizes, $context.VirtualAddresses
+ ))
+
+ $pattern = Convert-HexStringToBytes (
+ '48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 40 48 8B F9 ' +
+ '48 8D 2D 00 00 00 00 48 8B CD E8 00 00 00 00 48 8D 35 00 00 00 00 ' +
+ '48 8B D8 48 8B CE E8 00 00 00 00 45 33 C9 48 89 6C 24 20 4C 8D 44 24 20 ' +
+ '48 89 5C 24 28 48 8D 54 24 30 48 89 74 24 30 48 8B CF 48 89 44 24 38 ' +
+ '00 00 00 00 00 48 8B 5C 24 50 48 8B 6C 24 58 48 8B 74 24 60 48 83 C4 40 5F C3'
+ )
+ $mask = [byte[]](@(0xFF) * $pattern.Length)
+ foreach ($span in @(@(0x1A, 4), @(0x22, 4), @(0x29, 4), @(0x34, 4), @(0x5C, 5))) {
+ for ($i = 0; $i -lt $span[1]; $i++) { $mask[$span[0] + $i] = 0 }
+ }
+ $patchedBytes = Convert-HexStringToBytes 'B0 00 90 90 90'
+ $optionalBool = Convert-HexStringToBytes 'FF D7 33 FF 40 38 78 01 74 03 44 8A 30'
+ $returnBool = Convert-HexStringToBytes '41 8A C6 48 8B 4D F8'
+ $candidates = @{}
+ foreach ($reference in $anchorRefs) {
+ $refRva = Get-PERvaFromOffset -Sections $PeInfo.Sections -Offset $reference
+ $range = Get-BinaryPatchFunctionRange -Bytes $Bytes -PeInfo $PeInfo -Context $context -Rva $refRva
+ if ($range.Length -ne 2 -or $refRva - [int64]$range[0] -ne 0x17 -or
+ [int64]$range[1] - [int64]$range[0] -ne $pattern.Length) { continue }
+ $start = Get-PEOffsetFromRva -Sections $PeInfo.Sections -Rva $range[0]
+ if ($null -eq $start -or -not [BinaryScannerV3]::MatchMaskedBytes($Bytes, $start, $pattern, $mask)) { continue }
+ $ownerRva = [int64]$range[0] + 0x2D + [BitConverter]::ToInt32($Bytes, $start + 0x29)
+ $ownerOffset = Get-PEOffsetFromRva -Sections $PeInfo.Sections -Rva $ownerRva
+ if ($null -eq $ownerOffset -or -not [BinaryScannerV3]::MatchBytes($Bytes, $ownerOffset, $ownerBytes)) { continue }
+
+ # The registration function uses the same strings but does not return a boolean
+ $callers = @([BinaryScannerV3]::FindCrossfadeGateCallsToRva(
+ $Bytes, [int]$context.Text.RawPtr, [int]$context.Text.RawSize, $range[0],
+ $context.RawPtrs, $context.RawSizes, $context.VirtualAddresses
+ ) | Where-Object {
+ $callRva = Get-PERvaFromOffset -Sections $PeInfo.Sections -Offset $_
+ $callRange = Get-BinaryPatchFunctionRange -Bytes $Bytes -PeInfo $PeInfo -Context $context -Rva $callRva
+ $callRange.Length -eq 2 -and $callRva + 7 -le $callRange[1] -and ($Bytes[$_ + 6] -band 0x38) -eq 0
+ })
+ if ($callers.Count -eq 0) { continue }
+ $offset = $start + 0x5C
+ $originalBytes = $null
+ if ([BinaryScannerV3]::MatchBytes($Bytes, $offset, $patchedBytes)) {
+ $state = 'Patched'
+ }
+ elseif ($Bytes[$offset] -eq 0xE8) {
+ $targetRva = [int64]$range[0] + 0x61 + [BitConverter]::ToInt32($Bytes, $offset + 1)
+ $targetRange = Get-BinaryPatchFunctionRange -Bytes $Bytes -PeInfo $PeInfo -Context $context -Rva $targetRva
+ if ($targetRange.Length -ne 2 -or $targetRange[0] -ne $targetRva) { continue }
+ $target = Get-PEOffsetFromRva -Sections $PeInfo.Sections -Rva $targetRva
+ if ($null -eq $target) { continue }
+ $end = $target + $targetRange[1] - $targetRva
+ $optionalOffset = [BinaryScannerV3]::FindBytes($Bytes, $optionalBool, $target)
+ $returnOffset = [BinaryScannerV3]::FindBytes($Bytes, $returnBool, $target)
+ if ($optionalOffset -lt $target -or $optionalOffset + $optionalBool.Length -gt $end -or
+ $returnOffset -lt $optionalOffset -or $returnOffset + $returnBool.Length -gt $end) { continue }
+ $originalBytes = [byte[]]$Bytes[$offset..($offset + 4)]
+ $state = 'Original'
+ }
+ else { continue }
+ $candidates[$offset] = [PSCustomObject]@{
+ Architecture = 'x64'
+ PatchName = 'crossfade_enabled.enable_list_player=false'
+ FunctionRva = [int64]$range[0]
+ PatchOffset = [int64]$offset
+ OriginalBytes = $originalBytes
+ PatchedBytes = $patchedBytes
+ State = $state
+ AnchorRefCount = $anchorRefs.Count
+ }
+ }
+ if ($candidates.Count -ne 1) { throw "Expected one x64 ListPlayer boolean reader, found $($candidates.Count)" }
+ return @($candidates.Values)[0]
+}
+
+function Find-Arm64ListPlayerBinaryPatchLocations {
+ param([byte[]]$Bytes, [object]$PeInfo)
+
+ $context = Get-BinaryPatchContext -PeInfo $PeInfo
+ $anchor = Get-UniqueBinaryAnchor -Bytes $Bytes -PeInfo $PeInfo -Text 'enable_list_player' -NullTerminated
+ $anchorRefs = @([BinaryScannerV3]::FindXrefArm64(
+ $Bytes, [uint64]$anchor.Rva, [uint64]$context.Text.VirtualAddress,
+ [uint32]$context.Text.RawPtr, [uint32]$context.Text.RawSize
+ ))
+ $ownerBytes = [Text.Encoding]::ASCII.GetBytes("core-list-player`0")
+ $patchedBytes = Convert-HexStringToBytes '00 00 80 52'
+ $optionalBool = Convert-HexStringToBytes '80 02 3F D6 08 04 40 39 48 00 00 34 13 00 40 39'
+ $returnBool = Convert-HexStringToBytes 'E0 03 13 2A FF 43 01 91'
+ $registration = Convert-HexStringToBytes '13 41 40 F9 F4 03 05 2A'
+ $coveredRefs = @{}
+ $candidates = @{}
+ foreach ($reference in $anchorRefs) {
+ if ($coveredRefs.ContainsKey($reference)) { continue }
+ $refRva = Get-PERvaFromOffset -Sections $PeInfo.Sections -Offset $reference
+ $range = Get-BinaryPatchFunctionRange -Bytes $Bytes -PeInfo $PeInfo -Context $context -Rva $refRva
+ if ($range.Length -ne 2 -or $refRva + 0x44 -gt $range[1]) { continue }
+ $keyAdd = Read-Arm64Instruction -Bytes $Bytes -Offset ($reference + 4)
+ if (($keyAdd -band 31) -ne 0) { continue }
+
+ foreach ($distance in @(0x10, 0x14, 0x18)) {
+ $ownerRef = $reference + $distance
+ $ownerPage = Read-Arm64Instruction -Bytes $Bytes -Offset $ownerRef
+ $ownerAdd = Read-Arm64Instruction -Bytes $Bytes -Offset ($ownerRef + 4)
+ if (($ownerPage -band [uint32]0x9F000000L) -ne [uint32]0x90000000L -or
+ ($ownerAdd -band [uint32]0xFFC0001FL) -ne [uint32]0x91000000L -or
+ (($ownerAdd -shr 5) -band 31) -ne ($ownerPage -band 31)) { continue }
+ $pageImmediate = [int64]((($ownerPage -shr 5) -band 0x7FFFF) -shl 2) -bor [int64](($ownerPage -shr 29) -band 3)
+ $ownerRva = (($refRva + $distance) -band -4096L) +
+ ((ConvertFrom-Arm64SignedImmediate -Value $pageImmediate -Bits 21) -shl 12) + (($ownerAdd -shr 10) -band 0xFFF)
+ $ownerOffset = Get-PEOffsetFromRva -Sections $PeInfo.Sections -Rva $ownerRva
+ if ($null -eq $ownerOffset -or -not [BinaryScannerV3]::MatchBytes($Bytes, $ownerOffset, $ownerBytes)) { continue }
+ $saveLength = Read-Arm64Instruction -Bytes $Bytes -Offset ($ownerRef - 4)
+ $lengthRegister = $saveLength -band 31
+ if (($saveLength -band [uint32]0xFFFFFFE0L) -ne [uint32]0xAA0003E0L -or
+ $lengthRegister -lt 19 -or $lengthRegister -gt 28) { continue }
+ $keyLengthCall = Read-Arm64Instruction -Bytes $Bytes -Offset ($ownerRef - 8)
+ $ownerLengthCall = Read-Arm64Instruction -Bytes $Bytes -Offset ($ownerRef + 8)
+ if (($keyLengthCall -band [uint32]0xFC000000L) -ne [uint32]0x94000000L -or
+ ($ownerLengthCall -band [uint32]0xFC000000L) -ne [uint32]0x94000000L) { continue }
+ $keyLengthTarget = Get-Arm64BranchTargetRva -Instruction $keyLengthCall -InstructionRva ($refRva + $distance - 8) -Kind BL
+ $ownerLengthTarget = Get-Arm64BranchTargetRva -Instruction $ownerLengthCall -InstructionRva ($refRva + $distance + 8) -Kind BL
+ if ($keyLengthTarget -ne $ownerLengthTarget) { continue }
+
+ $setup = @()
+ for ($i = 0; $i -lt 8; $i++) { $setup += Read-Arm64Instruction -Bytes $Bytes -Offset ($ownerRef + 12 + 4 * $i) }
+ $reloadRef = $null
+ if ($setup[0] -eq [uint32]0xAA0003E2L) {
+ $keyRegister = ($keyAdd -shr 5) -band 31
+ $ownerRegister = $ownerPage -band 31
+ if ($keyRegister -lt 19 -or $keyRegister -gt 28 -or $ownerRegister -lt 19 -or $ownerRegister -gt 28 -or
+ $setup[2] -ne [uint32]0x52800005L -or
+ $setup[3] -ne ([uint32]0xAA0003E4L -bor ($lengthRegister -shl 16)) -or
+ $setup[4] -ne ($keyAdd -bor 3) -or $setup[5] -ne ($ownerAdd -bor 1)) { continue }
+ $resolver = $setup[1]
+ $patchOffset = $ownerRef + 36
+ }
+ else {
+ # Some inlined readers reload both string addresses after strlen
+ $reloadRef = $ownerRef + 12
+ if ($anchorRefs -notcontains $reloadRef -or $setup[1] -ne ($keyAdd -bor 3) -or
+ $setup[2] -ne $ownerPage -or $setup[3] -ne ($ownerAdd -bor 1) -or
+ $setup[4] -ne [uint32]0xAA0003E2L -or $setup[5] -ne [uint32]0x52800005L -or
+ $setup[6] -ne ([uint32]0xAA0003E4L -bor ($lengthRegister -shl 16))) { continue }
+ $resolver = $setup[7]
+ $patchOffset = $ownerRef + 44
+ }
+ $resolverMove = ($resolver -band [uint32]0xFFE0FFFFL) -eq [uint32]0xAA0003E0L
+ $resolverLoad = ($resolver -band [uint32]0xFFFFFC1FL) -eq [uint32]0xF9400000L
+ if (-not $resolverMove -and -not $resolverLoad) { continue }
+ $patchRva = $refRva + $patchOffset - $reference
+ if ($patchRva + 12 -gt $range[1]) { continue }
+ $instruction = Read-Arm64Instruction -Bytes $Bytes -Offset $patchOffset
+ $next = Read-Arm64Instruction -Bytes $Bytes -Offset ($patchOffset + 4)
+ $afterNext = Read-Arm64Instruction -Bytes $Bytes -Offset ($patchOffset + 8)
+ $usesBool = ($next -band [uint32]0xFFFFFFE0L) -eq [uint32]0x2A0003E0L -or
+ ($next -band [uint32]0xFFF8001FL) -eq [uint32]0x36000000L -or
+ (($next -band [uint32]0xFFC0001FL) -eq [uint32]0xF9400008L -and
+ ($afterNext -band [uint32]0xFFFFFFE0L) -eq [uint32]0x53001C00L)
+ $originalBytes = $null
+ if ($instruction -eq [uint32]0x52800000L -and $usesBool) {
+ $state = 'Patched'
+ }
+ elseif (($instruction -band [uint32]0xFC000000L) -eq [uint32]0x94000000L) {
+ $targetRva = Get-Arm64BranchTargetRva -Instruction $instruction -InstructionRva $patchRva -Kind BL
+ $targetRange = Get-BinaryPatchFunctionRange -Bytes $Bytes -PeInfo $PeInfo -Context $context -Rva $targetRva
+ if ($targetRange.Length -ne 2 -or $targetRange[0] -ne $targetRva) { continue }
+ $target = Get-PEOffsetFromRva -Sections $PeInfo.Sections -Rva $targetRva
+ if ($null -eq $target) { continue }
+ $end = $target + $targetRange[1] - $targetRva
+ $optionalOffset = [BinaryScannerV3]::FindBytes($Bytes, $optionalBool, $target)
+ $returnOffset = [BinaryScannerV3]::FindBytes($Bytes, $returnBool, $target)
+ if ($usesBool -and $optionalOffset -ge $target -and $optionalOffset + $optionalBool.Length -le $end -and
+ $returnOffset -gt $optionalOffset -and $returnOffset + $returnBool.Length -le $end) {
+ $state = 'Original'
+ $originalBytes = [BitConverter]::GetBytes([uint32]$instruction)
+ }
+ else {
+ $registrationOffset = [BinaryScannerV3]::FindBytes($Bytes, $registration, $target)
+ if (-not $usesBool -and $registrationOffset -ge $target -and $registrationOffset + $registration.Length -le $end) {
+ $coveredRefs[$reference] = $true
+ }
+ continue
+ }
+ }
+ else { continue }
+ $coveredRefs[$reference] = $true
+ if ($null -ne $reloadRef) { $coveredRefs[$reloadRef] = $true }
+ $candidates[$patchOffset] = [PSCustomObject]@{
+ Architecture = 'ARM64'
+ PatchName = 'crossfade_enabled.enable_list_player=false'
+ FunctionRva = [int64]$range[0]
+ PatchOffset = [int64]$patchOffset
+ OriginalBytes = $originalBytes
+ PatchedBytes = $patchedBytes
+ State = $state
+ AnchorRefCount = $anchorRefs.Count
+ }
+ }
+ }
+ if ($candidates.Count -eq 0 -or $coveredRefs.Count -ne $anchorRefs.Count) {
+ throw "Unrecognized ARM64 ListPlayer readers: $($coveredRefs.Count)/$($anchorRefs.Count) references resolved"
+ }
+ return $candidates.Values
+}
+
function Set-CrossfadeEnabledBinaryPatch {
[CmdletBinding()]
param (
- [string]$FilePath
+ [string]$FilePath,
+ [Parameter(Mandatory = $true)]
+ [version]$TargetVersion
)
return Invoke-VerifiedBinaryPatch `
@@ -4914,11 +5045,18 @@ function Set-CrossfadeEnabledBinaryPatch {
-Locator {
param([byte[]]$Bytes, [object]$PeInfo)
Find-CrossfadeEnabledBinaryPatchLocation -Bytes $Bytes -PeInfo $PeInfo
+ if ($TargetVersion -ge [version]'1.2.98') {
+ switch ($PeInfo.Architecture) {
+ 'x64' { Find-X64ListPlayerBinaryPatchLocation -Bytes $Bytes -PeInfo $PeInfo }
+ 'ARM64' { Find-Arm64ListPlayerBinaryPatchLocations -Bytes $Bytes -PeInfo $PeInfo }
+ }
+ }
} `
-DescribeLocation {
param([object]$Location)
- Write-Verbose ("crossfade_enabled {0} function RVA 0x{1:X}, references {2}" -f
- $Location.Architecture, $Location.FunctionRva, $Location.AnchorRefCount)
+ $name = if ($Location.PatchName) { $Location.PatchName } else { 'crossfade_enabled' }
+ Write-Verbose ("{0} {1} function RVA 0x{2:X}, references {3}" -f
+ $name, $Location.Architecture, $Location.FunctionRva, $Location.AnchorRefCount)
}
}
@@ -5040,9 +5178,259 @@ function Update-ZipEntry {
}
-Write-Host ($lang).ModSpoti`n
+function Get-SpotXResources {
+ param([version]$TargetVersion, [string]$DownloadFolder)
-Remove-TempDirectory -Directory $tempDirectory
+ $patches = Get-PatchesJson -LocalPath $CustomPatchesPath
+ if ($patches -isnot [PSCustomObject] -or $patches.others -isnot [PSCustomObject]) {
+ throw 'Failed to load a valid patches.json'
+ }
+
+ $resources = [ordered]@{
+ Patches = $patches
+ Translation = $null
+ CheckVersion = $null
+ Section = $null
+ Goofy = $null
+ LyricsRules = $null
+ LyricsColors = $null
+ Login = $null
+ }
+
+ if ($langCode -eq 'ru' -and $TargetVersion -ge [version]'1.1.92.644') {
+ $translation = Get -Url (Get-Link -e '/patches/Augmented%20translation/ru.json')
+ if ($translation -is [PSCustomObject]) { $resources.Translation = $translation }
+ else { Write-Warning 'Additional Russian translation is unavailable, skipping it' }
+ }
+
+ $needsSection = $podcast_off -or $adsections_off -or ($canvashome_off -and $TargetVersion -gt [version]'1.2.44.405')
+ $optionalFiles = @(
+ @{ Name = 'CheckVersion'; Path = '/js-helper/checkVersion.js'; Enabled = !$sendversion_off },
+ @{ Name = 'Section'; Path = '/js-helper/sectionBlock.js'; Enabled = $needsSection },
+ @{ Name = 'Goofy'; Path = '/js-helper/goofyHistory.js'; Enabled = $urlform_goofy -and $idbox_goofy },
+ @{ Name = 'LyricsRules'; Path = '/css-helper/lyrics-color/rules.css'; Enabled = [bool]$lyrics_stat },
+ @{ Name = 'LyricsColors'; Path = '/css-helper/lyrics-color/colors.css'; Enabled = [bool]$lyrics_stat }
+ )
+ foreach ($file in $optionalFiles) {
+ if (!$file.Enabled) { continue }
+ $content = Get -Url (Get-Link -e $file.Path)
+ if ($content -is [string] -and ![string]::IsNullOrWhiteSpace($content)) {
+ $resources[$file.Name] = $content
+ }
+ else {
+ Write-Warning ("Optional resource is unavailable, skipping it: {0}" -f $file.Path)
+ }
+ }
+ if (!$resources.LyricsRules -or !$resources.LyricsColors) {
+ $resources.LyricsRules = $null
+ $resources.LyricsColors = $null
+ }
+
+ if ($TargetVersion -ge [version]'1.1.87.612' -and $TargetVersion -le [version]'1.2.5.1006') {
+ $loginPath = Join-Path $DownloadFolder 'login.spa'
+ $null = Get -Url (Get-Link -e '/res/login.spa') -OutputPath $loginPath
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $archive = $null
+ try {
+ $archive = [IO.Compression.ZipFile]::OpenRead($loginPath)
+ if ($archive.Entries.Count -eq 0) { throw 'Downloaded login.spa is empty' }
+ foreach ($entry in $archive.Entries) {
+ $stream = $entry.Open()
+ try { $stream.CopyTo([IO.Stream]::Null) }
+ finally { $stream.Dispose() }
+ }
+ }
+ finally {
+ if ($null -ne $archive) { $archive.Dispose() }
+ }
+ $resources.Login = [IO.File]::ReadAllBytes($loginPath)
+ }
+
+ return [PSCustomObject]$resources
+}
+
+function Test-SpotifyInstall {
+ param([string]$Directory, [version]$ExpectedVersion, [string]$ExpectedArchitecture)
+
+ $requiredFiles = @('Spotify.exe', 'Apps\xpui.spa')
+ if ($ExpectedVersion -ge [version]'1.2.70.253') { $requiredFiles += 'Spotify.dll', 'chrome_elf.dll' }
+ if ($ExpectedVersion -ge [version]'1.2.84.476') { $requiredFiles += 'uninstall.exe' }
+ foreach ($name in $requiredFiles) {
+ $path = Join-Path $Directory $name
+ if (!(Test-Path -LiteralPath $path -PathType Leaf) -or (Get-Item -LiteralPath $path).Length -eq 0) {
+ throw "Extracted Spotify file is missing or empty: $name"
+ }
+ }
+
+ $executable = Join-Path $Directory 'Spotify.exe'
+ $actualVersion = Get-SpotifyVersionNumber -SpotifyVersion (Get-Item -LiteralPath $executable).VersionInfo.FileVersion
+ if ($actualVersion -ne $ExpectedVersion) {
+ throw "Extracted Spotify version mismatch: expected $ExpectedVersion, got $actualVersion"
+ }
+ $peInfo = Get-PEFileInfo -Bytes ([IO.File]::ReadAllBytes($executable))
+ if ($peInfo.Architecture -ine $ExpectedArchitecture) {
+ throw "Extracted Spotify architecture mismatch: expected $ExpectedArchitecture, got $($peInfo.Architecture)"
+ }
+
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ $archive = $null
+ try {
+ $archive = [IO.Compression.ZipFile]::OpenRead((Join-Path $Directory 'Apps\xpui.spa'))
+ $requiredEntries = @('index.html')
+ if ($null -ne $archive.GetEntry('xpui.js')) {
+ $requiredEntries += 'xpui.js', 'xpui.css'
+ }
+ else {
+ $requiredEntries += 'xpui-snapshot.js', 'xpui-snapshot.css'
+ $snapshots = @('v8_context_snapshot.bin', 'v8_context_snapshot.arm64.bin') | Where-Object {
+ $path = Join-Path $Directory $_
+ (Test-Path -LiteralPath $path -PathType Leaf) -and (Get-Item -LiteralPath $path).Length -gt 0
+ }
+ if (!$snapshots) { throw 'Extracted Spotify V8 snapshot is missing or empty' }
+ }
+ foreach ($name in $requiredEntries) {
+ $entry = $archive.GetEntry($name)
+ if ($null -eq $entry -or $entry.Length -eq 0) { throw "Extracted xpui.spa entry is missing or empty: $name" }
+ }
+ foreach ($entry in $archive.Entries) {
+ $stream = $entry.Open()
+ try { $stream.CopyTo([IO.Stream]::Null) }
+ finally { $stream.Dispose() }
+ }
+ }
+ finally {
+ if ($null -ne $archive) { $archive.Dispose() }
+ }
+}
+
+function Expand-SpotifyInstaller {
+ param([string]$DownloadFolder, [version]$ExpectedVersion, [string]$ExpectedArchitecture)
+
+ downloadSp -DownloadFolder $DownloadFolder
+ $setupExe = Join-Path $DownloadFolder 'SpotifySetup.exe'
+ $destination = Join-Path $DownloadFolder 'client'
+ $process = $null
+ try {
+ $process = Start-Process -FilePath $setupExe -ArgumentList ('/extract "{0}"' -f $destination) `
+ -Wait -PassThru -WindowStyle Hidden -ErrorAction Stop
+ if ($process.ExitCode -ne 0) { throw "Spotify extraction failed with exit code $($process.ExitCode)" }
+ }
+ finally {
+ if ($null -ne $process) { $process.Dispose() }
+ }
+ Test-SpotifyInstall -Directory $destination -ExpectedVersion $ExpectedVersion -ExpectedArchitecture $ExpectedArchitecture
+ return $destination
+}
+
+function Register-SpotifyInstall {
+ param([string]$Directory, [version]$InstalledVersion)
+
+ $executable = Join-Path $Directory 'Spotify.exe'
+ $uninstallCommand = if ($InstalledVersion -ge [version]'1.2.84.476') {
+ '"{0}"' -f (Join-Path $Directory 'uninstall.exe')
+ }
+ else { '"{0}" /uninstall' -f $executable }
+ $registryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Spotify'
+ if (!(Test-Path -LiteralPath $registryPath)) {
+ $null = New-Item -Path $registryPath -Force -ErrorAction Stop
+ }
+ $values = @{
+ DisplayName = 'Spotify'
+ DisplayVersion = $InstalledVersion.ToString()
+ DisplayIcon = '"{0}",0' -f $executable
+ Publisher = 'Spotify AB'
+ UninstallString = $uninstallCommand
+ URLInfoAbout = 'https://www.spotify.com'
+ }
+ foreach ($name in $values.Keys) {
+ $null = New-ItemProperty -LiteralPath $registryPath -Name $name -Value $values[$name] -PropertyType String -Force -ErrorAction Stop
+ }
+}
+
+try {
+ $tempDirectory = Join-Path ([IO.Path]::GetTempPath()) ('SpotX_Temp-' + [Guid]::NewGuid().ToString('N'))
+ $null = New-Item -ItemType Directory -Path $tempDirectory -ErrorAction Stop
+ $resources = Get-SpotXResources -TargetVersion ([version]$offline) -DownloadFolder $tempDirectory
+ $webjson = $resources.Patches
+ $webjsonru = $resources.Translation
+ $ru = $null -ne $webjsonru
+
+ if ($installSpotify) {
+ Write-Host ($lang).DownSpoti"" -NoNewline
+ Write-Host $online -ForegroundColor Green
+ Write-Host ($lang).DownSpoti2`n
+ $architecture = Get-SpotifyInstallerArchitecture -SystemArchitecture $systemArchitecture `
+ -SpotifyVersion ([version]$online) -LastX86SupportedVersion $last_x86
+ $preparedClient = Expand-SpotifyInstaller -DownloadFolder $tempDirectory `
+ -ExpectedVersion ([version]$online) -ExpectedArchitecture $architecture
+ Write-Host
+ }
+
+ Kill-Spotify
+ if ($uninstallStoreSpotify) {
+ $previousProgressPreference = $ProgressPreference
+ try {
+ $ProgressPreference = 'SilentlyContinue'
+ if ($confirm_uninstall_ms_spoti) { Write-Host ($lang).MsSpoti3`n }
+ else { Write-Host ($lang).MsSpoti4`n }
+ Get-AppxPackage -Name SpotifyAB.SpotifyMusic | Remove-AppxPackage -ErrorAction Stop
+ }
+ finally { $ProgressPreference = $previousProgressPreference }
+ }
+
+ if ($installSpotify) {
+ $null = Unlock-Folder
+ if ($uninstallSpotify) {
+ Write-Host ($lang).DelSpotify`n
+ Invoke-SpotifyUninstall -InstalledVersion $installedVersion
+ }
+ $destination = [IO.Path]::GetFullPath($spotifyDirectory)
+ $destinationPrefix = $destination.TrimEnd('\') + '\'
+ if (Test-Path -LiteralPath $destination) {
+ foreach ($item in Get-ChildItem -LiteralPath $destination -Force -ErrorAction Stop) {
+ if ($item.Name -in @('Users', 'prefs')) { continue }
+ if (!$item.FullName.StartsWith($destinationPrefix, [StringComparison]::OrdinalIgnoreCase)) {
+ throw "Unexpected Spotify cleanup path: $($item.FullName)"
+ }
+ Remove-Item -LiteralPath $item.FullName -Recurse -Force -ErrorAction Stop
+ }
+ }
+ else { $null = New-Item -ItemType Directory -Path $destination -ErrorAction Stop }
+ $preparedPrefix = [IO.Path]::GetFullPath($preparedClient).TrimEnd('\') + '\'
+ foreach ($item in Get-ChildItem -LiteralPath $preparedClient -Force -ErrorAction Stop) {
+ if (!$item.FullName.StartsWith($preparedPrefix, [StringComparison]::OrdinalIgnoreCase)) {
+ throw "Unexpected Spotify staging path: $($item.FullName)"
+ }
+ if (!$uninstallSpotify -and $item.Name -in @('Users', 'prefs') -and
+ (Test-Path -LiteralPath (Join-Path $destination $item.Name))) { continue }
+ Move-Item -LiteralPath $item.FullName -Destination $destination -Force -ErrorAction Stop
+ }
+ $offline = (Get-Item -LiteralPath $spotifyExecutable -ErrorAction Stop).VersionInfo.FileVersion
+ Register-SpotifyInstall -Directory $destination -InstalledVersion ([version]$offline)
+ }
+
+ if ($no_shortcut) {
+ $desktop_folder = DesktopFolder
+ Remove-Item -LiteralPath (Join-Path $desktop_folder 'Spotify.lnk') -Force -ErrorAction SilentlyContinue
+ }
+ if ($installSpotify -and $not_block_update -and (Test-Path -LiteralPath $exe_bak)) {
+ $offline_bak = (Get-Item -LiteralPath $exe_bak -ErrorAction Stop).VersionInfo.FileVersion
+ if ($offline -eq $offline_bak) {
+ Remove-Item -LiteralPath $spotifyExecutable -Force -ErrorAction Stop
+ Rename-Item -LiteralPath $exe_bak -NewName $spotifyExecutable -ErrorAction Stop
+ }
+ }
+}
+catch {
+ Write-Warning $_.Exception.Message
+ Stop-Script
+}
+finally {
+ Remove-TempDirectory -Directory $tempDirectory
+ $tempDirectory = $null
+}
+
+Write-Host ($lang).ModSpoti`n
$xpui_js_patch = Join-Path (Join-Path (Join-Path $spotifyDirectory 'Apps') 'xpui') 'xpui.js'
$test_spa = Test-Path -Path $xpui_spa_patch
@@ -5257,7 +5645,7 @@ if ($test_spa) {
# Send new versions
if (!($sendversion_off)) {
- $checkVersion = Get -Url (Get-Link -e "/js-helper/checkVersion.js")
+ $checkVersion = $resources.CheckVersion
if ($checkVersion -ne $null) {
injection -p $xpui_spa_patch -f "spotx-helper" -n "checkVersion.js" -c $checkVersion
@@ -5267,7 +5655,7 @@ if ($test_spa) {
# Hiding Ad-like sections or turn off podcasts from the homepage
if ($podcast_off -or $adsections_off -or $canvashome_off) {
- $section = Get -Url (Get-Link -e "/js-helper/sectionBlock.js")
+ $section = $resources.Section
if ($section -ne $null) {
@@ -5293,7 +5681,7 @@ if ($test_spa) {
# goofy History
if ($urlform_goofy -and $idbox_goofy) {
- $goofy = Get -Url (Get-Link -e "/js-helper/goofyHistory.js")
+ $goofy = $resources.Goofy
if ($goofy -ne $null) {
@@ -5302,9 +5690,9 @@ if ($test_spa) {
}
# Static color for lyrics
- if ($lyrics_stat) {
- $rulesContent = Get -Url (Get-Link -e "/css-helper/lyrics-color/rules.css")
- $colorsContent = Get -Url (Get-Link -e "/css-helper/lyrics-color/colors.css")
+ if ($lyrics_stat -and $resources.LyricsRules -and $resources.LyricsColors) {
+ $rulesContent = $resources.LyricsRules
+ $colorsContent = $resources.LyricsColors
$colorsContent = $colorsContent -replace '{{past}}', "$($webjson.others.themelyrics.theme.$lyrics_stat.pasttext)"
$colorsContent = $colorsContent -replace '{{current}}', "$($webjson.others.themelyrics.theme.$lyrics_stat.current)"
@@ -5485,13 +5873,13 @@ if ($spotify_binary_bak -eq $dll_bak -and !$premium -and [version]$offline -ge [
}
if ($spotify_binary_bak -eq $dll_bak -and !$premium -and [version]$offline -ge [version]'1.2.89') {
- $null = Set-CrossfadeEnabledBinaryPatch -FilePath $spotifyDll
+ $null = Set-CrossfadeEnabledBinaryPatch -FilePath $spotifyDll -TargetVersion ([version]$offline)
}
# fix login for old versions
if ([version]$offline -ge [version]"1.1.87.612" -and [version]$offline -le [version]"1.2.5.1006") {
$login_spa = Join-Path (Join-Path $spotifyDirectory 'Apps') 'login.spa'
- Get -Url (Get-Link -e "/res/login.spa") -OutputPath $login_spa
+ [IO.File]::WriteAllBytes($login_spa, $resources.Login)
}
# Disable Startup client