11import crypto from 'node:crypto' ;
22
3- import { net , shell } from 'electron' ;
3+ import { BrowserWindow , dialog , net } from 'electron' ;
44
55import { ScrobblerBase } from './base' ;
66
7- import { ScrobblerPluginConfig } from '../index' ;
8- import { SetConfType } from '../main' ;
7+ import { t } from '@/i18n' ;
98
9+ import type { ScrobblerPluginConfig } from '../index' ;
10+ import type { SetConfType } from '../main' ;
1011import type { SongInfo } from '@/providers/song-info' ;
1112
1213interface LastFmData {
@@ -28,11 +29,22 @@ interface LastFmSongData {
2829}
2930
3031export 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 {
3241 return ! ! config . scrobblers . lastfm . sessionKey ;
3342 }
3443
35- async createSession ( config : ScrobblerPluginConfig , setConfig : SetConfType ) : Promise < ScrobblerPluginConfig > {
44+ override async createSession (
45+ config : ScrobblerPluginConfig ,
46+ setConfig : SetConfType ,
47+ ) : Promise < ScrobblerPluginConfig > {
3648 // Get and store the session key
3749 const data = {
3850 api_key : config . scrobblers . lastfm . apiKey ,
@@ -52,8 +64,15 @@ export class LastFmScrobbler extends ScrobblerBase {
5264 } ;
5365 if ( json . error ) {
5466 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+ } ) ;
5776 }
5877 if ( json . session ) {
5978 config . scrobblers . lastfm . sessionKey = json . session . key ;
@@ -62,7 +81,7 @@ export class LastFmScrobbler extends ScrobblerBase {
6281 return config ;
6382 }
6483
65- setNowPlaying ( songInfo : SongInfo , config : ScrobblerPluginConfig , setConfig : SetConfType ) : void {
84+ override setNowPlaying ( songInfo : SongInfo , config : ScrobblerPluginConfig , setConfig : SetConfType ) : void {
6685 if ( ! config . scrobblers . lastfm . sessionKey ) {
6786 return ;
6887 }
@@ -74,7 +93,7 @@ export class LastFmScrobbler extends ScrobblerBase {
7493 this . postSongDataToAPI ( songInfo , config , data , setConfig ) ;
7594 }
7695
77- addScrobble ( songInfo : SongInfo , config : ScrobblerPluginConfig , setConfig : SetConfType ) : void {
96+ override addScrobble ( songInfo : SongInfo , config : ScrobblerPluginConfig , setConfig : SetConfType ) : void {
7897 if ( ! config . scrobblers . lastfm . sessionKey ) {
7998 return ;
8099 }
@@ -87,7 +106,7 @@ export class LastFmScrobbler extends ScrobblerBase {
87106 this . postSongDataToAPI ( songInfo , config , data , setConfig ) ;
88107 }
89108
90- async postSongDataToAPI (
109+ private async postSongDataToAPI (
91110 songInfo : SongInfo ,
92111 config : ScrobblerPluginConfig ,
93112 data : LastFmData ,
@@ -128,8 +147,14 @@ export class LastFmScrobbler extends ScrobblerBase {
128147 // Session key is invalid, so remove it from the config and reauthenticate
129148 config . scrobblers . lastfm . sessionKey = undefined ;
130149 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+ } ) ;
133158 } else {
134159 console . error ( error ) ;
135160 }
@@ -168,17 +193,17 @@ const createQueryString = (
168193
169194const createApiSig = ( parameters : LastFmSongData , secret : string ) => {
170195 // This function creates the api signature, see: https://www.last.fm/api/authspec
171- const keys = Object . keys ( parameters ) ;
172-
173- keys . sort ( ) ;
174196 let sig = '' ;
175- for ( const key of keys ) {
176- if ( key === 'format' ) {
177- continue ;
178- }
179197
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+ } ) ;
182207
183208 sig += secret ;
184209 sig = crypto . createHash ( 'md5' ) . update ( sig , 'utf-8' ) . digest ( 'hex' ) ;
@@ -195,7 +220,11 @@ const createToken = async ({
195220 }
196221} : ScrobblerPluginConfig ) => {
197222 // Creates and stores the auth token
198- const data = {
223+ const data : {
224+ method : string ;
225+ api_key : string ;
226+ format : string ;
227+ } = {
199228 method : 'auth.gettoken' ,
200229 api_key : apiKey ,
201230 format : 'json' ,
@@ -208,9 +237,68 @@ const createToken = async ({
208237 return json ?. token ;
209238} ;
210239
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+ } ) ;
216304} ;
0 commit comments