@@ -25,6 +25,8 @@ if (!navigator.serviceWorker) {
25
25
PopupLyrics ( ) ;
26
26
}
27
27
28
+ let CACHE = { } ;
29
+
28
30
function PopupLyrics ( ) {
29
31
const { Player, CosmosAsync, LocalStorage, ContextMenu } = Spicetify ;
30
32
@@ -324,7 +326,7 @@ function PopupLyrics() {
324
326
musixmatch : {
325
327
on : boolLocalStorage ( "popup-lyrics:services:musixmatch:on" ) ,
326
328
call : LyricProviders . fetchMusixmatch ,
327
- desc : ` Fully compatible with Spotify. Requires a token that can be retrieved from the official Musixmatch app. Follow instructions on <a href="https://spicetify.app/docs/faq#sometimes-popup- lyrics-andor-lyrics-plus-seem-to-not-work">Spicetify Docs</a>.` ,
329
+ desc : " Fully compatible with Spotify. Requires a token that can be retrieved from the official Musixmatch app. If you have problems with retrieving lyrics, try refreshing the token by clicking <code>Refresh Token</code> button." ,
328
330
token : LocalStorage . get ( "popup-lyrics:services:musixmatch:token" ) || "2005218b74f939209bda92cb633c7380612e14cb7fe92dcd6a780f" ,
329
331
} ,
330
332
spotify : {
@@ -421,9 +423,11 @@ function PopupLyrics() {
421
423
422
424
let sharedData = { } ;
423
425
424
- Player . addEventListener ( "songchange" , updateTrack ) ;
426
+ Player . addEventListener ( "songchange" , ( ) => {
427
+ updateTrack ( ) ;
428
+ } ) ;
425
429
426
- async function updateTrack ( ) {
430
+ async function updateTrack ( refresh = false ) {
427
431
if ( ! lyricVideoIsOpen ) {
428
432
return ;
429
433
}
@@ -443,20 +447,26 @@ function PopupLyrics() {
443
447
uri : Player . data . item . uri ,
444
448
} ;
445
449
446
- for ( const name of userConfigs . servicesOrder ) {
447
- const service = userConfigs . services [ name ] ;
448
- if ( ! service . on ) continue ;
449
- sharedData = { lyrics : [ ] } ;
450
+ if ( CACHE ?. [ info . uri ] ?. lyrics ?. length && ! refresh ) {
451
+ sharedData = CACHE [ info . uri ] ;
452
+ } else {
453
+ for ( const name of userConfigs . servicesOrder ) {
454
+ const service = userConfigs . services [ name ] ;
455
+ if ( ! service . on ) continue ;
456
+ sharedData = { lyrics : [ ] } ;
457
+
458
+ try {
459
+ const data = await service . call ( info ) ;
460
+ console . log ( data ) ;
461
+ sharedData = data ;
462
+ CACHE [ info . uri ] = sharedData ;
450
463
451
- try {
452
- const data = await service . call ( info ) ;
453
- console . log ( data ) ;
454
- sharedData = data ;
455
- if ( ! sharedData . error ) {
456
- return ;
464
+ if ( ! sharedData . error ) {
465
+ return ;
466
+ }
467
+ } catch ( err ) {
468
+ sharedData = { error : "No lyrics" } ;
457
469
}
458
- } catch ( err ) {
459
- sharedData = { error : "No lyrics" } ;
460
470
}
461
471
}
462
472
}
@@ -815,11 +825,20 @@ function PopupLyrics() {
815
825
816
826
function openConfig ( event ) {
817
827
event . preventDefault ( ) ;
818
- if ( ! configContainer ) {
828
+
829
+ // Reset on reopen
830
+ if ( configContainer ) {
831
+ resetTokenButton ( configContainer ) ;
832
+ } else {
819
833
configContainer = document . createElement ( "div" ) ;
820
834
configContainer . id = "popup-config-container" ;
821
835
const style = document . createElement ( "style" ) ;
822
836
style . innerHTML = `
837
+ .setting-row {
838
+ display: flex;
839
+ justify-content: space-between;
840
+ align-items: center;
841
+ }
823
842
.setting-row::after {
824
843
content: "";
825
844
display: table;
@@ -831,13 +850,16 @@ function PopupLyrics() {
831
850
align-items: center;
832
851
}
833
852
.setting-row .col.description {
834
- float: left;
835
853
padding-right: 15px;
836
854
cursor: default;
855
+ width: 50%;
837
856
}
838
857
.setting-row .col.action {
839
- float: right;
840
- text-align: right;
858
+ justify-content: flex-end;
859
+ width: 50%;
860
+ }
861
+ .popup-config-col-margin {
862
+ margin-top: 10px;
841
863
}
842
864
button.switch {
843
865
align-items: center;
@@ -859,6 +881,27 @@ button.switch.small {
859
881
height: 22px;
860
882
padding: 6px;
861
883
}
884
+ button.btn {
885
+ font-weight: 700;
886
+ display: block;
887
+ background-color: rgba(var(--spice-rgb-shadow), .7);
888
+ border-radius: 500px;
889
+ transition-duration: 33ms;
890
+ transition-property: background-color, border-color, color, box-shadow, filter, transform;
891
+ padding-inline: 15px;
892
+ border: 1px solid #727272;
893
+ color: var(--spice-text);
894
+ min-block-size: 32px;
895
+ cursor: pointer;
896
+ }
897
+ button.btn:hover {
898
+ transform: scale(1.04);
899
+ border-color: var(--spice-text);
900
+ }
901
+ button.btn:disabled {
902
+ opacity: 0.5;
903
+ cursor: not-allowed;
904
+ }
862
905
#popup-config-container select {
863
906
color: var(--spice-text);
864
907
background: rgba(var(--spice-rgb-shadow), .7);
@@ -945,6 +988,13 @@ button.switch.small {
945
988
userConfigs . delay = Number ( state ) ;
946
989
LocalStorage . set ( "popup-lyrics:delay" , state ) ;
947
990
} ) ;
991
+ const clearCache = descriptiveElement (
992
+ createButton ( "Clear Memory Cache" , "Clear Memory Cache" , ( ) => {
993
+ CACHE = { } ;
994
+ updateTrack ( ) ;
995
+ } ) ,
996
+ "Loaded lyrics are cached in memory for faster reloading. Press this button to clear the cached lyrics from memory without restarting Spotify."
997
+ ) ;
948
998
949
999
const serviceHeader = document . createElement ( "h2" ) ;
950
1000
serviceHeader . innerText = "Services" ;
@@ -975,7 +1025,7 @@ button.switch.small {
975
1025
const id = el . dataset . id ;
976
1026
userConfigs . services [ id ] . on = state ;
977
1027
LocalStorage . set ( `popup-lyrics:services:${ id } :on` , state ) ;
978
- updateTrack ( ) ;
1028
+ updateTrack ( true ) ;
979
1029
}
980
1030
981
1031
function posCallback ( el , dir ) {
@@ -990,23 +1040,28 @@ button.switch.small {
990
1040
LocalStorage . set ( "popup-lyrics:services-order" , JSON . stringify ( userConfigs . servicesOrder ) ) ;
991
1041
992
1042
stackServiceElements ( ) ;
993
- updateTrack ( ) ;
994
- }
995
-
996
- function tokenChangeCallback ( el , inputEl ) {
997
- const newVal = inputEl . value ;
998
- const id = el . dataset . id ;
999
- userConfigs . services [ id ] . token = newVal ;
1000
- LocalStorage . set ( `popup-lyrics:services:${ id } :token` , newVal ) ;
1001
- updateTrack ( ) ;
1043
+ updateTrack ( true ) ;
1002
1044
}
1003
1045
1004
1046
for ( const name of userConfigs . servicesOrder ) {
1005
- userConfigs . services [ name ] . element = createServiceOption ( name , userConfigs . services [ name ] , switchCallback , posCallback , tokenChangeCallback ) ;
1047
+ userConfigs . services [ name ] . element = createServiceOption ( name , userConfigs . services [ name ] , switchCallback , posCallback ) ;
1006
1048
}
1007
1049
stackServiceElements ( ) ;
1008
1050
1009
- configContainer . append ( style , optionHeader , smooth , center , cover , blurSize , fontSize , ratio , delay , serviceHeader , serviceContainer ) ;
1051
+ configContainer . append (
1052
+ style ,
1053
+ optionHeader ,
1054
+ smooth ,
1055
+ center ,
1056
+ cover ,
1057
+ blurSize ,
1058
+ fontSize ,
1059
+ ratio ,
1060
+ delay ,
1061
+ clearCache ,
1062
+ serviceHeader ,
1063
+ serviceContainer
1064
+ ) ;
1010
1065
}
1011
1066
Spicetify . PopupModal . display ( {
1012
1067
title : "Popup Lyrics" ,
@@ -1084,8 +1139,125 @@ button.switch.small {
1084
1139
1085
1140
return container ;
1086
1141
}
1142
+ // if name is null, the element can be used without a description.
1143
+ function createButton ( name , defaultValue , callback ) {
1144
+ let container ;
1145
+
1146
+ if ( name ) {
1147
+ container = document . createElement ( "div" ) ;
1148
+ container . innerHTML = `
1149
+ <div class="setting-row">
1150
+ <label class="col description">${ name } </label>
1151
+ <div class="col action">
1152
+ <button id="popup-lyrics-clickbutton" class="btn">${ defaultValue } </button>
1153
+ </div>
1154
+ </div>` ;
1155
+
1156
+ const button = container . querySelector ( "#popup-lyrics-clickbutton" ) ;
1157
+ button . onclick = ( ) => {
1158
+ callback ( ) ;
1159
+ } ;
1160
+ } else {
1161
+ container = document . createElement ( "button" ) ;
1162
+ container . innerHTML = defaultValue ;
1163
+ container . className = "btn " ;
1164
+
1165
+ container . onclick = ( ) => {
1166
+ callback ( ) ;
1167
+ } ;
1168
+ }
1169
+
1170
+ return container ;
1171
+ }
1172
+ // if name is null, the element can be used without a description.
1173
+ function createTextfield ( name , defaultValue , placeholder , callback ) {
1174
+ let container ;
1175
+
1176
+ if ( name ) {
1177
+ container = document . createElement ( "div" ) ;
1178
+ container . className = "setting-column" ;
1179
+ container . innerHTML = `
1180
+ <label class="row-description">${ name } </label>
1181
+ <div class="popup-row-option action">
1182
+ <input id="popup-lyrics-textfield" placeholder="${ placeholder } " value="${ defaultValue } " />
1183
+ </div>` ;
1184
+
1185
+ const textfield = container . querySelector ( "#popup-lyrics-textfield" ) ;
1186
+ textfield . onchange = ( ) => {
1187
+ callback ( ) ;
1188
+ } ;
1189
+ } else {
1190
+ container = document . createElement ( "input" ) ;
1191
+ container . placeholder = placeholder ;
1192
+ container . value = defaultValue ;
1193
+
1194
+ container . onchange = ( e ) => {
1195
+ callback ( e . target . value ) ;
1196
+ } ;
1197
+ }
1198
+
1199
+ return container ;
1200
+ }
1201
+ function descriptiveElement ( element , description ) {
1202
+ const desc = document . createElement ( "span" ) ;
1203
+ desc . innerHTML = description ;
1204
+ element . append ( desc ) ;
1205
+ return element ;
1206
+ }
1207
+
1208
+ function resetTokenButton ( container ) {
1209
+ const button = container . querySelector ( "#popup-lyrics-refresh-token" ) ;
1210
+ if ( button ) {
1211
+ button . innerHTML = "Refresh token" ;
1212
+ button . disabled = false ;
1213
+ }
1214
+ }
1215
+
1216
+ function musixmatchTokenElements ( defaultVal , id ) {
1217
+ const button = createButton ( null , "Refresh token" , clickRefresh ) ;
1218
+ button . className += "popup-config-col-margin" ;
1219
+ button . id = "popup-lyrics-refresh-token" ;
1220
+ const textfield = createTextfield ( null , defaultVal . token , `Place your ${ id } token here` , changeTokenfield ) ;
1221
+ textfield . className += "popup-config-col-margin" ;
1222
+
1223
+ function clickRefresh ( ) {
1224
+ button . innerHTML = "Refreshing token..." ;
1225
+ button . disabled = true ;
1226
+
1227
+ Spicetify . CosmosAsync . get ( "https://apic-desktop.musixmatch.com/ws/1.1/token.get?app_id=web-desktop-app-v1.0" , null , {
1228
+ authority : "apic-desktop.musixmatch.com" ,
1229
+ } )
1230
+ . then ( ( { message : response } ) => {
1231
+ if ( response . header . status_code === 200 && response . body . user_token ) {
1232
+ button . innerHTML = "Token refreshed" ;
1233
+ textfield . value = response . body . user_token ;
1234
+ textfield . dispatchEvent ( new Event ( "change" ) ) ;
1235
+ } else if ( response . header . status_code === 401 ) {
1236
+ button . innerHTML = "Too many attempts" ;
1237
+ } else {
1238
+ button . innerHTML = "Failed to refresh token" ;
1239
+ console . error ( "Failed to refresh token" , response ) ;
1240
+ }
1241
+ } )
1242
+ . catch ( ( error ) => {
1243
+ button . innerHTML = "Failed to refresh token" ;
1244
+ console . error ( "Failed to refresh token" , error ) ;
1245
+ } ) ;
1246
+ }
1247
+
1248
+ function changeTokenfield ( value ) {
1249
+ userConfigs . services . musixmatch . token = value ;
1250
+ LocalStorage . set ( "popup-lyrics:services:musixmatch:token" , value ) ;
1251
+ updateTrack ( true ) ;
1252
+ }
1253
+
1254
+ const container = document . createElement ( "div" ) ;
1255
+ container . append ( button ) ;
1256
+ container . append ( textfield ) ;
1257
+ return container ;
1258
+ }
1087
1259
1088
- function createServiceOption ( id , defaultVal , switchCallback , posCallback , tokenCallback ) {
1260
+ function createServiceOption ( id , defaultVal , switchCallback , posCallback ) {
1089
1261
const name = id . replace ( / ^ ./ , ( c ) => c . toUpperCase ( ) ) ;
1090
1262
1091
1263
const container = document . createElement ( "div" ) ;
@@ -1113,12 +1285,8 @@ button.switch.small {
1113
1285
</div>
1114
1286
<span>${ defaultVal . desc } </span>` ;
1115
1287
1116
- if ( defaultVal . token !== undefined ) {
1117
- const input = document . createElement ( "input" ) ;
1118
- input . placeholder = `Place your ${ id } token here` ;
1119
- input . value = defaultVal . token ;
1120
- input . onchange = ( ) => tokenCallback ( container , input ) ;
1121
- container . append ( input ) ;
1288
+ if ( id === "musixmatch" ) {
1289
+ container . append ( musixmatchTokenElements ( defaultVal ) ) ;
1122
1290
}
1123
1291
1124
1292
const [ up , down , slider ] = container . querySelectorAll ( "button" ) ;
0 commit comments