Skip to content

Commit 6b0d49c

Browse files
committed
Add MediaHandler
1 parent a3f8679 commit 6b0d49c

File tree

3 files changed

+232
-100
lines changed

3 files changed

+232
-100
lines changed

extension/main.js

Lines changed: 36 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,8 @@ const crypto = require('crypto');
44
const https = require('https');
55
const conf = vscode.workspace.getConfiguration("dzr");
66
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-
};
167
const hhmmss = (s) => (new Date(s * 1000)).toISOString().slice(11, 19).replace(/^00:/, '');
17-
const wait = (ms=1000) => new Promise(resolve => setTimeout(resolve, ms));
8+
const wait = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms));
189
// still no fetch() in 2023 ?
1910
const fetch = (url, opt, data) => new Promise((resolve, reject) => {
2011
const chunks = [], req = https.request(url, opt, res => {
@@ -29,56 +20,28 @@ const fetch = (url, opt, data) => new Promise((resolve, reject) => {
2920
// - not restful, so we can't infer it structure
3021
// - /track/:id gives contributors but /search/track?q= don't
3122
// - 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)
6424
async function browse(url_or_event, label) {
25+
console.log(url_or_event);
6526
try {
6627
const url = typeof (url_or_event) == "string" ? url_or_event : '/';
6728
const id = url.replace(/\d+/g, '0').replace(/[^\w]/g, '_');
29+
const menus = conf.get('menus');
6830
if (url.endsWith('=') || url.endsWith('/0')) { // query step
6931
const input = await vscode.window.showInputBox({ title: label });
7032
if (!input) return;
7133
return await browse(url.replace(/0$/, '') + input, label);
7234
} else if (menus[id]) { // menu step
7335
const pick = menus[id].length > 1 ? await vscode.window.showQuickPick(menus[id], { title: label || url }) : menus[id][0];
7436
if (!pick) return;
75-
return await browse(url + (pick.path || pick.label), pick.label);
37+
return await browse(url + pick.path, pick.label);
7638
} else { // fetch step
7739
const json = JSON.parse(await fetch("https://api.deezer.com" + url)); // todo: json.next?
7840
console.debug(json);
7941
const data = json.data?.tracks || json.data || json.tracks?.data;
8042
const picked = url.match(/\/(playlist|album)\//);
8143
const canPickMany = data.find(item => item.type == "track");
44+
const type2icon = conf.get('type2icon');
8245
const choices = data.map(entry => ({
8346
...entry, picked,
8447
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
11376
id, md5_image, duration,
11477
title: title_short?.replace(/ ?\(feat.*?\)/, '') || title,
11578
version: title_version || version,
116-
artists: artists??(contributors || [artist])?.map(({ id, name }) => ({ id, name })),
79+
artists: artists ?? (contributors || [artist])?.map(({ id, name }) => ({ id, name })),
11780
size: +SNG_NFO.data[i].FILESIZE,
11881
expire: SNG_NFO.data[i].TRACK_TOKEN_EXPIRE,
11982
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
12285
}) : [];
12386

12487
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) => {
12689
const item = vscode.window.createStatusBarItem(command, vscode.StatusBarAlignment.Left, 10000);
12790
item.color = new vscode.ThemeColor('statusBarItem.prominentBackground');
12891
item.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
@@ -132,7 +95,7 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
13295
return item;
13396
});
13497
panel = null;
135-
#state = { };
98+
#state = {};
13699
state = new Proxy(this.#state, {
137100
set: (target, key, value) => {
138101
target[key] = value;
@@ -149,31 +112,26 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
149112

150113
constructor() {
151114
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');
158117
}
159-
160118
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()}` : '';
162121
this.statuses[0].command = this.state.playing ? 'dzr.pause' : 'dzr.play';
163122
this.statuses[0].text = this.state.ready && (this.state.playing ? "$(debug-pause)" : "$(play)");
164123
this.statuses[1].tooltip = this.state.ready ? label : "Initiate interaction first";
165124
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 '+'";
169128
}
170129
async show(htmlUri) {
171130
if (this.panel) return this.panel.reveal(vscode.ViewColumn.One);
172131
this.panel = vscode.window.createWebviewPanel('dzr.player', 'Player', vscode.ViewColumn.One, {
173132
enableScripts: true,
174133
enableCommandUris: true,
175-
enableFindWidget: true,
176-
retainContextWhenHidden: true
134+
retainContextWhenHidden: true,
177135
});
178136
this.panel.webview.html = (await vscode.workspace.fs.readFile(htmlUri)).toString();
179137
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
189147
}
190148
player_playing() { this.state.ready = this.state.playing = true; }
191149
player_pause() { this.state.playing = false; }
192-
player_ended() { vscode.commands.executeCommand('dzr.next'); }
150+
player_ended() { vscode.commands.executeCommand('dzr.load', null); }
193151
user_interact() { this.state.ready = true; }
152+
user_next() { vscode.commands.executeCommand('dzr.load'); }
194153
error(msg) { vscode.window.showErrorMessage(msg); }
195154
badAction(action) { console.error(`unHandled action "${action}" from webview`); }
196155
// tree
@@ -206,7 +165,7 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
206165
label: item.title,
207166
description: item.artists.map(a => a.name).join(),
208167
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)] },
210169
tooltip: hhmmss(item.duration)//JSON.stringify(item, null, 2),
211170
})
212171
getChildren = () => this.state.queue
@@ -241,9 +200,10 @@ exports.activate = async function (/**@type {vscode.ExtensionContext}*/ context)
241200
vscode.commands.registerCommand('dzr.show', () => dzr.show(htmlUri)),
242201
vscode.commands.registerCommand("dzr.play", () => dzr.post('play')),
243202
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)) || []]),
247207
vscode.commands.registerCommand("dzr.remove", async (item, items) => (items || [item]).map(i => vscode.commands.executeCommand('dzr.removeAt', dzr.state.queue.indexOf(i)))),
248208
vscode.commands.registerCommand("dzr.removeAt", async (index) => index >= 0 && (dzr.state.queue = [...dzr.state.queue.slice(0, index), ...dzr.state.queue.slice(index + 1)])),
249209
vscode.commands.registerCommand("dzr.clear", async () => dzr.state.queue = []),
@@ -255,26 +215,27 @@ exports.activate = async function (/**@type {vscode.ExtensionContext}*/ context)
255215
}
256216
dzr.state.queue = shuffle;
257217
}),
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+
}
264224
if (!dzr.state.ready) {
265225
vscode.commands.executeCommand('dzr.show');
266226
while (!dzr.state.ready) await wait();
267227
}
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
270231
}
271232
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'));
273234
const key = Buffer.from(hex(conf.get('cbc')).map((c, i) => c ^ md5[i] ^ md5[i + 16]));
274235
const iv = Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]);
275236
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);
278239
for (let pos = 0; pos < buf_enc.length; pos += stripe) {
279240
if ((pos >> 11) % 3) continue;
280241
const ciph = crypto.createDecipheriv('bf-cbc', key, iv).setAutoPadding(false)

0 commit comments

Comments
 (0)