@@ -4,17 +4,8 @@ const crypto = require('crypto');
4
4
const https = require ( 'https' ) ;
5
5
const conf = vscode . workspace . getConfiguration ( "dzr" ) ;
6
6
const location = vscode . ProgressLocation . Notification ;
7
- const type2icon = {
8
- track : '$(play-circle) ' ,
9
- artist : '$(person) ' ,
10
- album : '$(issues) ' ,
11
- playlist : '$(list-unordered)' ,
12
- radio : '$(broadcast) ' ,
13
- genre : '$(telescope) ' ,
14
- user : '$(account) ' ,
15
- } ;
16
7
const hhmmss = ( s ) => ( new Date ( s * 1000 ) ) . toISOString ( ) . slice ( 11 , 19 ) . replace ( / ^ 0 0 : / , '' ) ;
17
- const wait = ( ms = 1000 ) => new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
8
+ const wait = ( ms = 1000 ) => new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
18
9
// still no fetch() in 2023 ?
19
10
const fetch = ( url , opt , data ) => new Promise ( ( resolve , reject ) => {
20
11
const chunks = [ ] , req = https . request ( url , opt , res => {
@@ -29,56 +20,28 @@ const fetch = (url, opt, data) => new Promise((resolve, reject) => {
29
20
// - not restful, so we can't infer it structure
30
21
// - /track/:id gives contributors but /search/track?q= don't
31
22
// - inconsistent listing structure (/playlist/:id => tracks.data, sometimes=>data, sometimes data.tracks)
32
- const menus = {
33
- _ : [
34
- { path : 'search/track?q=' , label : '$(play-circle) track search' } ,
35
- { path : 'search/artist?q=' , label : '$(person) artist search' } ,
36
- { path : 'search/album?q=' , label : '$(issues) album search' } ,
37
- { path : 'search/playlist?q=' , label : '$(list-unordered) playlist search' } ,
38
- { path : 'search/user?q=' , label : '$(account) user search' } ,
39
- { path : 'search/radio?q=' , label : '$(broadcast) radio search' } ,
40
- { path : 'genre' , label : '$(telescope) explore' } ,
41
- { path : 'radio' , label : '$(broadcast) radios list' } ,
42
- { path : 'user/0' , label : '$(account) user id' } ,
43
- ] ,
44
- _artist_0 : [
45
- { path : '/top?limit=50' , label : '$(play-circle) Top Tracks' } ,
46
- { path : '/albums' , label : '$(issues) Albums' } ,
47
- { path : '/related' , label : '$(person) Similar Artists' } ,
48
- { path : '/radio' , label : '$(broadcast) Flow' } ,
49
- { path : '/playlists' , label : '$(list-unordered) Playlists' }
50
- ] ,
51
- _user_0 : [
52
- { path : '/playlists' , label : '$(list-unordered) Playlists' } ,
53
- { path : '/tracks' , label : '$(play-circle) Favorite Tracks' } ,
54
- { path : '/albums' , label : '$(issues) Favorite Albums' } ,
55
- { path : '/artists' , label : '$(person) Favorite Artists' } ,
56
- { path : '/flow' , label : '$(broadcast) Flow' } ,
57
- { path : '/charts' , label : '$(play-circle) Charts' } ,
58
- ] ,
59
- _genre_0 : [ { label : '/radios' } , { label : '/artists' } ] ,
60
- _radio_0 : [ { label : '/tracks' } ] ,
61
- _album_0 : [ { label : '/tracks' } ] ,
62
- }
63
- // browse can be : user query / list(static) / list(fetch)
23
+ // browse can be called from: user query / self list(from static menu) / self list(from fetch result)
64
24
async function browse ( url_or_event , label ) {
25
+ console . log ( url_or_event ) ;
65
26
try {
66
27
const url = typeof ( url_or_event ) == "string" ? url_or_event : '/' ;
67
28
const id = url . replace ( / \d + / g, '0' ) . replace ( / [ ^ \w ] / g, '_' ) ;
29
+ const menus = conf . get ( 'menus' ) ;
68
30
if ( url . endsWith ( '=' ) || url . endsWith ( '/0' ) ) { // query step
69
31
const input = await vscode . window . showInputBox ( { title : label } ) ;
70
32
if ( ! input ) return ;
71
33
return await browse ( url . replace ( / 0 $ / , '' ) + input , label ) ;
72
34
} else if ( menus [ id ] ) { // menu step
73
35
const pick = menus [ id ] . length > 1 ? await vscode . window . showQuickPick ( menus [ id ] , { title : label || url } ) : menus [ id ] [ 0 ] ;
74
36
if ( ! pick ) return ;
75
- return await browse ( url + ( pick . path || pick . label ) , pick . label ) ;
37
+ return await browse ( url + pick . path , pick . label ) ;
76
38
} else { // fetch step
77
39
const json = JSON . parse ( await fetch ( "https://api.deezer.com" + url ) ) ; // todo: json.next?
78
40
console . debug ( json ) ;
79
41
const data = json . data ?. tracks || json . data || json . tracks ?. data ;
80
42
const picked = url . match ( / \/ ( p l a y l i s t | a l b u m ) \/ / ) ;
81
43
const canPickMany = data . find ( item => item . type == "track" ) ;
44
+ const type2icon = conf . get ( 'type2icon' ) ;
82
45
const choices = data . map ( entry => ( {
83
46
...entry , picked,
84
47
label : ( type2icon [ entry . type ] || '' ) + ( entry . title_short || entry . name || entry . title ) ,
@@ -113,7 +76,7 @@ const with_url = async (songs) => songs?.length ? await vscode.window.withProgre
113
76
id, md5_image, duration,
114
77
title : title_short ?. replace ( / ? \( f e a t .* ?\) / , '' ) || title ,
115
78
version : title_version || version ,
116
- artists : artists ?? ( contributors || [ artist ] ) ?. map ( ( { id, name } ) => ( { id, name } ) ) ,
79
+ artists : artists ?? ( contributors || [ artist ] ) ?. map ( ( { id, name } ) => ( { id, name } ) ) ,
117
80
size : + SNG_NFO . data [ i ] . FILESIZE ,
118
81
expire : SNG_NFO . data [ i ] . TRACK_TOKEN_EXPIRE ,
119
82
url : URL_NFO . data [ i ] . media ?. [ 0 ] ?. sources ?. [ 0 ] ?. url
@@ -122,7 +85,7 @@ const with_url = async (songs) => songs?.length ? await vscode.window.withProgre
122
85
} ) : [ ] ;
123
86
124
87
class DzrWebView { // can't Audio() in VSCode, we need a webview
125
- statuses = [ 'dzr.play' , 'dzr.show' , 'dzr.next ' ] . map ( ( command ) => {
88
+ statuses = [ 'dzr.play' , 'dzr.show' , 'dzr.load ' ] . map ( ( command ) => {
126
89
const item = vscode . window . createStatusBarItem ( command , vscode . StatusBarAlignment . Left , 10000 ) ;
127
90
item . color = new vscode . ThemeColor ( 'statusBarItem.prominentBackground' ) ;
128
91
item . backgroundColor = new vscode . ThemeColor ( 'statusBarItem.errorBackground' ) ;
@@ -132,7 +95,7 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
132
95
return item ;
133
96
} ) ;
134
97
panel = null ;
135
- #state = { } ;
98
+ #state = { } ;
136
99
state = new Proxy ( this . #state, {
137
100
set : ( target , key , value ) => {
138
101
target [ key ] = value ;
@@ -149,31 +112,26 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
149
112
150
113
constructor ( ) {
151
114
this . initAckSemaphore ( ) ;
152
- this . state . index = - 1 ;
153
- this . state . playing = false ;
154
- this . state . ready = false ;
155
- this . state . current = null ;
156
- this . state . looping = conf . get ( 'loop' ) ;
157
- this . state . queue = conf . get ( 'queue' ) ;
115
+ this . state . queue = conf . get ( 'queue' ) ; // first is best
116
+ this . state . looping = conf . get ( 'looping' ) ;
158
117
}
159
-
160
118
renderStatus ( ) {
161
- const label = this . state . current ? `${ this . state . current . title } - ${ item . artists ?. map ( a => a . name ) . join ( ) } ` : '' ;
119
+ const index = this . state . queue ?. indexOf ( this . state . current ) ;
120
+ const label = this . state . current ? `${ this . state . current . title } - ${ this . state . current . artists ?. map ( a => a . name ) . join ( ) } ` : '' ;
162
121
this . statuses [ 0 ] . command = this . state . playing ? 'dzr.pause' : 'dzr.play' ;
163
122
this . statuses [ 0 ] . text = this . state . ready && ( this . state . playing ? "$(debug-pause)" : "$(play)" ) ;
164
123
this . statuses [ 1 ] . tooltip = this . state . ready ? label : "Initiate interaction first" ;
165
124
this . statuses [ 1 ] . text = this . state . ready ? label . length < 20 ? label : ( label . slice ( 0 , 20 ) + '…' ) : "$(play)"
166
- this . statuses [ 2 ] . text = this . state . ready && this . state . queue . length ? `${ this . state . index + 1 } /${ this . state . queue . length } $(chevron-right)` : null ; //debug-step-over
167
- this . treeView . description = this . state . queue ?. length ? `${ this . state . index + 1 } /${ this . state . queue . length } ` : 'empty' ;
168
- this . treeView . message = this . state . queue ?. length ? null : "Empty Queue. Add tracks using the + button " ;
125
+ this . statuses [ 2 ] . text = this . state . ready && this . state . queue . length ? `${ index + 1 || '?' } /${ this . state . queue . length } $(chevron-right)` : null ; //debug-step-over
126
+ this . treeView . description = ( this . state . queue ?. length ? `${ index + 1 || '?' } /${ this . state . queue . length } ` : '' ) + ` loop: ${ this . state . looping } ` ;
127
+ this . treeView . message = this . state . queue ?. length ? null : "Empty Queue. Add tracks to queue using '+' " ;
169
128
}
170
129
async show ( htmlUri ) {
171
130
if ( this . panel ) return this . panel . reveal ( vscode . ViewColumn . One ) ;
172
131
this . panel = vscode . window . createWebviewPanel ( 'dzr.player' , 'Player' , vscode . ViewColumn . One , {
173
132
enableScripts : true ,
174
133
enableCommandUris : true ,
175
- enableFindWidget : true ,
176
- retainContextWhenHidden : true
134
+ retainContextWhenHidden : true ,
177
135
} ) ;
178
136
this . panel . webview . html = ( await vscode . workspace . fs . readFile ( htmlUri ) ) . toString ( ) ;
179
137
this . panel . webview . onDidReceiveMessage ( ( action , ...args ) => this [ action ] ? this [ action ] ( ...args ) : this . badAction ( action ) ) ;
@@ -189,8 +147,9 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
189
147
}
190
148
player_playing ( ) { this . state . ready = this . state . playing = true ; }
191
149
player_pause ( ) { this . state . playing = false ; }
192
- player_ended ( ) { vscode . commands . executeCommand ( 'dzr.next' ) ; }
150
+ player_ended ( ) { vscode . commands . executeCommand ( 'dzr.load' , null ) ; }
193
151
user_interact ( ) { this . state . ready = true ; }
152
+ user_next ( ) { vscode . commands . executeCommand ( 'dzr.load' ) ; }
194
153
error ( msg ) { vscode . window . showErrorMessage ( msg ) ; }
195
154
badAction ( action ) { console . error ( `unHandled action "${ action } " from webview` ) ; }
196
155
// tree
@@ -206,7 +165,7 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
206
165
label : item . title ,
207
166
description : item . artists . map ( a => a . name ) . join ( ) ,
208
167
contextValue : 'dzr.track' ,
209
- command : { title : 'Play' , command : 'dzr.next ' , tooltip : 'Play' , arguments : [ this . state . queue . indexOf ( item ) ] } ,
168
+ command : { title : 'Play' , command : 'dzr.load ' , tooltip : 'Play' , arguments : [ this . state . queue . indexOf ( item ) ] } ,
210
169
tooltip : hhmmss ( item . duration ) //JSON.stringify(item, null, 2),
211
170
} )
212
171
getChildren = ( ) => this . state . queue
@@ -241,9 +200,10 @@ exports.activate = async function (/**@type {vscode.ExtensionContext}*/ context)
241
200
vscode . commands . registerCommand ( 'dzr.show' , ( ) => dzr . show ( htmlUri ) ) ,
242
201
vscode . commands . registerCommand ( "dzr.play" , ( ) => dzr . post ( 'play' ) ) ,
243
202
vscode . commands . registerCommand ( "dzr.pause" , ( ) => dzr . post ( 'pause' ) ) ,
244
- vscode . commands . registerCommand ( "dzr.loopAll" , ( ) => dzr . looping = true ) ,
245
- vscode . commands . registerCommand ( "dzr.loopOff" , ( ) => dzr . looping = false ) ,
246
- vscode . commands . registerCommand ( "dzr.add" , async ( path ) => dzr . state . queue = [ ...dzr . state . queue , ...await with_url ( await browse ( path ) ) || [ ] ] ) ,
203
+ vscode . commands . registerCommand ( "dzr.loopQueue" , ( ) => dzr . state . looping = "queue" ) ,
204
+ vscode . commands . registerCommand ( "dzr.loopTrack" , ( ) => dzr . state . looping = "track" ) ,
205
+ vscode . commands . registerCommand ( "dzr.loopOff" , ( ) => dzr . state . looping = "off" ) ,
206
+ vscode . commands . registerCommand ( "dzr.add" , async ( path , label ) => dzr . state . queue = [ ...dzr . state . queue , ...await with_url ( await browse ( path , label ) ) || [ ] ] ) ,
247
207
vscode . commands . registerCommand ( "dzr.remove" , async ( item , items ) => ( items || [ item ] ) . map ( i => vscode . commands . executeCommand ( 'dzr.removeAt' , dzr . state . queue . indexOf ( i ) ) ) ) ,
248
208
vscode . commands . registerCommand ( "dzr.removeAt" , async ( index ) => index >= 0 && ( dzr . state . queue = [ ...dzr . state . queue . slice ( 0 , index ) , ...dzr . state . queue . slice ( index + 1 ) ] ) ) ,
249
209
vscode . commands . registerCommand ( "dzr.clear" , async ( ) => dzr . state . queue = [ ] ) ,
@@ -255,26 +215,27 @@ exports.activate = async function (/**@type {vscode.ExtensionContext}*/ context)
255
215
}
256
216
dzr . state . queue = shuffle ;
257
217
} ) ,
258
- vscode . commands . registerCommand ( "dzr.next " , async ( pos = dzr . state . index + 1 ) => {
259
- dzr . state . index = ( pos >= dzr . state . queue . length ) ? 0 : pos ;
260
- const item = dzr . state . queue [ dzr . state . index ] ;
261
- item && vscode . commands . executeCommand ( ' dzr.load' , item ) ;
262
- } ) ,
263
- vscode . commands . registerCommand ( "dzr.load" , async ( item ) => {
218
+ vscode . commands . registerCommand ( "dzr.load " , async ( pos ) => { //pos=null if player_end / pos=undefine if user click
219
+ pos = pos ?? dzr . state . queue . indexOf ( dzr . state . current ) + ( dzr . state . looping == 'track' ? 0 : 1 ) ;
220
+ if ( ! dzr . state . queue [ pos ] ) { // out of bound track
221
+ if ( dzr . state . looping == 'off' ) return ; // don't loop if unwanted
222
+ pos = 0 ; // loop position if looping
223
+ }
264
224
if ( ! dzr . state . ready ) {
265
225
vscode . commands . executeCommand ( 'dzr.show' ) ;
266
226
while ( ! dzr . state . ready ) await wait ( ) ;
267
227
}
268
- if ( item . expire < ( new Date ( ) / 1000 ) ) {
269
- with_url ( dzr . state . queue ) ; //TODO: hope item is now up to date
228
+ dzr . state . current = dzr . state . queue [ pos ] ;
229
+ if ( dzr . state . current . expire < ( new Date ( ) / 1000 ) ) {
230
+ dzr . state . queue = await with_url ( dzr . state . queue ) ; //TODO: hope item is now up to date
270
231
}
271
232
const hex = ( str ) => str . split ( '' ) . map ( c => c . charCodeAt ( 0 ) )
272
- const md5 = hex ( crypto . createHash ( 'md5' ) . update ( `${ item . id } ` ) . digest ( 'hex' ) ) ;
233
+ const md5 = hex ( crypto . createHash ( 'md5' ) . update ( `${ dzr . state . current . id } ` ) . digest ( 'hex' ) ) ;
273
234
const key = Buffer . from ( hex ( conf . get ( 'cbc' ) ) . map ( ( c , i ) => c ^ md5 [ i ] ^ md5 [ i + 16 ] ) ) ;
274
235
const iv = Buffer . from ( [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ] ) ;
275
236
const stripe = 2048 ; //TODO:use .pipe() API https://codereview.stackexchange.com/questions/57492/
276
- dzr . post ( 'open' , item ) ;
277
- const buf_enc = await fetch ( item . url ) ;
237
+ dzr . post ( 'open' , dzr . state . current ) ;
238
+ const buf_enc = await fetch ( dzr . state . current . url ) ;
278
239
for ( let pos = 0 ; pos < buf_enc . length ; pos += stripe ) {
279
240
if ( ( pos >> 11 ) % 3 ) continue ;
280
241
const ciph = crypto . createDecipheriv ( 'bf-cbc' , key , iv ) . setAutoPadding ( false )
0 commit comments