Skip to content

Commit

Permalink
fix dzr-srt + use specific icon in vscode action-bar
Browse files Browse the repository at this point in the history
  • Loading branch information
yne committed Sep 12, 2023
1 parent 6b0d49c commit 4344241
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 86 deletions.
106 changes: 41 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,89 +1,65 @@
![dzr logo](.github/.logo.svg)

# DZR: the command line deezer.com player [![ci](https://github.com/yne/dzr/actions/workflows/ci.yml/badge.svg)](https://github.com/yne/dzr/actions/workflows/ci.yml)
# DZR: the command line deezer.com player

> ⚠️ For [legal reasons](https://github.com/github/dmca/blob/master/2021/02/2021-02-10-deezer.md) this project
> - does not contain any track decryption key
> - does not cache any tracks on your machine
## Features

## Preview
[![asciicast](https://asciinema.org/a/406758.svg)](https://asciinema.org/a/406758)
- Cross-platform support: Linux, *BSD, MacOS, Android, Windows+WSL
- Little dependencies: `curl`, `jq`, `dialog`, `openssl` (`openssl-tool` in Android)
- Real-time Lyrics display
- Web interface support (see [dzr](https://github.com/topics/dzr)-tagged frontend)
- ID3v2 tag injector from Deezer metadata (cover, artist, ...)
- Play without storing/caching on your machine for [legal reasons](https://github.com/github/dmca/blob/master/2021/02/2021-02-10-deezer.md)
- No private deezer key in the source (auto-extracted from web player, also for legal reasons)
- VSCode extension [VSIX](https://github.com/yne/dzr/releases) experimental port

## Dependencies
## Preview (CLI)

- `mpv` for playback (because of `PLAYER="mpv -"` default env variable)
- `curl` for HTTP query
- `jq` for API parsing
- `dialog` for TUI
- `openssl` (or `openssl-tool` in Android) for track decryption
[![asciicast](https://asciinema.org/a/406758.svg)](https://asciinema.org/a/406758)

## Compatibility
## Preview (VSIX)

- Linux and {Free,Open}BSD
- Android (using [Termux](https://termux.com/) from F-droid)
- Window 10 (running dzr as CGI server from WSL and browsing http://127.0.0.1:8000 from Windows)
![Screenshot](https://github.com/yne/dzr/assets/5113053/37b6cd26-8876-4d77-92bb-293ff248e21d)

## Install

### From the AUR (Arch Linux)

```sh
yay -S dzr
```

### From [GURU](https://github.com/gentoo/guru) (Gentoo)
| Platform | command | version |
|----------|---------|---------|
| MacOS + [brew](https://formulae.brew.sh/formula/dzr) | `brew install dzr` | ![](https://repology.org/badge/version-for-repo/homebrew/dzr.svg?header=)
| Arch Linux + [AUR](https://aur.archlinux.org/packages/dzr) | `yay -S dzr` | ![](https://repology.org/badge/version-for-repo/aur/dzr.svg?header=)
| Gentoo + [GURU](https://github.com/gentoo/guru) | `emerge --ask dzr` | ![](https://repology.org/badge/version-for-repo/gentoo_ovl_guru/dzr.svg?header=)
| Ubuntu + [snap](https://snapcraft.io/dzr) | `snap install --edge dzr` | [Help Me](https://github.com/yne/dzr/issues/25)
| Android + [Termux](https://f-droid.org/packages/com.termux/) | `curl -sL github.com/yne/dzr/archive/master.tar.gz \| tar xzf -` <br> `sudo mv dzr-master/dzr* /usr/local/bin` | [![](https://img.shields.io/badge/-tar.gz-40c010?logo=hackthebox)](https://github.com/yne/dzr/archive/master.tar.gz)
| VSCode | `code --install-extension ./path/tos/dzr-x.y.z.vsix` | [![](https://img.shields.io/badge/VSIX-4c1?logo=visualstudiocode)](https://github.com/yne/dzr/releases)
## Usage

```sh
emerge --ask dzr
```

### From sources

Save source into a `dzr-master` folder, then copy into /usr/local/bin :

```bash
curl -sL github.com/yne/dzr/archive/master.tar.gz | tar xzf -
sudo mv dzr-master/dzr* /usr/local/bin
```

## Usage Examples
# browse api.deezer.com
dzr

```sh
dzr # welcome screen
dzr /artist/860 # browse deezer.com/en/artist/860
```
# browse a specific api.deezer.com url
dzr /artist/860

## Automatic ID3v2 Tagging
# play a specific track
dzr /track/1043317462

Use `dzr-id3` to rename (as `$ARTIST - $TITLE.mp3`) and tag a given MP3 using it deezer track id
# use a custom PLAYER (mpg123 v1.31+ is a lightweight alternative)
PLAYER="mpg123 -" dzr

```sh
# the following examples are all equivalent
dzr-id3 1043317462 daylight.mp3
dzr-id3 /track/1043317462 daylight.mp3
dzr-id3 https://deezer.com/en/track/1043317462 daylight.mp3
```
# inject deezer ID3v2 into MP3 (require eyeD3) and rename it as $ARTIST - $TITLE.mp3
dzr-id3 https://deezer.com/track/1043317462 tagme.mp3

## Real time Lyrics
# show track lyrics as srt
dzr-srt https://deezer.com/track/14408104

Use `dzr-srt` to extract lyrics of the current track and pass it to mpv as --sub-file :
# play track with it lyrics
PLAYER='dzr-srt $id > .srt ; mpv --sub-file=.srt -' dzr /track/14408104

```sh
# play track with it srt (using non-POSIX compliant process substitution)
PLAYER='mpv --sub-file=<(dzr-srt $id) -' dzr /track/14408104
```

## HTTP/Web interface

In addition to it command line interface, `dzr` also support being invoked from a cgi server :

```sh
mkdir -p cgi-bin
cp dzr* ./cgi-bin/
# install dzr into ./cgi-bin/. Then serve it
mkdir -p ./cgi-bin/ && install dzr* ./cgi-bin/
python3 -m http.server --cgi
open http://127.0.0.1:8000/cgi-bin/dzr?6113114
```

You shall then be able to play any track over HTTP (ex: http://127.0.0.1:8000/cgi-bin/dzr?6113114 )

A **basic** web interface is also available on http://127.0.0.1:8000

Feel free to create your own frontend an publish it as a new repository (not as a dzr fork) with the [dzr](https://github.com/topics/dzr) tag.
17 changes: 9 additions & 8 deletions dzr-srt
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,26 @@
# USAGE Example:
# ./dzr-srt 355777961

SNG_IDS=$(printf "%s" "$*" | sed 's/ /,/g')
SNG_ID=$(echo "$1" | tr -dc '0-9') # extract id from path,url,...
[ -z "$SNG_ID" ] && echo "USAGE: $0 5404528" && exit 1

FETCH=${FETCH:-curl -s} # FETCH="wget -q -O -" or FETCH="curl -s -k"
gw () {
method="$1"; session="$2" ;apiToken="$3" ; shift 3 # curl args ...
$FETCH "https://www.deezer.com/ajax/gw-light.php?method=$method&input=3&api_version=1.0&api_token=$apiToken" --header "Cookie: sid=$session" "$@"
}

[ -z "$SNG_IDS" ] && echo "USAGE: dzr-srt 5404528" && exit 1
DZR_URL="www.deezer.com/ajax/gw-light.php?method=deezer.ping&api_version=1.0&api_token"
DZR_SID=$($FETCH "$DZR_URL" | jq -r .results.SESSION)
USR_NFO=$(gw deezer.getUserData "$DZR_SID" "$API_TOK")
USR_TOK=$(printf "%s" "$USR_NFO" | jq -r .results.USER_TOKEN)
USR_LIC=$(printf "%s" "$USR_NFO" | jq -r .results.USER.OPTIONS.license_token)
API_TOK=$(printf "%s" "$USR_NFO" | jq -r .results.checkForm)
#printf "SID=$DZR_SID\nAPI=$API_TOK\nLIC=$USR_LIC\nTOK=$USR_TOK\nIDS=$SNG_IDS\n" 1>&2
#printf "SID=$DZR_SID\nAPI=$API_TOK\nLIC=$USR_LIC\nTOK=$USR_TOK\nIDS=$SNG_ID\n" 1>&2

gw song.getLyrics "$DZR_SID" "$API_TOK" --data "{\"sng_id\":$SNG_IDS}" | jq -r 'if (.results.LYRICS_SYNC_JSON) then .results.LYRICS_SYNC_JSON | map(select(.lrc_timestamp)) | to_entries | map([.key+1, "00:"+.value.lrc_timestamp[1:-1] + .value.milliseconds, (.value.duration|tonumber/1000|tostring), .value.line])[]|@tsv else "1\t00:00:00.0\t0.0\t" end' |
while IFS=$'\t' read -r id start length text; do
from=$(date +%H:%M:%S,%N --date "$start") ;
to=$(date +%H:%M:%S,%N --date "$start + $length second") ;
printf "$id\n${from::12} --> ${to::12}\n$text\n\n" ;
gw song.getLyrics "$DZR_SID" "$API_TOK" --data "{\"sng_id\":$SNG_ID}" | jq -r 'if (.results.LYRICS_SYNC_JSON) then .results.LYRICS_SYNC_JSON | map(select(.lrc_timestamp)) | to_entries | map([.key+1, "00:"+.value.lrc_timestamp[1:-1] + .value.milliseconds, (.value.duration|tonumber/1000|tostring), .value.line])[]|@tsv else "1\t00:00:00.0\t0.0\t" end' |
while IFS=' ' read -r id start length text ; do
from=$(date +%H:%M:%S,%N --date "$start" | cut -c1-12) ;
to=$(date +%H:%M:%S,%N --date "$start + $length second" | cut -c1-12) ;
printf "$id\n${from} --> ${to}\n$text\n\n" ;
done
1 change: 1 addition & 0 deletions extension/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 11 additions & 8 deletions extension/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ async function browse(url_or_event, label) {
const url = typeof (url_or_event) == "string" ? url_or_event : '/';
const id = url.replace(/\d+/g, '0').replace(/[^\w]/g, '_');
const menus = conf.get('menus');
const title = (label || '').replace(/\$\(.+?\)/g, '');
if (url.endsWith('=') || url.endsWith('/0')) { // query step
const input = await vscode.window.showInputBox({ title: label });
const input = await vscode.window.showInputBox({ title });
if (!input) return;
return await browse(url.replace(/0$/, '') + input, label);
return await browse(url.replace(/0$/, '') + input, `${label}: ${input}`);
} else if (menus[id]) { // menu step
const pick = menus[id].length > 1 ? await vscode.window.showQuickPick(menus[id], { title: label || url }) : menus[id][0];
const pick = menus[id].length > 1 ? await vscode.window.showQuickPick(menus[id], { title: title || url }) : menus[id][0];
if (!pick) return;
return await browse(url + pick.path, pick.label);
} else { // fetch step
Expand All @@ -48,15 +49,15 @@ async function browse(url_or_event, label) {
description: [entry.artist?.name, entry.title_version, entry.nb_tracks].join(' '),
path: `/${entry.type}/${entry.id}`,
}));
const picks = await vscode.window.showQuickPick(choices, { title: label || url, canPickMany });
const picks = await vscode.window.showQuickPick(choices, { title: title || url, canPickMany });
if (!picks) return;
return canPickMany ? picks : await browse(picks.path, picks.label);
}
} catch (e) { console.error(e) }
}
/* songs may came from API (full info) or storage (light info) */
const with_url = async (songs) => songs?.length ? await vscode.window.withProgress({ title: 'Fetching Song Info...', location }, async (progress) => {
try {
try { // take 7s (with, or without agent)
const next = (val) => (progress.report({ increment: 100 / 4 }), val);
const gw = async (method, sid, api_token = "", opt = {}, data) => JSON.parse(await fetch(`${base}&method=${method}&api_token=${api_token}`,
{ ...opt, headers: { Cookie: `sid=${sid}`, ...opt?.headers } }, data)).results;
Expand Down Expand Up @@ -126,13 +127,14 @@ class DzrWebView { // can't Audio() in VSCode, we need a webview
this.treeView.description = (this.state.queue?.length ? `${index + 1 || '?'}/${this.state.queue.length}` : '') + ` loop:${this.state.looping}`;
this.treeView.message = this.state.queue?.length ? null : "Empty Queue. Add tracks to queue using '+'";
}
async show(htmlUri) {
async show(htmlUri, iconPath) {
if (this.panel) return this.panel.reveal(vscode.ViewColumn.One);
this.panel = vscode.window.createWebviewPanel('dzr.player', 'Player', vscode.ViewColumn.One, {
enableScripts: true,
enableCommandUris: true,
retainContextWhenHidden: true,
});
this.panel.iconPath = iconPath;
this.panel.webview.html = (await vscode.workspace.fs.readFile(htmlUri)).toString();
this.panel.webview.onDidReceiveMessage((action, ...args) => this[action] ? this[action](...args) : this.badAction(action));
this.panel.onDidDispose(() => this.state.ready = this.panel = null);
Expand Down Expand Up @@ -196,8 +198,9 @@ exports.activate = async function (/**@type {vscode.ExtensionContext}*/ context)
});
const dzr = new DzrWebView();
const htmlUri = vscode.Uri.joinPath(context.extensionUri, 'webview.html');
const iconUri = vscode.Uri.joinPath(context.extensionUri, 'logo.svg'); //same for light+dark
context.subscriptions.push(...dzr.statuses, dzr.treeView,
vscode.commands.registerCommand('dzr.show', () => dzr.show(htmlUri)),
vscode.commands.registerCommand('dzr.show', () => dzr.show(htmlUri, iconUri)),
vscode.commands.registerCommand("dzr.play", () => dzr.post('play')),
vscode.commands.registerCommand("dzr.pause", () => dzr.post('pause')),
vscode.commands.registerCommand("dzr.loopQueue", () => dzr.state.looping = "queue"),
Expand All @@ -216,7 +219,7 @@ exports.activate = async function (/**@type {vscode.ExtensionContext}*/ context)
dzr.state.queue = shuffle;
}),
vscode.commands.registerCommand("dzr.load", async (pos) => { //pos=null if player_end / pos=undefine if user click
pos = pos ?? dzr.state.queue.indexOf(dzr.state.current) + (dzr.state.looping=='track' ? 0 : 1);
pos = pos ?? dzr.state.queue.indexOf(dzr.state.current) + (dzr.state.looping == 'track' ? 0 : 1);
if (!dzr.state.queue[pos]) { // out of bound track
if (dzr.state.looping == 'off') return; // don't loop if unwanted
pos = 0; // loop position if looping
Expand Down
13 changes: 11 additions & 2 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,21 @@
}
}
},
"viewsContainers": {
"activitybar": [
{
"id": "dzr",
"title": "dzr",
"icon": "logo.svg"
}
]
},
"views": {
"explorer": [
"dzr": [
{
"id": "dzr.queue",
"name": "Player Queue",
"icon": "play"
"icon": "logo.svg"
}
]
}
Expand Down
5 changes: 2 additions & 3 deletions extension/webview.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ <h1>Disclamer</h1>
</script>
<style>
a:not(:hover){text-decoration: none;}
dialog::backdrop {
background-color: rgba(0, 0, 0, .9);
}
img:not([src]){opacity: 0;}
dialog::backdrop {background-color: rgba(0, 0, 0, .9);}
</style>

0 comments on commit 4344241

Please sign in to comment.