Skip to content

Commit 104df71

Browse files
committed
Handle vscode://yne.dzr/ + display artist picture
The following url shall now be handled by dzr You can triger them from `xdg-open my-url` vscode://yne.dzr/pause vscode://yne.dzr/play vscode://yne.dzr/load # load next track vscode://yne.dzr/load?[0] # load track at [0] vscode://yne.dzr/add?[[76533108,76533118]] # add 2 track by id (used by "copy link" menu)
1 parent 977e1a6 commit 104df71

File tree

3 files changed

+55
-31
lines changed

3 files changed

+55
-31
lines changed

extension/main.js

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**@type import('vscode') */
1+
/**@type import('vscode') */ // soon: https://devblogs.microsoft.com/typescript/announcing-typescript-5-5-rc/#type-imports-in-jsdoc
22
const vscode = require("vscode");
33
const crypto = require('crypto');
44
const https = require('https');
@@ -21,19 +21,21 @@ const fetch = (url, opt, data) => new Promise((resolve, reject) => {
2121
// - /track/:id gives contributors but /search/track?q= don't
2222
// - inconsistent listing structure (/playlist/:id => tracks.data, sometimes=>data, sometimes data.tracks)
2323
// browse can be called from: user query / self list(from static menu) / self list(from fetch result)
24-
async function browse(url_or_event, label) {
25-
console.log(url_or_event);
24+
async function browse(url_or_event_or_ids, label) {
25+
console.log(url_or_event_or_ids);
2626
try {
27-
const url = typeof (url_or_event) == "string" ? url_or_event : '/';
27+
if (Array.isArray(url_or_event_or_ids)) return url_or_event_or_ids.map(id => ({ id }));
28+
const ignoreFocusOut = true;
29+
const url = typeof (url_or_event_or_ids) == "string" ? url_or_event_or_ids : '/';
2830
const id = url.replace(/\d+/g, '0').replace(/[^\w]/g, '_');
2931
const menus = conf.get('menus');
3032
const title = (label || '').replace(/\$\(.+?\)/g, '');
3133
if (url.endsWith('=') || url.endsWith('/0')) { // query step
32-
const input = await vscode.window.showInputBox({ title });
34+
const input = await vscode.window.showInputBox({ title, ignoreFocusOut });
3335
if (!input) return;
3436
return await browse(url.replace(/0$/, '') + input, `${label}: ${input}`);
3537
} else if (menus[id]) { // menu step
36-
const pick = menus[id].length > 1 ? await vscode.window.showQuickPick(menus[id], { title: title || url }) : menus[id][0];
38+
const pick = menus[id].length > 1 ? await vscode.window.showQuickPick(menus[id], { title: title || url, ignoreFocusOut }) : menus[id][0];
3739
if (!pick) return;
3840
return await browse(url + pick.path, pick.label);
3941
} else { // fetch step
@@ -49,7 +51,7 @@ async function browse(url_or_event, label) {
4951
description: [entry.artist?.name, entry.title_version, entry.nb_tracks].join(' '),
5052
path: `/${entry.type}/${entry.id}`,
5153
}));
52-
const picks = await vscode.window.showQuickPick(choices, { title: title || url, canPickMany });
54+
const picks = await vscode.window.showQuickPick(choices, { title: title || url, canPickMany, ignoreFocusOut });
5355
if (!picks) return;
5456
return canPickMany ? picks : await browse(picks.path, picks.label);
5557
}
@@ -70,14 +72,17 @@ const with_url = async (songs) => songs?.length ? await vscode.window.withProgre
7072
license_token: USR_NFO.USER.OPTIONS.license_token,
7173
media: [{ type: "FULL", formats: [{ cipher: "BF_CBC_STRIPE", format: "MP3_128" }] }]
7274
}))));
75+
console.log(SNG_NFO)
7376
const errors = URL_NFO.data.map((nfo, i) => [nfo.errors, songs[i]]).filter(([err]) => err).map(([[err], sng]) => `${sng.title}: ${err.message} (${err.code})`).join('\n');
7477
if (errors) setTimeout(() => vscode.window.showWarningMessage(errors), 500); // can't warn while progress ?
7578
return songs.map(({/* api :*/ id, md5_image, duration, title_short, title_version, artist, contributors,
7679
/*cache:*/ title, version, artists }, i) => ({
77-
id, md5_image, duration,
78-
title: title_short?.replace(/ ?\(feat.*?\)/, '') || title,
80+
id,
81+
md5_image: md5_image || SNG_NFO.data[i].ALB_PICTURE,
82+
duration: duration || +SNG_NFO.data[i].DURATION,
83+
title: title || SNG_NFO.data[i].SNG_TITLE.replace(/ ?\(feat.*?\)/, ''),
7984
version: title_version || version,
80-
artists: artists ?? (contributors || [artist])?.map(({ id, name }) => ({ id, name })),
85+
artists: artists || SNG_NFO.data[i].ARTISTS.map(a => ({ id: a.ART_ID, name: a.ART_NAME, md5: a.ART_PICTURE })),
8186
size: +SNG_NFO.data[i].FILESIZE,
8287
expire: SNG_NFO.data[i].TRACK_TOKEN_EXPIRE,
8388
url: URL_NFO.data[i].media?.[0]?.sources?.[0]?.url
@@ -115,6 +120,7 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
115120
this.initAckSemaphore();
116121
this.state.queue = conf.get('queue'); // first is best
117122
this.state.looping = conf.get('looping');
123+
console.log(vscode.ThemeIcon.File)
118124
}
119125
renderStatus() {
120126
const index = this.state.queue?.indexOf(this.state.current);
@@ -159,13 +165,13 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
159165
dragMimeTypes = ['text/uri-list'];
160166
_onDidChangeTreeData = new vscode.EventEmitter();
161167
onDidChangeTreeData = this._onDidChangeTreeData.event;
168+
/**@type {import('vscode').TreeView}*/
162169
treeView = vscode.window.createTreeView('dzr.queue', { treeDataProvider: this, dragAndDropController: this, canSelectMany: true });
163-
164170
/**@returns {vscode.TreeItem} */
165171
getTreeItem = (item) => ({
166-
iconPath: vscode.ThemeIcon.File,
172+
iconPath: new vscode.ThemeIcon("music"),
167173
label: item.title + ' - ' + item.artists.map(a => a.name).join(),
168-
description: hhmmss(item.duration) + " " + (item.version||''),
174+
description: hhmmss(item.duration || 0) + " " + (item.version || ''),
169175
contextValue: 'dzr.track',
170176
command: { title: 'Play', command: 'dzr.load', tooltip: 'Play', arguments: [this.state.queue.indexOf(item)] },
171177
//tooltip: JSON.stringify(item, null, 2),
@@ -182,7 +188,7 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
182188
this.state.queue = [...striped.slice(0, index), ...sources, ...striped.slice(index)];
183189
}
184190
}
185-
exports.activate = async function (/**@type {vscode.ExtensionContext}*/ context) {
191+
exports.activate = async function (/**@type {import('vscode').ExtensionContext}*/ context) {
186192
// deezer didn't DMCA'd dzr so let's follow the same path here
187193
conf.get('cbc') || vscode.window.withProgress({ title: 'Extracting CBC key...', location }, async () => {
188194
const html_url = 'https://www.deezer.com/en/channels/explore';
@@ -199,18 +205,25 @@ exports.activate = async function (/**@type {vscode.ExtensionContext}*/ context)
199205
const dzr = new DzrWebView();
200206
const htmlUri = vscode.Uri.joinPath(context.extensionUri, 'webview.html');
201207
const iconUri = vscode.Uri.joinPath(context.extensionUri, 'logo.svg'); //same for light+dark
208+
202209
context.subscriptions.push(...dzr.statuses, dzr.treeView,
210+
// catch vscode://yne.dzr/* urls
211+
vscode.window.registerUriHandler({ handleUri(uri) { (({ path, query }) => vscode.commands.executeCommand(`dzr.${path.slice(1)}`, ...(query ? JSON.parse(query) : [])))(vscode.Uri.parse(uri)); } }),
203212
vscode.commands.registerCommand('dzr.show', () => dzr.show(htmlUri, iconUri)),
204213
vscode.commands.registerCommand("dzr.play", () => dzr.post('play')),
205214
vscode.commands.registerCommand("dzr.pause", () => dzr.post('pause')),
206215
vscode.commands.registerCommand("dzr.href", (track) => vscode.env.openExternal(vscode.Uri.parse(`https://deezer.com/track/${track.id}`))),
207216
vscode.commands.registerCommand("dzr.loopQueue", () => dzr.state.looping = "queue"),
208217
vscode.commands.registerCommand("dzr.loopTrack", () => dzr.state.looping = "track"),
209218
vscode.commands.registerCommand("dzr.loopOff", () => dzr.state.looping = "off"),
210-
vscode.commands.registerCommand("dzr.add", async (path, label) => dzr.state.queue = [...dzr.state.queue, ...await with_url(await browse(path, label)) || []]),
219+
vscode.commands.registerCommand("dzr.add", async (path, label) => with_url(await browse(path, label)).then(tracks => dzr.state.queue = [...dzr.state.queue, ...tracks])),
211220
vscode.commands.registerCommand("dzr.remove", async (item, items) => (items || [item]).map(i => vscode.commands.executeCommand('dzr.removeAt', dzr.state.queue.indexOf(i)))),
212221
vscode.commands.registerCommand("dzr.removeAt", async (index) => index >= 0 && (dzr.state.queue = [...dzr.state.queue.slice(0, index), ...dzr.state.queue.slice(index + 1)])),
213222
vscode.commands.registerCommand("dzr.clear", async () => dzr.state.queue = []),
223+
vscode.commands.registerCommand("dzr.share", async (track, tracks) => {
224+
const ids = JSON.stringify(track ? [(tracks || [track]).map(e => e.id || track.id)] : [dzr.state.queue.map(q => q.id)]);
225+
vscode.env.clipboard.writeText(new vscode.Uri("vscode", context.extension.id, '/add', ids).toString())
226+
}),
214227
vscode.commands.registerCommand("dzr.shuffle", async () => {
215228
const shuffle = [...dzr.state.queue];
216229
for (let i = shuffle.length - 1; i > 0; i--) {
@@ -230,7 +243,7 @@ exports.activate = async function (/**@type {vscode.ExtensionContext}*/ context)
230243
while (!dzr.state.ready) await wait();
231244
}
232245
dzr.state.current = dzr.state.queue[pos];
233-
if (dzr.state.current.expire < (new Date() / 1000)) {
246+
if ((dzr.state.current.expire || 0) < (new Date() / 1000)) {
234247
dzr.state.queue = await with_url(dzr.state.queue);//TODO: hope item is now up to date
235248
}
236249
const hex = (str) => str.split('').map(c => c.charCodeAt(0))

extension/package.json

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,41 +96,45 @@
9696
"when": "false",
9797
"icon": "$(close)"
9898
},
99-
{
100-
"category": "dzr",
101-
"command": "dzr.removeAt",
102-
"title": "Queue Remove Track",
103-
"when": "false",
104-
"icon": "$(close)"
105-
},
10699
{
107100
"category": "dzr",
108101
"command": "dzr.clear",
109102
"title": "Queue Remove All",
110-
"icon": "$(close)"
103+
"icon": "$(clear-all)"
111104
},
112105
{
113106
"category": "dzr",
114107
"command": "dzr.shuffle",
115108
"title": "Queue Shuffle",
116109
"icon": "$(arrow-swap)"
110+
},
111+
{
112+
"category": "dzr",
113+
"command": "dzr.share",
114+
"title": "Copy vscode:// Link",
115+
"icon": "$(link)"
117116
}
118117
],
119118
"menus": {
120119
"view/title": [
121120
{
122121
"when": "view == dzr.queue",
123-
"group": "navigation@0",
122+
"group": "navigation@9",
124123
"command": "dzr.add"
125124
},
126125
{
127126
"when": "view == dzr.queue && dzr.queue!=''",
128-
"group": "navigation@1",
127+
"group": "navigation@8",
129128
"command": "dzr.clear"
130129
},
131130
{
132131
"when": "view == dzr.queue && dzr.queue!=''",
133-
"group": "navigation@1",
132+
"group": "navigation@7",
133+
"command": "dzr.share"
134+
},
135+
{
136+
"when": "view == dzr.queue && dzr.queue!=''",
137+
"group": "navigation@6",
134138
"command": "dzr.shuffle"
135139
},
136140
{
@@ -155,15 +159,20 @@
155159
"command": "dzr.remove",
156160
"when": "viewItem == dzr.track && !listMultiSelection"
157161
},
162+
{
163+
"group": "navigation",
164+
"command": "dzr.remove",
165+
"when": "viewItem == dzr.track && listMultiSelection"
166+
},
158167
{
159168
"group": "navigation",
160169
"command": "dzr.href",
161170
"when": "viewItem == dzr.track && !listMultiSelection"
162171
},
163172
{
164173
"group": "navigation",
165-
"command": "dzr.remove",
166-
"when": "viewItem == dzr.track && listMultiSelection"
174+
"command": "dzr.share",
175+
"when": "viewItem == dzr.track"
167176
}
168177
]
169178
},

extension/webview.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ <h1>Disclamer</h1>
2525
dialog.onclose=()=>{audio.play();post('user_interact');}
2626
['ended', 'pause', 'playing'].map(on => audio.addEventListener(on, () => post('player_' + on)));
2727
let mediaSource, sourceBuffer;
28+
const image = (type,md5,size=80) => `https://e-cdns-images.dzcdn.net/images/${type}/${md5}/${size}x${size}.jpg`
2829
const on = {// event from VSCode
2930
async open(item) {
3031
mediaSource = new window.MediaSource();
@@ -40,8 +41,8 @@ <h1>Disclamer</h1>
4041
state(state, updates=[]) {
4142
if (updates.includes('current')) {
4243
title.innerText = state.current.title;
43-
artists.replaceChildren(...state.current.artists.map(artist=>el('a',{href:cmd("dzr.add", `/artist/${artist.id}`, artist.name)},[new Text(artist.name)])));
44-
img.src = `https://e-cdns-images.dzcdn.net/images/cover/${state.current.md5_image}/1000x1000-000000-80-0-0.jpg`;
44+
artists.replaceChildren(...state.current.artists.map(artist=>el('a',{href:cmd("dzr.add", `/artist/${artist.id}`, artist.name),style:`background-image:url(${image('artist',artist.md5)})`},[new Text(artist.name)])));
45+
img.src = image("cover", state.current.md5_image, 1000);
4546
navigator.mediaSession.metadata = new MediaMetadata({
4647
title: state.current.title,
4748
artist: state.current.artists.map(a=>a.name).join(),
@@ -64,4 +65,5 @@ <h1>Disclamer</h1>
6465
a:not(:hover){text-decoration: none;}
6566
img:not([src]){opacity: 0;}
6667
dialog::backdrop {background-color: rgba(0, 0, 0, .9);}
68+
#artists>a {padding-left: 1.5em;background-repeat: no-repeat;background-size: contain;}
6769
</style>

0 commit comments

Comments
 (0)