1
1
import crypto from 'node:crypto' ;
2
2
3
- import { net , shell } from 'electron' ;
3
+ import { BrowserWindow , dialog , net } from 'electron' ;
4
4
5
5
import { ScrobblerBase } from './base' ;
6
6
7
- import { ScrobblerPluginConfig } from '../index' ;
8
- import { SetConfType } from '../main' ;
7
+ import { t } from '@/i18n' ;
9
8
9
+ import type { ScrobblerPluginConfig } from '../index' ;
10
+ import type { SetConfType } from '../main' ;
10
11
import type { SongInfo } from '@/providers/song-info' ;
11
12
12
13
interface LastFmData {
@@ -28,11 +29,22 @@ interface LastFmSongData {
28
29
}
29
30
30
31
export class LastFmScrobbler extends ScrobblerBase {
31
- isSessionCreated ( config : ScrobblerPluginConfig ) : boolean {
32
+ mainWindow : BrowserWindow ;
33
+
34
+ constructor ( mainWindow : BrowserWindow ) {
35
+ super ( ) ;
36
+
37
+ this . mainWindow = mainWindow ;
38
+ }
39
+
40
+ override isSessionCreated ( config : ScrobblerPluginConfig ) : boolean {
32
41
return ! ! config . scrobblers . lastfm . sessionKey ;
33
42
}
34
43
35
- async createSession ( config : ScrobblerPluginConfig , setConfig : SetConfType ) : Promise < ScrobblerPluginConfig > {
44
+ override async createSession (
45
+ config : ScrobblerPluginConfig ,
46
+ setConfig : SetConfType ,
47
+ ) : Promise < ScrobblerPluginConfig > {
36
48
// Get and store the session key
37
49
const data = {
38
50
api_key : config . scrobblers . lastfm . apiKey ,
@@ -52,8 +64,15 @@ export class LastFmScrobbler extends ScrobblerBase {
52
64
} ;
53
65
if ( json . error ) {
54
66
config . scrobblers . lastfm . token = await createToken ( config ) ;
55
- await authenticate ( config ) ;
56
- setConfig ( config ) ;
67
+ // If is successful, we need retry the request
68
+ authenticate ( config , this . mainWindow ) . then ( ( it ) => {
69
+ if ( it ) {
70
+ this . createSession ( config , setConfig ) ;
71
+ } else {
72
+ // failed
73
+ setConfig ( config ) ;
74
+ }
75
+ } ) ;
57
76
}
58
77
if ( json . session ) {
59
78
config . scrobblers . lastfm . sessionKey = json . session . key ;
@@ -62,7 +81,7 @@ export class LastFmScrobbler extends ScrobblerBase {
62
81
return config ;
63
82
}
64
83
65
- setNowPlaying ( songInfo : SongInfo , config : ScrobblerPluginConfig , setConfig : SetConfType ) : void {
84
+ override setNowPlaying ( songInfo : SongInfo , config : ScrobblerPluginConfig , setConfig : SetConfType ) : void {
66
85
if ( ! config . scrobblers . lastfm . sessionKey ) {
67
86
return ;
68
87
}
@@ -74,7 +93,7 @@ export class LastFmScrobbler extends ScrobblerBase {
74
93
this . postSongDataToAPI ( songInfo , config , data , setConfig ) ;
75
94
}
76
95
77
- addScrobble ( songInfo : SongInfo , config : ScrobblerPluginConfig , setConfig : SetConfType ) : void {
96
+ override addScrobble ( songInfo : SongInfo , config : ScrobblerPluginConfig , setConfig : SetConfType ) : void {
78
97
if ( ! config . scrobblers . lastfm . sessionKey ) {
79
98
return ;
80
99
}
@@ -87,7 +106,7 @@ export class LastFmScrobbler extends ScrobblerBase {
87
106
this . postSongDataToAPI ( songInfo , config , data , setConfig ) ;
88
107
}
89
108
90
- async postSongDataToAPI (
109
+ private async postSongDataToAPI (
91
110
songInfo : SongInfo ,
92
111
config : ScrobblerPluginConfig ,
93
112
data : LastFmData ,
@@ -128,8 +147,14 @@ export class LastFmScrobbler extends ScrobblerBase {
128
147
// Session key is invalid, so remove it from the config and reauthenticate
129
148
config . scrobblers . lastfm . sessionKey = undefined ;
130
149
config . scrobblers . lastfm . token = await createToken ( config ) ;
131
- await authenticate ( config ) ;
132
- setConfig ( config ) ;
150
+ authenticate ( config , this . mainWindow ) . then ( ( it ) => {
151
+ if ( it ) {
152
+ this . createSession ( config , setConfig ) ;
153
+ } else {
154
+ // failed
155
+ setConfig ( config ) ;
156
+ }
157
+ } ) ;
133
158
} else {
134
159
console . error ( error ) ;
135
160
}
@@ -168,17 +193,17 @@ const createQueryString = (
168
193
169
194
const createApiSig = ( parameters : LastFmSongData , secret : string ) => {
170
195
// This function creates the api signature, see: https://www.last.fm/api/authspec
171
- const keys = Object . keys ( parameters ) ;
172
-
173
- keys . sort ( ) ;
174
196
let sig = '' ;
175
- for ( const key of keys ) {
176
- if ( key === 'format' ) {
177
- continue ;
178
- }
179
197
180
- sig += `${ key } ${ parameters [ key as keyof LastFmSongData ] } ` ;
181
- }
198
+ Object
199
+ . entries ( parameters )
200
+ . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
201
+ . forEach ( ( [ key , value ] ) => {
202
+ if ( key === 'format' ) {
203
+ return ;
204
+ }
205
+ sig += key + value ;
206
+ } ) ;
182
207
183
208
sig += secret ;
184
209
sig = crypto . createHash ( 'md5' ) . update ( sig , 'utf-8' ) . digest ( 'hex' ) ;
@@ -195,7 +220,11 @@ const createToken = async ({
195
220
}
196
221
} : ScrobblerPluginConfig ) => {
197
222
// Creates and stores the auth token
198
- const data = {
223
+ const data : {
224
+ method : string ;
225
+ api_key : string ;
226
+ format : string ;
227
+ } = {
199
228
method : 'auth.gettoken' ,
200
229
api_key : apiKey ,
201
230
format : 'json' ,
@@ -208,9 +237,68 @@ const createToken = async ({
208
237
return json ?. token ;
209
238
} ;
210
239
211
- const authenticate = async ( config : ScrobblerPluginConfig ) => {
212
- // Asks the user for authentication
213
- await shell . openExternal (
214
- `https://www.last.fm/api/auth/?api_key=${ config . scrobblers . lastfm . apiKey } &token=${ config . scrobblers . lastfm . token } ` ,
215
- ) ;
240
+ let authWindowOpened = false ;
241
+ let latestAuthResult = false ;
242
+
243
+ const authenticate = async ( config : ScrobblerPluginConfig , mainWindow : BrowserWindow ) => {
244
+ return new Promise < boolean > ( ( resolve ) => {
245
+ if ( ! authWindowOpened ) {
246
+ authWindowOpened = true ;
247
+ const url = `https://www.last.fm/api/auth/?api_key=${ config . scrobblers . lastfm . apiKey } &token=${ config . scrobblers . lastfm . token } ` ;
248
+ const browserWindow = new BrowserWindow ( {
249
+ width : 500 ,
250
+ height : 600 ,
251
+ show : false ,
252
+ webPreferences : {
253
+ nodeIntegration : false ,
254
+ } ,
255
+ autoHideMenuBar : true ,
256
+ parent : mainWindow ,
257
+ minimizable : false ,
258
+ maximizable : false ,
259
+ paintWhenInitiallyHidden : true ,
260
+ modal : true ,
261
+ center : true ,
262
+ } ) ;
263
+ browserWindow . loadURL ( url ) . then ( ( ) => {
264
+ browserWindow . show ( ) ;
265
+ browserWindow . webContents . on ( 'did-navigate' , async ( _ , newUrl ) => {
266
+ const url = new URL ( newUrl ) ;
267
+ if ( url . hostname . endsWith ( 'last.fm' ) ) {
268
+ if ( url . pathname === '/api/auth' ) {
269
+ const isApproveScreen = await browserWindow . webContents . executeJavaScript (
270
+ '!!document.getElementsByName(\'confirm\').length'
271
+ ) as boolean ;
272
+ // successful authentication
273
+ if ( ! isApproveScreen ) {
274
+ resolve ( true ) ;
275
+ latestAuthResult = true ;
276
+ browserWindow . close ( ) ;
277
+ }
278
+ } else if ( url . pathname === '/api/None' ) {
279
+ resolve ( false ) ;
280
+ latestAuthResult = false ;
281
+ browserWindow . close ( ) ;
282
+ }
283
+ }
284
+ } ) ;
285
+ browserWindow . on ( 'closed' , ( ) => {
286
+ if ( ! latestAuthResult ) {
287
+ dialog . showMessageBox ( {
288
+ title : t ( 'plugins.scrobbler.dialog.lastfm.auth-failed.title' ) ,
289
+ message : t ( 'plugins.scrobbler.dialog.lastfm.auth-failed.message' ) ,
290
+ type : 'error'
291
+ } ) ;
292
+ }
293
+ authWindowOpened = false ;
294
+ } ) ;
295
+ } ) ;
296
+ } else {
297
+ // wait for the previous window to close
298
+ while ( authWindowOpened ) {
299
+ // wait
300
+ }
301
+ resolve ( latestAuthResult ) ;
302
+ }
303
+ } ) ;
216
304
} ;
0 commit comments