Skip to content

Commit b523a42

Browse files
committed
feat: add support for embedded xpui in v8 snapshot
1 parent 0a9a7ff commit b523a42

File tree

7 files changed

+194
-19
lines changed

7 files changed

+194
-19
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ require (
1212
require (
1313
github.com/mattn/go-isatty v0.0.20 // indirect
1414
github.com/stretchr/testify v1.7.1 // indirect
15+
golang.org/x/text v0.24.0 // indirect
1516
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
1616
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1717
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
1818
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
19+
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
20+
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
1921
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2022
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
2123
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

src/apply/apply.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,19 @@ type Flag struct {
2727

2828
// AdditionalOptions .
2929
func AdditionalOptions(appsFolderPath string, flags Flag) {
30+
jsModifiers := []func(path string, flags Flag){
31+
insertExpFeatures,
32+
insertSidebarConfig,
33+
insertHomeConfig,
34+
}
3035
filesToModified := map[string][]func(path string, flags Flag){
3136
filepath.Join(appsFolderPath, "xpui", "index.html"): {
3237
htmlMod,
3338
},
34-
filepath.Join(appsFolderPath, "xpui", "xpui.js"): {
39+
filepath.Join(appsFolderPath, "xpui", "xpui.js"): jsModifiers,
40+
filepath.Join(appsFolderPath, "xpui", "xpui-modules.js"): jsModifiers,
41+
filepath.Join(appsFolderPath, "xpui", "xpui-snapshot.js"): {
3542
insertCustomApp,
36-
insertExpFeatures,
37-
insertSidebarConfig,
38-
insertHomeConfig,
3943
},
4044
filepath.Join(appsFolderPath, "xpui", "home-v2.js"): {
4145
insertHomeConfig,
@@ -57,8 +61,10 @@ func AdditionalOptions(appsFolderPath string, flags Flag) {
5761
spotifyPatch, _ = strconv.Atoi(verParts[2])
5862
}
5963

64+
filesToModified[filepath.Join(appsFolderPath, "xpui", "xpui.js")] = append(filesToModified[filepath.Join(appsFolderPath, "xpui", "xpui.js")], insertCustomApp)
6065
if spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57 {
6166
filesToModified[filepath.Join(appsFolderPath, "xpui", "xpui.js")] = append(filesToModified[filepath.Join(appsFolderPath, "xpui", "xpui.js")], insertExpFeatures)
67+
filesToModified[filepath.Join(appsFolderPath, "xpui", "xpui-modules.js")] = append(filesToModified[filepath.Join(appsFolderPath, "xpui", "xpui-modukes.js")], insertExpFeatures)
6268
} else {
6369
filesToModified[filepath.Join(appsFolderPath, "xpui", "vendor~xpui.js")] = []func(string, Flag){insertExpFeatures}
6470
}
@@ -187,6 +193,12 @@ func htmlMod(htmlPath string, flags Flag) {
187193
}
188194

189195
utils.ModifyFile(htmlPath, func(content string) string {
196+
utils.Replace(
197+
&content,
198+
`<script defer="defer" src="/xpui-snapshot\.js"></script>`,
199+
func(submatches ...string) string {
200+
return `<script defer="defer" src="/xpui-modules.js"></script><script defer="defer" src="/xpui-snapshot.js"></script>`
201+
})
190202
utils.Replace(
191203
&content,
192204
`<\!-- spicetify helpers -->`,

src/cmd/backup.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,22 @@ Modded Spotify cannot be launched using original Shortcut/Start menu tile. To co
6565

6666
utils.PrintBold("Preprocessing:")
6767

68-
preprocess.Start(
69-
spicetifyVersion,
70-
rawFolder,
71-
preprocess.Flag{
72-
DisableSentry: preprocSection.Key("disable_sentry").MustBool(false),
73-
DisableLogging: preprocSection.Key("disable_ui_logging").MustBool(false),
74-
RemoveRTL: preprocSection.Key("remove_rtl_rule").MustBool(false),
75-
ExposeAPIs: preprocSection.Key("expose_apis").MustBool(false),
76-
SpotifyVer: utils.GetSpotifyVersion(prefsPath)},
77-
)
68+
spotifyBasePath := spotifyPath
69+
if spotifyBasePath == "" {
70+
utils.PrintError("Spotify installation path not found. Cannot preprocess V8 snapshots.")
71+
} else {
72+
preprocess.Start(
73+
spicetifyVersion,
74+
spotifyBasePath,
75+
rawFolder,
76+
preprocess.Flag{
77+
DisableSentry: preprocSection.Key("disable_sentry").MustBool(false),
78+
DisableLogging: preprocSection.Key("disable_ui_logging").MustBool(false),
79+
RemoveRTL: preprocSection.Key("remove_rtl_rule").MustBool(false),
80+
ExposeAPIs: preprocSection.Key("expose_apis").MustBool(false),
81+
SpotifyVer: utils.GetSpotifyVersion(prefsPath)},
82+
)
83+
}
7884
utils.PrintGreen("OK")
7985

8086
err = utils.Copy(rawFolder, themedFolder, true, []string{".html", ".js", ".css"})

src/preprocess/preprocess.go

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path"
1111
"path/filepath"
1212
"regexp"
13+
"runtime"
1314
"strconv"
1415
"strings"
1516

@@ -60,8 +61,7 @@ func readLocalCssMap(cssTranslationMap *map[string]string) error {
6061
return nil
6162
}
6263

63-
// Start preprocessing apps assets in extractedAppPath
64-
func Start(version string, extractedAppsPath string, flags Flag) {
64+
func Start(version string, spotifyBasePath string, extractedAppsPath string, flags Flag) {
6565
appPath := filepath.Join(extractedAppsPath, "xpui")
6666
var cssTranslationMap = make(map[string]string)
6767
// readSourceMapAndGenerateCSSMap(appPath)
@@ -95,14 +95,66 @@ func Start(version string, extractedAppsPath string, flags Flag) {
9595
spotifyPatch, _ = strconv.Atoi(verParts[2])
9696
}
9797

98+
frameworkResourcesPath := ""
99+
switch runtime.GOOS {
100+
case "darwin":
101+
frameworkResourcesPath = filepath.Join(spotifyBasePath, "Contents", "Frameworks", "Chromium Embedded Framework.framework", "Resources")
102+
case "windows":
103+
frameworkResourcesPath = spotifyBasePath
104+
case "linux":
105+
frameworkResourcesPath = spotifyBasePath
106+
default:
107+
utils.PrintWarning("Unsupported OS for V8 snapshot finding: " + runtime.GOOS)
108+
}
109+
110+
if frameworkResourcesPath != "" {
111+
files, err := os.ReadDir(frameworkResourcesPath)
112+
if err != nil {
113+
utils.PrintWarning(fmt.Sprintf("Could not read directory %s for V8 snapshots: %v", frameworkResourcesPath, err))
114+
} else {
115+
for _, file := range files {
116+
if !file.IsDir() && strings.HasPrefix(file.Name(), "v8_context_snapshot") && strings.HasSuffix(file.Name(), ".bin") {
117+
binFilePath := filepath.Join(frameworkResourcesPath, file.Name())
118+
utils.PrintInfo("Processing V8 snapshot file: " + binFilePath)
119+
120+
startMarker := []byte("var __webpack_modules__={")
121+
endMarker := []byte("xpui-modules.js.map")
122+
123+
embeddedString, _, _, err := utils.ReadStringFromUTF16Binary(binFilePath, startMarker, endMarker)
124+
if err != nil {
125+
utils.PrintWarning(fmt.Sprintf("Could not process %s: %v", binFilePath, err))
126+
utils.PrintInfo("You can ignore this warning if you're on a Spotify version that didn't yet add xpui modules to the V8 snapshot")
127+
continue
128+
}
129+
130+
err = utils.CreateFile(filepath.Join(appPath, "xpui-modules.js"), embeddedString)
131+
if err != nil {
132+
utils.PrintWarning(fmt.Sprintf("Could not create xpui-modules.js: %v", err))
133+
continue
134+
} else {
135+
utils.PrintInfo("Extracted V8 snapshot blob (remaining xpui) to xpui-modules.js")
136+
}
137+
}
138+
}
139+
}
140+
}
141+
98142
filepath.Walk(appPath, func(path string, info os.FileInfo, err error) error {
143+
if err != nil {
144+
fmt.Printf("Error accessing path %q: %v\n", path, err)
145+
return err
146+
}
147+
if info.IsDir() {
148+
return nil
149+
}
150+
99151
fileName := info.Name()
100152
extension := filepath.Ext(fileName)
101153

102154
switch extension {
103155
case ".js":
104156
utils.ModifyFile(path, func(content string) string {
105-
if flags.DisableSentry && fileName == "xpui.js" {
157+
if flags.DisableSentry && (fileName == "xpui.js" || fileName == "xpui-snapshot.js") {
106158
content = disableSentry(content)
107159
}
108160

@@ -112,6 +164,9 @@ func Start(version string, extractedAppsPath string, flags Flag) {
112164

113165
if flags.ExposeAPIs {
114166
switch fileName {
167+
case "xpui-modules.js", "xpui-snapshot.js":
168+
content = exposeAPIs_main(content)
169+
content = exposeAPIs_vendor(content)
115170
case "xpui.js":
116171
content = exposeAPIs_main(content)
117172
if spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57 {
@@ -576,7 +631,6 @@ func exposeAPIs_main(input string) string {
576631
&input,
577632
`(;const [\w\d]+=)((?:\(0,[\w\d]+\.memo\))[\(\d,\w\.\){:}=]+\=[\d\w]+\.[\d\w]+\.getLocaleForURLPath\(\))`,
578633
func(submatches ...string) string {
579-
fmt.Println(submatches)
580634
return fmt.Sprintf("%sSpicetify.ReactComponent.Navigation=%s", submatches[1], submatches[2])
581635
})
582636

@@ -585,7 +639,12 @@ func exposeAPIs_main(input string) string {
585639
return fmt.Sprintf("%s[Spicetify.ContextMenuV2.renderItems(),%s].flat()", submatches[1], submatches[2])
586640
})
587641

588-
croppedInput := utils.FindFirstMatch(input, `.*value:"contextmenu"`)[0]
642+
inputContextMenu := utils.FindFirstMatch(input, `.*value:"contextmenu"`)
643+
if len(inputContextMenu) == 0 {
644+
return input
645+
}
646+
647+
croppedInput := inputContextMenu[0]
589648
react := utils.FindLastMatch(croppedInput, `([a-zA-Z_\$][\w\$]*)\.useRef`)[1]
590649
candicates := utils.FindLastMatch(croppedInput, `\(\{[^}]*menu:([a-zA-Z_\$][\w\$]*),[^}]*trigger:([a-zA-Z_\$][\w\$]*),[^}]*triggerRef:([a-zA-Z_\$][\w\$]*)`)
591650
oldCandicates := utils.FindLastMatch(croppedInput, `([a-zA-Z_\$][\w\$]*)=[\w_$]+\.menu[^}]*,([a-zA-Z_\$][\w\$]*)=[\w_$]+\.trigger[^}]*,([a-zA-Z_\$][\w\$]*)=[\w_$]+\.triggerRef`)

src/utils/file-utils.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package utils
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"fmt"
7+
"os"
8+
"unicode/utf16"
9+
)
10+
11+
func ReadStringFromUTF16Binary(inputFile string, startMarker []byte, endMarker []byte) (string, int, int, error) {
12+
fileContent, err := os.ReadFile(inputFile)
13+
if err != nil {
14+
return "", -1, -1, fmt.Errorf("error reading file %s: %w", inputFile, err)
15+
}
16+
17+
isUTF16LE := false
18+
if len(fileContent) >= 2 && fileContent[0] == 0xFF && fileContent[1] == 0xFE {
19+
isUTF16LE = true
20+
}
21+
22+
if !isUTF16LE && len(fileContent) > 100 && fileContent[1] == 0x00 {
23+
isUTF16LE = true
24+
}
25+
26+
var startIdx, endIdx int
27+
var contentToSearch []byte
28+
var searchStartMarker, searchEndMarker []byte
29+
30+
if !isUTF16LE {
31+
return "", -1, -1, fmt.Errorf("file is not in UTF-16LE format: %s", inputFile)
32+
}
33+
34+
contentToSearch = fileContent[2:]
35+
searchStartMarker = encodeUTF16LE(startMarker)
36+
searchEndMarker = encodeUTF16LE(endMarker)
37+
38+
startIdx = bytes.Index(contentToSearch, searchStartMarker)
39+
if startIdx == -1 {
40+
return "", -1, -1, fmt.Errorf("start marker not found: %s", string(startMarker))
41+
}
42+
43+
searchSpace := contentToSearch[startIdx+len(searchStartMarker):]
44+
endIdx = bytes.Index(searchSpace, searchEndMarker)
45+
if endIdx == -1 {
46+
return "", -1, -1, fmt.Errorf("end marker not found after start index %d: %s", startIdx+len(searchStartMarker), string(endMarker))
47+
}
48+
49+
stringContentBytes := contentToSearch[startIdx : startIdx+len(searchStartMarker)+endIdx+len(searchEndMarker)]
50+
51+
decodedStringBytes, err := decodeUTF16LE(stringContentBytes)
52+
if err != nil {
53+
return "", -1, -1, fmt.Errorf("error decoding UTF-16LE content: %w", err)
54+
}
55+
56+
// Adjust indices to be byte offsets in the original file
57+
originalStartIdx := 2 + startIdx
58+
originalEndIdx := 2 + endIdx + len(stringContentBytes)
59+
return string(decodedStringBytes), originalStartIdx, originalEndIdx, nil
60+
}
61+
62+
// Helper function to encode a byte slice (assumed UTF-8) to UTF-16LE
63+
func encodeUTF16LE(data []byte) []byte {
64+
utf16Bytes := utf16.Encode([]rune(string(data)))
65+
byteSlice := make([]byte, len(utf16Bytes)*2)
66+
for i, r := range utf16Bytes {
67+
binary.LittleEndian.PutUint16(byteSlice[i*2:], r)
68+
}
69+
70+
return byteSlice
71+
}
72+
73+
// Helper function to decode a byte slice (UTF-16LE) to UTF-8
74+
func decodeUTF16LE(data []byte) ([]byte, error) {
75+
if len(data)%2 != 0 {
76+
return nil, fmt.Errorf("invalid UTF-16LE data length")
77+
}
78+
79+
uint16s := make([]uint16, len(data)/2)
80+
for i := 0; i < len(data)/2; i++ {
81+
uint16s[i] = binary.LittleEndian.Uint16(data[i*2:])
82+
}
83+
84+
runes := utf16.Decode(uint16s)
85+
return []byte(string(runes)), nil
86+
}

src/utils/utils.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,15 @@ func ModifyFile(path string, repl func(string) string) {
223223
os.WriteFile(path, []byte(content), 0700)
224224
}
225225

226+
// CreateFile creates a file with given path and content.
227+
func CreateFile(path string, content string) error {
228+
err := os.WriteFile(path, []byte(content), 0600)
229+
if err != nil {
230+
return err
231+
}
232+
return nil
233+
}
234+
226235
// GetSpotifyVersion .
227236
func GetSpotifyVersion(prefsPath string) string {
228237
pref, err := ini.Load(prefsPath)

0 commit comments

Comments
 (0)