Full Code of yne/dzr for AI

master e2e059468f72 cached
13 files
57.8 KB
19.1k tokens
20 symbols
1 requests
Download .txt
Repository: yne/dzr
Branch: master
Commit: e2e059468f72
Files: 13
Total size: 57.8 KB

Directory structure:
gitextract_rsjai3cw/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── LICENSE
├── README.md
├── dzr
├── dzr-dec
├── dzr-id3
├── dzr-srt
├── dzr-url
├── extension/
│   ├── main.js
│   ├── package.json
│   └── webview.html
└── index.html

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
github: yne


================================================
FILE: .github/workflows/ci.yml
================================================
name: ci

on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: check key extract
      run: |
        [ "$(DZR_CBC= ./dzr ! 2>/dev/null | sha1sum)" = '3ad58d9232a3745ad9308b0669c83b6f7e8dba4d  -' ]
    - name: bundle VSIX
      run: zip -r $(jq -r '.name+"-"+.version+".vsix"' < extension/package.json) extension
    - name: publish
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        RELEASE_FILES: dzr*
      run: |
        RELEASE_TAG="$(date +%y%m%d)"
        curl -s -XPOST -d '{"tag_name": "'$RELEASE_TAG'"}' \
          -H "Authorization: Bearer $GITHUB_TOKEN" \
          -H 'Content-Type: application/json' \
          "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases" || :
        RELEASE_ID=$(curl -s https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/$RELEASE_TAG | jq .id)
        echo tag=$RELEASE_TAG has id=$RELEASE_ID
        for RELEASE_FILE in $RELEASE_FILES; do
          curl -s -XPOST -T $RELEASE_FILE \
          -H "Authorization: token $GITHUB_TOKEN" \
          -H "Content-Type:application/octet-stream" \
          "https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets?name=$RELEASE_FILE" || :
        done


================================================
FILE: LICENSE
================================================
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org>



================================================
FILE: README.md
================================================
![dzr logo](.github/.logo.svg)

# DZR: the command line deezer.com player

## Features

- 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

## Preview (CLI)

[![asciicast](https://asciinema.org/a/406758.svg)](https://asciinema.org/a/406758)

## Preview (VSIX)

![Screenshot](https://github.com/yne/dzr/assets/5113053/37b6cd26-8876-4d77-92bb-293ff248e21d)

## Install

| 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)
| Linux + [Flatpak](https://www.flatpak.org/) | `flatpak install dzr` | [Help Me](https://github.com/yne/dzr/issues/25)
| Nix + [Flake](https://wiki.nixos.org/wiki/flakes) | `nix run github.com/yne/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> `mv dzr-master/dzr* $PREFIX/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/to/dzr-*.vsix` | [![](https://img.shields.io/badge/VSIX-4c1?logo=visualstudiocode)](https://github.com/yne/dzr/releases)

## Usage

```sh
# browse api.deezer.com
dzr

# browse a specific api.deezer.com url
dzr /artist/860

# play a specific track
dzr /track/1043317462

# use a custom PLAYER (mpg123 v1.31+ is a lightweight alternative)
PLAYER="mpg123 -" dzr

# inject deezer ID3v2 into MP3 (require eyeD3) and rename it as $ARTIST - $TITLE.mp3
dzr-id3 https://deezer.com/track/1043317462 tagme.mp3

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

# play track with it lyrics
PLAYER='dzr-srt $id > .srt ; mpv --sub-file=.srt -' dzr /track/14408104

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

# 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
```


================================================
FILE: dzr
================================================
#!/bin/sh

# CGI call handling, for example moving ./dzr* bin to ~/cgi-bin and running :
#     python3 -m http.server --cgi
# from your ~ (as non-root !) will reply to http://0.0.0.0:8000/cgi-bin/dzr?6113114,806027
FETCH=${FETCH:-curl -s} # FETCH="wget -q -O -" or FETCH="curl -s -k"
PLAYER=${PLAYER:-'mpv -'} # PLAYER="ffplay -"
DZR_SPONGE=${DZR_SPONGE:-cat -} # DZR_SPONGE="sponge" from package "moreutils"
DZR_RC="${DZR_RC:-${XDG_CONFIG_HOME:-$HOME/.config}/dzrrc}"
[ -f "$DZR_RC" ] && . "$DZR_RC" && export DZR_CBC DZR_ARL
if [ "$REQUEST_METHOD" = "HEAD" ] ; then
	printf 'Access-Control-Allow-Origin: *\ndzr-api: 0\n\n'
elif [ "$REQUEST_METHOD" = "GET" ] ; then
	printf "Cache-Control: max-age=31536000, immutable\nContent-Type: audio/mpeg\n\n"
	echo "$QUERY_STRING" | xargs basename | xargs $0-url | while IFS= read -r line; do
		url=$(printf '%s\n' "$line" | cut -f1)
		id=$(printf '%s\n' "$line" | cut -f2)
		$FETCH "$url" | $DZR_SPONGE | $0-dec $id
	done
fi
[ ! -z "$REQUEST_METHOD" ] && exit

# extraction + warning by charleywright, see: https://github.com/yne/dzr/issues/11
unscramble(){ printf "${8}${16}${7}${15}${6}${14}${5}${13}${4}${12}${3}${11}${2}${10}${1}${9}";}
if [ -z "$DZR_CBC" ]; then
	command -v $(echo "$FETCH" | cut -f 1 -d " ") >/dev/null || { echo "key extraction require $FETCH" 1>&2 ; exit 1 ;}
	APP_WEB=$($FETCH -L deezer.com/en/channels/explore | sed -n 's/.*src="\([^"]*app-web[^"]*\).*/\1/p' | xargs $FETCH -L)
	TMP_CBC=$(echo "$APP_WEB"  | tr ,_ '\n'  | sed -n 's/.*\(%5B0x..%2C.\{39\}%2C0x..%5D\).*/\1/p' | sed 's/%../\n/g' | xargs printf '\\%o ')
	export DZR_CBC=$(unscramble $TMP_CBC);
	[ "-$@" = "-!" ] && printf '%s' "$DZR_CBC" && exit
	mkdir -p "$(dirname "$DZR_RC")"
	grep -q '^DZR_CBC=' "$DZR_RC" 2>/dev/null && sed -i "s/^DZR_CBC=.*/DZR_CBC=$DZR_CBC/" "$DZR_RC" || echo "DZR_CBC=$DZR_CBC" >> "$DZR_RC"
	printf "DZR_CBC saved to %s\n" "$DZR_RC" >&2
fi
if [ -z "$DZR_ARL" ]; then
	echo "Starting on 2025-03-08, Anonymous playback are blocked."
	echo "To authenticate DZR to your account:"
	echo "- Sign in/up to https://www.deezer.com/"
	echo "- Then open https://www.deezer.com/desktop/login/electron/callback"
	echo "- Right click on the 'Open' button, then 'copy link address'."
	while true; do
		echo "past the deezer:// link here:"
		read DZR_ARL
		export DZR_ARL=$(printf $DZR_ARL | cut -d'/' -f4)
		echo "$DZR_ARL" | grep -q '^[0-9a-f]\+$' && break;
		echo "Invalid ARL format (expect: ^[0-9a-f]+$):"
		echo $DZR_ARL
	done
	mkdir -p "$(dirname "$DZR_RC")"
	grep -q '^DZR_ARL=' "$DZR_RC" 2>/dev/null && sed -i "s/^DZR_ARL=.*/DZR_ARL=$DZR_ARL/" "$DZR_RC" || echo "DZR_ARL=$DZR_ARL" >> "$DZR_RC"
	printf "DZR_ARL saved to %s\n" "$DZR_RC" >&2
fi
# dependencies check, see: https://github.com/yne/dzr/issues/12
for c in jq curl dialog openssl $(echo "$PLAYER" | cut -f 1 -d " "); do
	command -v $c >/dev/null || { echo "$c is required" 1>&2 ; UNMET_DEPENDENCIES=1;}
done
[ -n "$UNMET_DEPENDENCIES" ] && exit 1;

# fetch user info via gw-light API
export GW="https://www.deezer.com/ajax/gw-light.php"
gw() { $FETCH "$GW?method=$1&input=3&api_version=1.0&api_token=$DZR_API_TOK" --header "Cookie: sid=$DZR_SID; arl=$DZR_ARL" ${2:+-d "$2"}; }
if [ -n "$DZR_ARL" ] && [ -z "$DZR_SID" ]; then
	export DZR_SID=$($FETCH "www.deezer.com/ajax/gw-light.php?method=deezer.ping&api_version=1.0&api_token" | jq -r .results.SESSION)
	_DZR_USR=$(DZR_API_TOK= gw deezer.getUserData '{}')
	export DZR_LOVED=$(printf "%s" "$_DZR_USR" | jq -r .results.USER.LOVEDTRACKS_ID)
	export DZR_USER_ID=$(printf "%s" "$_DZR_USR" | jq -r .results.USER.USER_ID)
	export DZR_API_TOK=$(printf "%s" "$_DZR_USR" | jq -r .results.checkForm)
	export DZR_LIC=$(printf "%s" "$_DZR_USR" | jq -r .results.USER.OPTIONS.license_token)
fi

# main
trap 'printf "\033]0;\007" >&2' EXIT
SELF=$(command -v "$0") || SELF="$0"
DLG_LIST="dialog --keep-tite --output-fd 1 --menu $1: 0 0 0"
DLG_TEXT="dialog --keep-tite --output-fd 1 --inputbox $1: 0 0"
FMT_FUNC='s@[,?].*@@;s@[^a-zA-Z0-9]\{1,\}@_@g;s@[0-9,]\{1,\}@0@g;s@_\{1,\}$@@'

# jq formatters for gw-light track lists
GW_FMT_LIST='(.results.data[]|("/track/"+(.SNG_ID|tostring), (.SNG_TITLE+" - "+.ART_NAME|gsub("\\x22";""))))'
GW_FMT_TRKS='"/track/"+([.results.data[]|.SNG_ID]|map(tostring)|join(","))'

# -- helpers to extract IDs from paths --
_id() { echo "$1" | sed 's@^/[^/]*/@@;s@/.*@@'; } # /artist/123/top -> 123
_q()  { echo "$1" | sed 's/.*q=//'; }             # /search/track?q=foo -> foo
# -- gw data fetchers (raw JSON, reused by browse + play) --
gw_loved()           { gw playlist.getSongs "{\"playlist_id\":\"$DZR_LOVED\",\"start\":0,\"nb\":10000}"; }
gw_flow()            { gw radio.getUserRadio "{\"user_id\":\"$DZR_USER_ID\"}"; }
gw_playlists()       { gw deezer.userMenu '{}'; }
gw_artists()         { gw deezer.pageProfile "{\"user_id\":\"$DZR_USER_ID\",\"tab\":\"artists\",\"start\":0,\"nb\":10000}"; }
gw_albums()          { gw deezer.pageProfile "{\"user_id\":\"$DZR_USER_ID\",\"tab\":\"albums\",\"start\":0,\"nb\":10000}"; }
gw_search_track()    { gw search.music "{\"query\":\"$(_q "$1")\",\"filter\":\"ALL\",\"output\":\"TRACK\",\"start\":0,\"nb\":50}"; }
gw_search_artist()   { gw search.music "{\"query\":\"$(_q "$1")\",\"filter\":\"ALL\",\"output\":\"ARTIST\",\"start\":0,\"nb\":50}"; }
gw_search_album()    { gw search.music "{\"query\":\"$(_q "$1")\",\"filter\":\"ALL\",\"output\":\"ALBUM\",\"start\":0,\"nb\":50}"; }
gw_search_playlist() { gw search.music "{\"query\":\"$(_q "$1")\",\"filter\":\"ALL\",\"output\":\"PLAYLIST\",\"start\":0,\"nb\":50}"; }
gw_search_radio()    { gw search.music "{\"query\":\"$(_q "$1")\",\"filter\":\"ALL\",\"output\":\"RADIO\",\"start\":0,\"nb\":50}"; }
gw_artist_top()      { gw artist.getTopTrack "{\"ART_ID\":$(_id "$1"),\"nb\":100}"; }
gw_artist_albums()   { gw album.getDiscography "{\"ART_ID\":$(_id "$1"),\"nb\":50,\"nb_songs\":0,\"start\":0,\"discography_mode\":\"all\"}"; }
gw_artist_related()  { gw artist.getRelatedArtist "{\"ART_ID\":$(_id "$1"),\"nb\":25,\"start\":0}"; }
gw_artist_radio()    { gw radio.getArtistRadio "{\"ART_ID\":$(_id "$1"),\"nb\":40}"; }
gw_album()           { gw song.getListByAlbum "{\"ALB_ID\":$(_id "$1"),\"nb\":-1}"; }
gw_playlist()        { gw playlist.getSongs "{\"playlist_id\":\"$(_id "$1")\",\"start\":0,\"nb\":10000}"; }
gw_radio()           { gw radio.getSongs "{\"RADIO_ID\":$(_id "$1"),\"nb\":40}"; }

# -- jq formatters for non-track lists --
FMT_ARTIST='(.results.data[]|("/artist/"+(.ART_ID|tostring), (.ART_NAME|gsub("\\x22";""))))'
FMT_ALBUM='(.results.data[]|("/album/"+(.ALB_ID|tostring), ((.ALB_TITLE+" "+.ART_NAME)|gsub("\\x22";""))))'
FMT_DISCOG='(.results.data[]|("/album/"+(.ALB_ID|tostring), ((.ALB_TITLE+" ("+(.PHYSICAL_RELEASE_DATE//""|split("-")[0])+")")|gsub("\\x22";""))))'
FMT_PLAYLIST='(.results.data[]|("/playlist/"+(.PLAYLIST_ID|tostring), (.TITLE|gsub("\\x22";""))))'
FMT_MY_PLAYLIST='(.results.PLAYLISTS.data[]|select(.TYPE!="4")|("/playlist/"+(.PLAYLIST_ID|tostring), ((.TITLE+" ("+(.NB_SONG|tostring)+")")|gsub("\\x22";""))))'
FMT_MY_ARTIST='(.results.TAB.artists.data[]|("/artist/"+(.ART_ID|tostring), (.ART_NAME|gsub("\\x22";""))))'
FMT_MY_ALBUM='(.results.TAB.albums.data[]|("/album/"+(.ALB_ID|tostring), ((.ALB_TITLE+" "+.ART_NAME)|gsub("\\x22";""))))'
FMT_RADIO='(.results.data[]|("/radio/"+(.RADIO_ID|tostring), (.TITLE|gsub("\\x22";""))))'

# public API for browse-only endpoints (no gw-light equivalent)
API="api.deezer.com"
PUB_FMT_GENRE='(.data[]|("/genre/"+(.id|tostring), (.name|gsub("\\x22";""))))'
PUB_FMT_RADIO='(.data[]|("/radio/"+(.id|tostring), (.title|gsub("\\x22";""))))'

# -- menu functions --
dzr() {
	$DLG_LIST \
		/flow 'my personalised mix' \
		/loved 'my favourites' \
		/playlists 'my playlists' \
		/artists 'my artists' \
		/albums 'my albums' \
		/search/track?q= 'search track' \
		/search/artist?q= 'search artist' \
		/search/album?q= 'search album' \
		/search/playlist?q= 'search playlist' \
		/search/radio?q= 'search radio' \
		/genre 'list genres' \
		/radio 'list radios'
}

dzr_flow()             { gw_flow | jq "$GW_FMT_LIST" | xargs $DLG_LIST ∞ "continuous flow" ▸ "play all" ⇄ "shuffle" ;}
dzr_loved()            { gw_loved | jq "$GW_FMT_LIST" | xargs $DLG_LIST ▸ "play all" ⇄ "shuffle" ;}
dzr_playlists()        { gw_playlists | jq "$FMT_MY_PLAYLIST" | xargs $DLG_LIST ;}
dzr_artists()          { gw_artists | jq "$FMT_MY_ARTIST" | xargs $DLG_LIST ;}
dzr_albums()           { gw_albums | jq "$FMT_MY_ALBUM" | xargs $DLG_LIST ;}
# search
dzr_search_track()     { gw_search_track "$1" | jq "$GW_FMT_LIST" | xargs $DLG_LIST ▸ "play all" ⇄ "shuffle" ;}
dzr_search_artist()    { gw_search_artist "$1" | jq "$FMT_ARTIST" | xargs $DLG_LIST ;}
dzr_search_album()     { gw_search_album "$1" | jq "$FMT_ALBUM" | xargs $DLG_LIST ;}
dzr_search_playlist()  { gw_search_playlist "$1" | jq "$FMT_PLAYLIST" | xargs $DLG_LIST ;}
dzr_search_radio()     { gw_search_radio "$1" | jq "$FMT_RADIO" | xargs $DLG_LIST ;}
# genre
dzr_genre()            { $FETCH "$API/genre" | jq "$PUB_FMT_GENRE" | xargs $DLG_LIST ;} # public API - no gw-light equivalent
dzr_genre_0()          { $DLG_LIST radios '' artists '' ;}
dzr_genre_0_radios()   { $FETCH "$API/genre/$(_id "$1")/radios" | jq "$PUB_FMT_RADIO" | xargs $DLG_LIST ;}
dzr_genre_0_artists()  { $FETCH "$API/genre/$(_id "$1")/artists" | jq '(.data[]|("/artist/"+(.id|tostring), (.name|gsub("\\x22";""))))' | xargs $DLG_LIST ;}
# radio
dzr_radio()            { $FETCH "$API/radio" | jq "$PUB_FMT_RADIO" | xargs $DLG_LIST ;} # public API - no gw-light equivalent
dzr_radio_0()          { gw_radio "$1" | jq "$GW_FMT_LIST" | xargs $DLG_LIST ▸ "play all" ⇄ "shuffle" ;}
# artist
dzr_artist_0()         { $DLG_LIST top 'top tracks' albums '' related '' radio '' ;}
dzr_artist_0_top()     { gw_artist_top "$1" | jq "$GW_FMT_LIST" | xargs $DLG_LIST ▸ "play all" ⇄ "shuffle" ;}
dzr_artist_0_albums()  { gw_artist_albums "$1" | jq "$FMT_DISCOG" | xargs $DLG_LIST ;}
dzr_artist_0_related() { gw_artist_related "$1" | jq "$FMT_ARTIST" | xargs $DLG_LIST ;}
dzr_artist_0_radio()   { gw_artist_radio "$1" | jq "$GW_FMT_LIST" | xargs $DLG_LIST ▸ "play all" ⇄ "shuffle" ;}
# album
dzr_album_0()          { gw_album "$1" | jq "$GW_FMT_LIST" | xargs $DLG_LIST ▸ "play all" ⇄ "shuffle" ;}
# playlist
dzr_playlist_0()       { gw_playlist "$1" | jq "$GW_FMT_LIST" | xargs $DLG_LIST ▸ "play all" ⇄ "shuffle" ;}


play() { # receive /track/1,2,3 from stdin
	xargs basename | xargs $SELF-url | ${1:-cat} | while IFS= read -r line; do
		printf '%s\n' "$line" | grep -q '	' || { echo "error: unexpected dzr-url output: $line" >&2; break; }
		url=$(printf '%s\n' "$line" | cut -f1)
		id=$(printf '%s\n' "$line" | cut -f2)
		info=$(printf '%s\n' "$line" | cut -f3-)
		if [ -n "$info" ]; then
			printf '%s %s # %s\n' "$SELF" "/track/$id" "$info" >&2
			printf '\033]0;🎶 %s\007' "$(printf '%s' "$info" | sed 's/ \[.*//;s/ (.*)//')" >&2
		fi
		$FETCH "$url" | $DZR_SPONGE | $SELF-dec $id | eval ${PLAYER:-'mpv -'} || break # stop if Ctrl+C : $? = 4
	done
}
# play_all: call the gw fetcher for this context, extract track IDs, play
play_all() { # $1=url $2=FUNC $3=order(cat|shuf)
	case "$2" in
	dzr_flow)           gw_flow ;;
	dzr_loved)          gw_loved ;;
	dzr_search_track)   gw_search_track "$1" ;;
	dzr_artist_0_top)   gw_artist_top "$1" ;;
	dzr_artist_0_radio) gw_artist_radio "$1" ;;
	dzr_album_0)        gw_album "$1" ;;
	dzr_playlist_0)     gw_playlist "$1" ;;
	dzr_radio_0)        gw_radio "$1" ;;
	*) return 1 ;;
	esac | jq "$GW_FMT_TRKS" | play "$3"
}
for url in "$@"; do
	case $url in
		/track/*) echo $url | play; continue ;; # direct play
	esac

	FUNC=$(echo "dzr_$url" | sed "$FMT_FUNC") # path (/search/artist?q=x) to function (dzr_search_artist)
	type $FUNC 1>/dev/null 2>/dev/null || { echo "error: unknown path $url" >&2; continue; }
	case $url in # catch url that need user input (query, id)
		*=)  url="$url$($DLG_TEXT ''|jq -rR '.|@uri')" ;; # escaped query
		*/0) url="$url$($DLG_TEXT ''|jq -rR '.|@uri')" ;; # escaped id
	esac

	echo "$SELF $url" >&2
	while path=$($FUNC $url); do # browse REPL
		case "$path" in
		▸) play_all "$url" "$FUNC" cat; break ;;
		⇄) play_all "$url" "$FUNC" shuf; break ;;
		∞) while true; do gw_flow | jq "$GW_FMT_TRKS" | play cat || break; done; break ;;
		/*) $SELF "$path";;
		*)  $SELF "$url/$path";;
		esac
	done
done
[ $# -eq 0 ] && $SELF '' # give an argument to iterate over (if none given)


================================================
FILE: dzr-dec
================================================
#!/bin/sh
# NAME
# 	dzr-dec - decode a deezer track by it ID
# USAGE:
# 	dzr-dec 321654 < enc.mp3 > dec.mp3

SNG_ID="$1"
[ -z "$DZR_CBC" ] && echo "Missing 'DZR_CBC' env variable" 1>&2 && exit 1
[ -z "$SNG_ID"  ] && echo "USAGE: DZR_CBC=XXXX dzr-dec 1234 < enc.mp3 > dec.mp3" 1>&2 && exit 1
dzr_cbc_hex=$(printf "$DZR_CBC"                                  | od -An -t x1 |tr -d '\n ')
track_md5_l=$(printf "%s" "$SNG_ID" | openssl md5 -r|cut -b1-16  | od -An -t x1 |tr -d '\n ')
track_md5_r=$(printf "%s" "$SNG_ID" | openssl md5 -r|cut -b17-32 | od -An -t x1 |tr -d '\n ')
track_key=$(for k in 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31; do # no seq in BSD
	a=$(printf $dzr_cbc_hex | cut -b $k-$(($k+1)))
	b=$(printf $track_md5_l | cut -b $k-$(($k+1)))
	c=$(printf $track_md5_r | cut -b $k-$(($k+1)))
	printf '%02x' "$((0x$a ^ 0x$b ^ 0x$c))"
done)

stripe_size=2048
openssl_opt="-d -nopad -bufsize $stripe_size -K $track_key -iv 0001020304050607"
# OpenSSL 3 require som extra argument
if openssl bf-cbc -help 2>&1 | grep -q provider; then
	openssl_opt="$openssl_opt -provider legacy";
fi
# And now for my next loop, I'd like to return to the classics
set -e; # if an iteration fail we stop the loop (you better pipe me with curl)
while true; do
    LC_ALL=POSIX dd bs=$stripe_size count=1 status=none | openssl bf-cbc $openssl_opt 2>/dev/null 1>&4
  { LC_ALL=POSIX dd bs=$stripe_size count=2 2>&3 >&4; } 3>&1 | grep -qe '^0[+]0 ' && break
done 4>&1


================================================
FILE: dzr-id3
================================================
#!/bin/sh
# USAGE Example:
# ./dzr-tag 5404528 tagMe.mp3

SNG_ID=$(echo "$1" | tr -dc '0-9') # extract id from path,url,...
if [ -z "$SNG_ID" -o ! -f "$2" ]; then
    echo "USAGE: dzr-id3 5404528 target.mp3" && exit 1
fi

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" "$@"
}

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)
SNG_NFO=$(gw song.getListData "$DZR_SID" "$API_TOK" --data "{\"sng_ids\":[$SNG_ID]}")

# extract ID3 field we need
ART_NAME=$(echo "${SNG_NFO}" | jq -r .results.data[0].ART_NAME)
REL_DATE=$(echo "${SNG_NFO}" | jq -r .results.data[0].PHYSICAL_RELEASE_DATE) # TODO: sometimes 0000-00-00
SNG_TITLE=$(echo "${SNG_NFO}" | jq -r .results.data[0].SNG_TITLE)
ALB_TITLE=$(echo "${SNG_NFO}" | jq -r .results.data[0].ALB_TITLE)
TRK_NUMBR=$(echo "${SNG_NFO}" | jq -r .results.data[0].TRACK_NUMBER)
ALB_PICTURE=$(echo "${SNG_NFO}" | jq -r .results.data[0].ALB_PICTURE)
APICNAME=".$ALB_PICTURE.jpg"
$FETCH "https://e-cdn-images.dzcdn.net/images/cover/$ALB_PICTURE/1024x1024-000000-100-0-0.jpg" > "$APICNAME"
FILENAME="$ART_NAME - $SNG_TITLE.mp3"
# eyeD3 mid3/mutagen (both python lib) seems to be the only one supporting APIC tag
# contact me if you find a dependency-less widely-available id3tag editor
eyeD3 \
  --artist "$ART_NAME" \
  --title "$SNG_TITLE" \
  --album "$ALB_TITLE" \
  --track "$TRK_NUMBR" \
  --release-date "$REL_DATE" \
  --add-image "$APICNAME:FRONT_COVER" \
  "$2" &&
rm "$APICNAME" &&
mv "$2" "$FILENAME"


================================================
FILE: dzr-srt
================================================
#!/bin/sh
# USAGE Example:
# ./dzr-srt 355777961

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" "$@"
}

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_ID\n" 1>&2

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


================================================
FILE: dzr-url
================================================
#!/bin/sh
# USAGE Example:
# ./dzr-url 355777961 650744592 | while read url id; do curl -s "$url" | ./dzr-dec $id | mpv - ; done

SNG_IDS=$(printf "%s" "$*" | sed 's/ /,/g')
FETCH=${FETCH:-curl -s} # FETCH="wget -q -O -" or FETCH="curl -s -k"
die() { echo "$1" >&2; exit 1; }
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; arl=$DZR_ARL" "$@"
}

[ -z "$SNG_IDS" ] && die "USAGE: dzr-url 5404528,664107"
# reuse session from parent dzr process, or create a new one
if [ -n "$DZR_SID" ] && [ -n "$DZR_API_TOK" ] && [ -n "$DZR_LIC" ]; then
	API_TOK="$DZR_API_TOK"
	USR_LIC="$DZR_LIC"
else
	DZR_SID=$($FETCH "www.deezer.com/ajax/gw-light.php?method=deezer.ping&api_version=1.0&api_token" | jq -r .results.SESSION)
	[ -z "$DZR_SID" ] || [ "$DZR_SID" = "null" ] && die "error: deezer.ping failed (no session)"
	USR_NFO=$(gw deezer.getUserData "$DZR_SID" "$API_TOK")
	USR_LIC=$(printf "%s" "$USR_NFO" | jq -r .results.USER.OPTIONS.license_token)
	API_TOK=$(printf "%s" "$USR_NFO" | jq -r .results.checkForm)
	[ -z "$USR_LIC" ] || [ "$USR_LIC" = "null" ] && die "error: getUserData failed (no license token)"
	[ -z "$API_TOK" ] || [ "$API_TOK" = "null" ] && die "error: getUserData failed (no api token)"
fi

SNG_NFO=$(gw song.getListData "$DZR_SID" "$API_TOK" --data "{\"sng_ids\":[$SNG_IDS]}")
printf "%s" "$SNG_NFO" | jq -e '.results.data // empty' >/dev/null 2>&1 || die "error: song.getListData returned no data"
SNG_TOK=$(printf "%s" "$SNG_NFO" | jq '[.results.data[]|(.FALLBACK.TRACK_TOKEN//.TRACK_TOKEN)]')
# reset to resolvable id, eg:no personal uploads (geoblocked track will fail later)
SNG_IDS=$(printf "%s" "$SNG_NFO" | jq '[.results.data[]|(.FALLBACK.SNG_ID//.SNG_ID)]')
SNG_INF=$(printf "%s" "$SNG_NFO" | jq '[.results.data[]|"\(.ART_NAME) - \(.SNG_TITLE)\(if .VERSION != "" then " \(.VERSION)" else "" end) [\(.ALB_TITLE)] (\(.DURATION|tonumber|"\(./60|floor):\(. % 60|tostring|if length<2 then "0"+. else . end)"))"]')
URL_NFO=$(printf "{\"license_token\":\"%s\",\"media\":[{\"type\":\"FULL\",\"formats\":[{\"cipher\":\"BF_CBC_STRIPE\",\"format\":\"${DZR_FMT:-MP3_128}\"}]}],\"track_tokens\":%s}" "$USR_LIC" "$SNG_TOK" | $FETCH 'https://media.deezer.com/v1/get_url' --data-binary @-)
printf "%s" "$URL_NFO" | jq -e '.data // empty' >/dev/null 2>&1 || die "error: get_url returned no data"
URL_IDS=$(printf "%s" "$URL_NFO" | jq --argjson ids "$SNG_IDS" --argjson inf "$SNG_INF" '[.data , ($ids|map({id:.})) , ($inf|map({info:.}))]|transpose|map(add)')
printf "%s" "$URL_IDS" | jq -r '.[]|select(.errors)|[.errors[0].message     ,.id]|@tsv' 1>&2
printf "%s" "$URL_IDS" | jq -r '.[]|select(.media[0].sources[0].url)|[.media[0].sources[0].url,.id,.info]|@tsv'


================================================
FILE: extension/main.js
================================================
// @ts-check
/// curl -Ok https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.d.ts
/// <reference path="./vscode.d.ts" />
const vscode = require("vscode");
const crypto = require('crypto');
const https = require('https');
const conf = () => vscode.workspace.getConfiguration("dzr");
const location = vscode.ProgressLocation.Notification;
const hhmmss = (s) => (new Date(s * 1000)).toISOString().slice(11, 19).replace(/^00:/, '');
const wait = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms));
// still no fetch() in 2023 ?
const fetch = (url, opt, data) => new Promise((resolve, reject) => {
	console.log({ url, opt, data })
	const chunks = [], req = https.request(url, { rejectUnauthorized: conf().get('secure'), ...opt }, res => {
		res.on('data', chunk => chunks.push(chunk));
		res.on('end', () => resolve(Buffer.concat(chunks)));
	}).on('error', reject);
	if (data) req.write(data);
	req.end();
});

// deezer API wall of shame:
// - not restful, so we can't infer it structure
// - /track/:id gives contributors but /search/track?q= don't
// - inconsistent listing structure (/playlist/:id => tracks.data, sometimes=>data, sometimes data.tracks)

// browse can be called from: user query / self list(from static menu) / self list(from fetch result)
async function browse(url_or_event_or_ids, label) {
	try {
		if (Array.isArray(url_or_event_or_ids)) return url_or_event_or_ids.map(id => ({ id }));
		const ignoreFocusOut = true;
		const url = typeof (url_or_event_or_ids) == "string" ? url_or_event_or_ids : '/';
		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, ignoreFocusOut });
			if (!input) return;
			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: title || url, ignoreFocusOut }) : menus[id][0];
			if (!pick) return;
			return await browse(url + pick.path, pick.label);
		} else { // fetch step
			/**@type {{type:string,title_short:string,artist:{name:string},title_version:string,title:string,name:string,nb_tracks:string,id:number}[]} */
			const data = [];
			for (let n = conf().get('nextCount'), json, nextUrl = "https://api.deezer.com" + url; --n && nextUrl && (json = JSON.parse(await fetch(nextUrl))); nextUrl = json.next) {
				data.push(...(json.data?.tracks || json.data || json.tracks?.data));
			}
			const picked = !!url.match(/\/(playlist|album)\//);
			const canPickMany = !!data.find(item => item.type == "track");
			const type2icon = conf().get('type2icon');
			const choices = data.map(entry => ({
				...entry, picked,
				label: (type2icon[entry.type] || '') + (entry.title_short || entry.name || entry.title),
				description: [entry.artist?.name, entry.title_version, entry.nb_tracks].join(' '),
				path: `/${entry.type}/${entry.id}`,
			}));
			const picks = await vscode.window.showQuickPick(choices, { title: title || url, canPickMany, ignoreFocusOut });
			if (!picks) return;
			return canPickMany ? picks : await browse(picks.path, picks.label);
		}
	} catch (e) { console.error(e) }
}
let DZR_PNG, USR_NFO;
const with_url = async (songs) => songs?.length ? await vscode.window.withProgress({ title: 'Fetching', location }, async (progress) => {
	try { // take 7s (with, or without agent)
		const next = (message, val) => (progress.report({ increment: 100 / 4, message }), val);
		const gw = async (method, arl, sid, api_token = "", opt = {}, data) => JSON.parse(await fetch(`${base}&method=${method}&api_token=${api_token}`,
			{ ...opt, headers: { Cookie: `sid=${sid}; arl=${arl}`, ...opt?.headers } }, data)).results;
		const base = "https://www.deezer.com/ajax/gw-light.php?input=3&api_version=1.0";
		let DZR_ARL = next("ARL", conf().get('arl'));
		const format = conf().get('format') || 'MP3_128';
		if (!DZR_ARL) {
			DZR_ARL = (await vscode.window.showInputBox({
				ignoreFocusOut: true,
				placeHolder: "deezer://autolog/xxxx",
				prompt: "Login on [deezer.com](https://www.deezer.com/), then copy the button address displayed on [this page](https://www.deezer.com/desktop/login/electron/callback)"
			}))?.match(/[0-9a-f]{192}/)?.[0];
			if (!DZR_ARL) return vscode.window.showErrorMessage("No ARL found");
			conf().update('arl', DZR_ARL, vscode.ConfigurationTarget.Global);
		}
		DZR_PNG = next("session", DZR_PNG || await gw('deezer.ping', DZR_ARL));
		USR_NFO = next("user right", USR_NFO || await gw('deezer.getUserData', DZR_ARL, DZR_PNG.SESSION));
		const SNG_NFO = next("song info", await gw('song.getListData', DZR_ARL, DZR_PNG.SESSION, USR_NFO.checkForm, { method: 'POST' }, JSON.stringify({ sng_ids: songs.map(s => s.id) }))).data.map(e => e.FALLBACK || e);
		const GET_URL = next("song stream", JSON.parse(await fetch('https://media.deezer.com/v1/get_url', { method: 'POST' }, JSON.stringify({
			track_tokens: SNG_NFO.map(d => d.TRACK_TOKEN),
			license_token: USR_NFO.USER.OPTIONS.license_token,
			media: [{ type: "FULL", formats: [{ cipher: "BF_CBC_STRIPE", format }] }]
		}))))?.data?.map((url, i) => Object.assign(SNG_NFO[i], url));
		const errors = SNG_NFO.filter(e => e.errors).map(e => `[${e.SNG_ID}] ${e.ART_NAME} - ${e.SNG_TITLE}:${JSON.stringify(e.errors)}`);
		const skiped = SNG_NFO.filter(s => !s.media?.[0]?.sources?.[0]?.url).map(e => `[${e.SNG_ID}] no MEDIA`);
		if (errors.length || skiped.length) {
			vscode.window.showWarningMessage([...errors, ...skiped].join('\n'), "Continue", "Flush ARL")
				.then(e => { if (e == "Flush ARL") conf().update('arl', undefined, vscode.ConfigurationTarget.Global) });
		}
		return SNG_NFO.map(nfo => ({
			id: nfo.SNG_ID,
			md5_image: nfo.ALB_PICTURE,
			duration: +nfo.DURATION,
			title: nfo.SNG_TITLE.replace(/ ?\(feat.*?\)/, ''),
			artists: (nfo.ARTISTS || []).map(a => ({ id: a.ART_ID, name: a.ART_NAME, md5: a.ART_PICTURE })),
			size: +nfo.FILESIZE,
			expire: nfo.TRACK_TOKEN_EXPIRE,
			url: nfo.media?.[0]?.sources?.[0]?.url
		})).filter(nfo => nfo.url);
	} catch (e) {
		console.error(e);
		vscode.window.showErrorMessage(e);
		return []
	}
}) : [];

class DzrWebView { // can't Audio() in VSCode, we need a webview
	statuses = ['dzr.play', 'dzr.show', 'dzr.load'].map((command) => {
		const item = vscode.window.createStatusBarItem(command, vscode.StatusBarAlignment.Left, 10000);
		item.color = new vscode.ThemeColor('statusBarItem.prominentBackground');
		item.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
		item.command = command;
		item.text = command;
		item.show();
		return item;
	});
	/**@type {vscode.WebviewPanel|null}*/
	panel = null;
	#state = {};
	state = new Proxy(this.#state, {
		set: (target, k, value) => {
			const key = String(k);
			target[key] = value;
			if (['queue', 'looping'].includes(key)) { // persist those values across reboot
				conf().update(key, value, vscode.ConfigurationTarget.Global);
			}
			if (key == 'queue') this._onDidChangeTreeData.fire(null);
			vscode.commands.executeCommand('setContext', `dzr.${key}`, value);
			this.post('state', target, [key]);
			this.renderStatus();
			return true;
		}
	});

	constructor() {
		this.initAckSemaphore();
		this.state.queue = conf().get('queue'); // first is best
		this.state.looping = conf().get('looping');
	}
	renderStatus() {
		const index = this.state.queue?.indexOf(this.state.current);
		const label = this.state.current ? `${this.state.current.title} - ${this.state.current.artists?.map(a => a.name).join()}` : '';
		this.statuses[0].command = this.state.playing ? 'dzr.pause' : 'dzr.play';
		this.statuses[0].text = this.state.ready && (this.state.playing ? "$(debug-pause)" : "$(play)");
		this.statuses[1].tooltip = this.state.ready ? label : "Initiate interaction first";
		this.statuses[1].text = this.state.ready ? label.length < 20 ? label : (label.slice(0, 20) + '…') : "$(play)"
		this.statuses[2].text = this.state.ready && this.state.queue.length ? `${index + 1 || '?'}/${this.state.queue.length} $(chevron-right)` : '';//debug-step-over
		this.treeView.title = (this.state.queue?.length ? `${index + 1 || '?'}/${this.state.queue.length}` : '') + ` loop:${this.state.looping}`;
		this.treeView.message = this.state.queue?.length ? "" : "Empty Queue. Add tracks from the '+' menu";
		this.treeView.badge = { tooltip: label, value: this.state.playing ? index + 1 : 0 }
	}
	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);
		this.post('state', this.state, Object.keys(this.state));
	}
	initAckSemaphore() { this.postAck = new Promise((then) => this.waitAckSemaphore = then); }
	post = (action, ...arg) => this.panel?.webview.postMessage([action, ...arg]);
	// event from webview player
	player_bufferized() {
		this.waitAckSemaphore?.(0);
		this.initAckSemaphore();
	}
	player_volumechange({ volume }) { conf().update("volume", volume, vscode.ConfigurationTarget.Global); }
	player_playing() { this.state.ready = this.state.playing = true; }
	player_pause() { this.state.playing = false; }
	player_ended() { vscode.commands.executeCommand('dzr.load', null); }
	user_interact() { this.state.ready = true; }
	user_next() { vscode.commands.executeCommand('dzr.load'); }
	error(msg) { vscode.window.showErrorMessage(msg); }
	badAction(action) { console.error(`unHandled action "${action}" from webview`); }
	// tree
	dropMimeTypes = ['application/vnd.code.tree.dzrQueue'];
	dragMimeTypes = ['text/uri-list'];
	_onDidChangeTreeData = new vscode.EventEmitter();
	onDidChangeTreeData = this._onDidChangeTreeData.event;
	/**@type {import('vscode').TreeView}*/
	treeView = vscode.window.createTreeView('dzr.queue', { treeDataProvider: this, dragAndDropController: this, canSelectMany: true });
	highlighted = (label, active) => ({ label, highlights:/**@type {[number, number][]}*/(active ? [[0, label.length]] : []) })
	/**@returns {vscode.TreeItem} */
	getTreeItem = (item) => ({
		iconPath: new vscode.ThemeIcon("music"),
		label: this.highlighted(item.title + ' - ' + item.artists.map(a => a.name).join(), item == this.state.current),
		description: hhmmss(item.duration || 0) + " " + (item.version || ''),
		contextValue: 'dzr.track',
		command: { title: 'Play', command: 'dzr.load', tooltip: 'Play', arguments: [this.state.queue.indexOf(item)] },
	})
	getParent = () => null
	getChildren = () => {

		return this.state.queue
	}
	async handleDrag(sources, treeDataTransfer) {
		treeDataTransfer.set(this.dropMimeTypes[0], new vscode.DataTransferItem(sources));
	}
	async handleDrop(onto, transfer) {
		const sources = transfer.get(this.dropMimeTypes[0])?.value;
		if (!sources || sources.includes(onto)) return; //don't move selection onto one of it members
		const striped = this.state.queue.filter(item => !sources.includes(item));
		const index = this.state.queue.indexOf(onto);
		this.state.queue = [...striped.slice(0, index), ...sources, ...striped.slice(index)];
	}
}
exports.activate = async function (/**@type {import('vscode').ExtensionContext}*/ context) {
	// deezer didn't DMCA'd dzr so let's follow the same path here
	conf().get('cbc') || vscode.window.withProgress({ title: 'Extracting CBC key...', location }, async () => {
		const html_url = 'https://www.deezer.com/en/channels/explore';
		const html = (await fetch(html_url)).toString('utf-8');
		const js_url = html.match(/src="(http[^"]+app-web\.[^"]+\.js)"/)?.[1];
		if (!js_url) return await vscode.window.showErrorMessage('CBC Extract: No JS WebApp found');
		const keys = (await fetch(js_url)).toString('utf-8').match(/%5B0x..%2C.{39}%2C0x..%5D/g);
		const [a, b] = keys.map(part => part.slice(3, -3).split('%2C').map(i => String.fromCharCode(parseInt(i))).reverse());
		const cbc = a.map((a, i) => `${a}${b[i]}`).join('');// zip a+b
		const sha = crypto.createHash('sha1').update(cbc).digest('hex').slice(0, 8);
		if (sha != '3ad58d92') return await vscode.window.showErrorMessage('Bad extracted key');
		conf().update('cbc', cbc, vscode.ConfigurationTarget.Global);
	});
	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,
		// catch vscode://yne.dzr/* urls
		vscode.window.registerUriHandler({ handleUri(uri) { (({ path, query }) => vscode.commands.executeCommand(`dzr.${path.slice(1)}`, ...(query ? JSON.parse(query) : [])))(uri); } }),
		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.href", (track) => vscode.env.openExternal(vscode.Uri.parse(`https://deezer.com/track/${track.id}`))),
		vscode.commands.registerCommand("dzr.loopQueue", () => dzr.state.looping = "queue"),
		vscode.commands.registerCommand("dzr.loopTrack", () => dzr.state.looping = "track"),
		vscode.commands.registerCommand("dzr.loopOff", () => dzr.state.looping = "off"),
		vscode.commands.registerCommand("dzr.add", async (path, label) => with_url(await browse(path, label)).then(tracks => dzr.state.queue = [...dzr.state.queue, ...tracks])),
		vscode.commands.registerCommand("dzr.remove", async (item, items) => (items || [item]).map(i => vscode.commands.executeCommand('dzr.removeAt', dzr.state.queue.indexOf(i)))),
		vscode.commands.registerCommand("dzr.removeAt", async (index) => index >= 0 && (dzr.state.queue = [...dzr.state.queue.slice(0, index), ...dzr.state.queue.slice(index + 1)])),
		vscode.commands.registerCommand("dzr.clear", async () => dzr.state.queue = []),
		vscode.commands.registerCommand("dzr.shareAll", async () => vscode.commands.executeCommand("dzr.share")),
		vscode.commands.registerCommand("dzr.share", async (track, tracks) => {
			const ids = JSON.stringify(track ? [(tracks || [track]).map(e => e.id || track.id)] : [dzr.state.queue.map(q => q.id)]);
			vscode.env.clipboard.writeText(vscode.Uri.from({ scheme: "vscode", authority: context.extension.id, path: '/add', query: ids }).toString())
		}),
		vscode.commands.registerCommand("dzr.shuffle", async () => {
			const shuffle = [...dzr.state.queue];
			for (let i = shuffle.length - 1; i > 0; i--) {
				const j = Math.floor(Math.random() * (i + 1));
				[shuffle[i], shuffle[j]] = [shuffle[j], shuffle[i]];
			}
			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);
			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
			}
			if (!dzr.state.ready) {
				vscode.commands.executeCommand('dzr.show');
				while (!dzr.state.ready) await wait();
			}
			if ((dzr.state.queue[pos].expire || 0) < (+new Date() / 1000)) {
				dzr.state.queue = await with_url(dzr.state.queue);//TODO: hope item is now up to date
			}
			const prev = dzr.state?.current;
			dzr.state.current = dzr.state.queue[pos];
			dzr._onDidChangeTreeData.fire(prev);
			dzr._onDidChangeTreeData.fire(this.state?.current);
			const hex = (str) => str.split('').map(c => c.charCodeAt(0))
			const md5 = hex(crypto.createHash('md5').update(`${dzr.state.current.id}`).digest('hex'));
			const key = Buffer.from(hex(conf().get('cbc')).map((c, i) => c ^ md5[i] ^ md5[i + 16]));
			const iv = Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]);
			const stripe = 2048;//TODO:use .pipe() API https://codereview.stackexchange.com/questions/57492/
			dzr.post('open', dzr.state.current, conf().get("volume"));
			const buf_enc = await fetch(dzr.state.current.url);
			for (let pos = 0; pos < buf_enc.length; pos += stripe) {
				if ((pos >> 11) % 3) continue;
				const ciph = crypto.createDecipheriv('bf-cbc', key, iv).setAutoPadding(false)
				const deco = ciph.update(buf_enc.subarray(pos, pos + stripe));
				buf_enc.set(deco, pos);
			}
			dzr.post('append', Uint8Array.from(buf_enc));
			await dzr.postAck;
			dzr.post('close');
		}),
	)
}

================================================
FILE: extension/package.json
================================================
{
	"name": "dzr",
	"displayName": "DZR player",
	"description": "deezer.com player",
	"version": "0.3.0",
	"publisher": "yne",
	"engines": {
		"vscode": "^1.73.0"
	},
	"main": "./main.js",
	"extensionKind": [
		"ui",
		"workspace"
	],
	"activationEvents": [
		"onStartupFinished"
	],
	"capabilities": {
		"untrustedWorkspaces": {
			"supported": true
		}
	},
	"contributes": {
		"viewsWelcome": [
			{
				"view": "explorer.dzr",
				"contents": "not shown"
			}
		],
		"keybindings (displayed but dont work)": [
			{
				"command": "dzr.remove",
				"key": "delete"
			}
		],
		"commands": [
			{
				"category": "dzr",
				"command": "dzr.show",
				"title": "Show Player Tab",
				"icon": "$(eye)"
			},
			{
				"category": "dzr",
				"command": "dzr.play",
				"title": "Play",
				"icon": "$(debug-run)"
			},
			{
				"category": "dzr",
				"command": "dzr.href",
				"title": "Open Web",
				"when": "false",
				"icon": "$(link-external)"
			},
			{
				"category": "dzr",
				"command": "dzr.pause",
				"title": "Pause",
				"icon": "$(debug-pause)"
			},
			{
				"category": "dzr",
				"command": "dzr.load",
				"title": "Play next",
				"icon": "$(chevron-right)"
			},
			{
				"category": "dzr",
				"command": "dzr.loopQueue",
				"title": "Loop Queue",
				"icon": "$(sync)"
			},
			{
				"category": "dzr",
				"command": "dzr.loopTrack",
				"title": "Loop Track",
				"icon": "$(redo)"
			},
			{
				"category": "dzr",
				"command": "dzr.loopOff",
				"title": "Loop Disabled",
				"icon": "$(sync-ignored)"
			},
			{
				"category": "dzr",
				"command": "dzr.add",
				"title": "Queue Add",
				"icon": "$(add)"
			},
			{
				"category": "dzr",
				"command": "dzr.remove",
				"title": "Queue Remove Track",
				"when": "false",
				"icon": "$(close)"
			},
			{
				"category": "dzr",
				"command": "dzr.clear",
				"title": "Queue Remove All",
				"icon": "$(clear-all)"
			},
			{
				"category": "dzr",
				"command": "dzr.shuffle",
				"title": "Queue Shuffle",
				"icon": "$(arrow-swap)"
			},
			{
				"category": "dzr",
				"command": "dzr.share",
				"title": "Copy vscode:// Link(s)",
				"icon": "$(link)"
			},
			{
				"category": "dzr",
				"command": "dzr.shareAll",
				"title": "Copy all vscode:// Links",
				"icon": "$(link)"
			}
		],
		"menus": {
			"view/title": [
				{
					"when": "view == dzr.queue",
					"group": "navigation@9",
					"command": "dzr.add"
				},
				{
					"when": "view == dzr.queue && dzr.queue!=''",
					"group": "navigation@8",
					"command": "dzr.clear"
				},
				{
					"when": "view == dzr.queue && dzr.queue!=''",
					"group": "navigation@7",
					"command": "dzr.shareAll"
				},
				{
					"when": "view == dzr.queue && dzr.queue!=''",
					"group": "navigation@6",
					"command": "dzr.shuffle"
				},
				{
					"when": "view == dzr.queue && dzr.queue!='' && dzr.looping == 'off'",
					"group": "navigation@2",
					"command": "dzr.loopQueue"
				},
				{
					"when": "view == dzr.queue && dzr.queue!='' && dzr.looping == 'queue'",
					"group": "navigation@2",
					"command": "dzr.loopTrack"
				},
				{
					"when": "view == dzr.queue && dzr.queue!='' && dzr.looping == 'track'",
					"group": "navigation@2",
					"command": "dzr.loopOff"
				}
			],
			"view/item/context": [
				{
					"group": "inline",
					"command": "dzr.remove",
					"when": "viewItem == dzr.track && !listMultiSelection"
				},
				{
					"group": "navigation",
					"command": "dzr.remove",
					"when": "viewItem == dzr.track && listMultiSelection"
				},
				{
					"group": "navigation",
					"command": "dzr.href",
					"when": "viewItem == dzr.track && !listMultiSelection"
				},
				{
					"group": "navigation",
					"command": "dzr.share",
					"when": "viewItem == dzr.track"
				}
			]
		},
		"configuration": {
			"type": "object",
			"title": "dzr configuration",
			"properties": {
				"dzr.cbc": {
					"type": "string",
					"default": "",
					"description": "track decryption key"
				},
				"dzr.arl": {
					"type": "string",
					"default": "",
					"description": "Access Rights Locator"
				},
				"dzr.format": {
					"type": "string",
					"default": "MP3_128",
					"enum": [
						"AAC_64",
						"FLAC",
						"MP3_64",
						"MP3_128",
						"MP3_256",
						"MP3_320",
						"MP4_RA1",
						"MP4_RA2",
						"MP4_RA3"
					],	
					"description": "Requested format"
				},
				"dzr.nextCount": {
					"type": "number",
					"default": 5,
					"markdownDescription": "Number of `.next` page to fetch before showing quickpick (`-1 for unlimited`)"
				},
				"dzr.secure": {
					"type": "boolean",
					"default": false,
					"description": "Disable if you are behind a corporate proxy"
				},
				"dzr.volume": {
					"type": "number",
					"default": 1,
					"minimum": 0,
					"maximum": 1,
					"description": "Player volume [0..1], Applied at next track play"
				},
				"dzr.queue": {
					"type": "array",
					"default": [],
					"description": "Persistent track queue"
				},
				"dzr.looping": {
					"type": "string",
					"enum": [
						"queue",
						"track",
						"off"
					],
					"default": "queue",
					"description": "Queue looping"
				},
				"dzr.menus": {
					"type": "object",
					"description": "API tree (since deezer endpoint are not RESTful)",
					"default": {
						"_": [
							{
								"path": "search/track?q=",
								"label": "$(play-circle) track search"
							},
							{
								"path": "search/artist?q=",
								"label": "$(person) artist search"
							},
							{
								"path": "search/album?q=",
								"label": "$(issues) album search"
							},
							{
								"path": "search/playlist?q=",
								"label": "$(list-unordered) playlist search"
							},
							{
								"path": "search/user?q=",
								"label": "$(account) user search"
							},
							{
								"path": "search/radio?q=",
								"label": "$(broadcast) radio search"
							},
							{
								"path": "genre",
								"label": "$(telescope) explore"
							},
							{
								"path": "radio",
								"label": "$(broadcast) radios list"
							},
							{
								"path": "user/0",
								"label": "$(account) user id"
							}
						],
						"_artist_0": [
							{
								"path": "/top?limit=50",
								"label": "$(star) Top Tracks"
							},
							{
								"path": "/albums",
								"label": "$(issues) Albums"
							},
							{
								"path": "/related",
								"label": "$(person) Similar Artists"
							},
							{
								"path": "/radio",
								"label": "$(broadcast) Flow"
							},
							{
								"path": "/playlists",
								"label": "$(list-unordered) Playlists"
							}
						],
						"_user_0": [
							{
								"path": "/playlists",
								"label": "$(list-unordered) Playlists"
							},
							{
								"path": "/tracks",
								"label": "$(play-circle) Favorite Tracks"
							},
							{
								"path": "/albums",
								"label": "$(issues) Favorite Albums"
							},
							{
								"path": "/artists",
								"label": "$(person) Favorite Artists"
							},
							{
								"path": "/flow",
								"label": "$(broadcast) Flow"
							},
							{
								"path": "/charts",
								"label": "$(play-circle) Charts"
							}
						],
						"_genre_0": [
							{
								"path": "/radios",
								"label": "$(broadcast) radios"
							},
							{
								"path": "/artists",
								"label": "$(person) Artists"
							}
						],
						"_radio_0": [
							{
								"path": "/tracks",
								"label": "$(list-unordered) tracks"
							}
						],
						"_album_0": [
							{
								"path": "/tracks",
								"label": "$(list-unordered) tracks"
							}
						]
					}
				},
				"dzr.type2icon": {
					"type": "object",
					"description": "VSCode picker icon to display according to item type",
					"additionalProperties": true,
					"default": {
						"track": "$(play-circle) ",
						"artist": "$(person) ",
						"album": "$(issues) ",
						"playlist": "$(list-unordered)",
						"radio": "$(broadcast) ",
						"genre": "$(telescope) ",
						"user": "$(account) "
					},
					"patternProperties": {
						".*": {
							"type": "string"
						}
					}
				}
			}
		},
		"viewsContainers": {
			"activitybar": [
				{
					"id": "dzr",
					"title": "dzr",
					"icon": "logo.svg"
				}
			]
		},
		"views": {
			"dzr": [
				{
					"id": "dzr.queue",
					"name": "Player Queue",
					"icon": "logo.svg"
				}
			]
		}
	}
}

================================================
FILE: extension/webview.html
================================================
<!--VSCode require at least 1 (one) user interaction for Audio playback: use this popup -->
<dialog open style="border:none;border-radius:25px;box-shadow: 0 0 0 2000px rgb(20 20 20 / 95%);z-index: 9;">
    <form method="dialog">
        <h1>Disclamer</h1>
        <ul>
            <li>Use the <b>Player Queue side-panel</b> to add/remove track</li>
            <li>Closing <b>this player tab</b> will stop the playback</li>
        </ul>
        <div><button style="width: 100%;">OK</button></div>
    </form>
</dialog>
<img style="display: block;border:0;margin: auto; width: 50vmin; height: 50vmin; margin-top: 10vmin;border-radius:27px">
<div id="title" style="font-size:20px;margin: 20px 0 0 0;text-align: center;"></div>
<div id="artists" style="font-size:20px;margin: 20px;display: flex;justify-content: center;gap: 0.5em;flex-flow: wrap;"></div>
<audio controls onloadstart="play()" style="display: block;margin: auto;width: 50vmin;"></audio>
<script type=module>
const el = (tag, props={}, ch=[]) => ch.reduce((e,c) => (e.appendChild(c),e),Object.assign(document.createElement(tag),props));
const post = acquireVsCodeApi().postMessage;
const [img] = document.getElementsByTagName('img');
const [audio] = document.getElementsByTagName('audio');
const [dialog] = document.getElementsByTagName('dialog');
const title = document.getElementById('title');
const artists = document.getElementById('artists');
const cmd = (name, ...args) => `command:${name}?` + encodeURIComponent(JSON.stringify(args));
dialog.onclose=()=>{audio.play();post(['user_interact']);}
['ended', 'pause', 'playing', 'volumechange'].map(on => audio.addEventListener(on, () => post(['player_' + on, {volume:audio.volume}])));
let mediaSource, sourceBuffer;
const image = (type,md5,size=80) => `https://e-cdns-images.dzcdn.net/images/${type}/${md5}/${size}x${size}.jpg`
const on = {// event from VSCode
    async open(item,volume) {
        mediaSource = new window.MediaSource();
        audio.volume = volume;
        audio.src = window.URL.createObjectURL(mediaSource);
        await new Promise(then => mediaSource.onsourceopen = () => then());
        sourceBuffer = mediaSource.addSourceBuffer("audio/mpeg");
        sourceBuffer.addEventListener("updateend", (ev) => post(['player_bufferized']));
    },
    append(buf) { sourceBuffer.appendBuffer(buf) },
    close() { mediaSource.endOfStream() },
    play() { audio.play().catch(e=>post(['error', `${e}`])) },
    pause() { audio.pause() },
    state(state, updates=[]) {
        if (updates.includes('current')) {
            title.innerText = state.current.title;
            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)])));
            img.src = image("cover", state.current.md5_image, 1000);
            navigator.mediaSession.metadata = new MediaMetadata({
                title: state.current.title,
                artist: state.current.artists.map(a=>a.name).join(),
                album: 'dzr',
                artwork: [{ src: img.src, sizes: "1000x1000", type: "image/jpg" }]
            });
        }
    },
};
window.addEventListener('message', ({ data: [cmd,...args] }) => {
    if (!on[cmd]) return console.log("bad message:" + JSON.stringify([cmd,...args]));
    on[cmd](...args);
});
navigator.mediaSession.setActionHandler("play", on.play);
navigator.mediaSession.setActionHandler("pause", on.pause);
navigator.mediaSession.setActionHandler("nexttrack", ()=>post(['user_next']))
//navigator.mediaSession.setActionHandler("previoustrack", ()=>{})
</script>
<style>
    a:not(:hover){text-decoration: none;}
    img:not([src]){opacity: 0;}
    dialog::backdrop {background-color: rgba(0, 0, 0, .9);}
    #artists>a {padding-left: 1.5em;background-repeat: no-repeat;background-size: contain;}
</style>

================================================
FILE: index.html
================================================
<!doctype html>
<html lang=en>
<meta name="viewport" content="width=device-width, user-scalable=no">
<script type=module>
const $$ = document.querySelectorAll.bind(document);
const el = (tag, props={}, ch=[]) => ch.reduce((e,c) => (e.appendChild(c),e),Object.assign(document.createElement(tag),props))
const api = (path, callback=`cb_${+new Date()}`) => new Promise(function(ok, ko) {
	window[callback] = (data) => {delete window[callback]; data?.error?ko(data.error):ok(data)};
	const src = new URL(`https://api.deezer.com/${path}`);
	src.searchParams.append('output','jsonp');
	src.searchParams.append('callback',callback);
	document.head.append(el('script', { src, onload:({target})=>target.parentNode.removeChild(target)}));
});
const rel = (h) => h.startsWith('/') ? h : `${location.hash.slice(1)}/${h}`
const list = (tags) => document.forms.results.replaceChildren(...tags.map(([tag, props]) => el(tag, {innerText:props?.href, ...props, href:`#${rel(props.href)}`})))
const toLink = (...links) => links.map(href => ['a', {href}])
const routes = {
	'' :     (_) => list(toLink('/search/track?q=','/search/artist?q=','/search/album?q=','/search/playlist?q=','/search/radio?q=','/search/user?q=','/user/0','/genre','/radio')),
	genre_0: (_) => list(toLink('radios','artists')),
	radio_0: (_) => list(toLink('tracks', 'fans')),
	album_0: (_) => list(toLink('tracks', 'fans')),
	user_0:  (_) => list(toLink('charts','albums','playlist','flow','tracks','artists')),
	artist_0:(_) => list(toLink('top?limit=50','albums','fans','related','radio','playlist')),
	default: async (h) => list( (await api(h)).data.map(d => ['a', {
		href:`/${d.type}/${d.id}`,
		innerText: `${d.title||d.name} ${d.artist?.name||''}`
	}]) )
};
const modes = {
	none: (p) => {},
	replay: (p) => p.play(),
	random: (p) => window.onhashchange({newURL:$$('a[href^="#/track/"]')[0].href}),
	next: (p) => window.onhashchange({newURL:$$(`a[href="${p.dataset.href}"]`)[0].nextElementSibling.href}),
}
window.onhashchange = function(event) {
	const newurl = new URL(event.newURL);
	const path = newurl.hash.replace(/#?\/?/,'');
	if (path.endsWith('=')||path.endsWith('/0'))
	return location.hash += prompt(`query for ${path}`);
	if (path.startsWith('track')){
		if(!window.cgi.value) return alert('No dzr cgi url detected! (is dzr running as cgi ?)\nplease manually set it up in options');
		window.player.src = `${window.cgi.value}?`+newurl.hash.match(/\d+/);
		window.player.dataset.href = newurl.hash; // to find it back in next track
		window.document.title = path;
		return location.hash = (new URL(event.oldURL||location)).hash; // don't change URL
	}
	const route = path.replace(/[,?].*/,'').replace(/[^a-zA-Z0-9]+/g,'_').replace(/[0-9,]+/g,'0').replace(/_+/g,'_');
	(routes[route]||routes.default)(path);
}
window.onload = function() {
	window.player.onended = ()=>modes[window.mode.value](window.player);
	window.https.hidden = location.protocol=='http:';
	if (localStorage.dzr_mode) window.mode.value = localStorage.dzr_mode;
	if (localStorage.dzr_cgi) window.cgi.value = localStorage.dzr_cgi;
	else ['/cgi-bin/dzr', '//0.0.0.0:8000/cgi-bin/dzr', '//127.0.0.1:8000/cgi-bin/dzr', '//localhost:8000/cgi-bin/dzr']
	     .forEach(url => fetch(url,{method:'HEAD'}).then(e=>e.ok ? window.cgi.value = localStorage.dzr_cgi = url:0))
	window.onhashchange({newURL:`${location}`});
}
</script>
<body style="margin-bottom:100px;font-size:2em;font-family:monospace">
	<cite id=https>Warning: dzr don't normaly use https, please downgrade to http://</cite>
	<details style="display: grid">
		<summary>Options</summary>
		<label>dzr cgi url: <input id="cgi" placeholder="http://example:8000/cgi-bin/dzr"></label>
		<label>next track: <select id="mode" onchange="localStorage.dzr_mode=value"><option>none<option>replay<option>random<option>next</select></label>
	</details>
	<form name=results style="display: grid"></form>
	<audio id="player" autoplay controls style="position:fixed;bottom:0;left:0;width:100%"></audio>
</body>
Download .txt
gitextract_rsjai3cw/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── LICENSE
├── README.md
├── dzr
├── dzr-dec
├── dzr-id3
├── dzr-srt
├── dzr-url
├── extension/
│   ├── main.js
│   ├── package.json
│   └── webview.html
└── index.html
Download .txt
SYMBOL INDEX (20 symbols across 1 files)

FILE: extension/main.js
  function browse (line 28) | async function browse(url_or_event_or_ids, label) {
  constant DZR_PNG (line 65) | let DZR_PNG, USR_NFO;
  constant USR_NFO (line 65) | let DZR_PNG, USR_NFO;
  class DzrWebView (line 114) | class DzrWebView { // can't Audio() in VSCode, we need a webview
    method constructor (line 142) | constructor() {
    method renderStatus (line 147) | renderStatus() {
    method show (line 159) | async show(htmlUri, iconPath) {
    method initAckSemaphore (line 172) | initAckSemaphore() { this.postAck = new Promise((then) => this.waitAck...
    method player_bufferized (line 175) | player_bufferized() {
    method player_volumechange (line 179) | player_volumechange({ volume }) { conf().update("volume", volume, vsco...
    method player_playing (line 180) | player_playing() { this.state.ready = this.state.playing = true; }
    method player_pause (line 181) | player_pause() { this.state.playing = false; }
    method player_ended (line 182) | player_ended() { vscode.commands.executeCommand('dzr.load', null); }
    method user_interact (line 183) | user_interact() { this.state.ready = true; }
    method user_next (line 184) | user_next() { vscode.commands.executeCommand('dzr.load'); }
    method error (line 185) | error(msg) { vscode.window.showErrorMessage(msg); }
    method badAction (line 186) | badAction(action) { console.error(`unHandled action "${action}" from w...
    method handleDrag (line 208) | async handleDrag(sources, treeDataTransfer) {
    method handleDrop (line 211) | async handleDrop(onto, transfer) {
  method handleUri (line 239) | handleUri(uri) { (({ path, query }) => vscode.commands.executeCommand(`d...
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (66K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 12,
    "preview": "github: yne\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1291,
    "preview": "name: ci\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: act"
  },
  {
    "path": "LICENSE",
    "chars": 1211,
    "preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
  },
  {
    "path": "README.md",
    "chars": 3185,
    "preview": "![dzr logo](.github/.logo.svg)\n\n# DZR: the command line deezer.com player\n\n## Features\n\n- Cross-platform support: Linux,"
  },
  {
    "path": "dzr",
    "chars": 12385,
    "preview": "#!/bin/sh\n\n# CGI call handling, for example moving ./dzr* bin to ~/cgi-bin and running :\n#     python3 -m http.server --"
  },
  {
    "path": "dzr-dec",
    "chars": 1450,
    "preview": "#!/bin/sh\n# NAME\n# \tdzr-dec - decode a deezer track by it ID\n# USAGE:\n# \tdzr-dec 321654 < enc.mp3 > dec.mp3\n\nSNG_ID=\"$1\""
  },
  {
    "path": "dzr-id3",
    "chars": 2034,
    "preview": "#!/bin/sh\n# USAGE Example:\n# ./dzr-tag 5404528 tagMe.mp3\n\nSNG_ID=$(echo \"$1\" | tr -dc '0-9') # extract id from path,url,"
  },
  {
    "path": "dzr-srt",
    "chars": 1540,
    "preview": "#!/bin/sh\n# USAGE Example:\n# ./dzr-srt 355777961\n\nSNG_ID=$(echo \"$1\" | tr -dc '0-9') # extract id from path,url,...\n[ -z"
  },
  {
    "path": "dzr-url",
    "chars": 2838,
    "preview": "#!/bin/sh\n# USAGE Example:\n# ./dzr-url 355777961 650744592 | while read url id; do curl -s \"$url\" | ./dzr-dec $id | mpv "
  },
  {
    "path": "extension/main.js",
    "chars": 16922,
    "preview": "// @ts-check\n/// curl -Ok https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.d.ts\n/// <refere"
  },
  {
    "path": "extension/package.json",
    "chars": 8422,
    "preview": "{\n\t\"name\": \"dzr\",\n\t\"displayName\": \"DZR player\",\n\t\"description\": \"deezer.com player\",\n\t\"version\": \"0.3.0\",\n\t\"publisher\": "
  },
  {
    "path": "extension/webview.html",
    "chars": 3916,
    "preview": "<!--VSCode require at least 1 (one) user interaction for Audio playback: use this popup -->\n<dialog open style=\"border:n"
  },
  {
    "path": "index.html",
    "chars": 3988,
    "preview": "<!doctype html>\n<html lang=en>\n<meta name=\"viewport\" content=\"width=device-width, user-scalable=no\">\n<script type=module"
  }
]

About this extraction

This page contains the full source code of the yne/dzr GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (57.8 KB), approximately 19.1k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!