[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: yne\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: ci\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v1\n    - name: check key extract\n      run: |\n        [ \"$(DZR_CBC= ./dzr ! 2>/dev/null | sha1sum)\" = '3ad58d9232a3745ad9308b0669c83b6f7e8dba4d  -' ]\n    - name: bundle VSIX\n      run: zip -r $(jq -r '.name+\"-\"+.version+\".vsix\"' < extension/package.json) extension\n    - name: publish\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        RELEASE_FILES: dzr*\n      run: |\n        RELEASE_TAG=\"$(date +%y%m%d)\"\n        curl -s -XPOST -d '{\"tag_name\": \"'$RELEASE_TAG'\"}' \\\n          -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n          -H 'Content-Type: application/json' \\\n          \"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases\" || :\n        RELEASE_ID=$(curl -s https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/$RELEASE_TAG | jq .id)\n        echo tag=$RELEASE_TAG has id=$RELEASE_ID\n        for RELEASE_FILE in $RELEASE_FILES; do\n          curl -s -XPOST -T $RELEASE_FILE \\\n          -H \"Authorization: token $GITHUB_TOKEN\" \\\n          -H \"Content-Type:application/octet-stream\" \\\n          \"https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets?name=$RELEASE_FILE\" || :\n        done\n"
  },
  {
    "path": "LICENSE",
    "content": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, compile, sell, or\ndistribute this software, either in source code form or as a compiled\nbinary, for any purpose, commercial or non-commercial, and by any\nmeans.\n\nIn jurisdictions that recognize copyright laws, the author or authors\nof this software dedicate any and all copyright interest in the\nsoftware to the public domain. We make this dedication for the benefit\nof the public at large and to the detriment of our heirs and\nsuccessors. We intend this dedication to be an overt act of\nrelinquishment in perpetuity of all present and future rights to this\nsoftware under copyright law.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR\nOTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\nARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\nFor more information, please refer to <http://unlicense.org>\n\n"
  },
  {
    "path": "README.md",
    "content": "![dzr logo](.github/.logo.svg)\n\n# DZR: the command line deezer.com player\n\n## Features\n\n- Cross-platform support: Linux, *BSD, MacOS, Android, Windows+WSL\n- Little dependencies: `curl`, `jq`, `dialog`, `openssl` (`openssl-tool` in Android)\n- Real-time Lyrics display\n- Web interface support (see [dzr](https://github.com/topics/dzr)-tagged frontend)\n- ID3v2 tag injector from Deezer metadata (cover, artist, ...)\n- Play without storing/caching on your machine for [legal reasons](https://github.com/github/dmca/blob/master/2021/02/2021-02-10-deezer.md)\n- No private deezer key in the source (auto-extracted from web player, also for legal reasons)\n- VSCode extension [VSIX](https://github.com/yne/dzr/releases) experimental port\n\n## Preview (CLI)\n\n[![asciicast](https://asciinema.org/a/406758.svg)](https://asciinema.org/a/406758)\n\n## Preview (VSIX)\n\n![Screenshot](https://github.com/yne/dzr/assets/5113053/37b6cd26-8876-4d77-92bb-293ff248e21d)\n\n## Install\n\n| Platform | command | version |\n|----------|---------|---------|\n| MacOS + [brew](https://formulae.brew.sh/formula/dzr)       | `brew install dzr` | ![](https://repology.org/badge/version-for-repo/homebrew/dzr.svg?header=)\n| Arch Linux + [AUR](https://aur.archlinux.org/packages/dzr) | `yay -S dzr`       | ![](https://repology.org/badge/version-for-repo/aur/dzr.svg?header=)\n| Gentoo + [GURU](https://github.com/gentoo/guru)            | `emerge --ask dzr` | ![](https://repology.org/badge/version-for-repo/gentoo_ovl_guru/dzr.svg?header=)\n| Ubuntu + [Snap](https://snapcraft.io/dzr) | `snap install --edge dzr` | [Help Me](https://github.com/yne/dzr/issues/25)\n| Linux + [Flatpak](https://www.flatpak.org/) | `flatpak install dzr` | [Help Me](https://github.com/yne/dzr/issues/25)\n| Nix + [Flake](https://wiki.nixos.org/wiki/flakes) | `nix run github.com/yne/dzr` | [Help Me](https://github.com/yne/dzr/issues/25)\n| 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)\n| VSCode | `code --install-extension ./path/to/dzr-*.vsix` | [![](https://img.shields.io/badge/VSIX-4c1?logo=visualstudiocode)](https://github.com/yne/dzr/releases)\n\n## Usage\n\n```sh\n# browse api.deezer.com\ndzr\n\n# browse a specific api.deezer.com url\ndzr /artist/860\n\n# play a specific track\ndzr /track/1043317462\n\n# use a custom PLAYER (mpg123 v1.31+ is a lightweight alternative)\nPLAYER=\"mpg123 -\" dzr\n\n# inject deezer ID3v2 into MP3 (require eyeD3) and rename it as $ARTIST - $TITLE.mp3\ndzr-id3 https://deezer.com/track/1043317462 tagme.mp3\n\n# show track lyrics as srt\ndzr-srt https://deezer.com/track/14408104\n\n# play track with it lyrics\nPLAYER='dzr-srt $id > .srt ; mpv --sub-file=.srt -' dzr /track/14408104\n\n# play track with it srt (using non-POSIX compliant process substitution)\nPLAYER='mpv --sub-file=<(dzr-srt $id) -' dzr /track/14408104\n\n# install dzr into ./cgi-bin/. Then serve it\nmkdir -p ./cgi-bin/ && install dzr* ./cgi-bin/\npython3 -m http.server --cgi\nopen http://127.0.0.1:8000/cgi-bin/dzr?6113114\n```\n"
  },
  {
    "path": "dzr",
    "content": "#!/bin/sh\n\n# CGI call handling, for example moving ./dzr* bin to ~/cgi-bin and running :\n#     python3 -m http.server --cgi\n# from your ~ (as non-root !) will reply to http://0.0.0.0:8000/cgi-bin/dzr?6113114,806027\nFETCH=${FETCH:-curl -s} # FETCH=\"wget -q -O -\" or FETCH=\"curl -s -k\"\nPLAYER=${PLAYER:-'mpv -'} # PLAYER=\"ffplay -\"\nDZR_SPONGE=${DZR_SPONGE:-cat -} # DZR_SPONGE=\"sponge\" from package \"moreutils\"\nDZR_RC=\"${DZR_RC:-${XDG_CONFIG_HOME:-$HOME/.config}/dzrrc}\"\n[ -f \"$DZR_RC\" ] && . \"$DZR_RC\" && export DZR_CBC DZR_ARL\nif [ \"$REQUEST_METHOD\" = \"HEAD\" ] ; then\n\tprintf 'Access-Control-Allow-Origin: *\\ndzr-api: 0\\n\\n'\nelif [ \"$REQUEST_METHOD\" = \"GET\" ] ; then\n\tprintf \"Cache-Control: max-age=31536000, immutable\\nContent-Type: audio/mpeg\\n\\n\"\n\techo \"$QUERY_STRING\" | xargs basename | xargs $0-url | while IFS= read -r line; do\n\t\turl=$(printf '%s\\n' \"$line\" | cut -f1)\n\t\tid=$(printf '%s\\n' \"$line\" | cut -f2)\n\t\t$FETCH \"$url\" | $DZR_SPONGE | $0-dec $id\n\tdone\nfi\n[ ! -z \"$REQUEST_METHOD\" ] && exit\n\n# extraction + warning by charleywright, see: https://github.com/yne/dzr/issues/11\nunscramble(){ printf \"${8}${16}${7}${15}${6}${14}${5}${13}${4}${12}${3}${11}${2}${10}${1}${9}\";}\nif [ -z \"$DZR_CBC\" ]; then\n\tcommand -v $(echo \"$FETCH\" | cut -f 1 -d \" \") >/dev/null || { echo \"key extraction require $FETCH\" 1>&2 ; exit 1 ;}\n\tAPP_WEB=$($FETCH -L deezer.com/en/channels/explore | sed -n 's/.*src=\"\\([^\"]*app-web[^\"]*\\).*/\\1/p' | xargs $FETCH -L)\n\tTMP_CBC=$(echo \"$APP_WEB\"  | tr ,_ '\\n'  | sed -n 's/.*\\(%5B0x..%2C.\\{39\\}%2C0x..%5D\\).*/\\1/p' | sed 's/%../\\n/g' | xargs printf '\\\\%o ')\n\texport DZR_CBC=$(unscramble $TMP_CBC);\n\t[ \"-$@\" = \"-!\" ] && printf '%s' \"$DZR_CBC\" && exit\n\tmkdir -p \"$(dirname \"$DZR_RC\")\"\n\tgrep -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\"\n\tprintf \"DZR_CBC saved to %s\\n\" \"$DZR_RC\" >&2\nfi\nif [ -z \"$DZR_ARL\" ]; then\n\techo \"Starting on 2025-03-08, Anonymous playback are blocked.\"\n\techo \"To authenticate DZR to your account:\"\n\techo \"- Sign in/up to https://www.deezer.com/\"\n\techo \"- Then open https://www.deezer.com/desktop/login/electron/callback\"\n\techo \"- Right click on the 'Open' button, then 'copy link address'.\"\n\twhile true; do\n\t\techo \"past the deezer:// link here:\"\n\t\tread DZR_ARL\n\t\texport DZR_ARL=$(printf $DZR_ARL | cut -d'/' -f4)\n\t\techo \"$DZR_ARL\" | grep -q '^[0-9a-f]\\+$' && break;\n\t\techo \"Invalid ARL format (expect: ^[0-9a-f]+$):\"\n\t\techo $DZR_ARL\n\tdone\n\tmkdir -p \"$(dirname \"$DZR_RC\")\"\n\tgrep -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\"\n\tprintf \"DZR_ARL saved to %s\\n\" \"$DZR_RC\" >&2\nfi\n# dependencies check, see: https://github.com/yne/dzr/issues/12\nfor c in jq curl dialog openssl $(echo \"$PLAYER\" | cut -f 1 -d \" \"); do\n\tcommand -v $c >/dev/null || { echo \"$c is required\" 1>&2 ; UNMET_DEPENDENCIES=1;}\ndone\n[ -n \"$UNMET_DEPENDENCIES\" ] && exit 1;\n\n# fetch user info via gw-light API\nexport GW=\"https://www.deezer.com/ajax/gw-light.php\"\ngw() { $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\"}; }\nif [ -n \"$DZR_ARL\" ] && [ -z \"$DZR_SID\" ]; then\n\texport DZR_SID=$($FETCH \"www.deezer.com/ajax/gw-light.php?method=deezer.ping&api_version=1.0&api_token\" | jq -r .results.SESSION)\n\t_DZR_USR=$(DZR_API_TOK= gw deezer.getUserData '{}')\n\texport DZR_LOVED=$(printf \"%s\" \"$_DZR_USR\" | jq -r .results.USER.LOVEDTRACKS_ID)\n\texport DZR_USER_ID=$(printf \"%s\" \"$_DZR_USR\" | jq -r .results.USER.USER_ID)\n\texport DZR_API_TOK=$(printf \"%s\" \"$_DZR_USR\" | jq -r .results.checkForm)\n\texport DZR_LIC=$(printf \"%s\" \"$_DZR_USR\" | jq -r .results.USER.OPTIONS.license_token)\nfi\n\n# main\ntrap 'printf \"\\033]0;\\007\" >&2' EXIT\nSELF=$(command -v \"$0\") || SELF=\"$0\"\nDLG_LIST=\"dialog --keep-tite --output-fd 1 --menu $1: 0 0 0\"\nDLG_TEXT=\"dialog --keep-tite --output-fd 1 --inputbox $1: 0 0\"\nFMT_FUNC='s@[,?].*@@;s@[^a-zA-Z0-9]\\{1,\\}@_@g;s@[0-9,]\\{1,\\}@0@g;s@_\\{1,\\}$@@'\n\n# jq formatters for gw-light track lists\nGW_FMT_LIST='(.results.data[]|(\"/track/\"+(.SNG_ID|tostring), (.SNG_TITLE+\" - \"+.ART_NAME|gsub(\"\\\\x22\";\"\"))))'\nGW_FMT_TRKS='\"/track/\"+([.results.data[]|.SNG_ID]|map(tostring)|join(\",\"))'\n\n# -- helpers to extract IDs from paths --\n_id() { echo \"$1\" | sed 's@^/[^/]*/@@;s@/.*@@'; } # /artist/123/top -> 123\n_q()  { echo \"$1\" | sed 's/.*q=//'; }             # /search/track?q=foo -> foo\n# -- gw data fetchers (raw JSON, reused by browse + play) --\ngw_loved()           { gw playlist.getSongs \"{\\\"playlist_id\\\":\\\"$DZR_LOVED\\\",\\\"start\\\":0,\\\"nb\\\":10000}\"; }\ngw_flow()            { gw radio.getUserRadio \"{\\\"user_id\\\":\\\"$DZR_USER_ID\\\"}\"; }\ngw_playlists()       { gw deezer.userMenu '{}'; }\ngw_artists()         { gw deezer.pageProfile \"{\\\"user_id\\\":\\\"$DZR_USER_ID\\\",\\\"tab\\\":\\\"artists\\\",\\\"start\\\":0,\\\"nb\\\":10000}\"; }\ngw_albums()          { gw deezer.pageProfile \"{\\\"user_id\\\":\\\"$DZR_USER_ID\\\",\\\"tab\\\":\\\"albums\\\",\\\"start\\\":0,\\\"nb\\\":10000}\"; }\ngw_search_track()    { gw search.music \"{\\\"query\\\":\\\"$(_q \"$1\")\\\",\\\"filter\\\":\\\"ALL\\\",\\\"output\\\":\\\"TRACK\\\",\\\"start\\\":0,\\\"nb\\\":50}\"; }\ngw_search_artist()   { gw search.music \"{\\\"query\\\":\\\"$(_q \"$1\")\\\",\\\"filter\\\":\\\"ALL\\\",\\\"output\\\":\\\"ARTIST\\\",\\\"start\\\":0,\\\"nb\\\":50}\"; }\ngw_search_album()    { gw search.music \"{\\\"query\\\":\\\"$(_q \"$1\")\\\",\\\"filter\\\":\\\"ALL\\\",\\\"output\\\":\\\"ALBUM\\\",\\\"start\\\":0,\\\"nb\\\":50}\"; }\ngw_search_playlist() { gw search.music \"{\\\"query\\\":\\\"$(_q \"$1\")\\\",\\\"filter\\\":\\\"ALL\\\",\\\"output\\\":\\\"PLAYLIST\\\",\\\"start\\\":0,\\\"nb\\\":50}\"; }\ngw_search_radio()    { gw search.music \"{\\\"query\\\":\\\"$(_q \"$1\")\\\",\\\"filter\\\":\\\"ALL\\\",\\\"output\\\":\\\"RADIO\\\",\\\"start\\\":0,\\\"nb\\\":50}\"; }\ngw_artist_top()      { gw artist.getTopTrack \"{\\\"ART_ID\\\":$(_id \"$1\"),\\\"nb\\\":100}\"; }\ngw_artist_albums()   { gw album.getDiscography \"{\\\"ART_ID\\\":$(_id \"$1\"),\\\"nb\\\":50,\\\"nb_songs\\\":0,\\\"start\\\":0,\\\"discography_mode\\\":\\\"all\\\"}\"; }\ngw_artist_related()  { gw artist.getRelatedArtist \"{\\\"ART_ID\\\":$(_id \"$1\"),\\\"nb\\\":25,\\\"start\\\":0}\"; }\ngw_artist_radio()    { gw radio.getArtistRadio \"{\\\"ART_ID\\\":$(_id \"$1\"),\\\"nb\\\":40}\"; }\ngw_album()           { gw song.getListByAlbum \"{\\\"ALB_ID\\\":$(_id \"$1\"),\\\"nb\\\":-1}\"; }\ngw_playlist()        { gw playlist.getSongs \"{\\\"playlist_id\\\":\\\"$(_id \"$1\")\\\",\\\"start\\\":0,\\\"nb\\\":10000}\"; }\ngw_radio()           { gw radio.getSongs \"{\\\"RADIO_ID\\\":$(_id \"$1\"),\\\"nb\\\":40}\"; }\n\n# -- jq formatters for non-track lists --\nFMT_ARTIST='(.results.data[]|(\"/artist/\"+(.ART_ID|tostring), (.ART_NAME|gsub(\"\\\\x22\";\"\"))))'\nFMT_ALBUM='(.results.data[]|(\"/album/\"+(.ALB_ID|tostring), ((.ALB_TITLE+\" \"+.ART_NAME)|gsub(\"\\\\x22\";\"\"))))'\nFMT_DISCOG='(.results.data[]|(\"/album/\"+(.ALB_ID|tostring), ((.ALB_TITLE+\" (\"+(.PHYSICAL_RELEASE_DATE//\"\"|split(\"-\")[0])+\")\")|gsub(\"\\\\x22\";\"\"))))'\nFMT_PLAYLIST='(.results.data[]|(\"/playlist/\"+(.PLAYLIST_ID|tostring), (.TITLE|gsub(\"\\\\x22\";\"\"))))'\nFMT_MY_PLAYLIST='(.results.PLAYLISTS.data[]|select(.TYPE!=\"4\")|(\"/playlist/\"+(.PLAYLIST_ID|tostring), ((.TITLE+\" (\"+(.NB_SONG|tostring)+\")\")|gsub(\"\\\\x22\";\"\"))))'\nFMT_MY_ARTIST='(.results.TAB.artists.data[]|(\"/artist/\"+(.ART_ID|tostring), (.ART_NAME|gsub(\"\\\\x22\";\"\"))))'\nFMT_MY_ALBUM='(.results.TAB.albums.data[]|(\"/album/\"+(.ALB_ID|tostring), ((.ALB_TITLE+\" \"+.ART_NAME)|gsub(\"\\\\x22\";\"\"))))'\nFMT_RADIO='(.results.data[]|(\"/radio/\"+(.RADIO_ID|tostring), (.TITLE|gsub(\"\\\\x22\";\"\"))))'\n\n# public API for browse-only endpoints (no gw-light equivalent)\nAPI=\"api.deezer.com\"\nPUB_FMT_GENRE='(.data[]|(\"/genre/\"+(.id|tostring), (.name|gsub(\"\\\\x22\";\"\"))))'\nPUB_FMT_RADIO='(.data[]|(\"/radio/\"+(.id|tostring), (.title|gsub(\"\\\\x22\";\"\"))))'\n\n# -- menu functions --\ndzr() {\n\t$DLG_LIST \\\n\t\t/flow 'my personalised mix' \\\n\t\t/loved 'my favourites' \\\n\t\t/playlists 'my playlists' \\\n\t\t/artists 'my artists' \\\n\t\t/albums 'my albums' \\\n\t\t/search/track?q= 'search track' \\\n\t\t/search/artist?q= 'search artist' \\\n\t\t/search/album?q= 'search album' \\\n\t\t/search/playlist?q= 'search playlist' \\\n\t\t/search/radio?q= 'search radio' \\\n\t\t/genre 'list genres' \\\n\t\t/radio 'list radios'\n}\n\ndzr_flow()             { gw_flow | jq \"$GW_FMT_LIST\" | xargs $DLG_LIST ∞ \"continuous flow\" ▸ \"play all\" ⇄ \"shuffle\" ;}\ndzr_loved()            { gw_loved | jq \"$GW_FMT_LIST\" | xargs $DLG_LIST ▸ \"play all\" ⇄ \"shuffle\" ;}\ndzr_playlists()        { gw_playlists | jq \"$FMT_MY_PLAYLIST\" | xargs $DLG_LIST ;}\ndzr_artists()          { gw_artists | jq \"$FMT_MY_ARTIST\" | xargs $DLG_LIST ;}\ndzr_albums()           { gw_albums | jq \"$FMT_MY_ALBUM\" | xargs $DLG_LIST ;}\n# search\ndzr_search_track()     { gw_search_track \"$1\" | jq \"$GW_FMT_LIST\" | xargs $DLG_LIST ▸ \"play all\" ⇄ \"shuffle\" ;}\ndzr_search_artist()    { gw_search_artist \"$1\" | jq \"$FMT_ARTIST\" | xargs $DLG_LIST ;}\ndzr_search_album()     { gw_search_album \"$1\" | jq \"$FMT_ALBUM\" | xargs $DLG_LIST ;}\ndzr_search_playlist()  { gw_search_playlist \"$1\" | jq \"$FMT_PLAYLIST\" | xargs $DLG_LIST ;}\ndzr_search_radio()     { gw_search_radio \"$1\" | jq \"$FMT_RADIO\" | xargs $DLG_LIST ;}\n# genre\ndzr_genre()            { $FETCH \"$API/genre\" | jq \"$PUB_FMT_GENRE\" | xargs $DLG_LIST ;} # public API - no gw-light equivalent\ndzr_genre_0()          { $DLG_LIST radios '' artists '' ;}\ndzr_genre_0_radios()   { $FETCH \"$API/genre/$(_id \"$1\")/radios\" | jq \"$PUB_FMT_RADIO\" | xargs $DLG_LIST ;}\ndzr_genre_0_artists()  { $FETCH \"$API/genre/$(_id \"$1\")/artists\" | jq '(.data[]|(\"/artist/\"+(.id|tostring), (.name|gsub(\"\\\\x22\";\"\"))))' | xargs $DLG_LIST ;}\n# radio\ndzr_radio()            { $FETCH \"$API/radio\" | jq \"$PUB_FMT_RADIO\" | xargs $DLG_LIST ;} # public API - no gw-light equivalent\ndzr_radio_0()          { gw_radio \"$1\" | jq \"$GW_FMT_LIST\" | xargs $DLG_LIST ▸ \"play all\" ⇄ \"shuffle\" ;}\n# artist\ndzr_artist_0()         { $DLG_LIST top 'top tracks' albums '' related '' radio '' ;}\ndzr_artist_0_top()     { gw_artist_top \"$1\" | jq \"$GW_FMT_LIST\" | xargs $DLG_LIST ▸ \"play all\" ⇄ \"shuffle\" ;}\ndzr_artist_0_albums()  { gw_artist_albums \"$1\" | jq \"$FMT_DISCOG\" | xargs $DLG_LIST ;}\ndzr_artist_0_related() { gw_artist_related \"$1\" | jq \"$FMT_ARTIST\" | xargs $DLG_LIST ;}\ndzr_artist_0_radio()   { gw_artist_radio \"$1\" | jq \"$GW_FMT_LIST\" | xargs $DLG_LIST ▸ \"play all\" ⇄ \"shuffle\" ;}\n# album\ndzr_album_0()          { gw_album \"$1\" | jq \"$GW_FMT_LIST\" | xargs $DLG_LIST ▸ \"play all\" ⇄ \"shuffle\" ;}\n# playlist\ndzr_playlist_0()       { gw_playlist \"$1\" | jq \"$GW_FMT_LIST\" | xargs $DLG_LIST ▸ \"play all\" ⇄ \"shuffle\" ;}\n\n\nplay() { # receive /track/1,2,3 from stdin\n\txargs basename | xargs $SELF-url | ${1:-cat} | while IFS= read -r line; do\n\t\tprintf '%s\\n' \"$line\" | grep -q '\t' || { echo \"error: unexpected dzr-url output: $line\" >&2; break; }\n\t\turl=$(printf '%s\\n' \"$line\" | cut -f1)\n\t\tid=$(printf '%s\\n' \"$line\" | cut -f2)\n\t\tinfo=$(printf '%s\\n' \"$line\" | cut -f3-)\n\t\tif [ -n \"$info\" ]; then\n\t\t\tprintf '%s %s # %s\\n' \"$SELF\" \"/track/$id\" \"$info\" >&2\n\t\t\tprintf '\\033]0;🎶 %s\\007' \"$(printf '%s' \"$info\" | sed 's/ \\[.*//;s/ (.*)//')\" >&2\n\t\tfi\n\t\t$FETCH \"$url\" | $DZR_SPONGE | $SELF-dec $id | eval ${PLAYER:-'mpv -'} || break # stop if Ctrl+C : $? = 4\n\tdone\n}\n# play_all: call the gw fetcher for this context, extract track IDs, play\nplay_all() { # $1=url $2=FUNC $3=order(cat|shuf)\n\tcase \"$2\" in\n\tdzr_flow)           gw_flow ;;\n\tdzr_loved)          gw_loved ;;\n\tdzr_search_track)   gw_search_track \"$1\" ;;\n\tdzr_artist_0_top)   gw_artist_top \"$1\" ;;\n\tdzr_artist_0_radio) gw_artist_radio \"$1\" ;;\n\tdzr_album_0)        gw_album \"$1\" ;;\n\tdzr_playlist_0)     gw_playlist \"$1\" ;;\n\tdzr_radio_0)        gw_radio \"$1\" ;;\n\t*) return 1 ;;\n\tesac | jq \"$GW_FMT_TRKS\" | play \"$3\"\n}\nfor url in \"$@\"; do\n\tcase $url in\n\t\t/track/*) echo $url | play; continue ;; # direct play\n\tesac\n\n\tFUNC=$(echo \"dzr_$url\" | sed \"$FMT_FUNC\") # path (/search/artist?q=x) to function (dzr_search_artist)\n\ttype $FUNC 1>/dev/null 2>/dev/null || { echo \"error: unknown path $url\" >&2; continue; }\n\tcase $url in # catch url that need user input (query, id)\n\t\t*=)  url=\"$url$($DLG_TEXT ''|jq -rR '.|@uri')\" ;; # escaped query\n\t\t*/0) url=\"$url$($DLG_TEXT ''|jq -rR '.|@uri')\" ;; # escaped id\n\tesac\n\n\techo \"$SELF $url\" >&2\n\twhile path=$($FUNC $url); do # browse REPL\n\t\tcase \"$path\" in\n\t\t▸) play_all \"$url\" \"$FUNC\" cat; break ;;\n\t\t⇄) play_all \"$url\" \"$FUNC\" shuf; break ;;\n\t\t∞) while true; do gw_flow | jq \"$GW_FMT_TRKS\" | play cat || break; done; break ;;\n\t\t/*) $SELF \"$path\";;\n\t\t*)  $SELF \"$url/$path\";;\n\t\tesac\n\tdone\ndone\n[ $# -eq 0 ] && $SELF '' # give an argument to iterate over (if none given)\n"
  },
  {
    "path": "dzr-dec",
    "content": "#!/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\"\n[ -z \"$DZR_CBC\" ] && echo \"Missing 'DZR_CBC' env variable\" 1>&2 && exit 1\n[ -z \"$SNG_ID\"  ] && echo \"USAGE: DZR_CBC=XXXX dzr-dec 1234 < enc.mp3 > dec.mp3\" 1>&2 && exit 1\ndzr_cbc_hex=$(printf \"$DZR_CBC\"                                  | od -An -t x1 |tr -d '\\n ')\ntrack_md5_l=$(printf \"%s\" \"$SNG_ID\" | openssl md5 -r|cut -b1-16  | od -An -t x1 |tr -d '\\n ')\ntrack_md5_r=$(printf \"%s\" \"$SNG_ID\" | openssl md5 -r|cut -b17-32 | od -An -t x1 |tr -d '\\n ')\ntrack_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\n\ta=$(printf $dzr_cbc_hex | cut -b $k-$(($k+1)))\n\tb=$(printf $track_md5_l | cut -b $k-$(($k+1)))\n\tc=$(printf $track_md5_r | cut -b $k-$(($k+1)))\n\tprintf '%02x' \"$((0x$a ^ 0x$b ^ 0x$c))\"\ndone)\n\nstripe_size=2048\nopenssl_opt=\"-d -nopad -bufsize $stripe_size -K $track_key -iv 0001020304050607\"\n# OpenSSL 3 require som extra argument\nif openssl bf-cbc -help 2>&1 | grep -q provider; then\n\topenssl_opt=\"$openssl_opt -provider legacy\";\nfi\n# And now for my next loop, I'd like to return to the classics\nset -e; # if an iteration fail we stop the loop (you better pipe me with curl)\nwhile true; do\n    LC_ALL=POSIX dd bs=$stripe_size count=1 status=none | openssl bf-cbc $openssl_opt 2>/dev/null 1>&4\n  { LC_ALL=POSIX dd bs=$stripe_size count=2 2>&3 >&4; } 3>&1 | grep -qe '^0[+]0 ' && break\ndone 4>&1\n"
  },
  {
    "path": "dzr-id3",
    "content": "#!/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,...\nif [ -z \"$SNG_ID\" -o ! -f \"$2\" ]; then\n    echo \"USAGE: dzr-id3 5404528 target.mp3\" && exit 1\nfi\n\nFETCH=${FETCH:-curl -s} # FETCH=\"wget -q -O -\" or FETCH=\"curl -s -k\"\ngw () {\n  method=\"$1\"; session=\"$2\" ;apiToken=\"$3\" ; shift 3 # curl args ...\n  $FETCH \"https://www.deezer.com/ajax/gw-light.php?method=$method&input=3&api_version=1.0&api_token=$apiToken\" --header \"Cookie: sid=$session\" \"$@\"\n}\n\nDZR_URL=\"www.deezer.com/ajax/gw-light.php?method=deezer.ping&api_version=1.0&api_token\"\nDZR_SID=$($FETCH \"$DZR_URL\" | jq -r .results.SESSION)\nUSR_NFO=$(gw deezer.getUserData \"$DZR_SID\" \"$API_TOK\")\nUSR_TOK=$(printf \"%s\" \"$USR_NFO\" | jq -r .results.USER_TOKEN)\nUSR_LIC=$(printf \"%s\" \"$USR_NFO\" | jq -r .results.USER.OPTIONS.license_token)\nAPI_TOK=$(printf \"%s\" \"$USR_NFO\" | jq -r .results.checkForm)\nSNG_NFO=$(gw song.getListData \"$DZR_SID\" \"$API_TOK\" --data \"{\\\"sng_ids\\\":[$SNG_ID]}\")\n\n# extract ID3 field we need\nART_NAME=$(echo \"${SNG_NFO}\" | jq -r .results.data[0].ART_NAME)\nREL_DATE=$(echo \"${SNG_NFO}\" | jq -r .results.data[0].PHYSICAL_RELEASE_DATE) # TODO: sometimes 0000-00-00\nSNG_TITLE=$(echo \"${SNG_NFO}\" | jq -r .results.data[0].SNG_TITLE)\nALB_TITLE=$(echo \"${SNG_NFO}\" | jq -r .results.data[0].ALB_TITLE)\nTRK_NUMBR=$(echo \"${SNG_NFO}\" | jq -r .results.data[0].TRACK_NUMBER)\nALB_PICTURE=$(echo \"${SNG_NFO}\" | jq -r .results.data[0].ALB_PICTURE)\nAPICNAME=\".$ALB_PICTURE.jpg\"\n$FETCH \"https://e-cdn-images.dzcdn.net/images/cover/$ALB_PICTURE/1024x1024-000000-100-0-0.jpg\" > \"$APICNAME\"\nFILENAME=\"$ART_NAME - $SNG_TITLE.mp3\"\n# eyeD3 mid3/mutagen (both python lib) seems to be the only one supporting APIC tag\n# contact me if you find a dependency-less widely-available id3tag editor\neyeD3 \\\n  --artist \"$ART_NAME\" \\\n  --title \"$SNG_TITLE\" \\\n  --album \"$ALB_TITLE\" \\\n  --track \"$TRK_NUMBR\" \\\n  --release-date \"$REL_DATE\" \\\n  --add-image \"$APICNAME:FRONT_COVER\" \\\n  \"$2\" &&\nrm \"$APICNAME\" &&\nmv \"$2\" \"$FILENAME\"\n"
  },
  {
    "path": "dzr-srt",
    "content": "#!/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 \"$SNG_ID\" ] && echo \"USAGE: $0 5404528\" && exit 1\n\nFETCH=${FETCH:-curl -s} # FETCH=\"wget -q -O -\" or FETCH=\"curl -s -k\"\ngw () {\n  method=\"$1\"; session=\"$2\" ;apiToken=\"$3\" ; shift 3 # curl args ...\n  $FETCH \"https://www.deezer.com/ajax/gw-light.php?method=$method&input=3&api_version=1.0&api_token=$apiToken\" --header \"Cookie: sid=$session\" \"$@\"\n}\n\nDZR_URL=\"www.deezer.com/ajax/gw-light.php?method=deezer.ping&api_version=1.0&api_token\"\nDZR_SID=$($FETCH \"$DZR_URL\" | jq -r .results.SESSION)\nUSR_NFO=$(gw deezer.getUserData \"$DZR_SID\" \"$API_TOK\")\nUSR_TOK=$(printf \"%s\" \"$USR_NFO\" | jq -r .results.USER_TOKEN)\nUSR_LIC=$(printf \"%s\" \"$USR_NFO\" | jq -r .results.USER.OPTIONS.license_token)\nAPI_TOK=$(printf \"%s\" \"$USR_NFO\" | jq -r .results.checkForm)\n#printf \"SID=$DZR_SID\\nAPI=$API_TOK\\nLIC=$USR_LIC\\nTOK=$USR_TOK\\nIDS=$SNG_ID\\n\" 1>&2\n\ngw 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' | \nwhile IFS='\t' read -r id start length text ; do\n  from=$(date +%H:%M:%S,%N --date \"$start\" | cut -c1-12) ;\n  to=$(date +%H:%M:%S,%N --date \"$start + $length second\" | cut -c1-12) ;\n  printf \"$id\\n${from} --> ${to}\\n$text\\n\\n\" ;\ndone\n"
  },
  {
    "path": "dzr-url",
    "content": "#!/bin/sh\n# USAGE Example:\n# ./dzr-url 355777961 650744592 | while read url id; do curl -s \"$url\" | ./dzr-dec $id | mpv - ; done\n\nSNG_IDS=$(printf \"%s\" \"$*\" | sed 's/ /,/g')\nFETCH=${FETCH:-curl -s} # FETCH=\"wget -q -O -\" or FETCH=\"curl -s -k\"\ndie() { echo \"$1\" >&2; exit 1; }\ngw () {\n  method=\"$1\"; session=\"$2\" ;apiToken=\"$3\" ; shift 3 # curl args ...\n  $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\" \"$@\"\n}\n\n[ -z \"$SNG_IDS\" ] && die \"USAGE: dzr-url 5404528,664107\"\n# reuse session from parent dzr process, or create a new one\nif [ -n \"$DZR_SID\" ] && [ -n \"$DZR_API_TOK\" ] && [ -n \"$DZR_LIC\" ]; then\n\tAPI_TOK=\"$DZR_API_TOK\"\n\tUSR_LIC=\"$DZR_LIC\"\nelse\n\tDZR_SID=$($FETCH \"www.deezer.com/ajax/gw-light.php?method=deezer.ping&api_version=1.0&api_token\" | jq -r .results.SESSION)\n\t[ -z \"$DZR_SID\" ] || [ \"$DZR_SID\" = \"null\" ] && die \"error: deezer.ping failed (no session)\"\n\tUSR_NFO=$(gw deezer.getUserData \"$DZR_SID\" \"$API_TOK\")\n\tUSR_LIC=$(printf \"%s\" \"$USR_NFO\" | jq -r .results.USER.OPTIONS.license_token)\n\tAPI_TOK=$(printf \"%s\" \"$USR_NFO\" | jq -r .results.checkForm)\n\t[ -z \"$USR_LIC\" ] || [ \"$USR_LIC\" = \"null\" ] && die \"error: getUserData failed (no license token)\"\n\t[ -z \"$API_TOK\" ] || [ \"$API_TOK\" = \"null\" ] && die \"error: getUserData failed (no api token)\"\nfi\n\nSNG_NFO=$(gw song.getListData \"$DZR_SID\" \"$API_TOK\" --data \"{\\\"sng_ids\\\":[$SNG_IDS]}\")\nprintf \"%s\" \"$SNG_NFO\" | jq -e '.results.data // empty' >/dev/null 2>&1 || die \"error: song.getListData returned no data\"\nSNG_TOK=$(printf \"%s\" \"$SNG_NFO\" | jq '[.results.data[]|(.FALLBACK.TRACK_TOKEN//.TRACK_TOKEN)]')\n# reset to resolvable id, eg:no personal uploads (geoblocked track will fail later)\nSNG_IDS=$(printf \"%s\" \"$SNG_NFO\" | jq '[.results.data[]|(.FALLBACK.SNG_ID//.SNG_ID)]')\nSNG_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)\"))\"]')\nURL_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 @-)\nprintf \"%s\" \"$URL_NFO\" | jq -e '.data // empty' >/dev/null 2>&1 || die \"error: get_url returned no data\"\nURL_IDS=$(printf \"%s\" \"$URL_NFO\" | jq --argjson ids \"$SNG_IDS\" --argjson inf \"$SNG_INF\" '[.data , ($ids|map({id:.})) , ($inf|map({info:.}))]|transpose|map(add)')\nprintf \"%s\" \"$URL_IDS\" | jq -r '.[]|select(.errors)|[.errors[0].message     ,.id]|@tsv' 1>&2\nprintf \"%s\" \"$URL_IDS\" | jq -r '.[]|select(.media[0].sources[0].url)|[.media[0].sources[0].url,.id,.info]|@tsv'\n"
  },
  {
    "path": "extension/main.js",
    "content": "// @ts-check\n/// curl -Ok https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.d.ts\n/// <reference path=\"./vscode.d.ts\" />\nconst vscode = require(\"vscode\");\nconst crypto = require('crypto');\nconst https = require('https');\nconst conf = () => vscode.workspace.getConfiguration(\"dzr\");\nconst location = vscode.ProgressLocation.Notification;\nconst hhmmss = (s) => (new Date(s * 1000)).toISOString().slice(11, 19).replace(/^00:/, '');\nconst wait = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms));\n// still no fetch() in 2023 ?\nconst fetch = (url, opt, data) => new Promise((resolve, reject) => {\n\tconsole.log({ url, opt, data })\n\tconst chunks = [], req = https.request(url, { rejectUnauthorized: conf().get('secure'), ...opt }, res => {\n\t\tres.on('data', chunk => chunks.push(chunk));\n\t\tres.on('end', () => resolve(Buffer.concat(chunks)));\n\t}).on('error', reject);\n\tif (data) req.write(data);\n\treq.end();\n});\n\n// deezer API wall of shame:\n// - not restful, so we can't infer it structure\n// - /track/:id gives contributors but /search/track?q= don't\n// - inconsistent listing structure (/playlist/:id => tracks.data, sometimes=>data, sometimes data.tracks)\n\n// browse can be called from: user query / self list(from static menu) / self list(from fetch result)\nasync function browse(url_or_event_or_ids, label) {\n\ttry {\n\t\tif (Array.isArray(url_or_event_or_ids)) return url_or_event_or_ids.map(id => ({ id }));\n\t\tconst ignoreFocusOut = true;\n\t\tconst url = typeof (url_or_event_or_ids) == \"string\" ? url_or_event_or_ids : '/';\n\t\tconst id = url.replace(/\\d+/g, '0').replace(/[^\\w]/g, '_');\n\t\tconst menus = conf().get('menus');\n\t\tconst title = (label || '').replace(/\\$\\(.+?\\)/g, '');\n\t\tif (url.endsWith('=') || url.endsWith('/0')) { // query step\n\t\t\tconst input = await vscode.window.showInputBox({ title, ignoreFocusOut });\n\t\t\tif (!input) return;\n\t\t\treturn await browse(url.replace(/0$/, '') + input, `${label}: ${input}`);\n\t\t} else if (menus[id]) { // menu step\n\t\t\tconst pick = menus[id].length > 1 ? await vscode.window.showQuickPick(menus[id], { title: title || url, ignoreFocusOut }) : menus[id][0];\n\t\t\tif (!pick) return;\n\t\t\treturn await browse(url + pick.path, pick.label);\n\t\t} else { // fetch step\n\t\t\t/**@type {{type:string,title_short:string,artist:{name:string},title_version:string,title:string,name:string,nb_tracks:string,id:number}[]} */\n\t\t\tconst data = [];\n\t\t\tfor (let n = conf().get('nextCount'), json, nextUrl = \"https://api.deezer.com\" + url; --n && nextUrl && (json = JSON.parse(await fetch(nextUrl))); nextUrl = json.next) {\n\t\t\t\tdata.push(...(json.data?.tracks || json.data || json.tracks?.data));\n\t\t\t}\n\t\t\tconst picked = !!url.match(/\\/(playlist|album)\\//);\n\t\t\tconst canPickMany = !!data.find(item => item.type == \"track\");\n\t\t\tconst type2icon = conf().get('type2icon');\n\t\t\tconst choices = data.map(entry => ({\n\t\t\t\t...entry, picked,\n\t\t\t\tlabel: (type2icon[entry.type] || '') + (entry.title_short || entry.name || entry.title),\n\t\t\t\tdescription: [entry.artist?.name, entry.title_version, entry.nb_tracks].join(' '),\n\t\t\t\tpath: `/${entry.type}/${entry.id}`,\n\t\t\t}));\n\t\t\tconst picks = await vscode.window.showQuickPick(choices, { title: title || url, canPickMany, ignoreFocusOut });\n\t\t\tif (!picks) return;\n\t\t\treturn canPickMany ? picks : await browse(picks.path, picks.label);\n\t\t}\n\t} catch (e) { console.error(e) }\n}\nlet DZR_PNG, USR_NFO;\nconst with_url = async (songs) => songs?.length ? await vscode.window.withProgress({ title: 'Fetching', location }, async (progress) => {\n\ttry { // take 7s (with, or without agent)\n\t\tconst next = (message, val) => (progress.report({ increment: 100 / 4, message }), val);\n\t\tconst gw = async (method, arl, sid, api_token = \"\", opt = {}, data) => JSON.parse(await fetch(`${base}&method=${method}&api_token=${api_token}`,\n\t\t\t{ ...opt, headers: { Cookie: `sid=${sid}; arl=${arl}`, ...opt?.headers } }, data)).results;\n\t\tconst base = \"https://www.deezer.com/ajax/gw-light.php?input=3&api_version=1.0\";\n\t\tlet DZR_ARL = next(\"ARL\", conf().get('arl'));\n\t\tconst format = conf().get('format') || 'MP3_128';\n\t\tif (!DZR_ARL) {\n\t\t\tDZR_ARL = (await vscode.window.showInputBox({\n\t\t\t\tignoreFocusOut: true,\n\t\t\t\tplaceHolder: \"deezer://autolog/xxxx\",\n\t\t\t\tprompt: \"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)\"\n\t\t\t}))?.match(/[0-9a-f]{192}/)?.[0];\n\t\t\tif (!DZR_ARL) return vscode.window.showErrorMessage(\"No ARL found\");\n\t\t\tconf().update('arl', DZR_ARL, vscode.ConfigurationTarget.Global);\n\t\t}\n\t\tDZR_PNG = next(\"session\", DZR_PNG || await gw('deezer.ping', DZR_ARL));\n\t\tUSR_NFO = next(\"user right\", USR_NFO || await gw('deezer.getUserData', DZR_ARL, DZR_PNG.SESSION));\n\t\tconst 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);\n\t\tconst GET_URL = next(\"song stream\", JSON.parse(await fetch('https://media.deezer.com/v1/get_url', { method: 'POST' }, JSON.stringify({\n\t\t\ttrack_tokens: SNG_NFO.map(d => d.TRACK_TOKEN),\n\t\t\tlicense_token: USR_NFO.USER.OPTIONS.license_token,\n\t\t\tmedia: [{ type: \"FULL\", formats: [{ cipher: \"BF_CBC_STRIPE\", format }] }]\n\t\t}))))?.data?.map((url, i) => Object.assign(SNG_NFO[i], url));\n\t\tconst errors = SNG_NFO.filter(e => e.errors).map(e => `[${e.SNG_ID}] ${e.ART_NAME} - ${e.SNG_TITLE}:${JSON.stringify(e.errors)}`);\n\t\tconst skiped = SNG_NFO.filter(s => !s.media?.[0]?.sources?.[0]?.url).map(e => `[${e.SNG_ID}] no MEDIA`);\n\t\tif (errors.length || skiped.length) {\n\t\t\tvscode.window.showWarningMessage([...errors, ...skiped].join('\\n'), \"Continue\", \"Flush ARL\")\n\t\t\t\t.then(e => { if (e == \"Flush ARL\") conf().update('arl', undefined, vscode.ConfigurationTarget.Global) });\n\t\t}\n\t\treturn SNG_NFO.map(nfo => ({\n\t\t\tid: nfo.SNG_ID,\n\t\t\tmd5_image: nfo.ALB_PICTURE,\n\t\t\tduration: +nfo.DURATION,\n\t\t\ttitle: nfo.SNG_TITLE.replace(/ ?\\(feat.*?\\)/, ''),\n\t\t\tartists: (nfo.ARTISTS || []).map(a => ({ id: a.ART_ID, name: a.ART_NAME, md5: a.ART_PICTURE })),\n\t\t\tsize: +nfo.FILESIZE,\n\t\t\texpire: nfo.TRACK_TOKEN_EXPIRE,\n\t\t\turl: nfo.media?.[0]?.sources?.[0]?.url\n\t\t})).filter(nfo => nfo.url);\n\t} catch (e) {\n\t\tconsole.error(e);\n\t\tvscode.window.showErrorMessage(e);\n\t\treturn []\n\t}\n}) : [];\n\nclass DzrWebView { // can't Audio() in VSCode, we need a webview\n\tstatuses = ['dzr.play', 'dzr.show', 'dzr.load'].map((command) => {\n\t\tconst item = vscode.window.createStatusBarItem(command, vscode.StatusBarAlignment.Left, 10000);\n\t\titem.color = new vscode.ThemeColor('statusBarItem.prominentBackground');\n\t\titem.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');\n\t\titem.command = command;\n\t\titem.text = command;\n\t\titem.show();\n\t\treturn item;\n\t});\n\t/**@type {vscode.WebviewPanel|null}*/\n\tpanel = null;\n\t#state = {};\n\tstate = new Proxy(this.#state, {\n\t\tset: (target, k, value) => {\n\t\t\tconst key = String(k);\n\t\t\ttarget[key] = value;\n\t\t\tif (['queue', 'looping'].includes(key)) { // persist those values across reboot\n\t\t\t\tconf().update(key, value, vscode.ConfigurationTarget.Global);\n\t\t\t}\n\t\t\tif (key == 'queue') this._onDidChangeTreeData.fire(null);\n\t\t\tvscode.commands.executeCommand('setContext', `dzr.${key}`, value);\n\t\t\tthis.post('state', target, [key]);\n\t\t\tthis.renderStatus();\n\t\t\treturn true;\n\t\t}\n\t});\n\n\tconstructor() {\n\t\tthis.initAckSemaphore();\n\t\tthis.state.queue = conf().get('queue'); // first is best\n\t\tthis.state.looping = conf().get('looping');\n\t}\n\trenderStatus() {\n\t\tconst index = this.state.queue?.indexOf(this.state.current);\n\t\tconst label = this.state.current ? `${this.state.current.title} - ${this.state.current.artists?.map(a => a.name).join()}` : '';\n\t\tthis.statuses[0].command = this.state.playing ? 'dzr.pause' : 'dzr.play';\n\t\tthis.statuses[0].text = this.state.ready && (this.state.playing ? \"$(debug-pause)\" : \"$(play)\");\n\t\tthis.statuses[1].tooltip = this.state.ready ? label : \"Initiate interaction first\";\n\t\tthis.statuses[1].text = this.state.ready ? label.length < 20 ? label : (label.slice(0, 20) + '…') : \"$(play)\"\n\t\tthis.statuses[2].text = this.state.ready && this.state.queue.length ? `${index + 1 || '?'}/${this.state.queue.length} $(chevron-right)` : '';//debug-step-over\n\t\tthis.treeView.title = (this.state.queue?.length ? `${index + 1 || '?'}/${this.state.queue.length}` : '') + ` loop:${this.state.looping}`;\n\t\tthis.treeView.message = this.state.queue?.length ? \"\" : \"Empty Queue. Add tracks from the '+' menu\";\n\t\tthis.treeView.badge = { tooltip: label, value: this.state.playing ? index + 1 : 0 }\n\t}\n\tasync show(htmlUri, iconPath) {\n\t\tif (this.panel) return this.panel.reveal(vscode.ViewColumn.One);\n\t\tthis.panel = vscode.window.createWebviewPanel('dzr.player', 'Player', vscode.ViewColumn.One, {\n\t\t\tenableScripts: true,\n\t\t\tenableCommandUris: true,\n\t\t\tretainContextWhenHidden: true,\n\t\t});\n\t\tthis.panel.iconPath = iconPath;\n\t\tthis.panel.webview.html = (await vscode.workspace.fs.readFile(htmlUri)).toString();\n\t\tthis.panel.webview.onDidReceiveMessage(([action, ...args] = []) => this[action] ? this[action](...args) : this.badAction(action));\n\t\tthis.panel.onDidDispose(() => this.state.ready = this.panel = null);\n\t\tthis.post('state', this.state, Object.keys(this.state));\n\t}\n\tinitAckSemaphore() { this.postAck = new Promise((then) => this.waitAckSemaphore = then); }\n\tpost = (action, ...arg) => this.panel?.webview.postMessage([action, ...arg]);\n\t// event from webview player\n\tplayer_bufferized() {\n\t\tthis.waitAckSemaphore?.(0);\n\t\tthis.initAckSemaphore();\n\t}\n\tplayer_volumechange({ volume }) { conf().update(\"volume\", volume, vscode.ConfigurationTarget.Global); }\n\tplayer_playing() { this.state.ready = this.state.playing = true; }\n\tplayer_pause() { this.state.playing = false; }\n\tplayer_ended() { vscode.commands.executeCommand('dzr.load', null); }\n\tuser_interact() { this.state.ready = true; }\n\tuser_next() { vscode.commands.executeCommand('dzr.load'); }\n\terror(msg) { vscode.window.showErrorMessage(msg); }\n\tbadAction(action) { console.error(`unHandled action \"${action}\" from webview`); }\n\t// tree\n\tdropMimeTypes = ['application/vnd.code.tree.dzrQueue'];\n\tdragMimeTypes = ['text/uri-list'];\n\t_onDidChangeTreeData = new vscode.EventEmitter();\n\tonDidChangeTreeData = this._onDidChangeTreeData.event;\n\t/**@type {import('vscode').TreeView}*/\n\ttreeView = vscode.window.createTreeView('dzr.queue', { treeDataProvider: this, dragAndDropController: this, canSelectMany: true });\n\thighlighted = (label, active) => ({ label, highlights:/**@type {[number, number][]}*/(active ? [[0, label.length]] : []) })\n\t/**@returns {vscode.TreeItem} */\n\tgetTreeItem = (item) => ({\n\t\ticonPath: new vscode.ThemeIcon(\"music\"),\n\t\tlabel: this.highlighted(item.title + ' - ' + item.artists.map(a => a.name).join(), item == this.state.current),\n\t\tdescription: hhmmss(item.duration || 0) + \" \" + (item.version || ''),\n\t\tcontextValue: 'dzr.track',\n\t\tcommand: { title: 'Play', command: 'dzr.load', tooltip: 'Play', arguments: [this.state.queue.indexOf(item)] },\n\t})\n\tgetParent = () => null\n\tgetChildren = () => {\n\n\t\treturn this.state.queue\n\t}\n\tasync handleDrag(sources, treeDataTransfer) {\n\t\ttreeDataTransfer.set(this.dropMimeTypes[0], new vscode.DataTransferItem(sources));\n\t}\n\tasync handleDrop(onto, transfer) {\n\t\tconst sources = transfer.get(this.dropMimeTypes[0])?.value;\n\t\tif (!sources || sources.includes(onto)) return; //don't move selection onto one of it members\n\t\tconst striped = this.state.queue.filter(item => !sources.includes(item));\n\t\tconst index = this.state.queue.indexOf(onto);\n\t\tthis.state.queue = [...striped.slice(0, index), ...sources, ...striped.slice(index)];\n\t}\n}\nexports.activate = async function (/**@type {import('vscode').ExtensionContext}*/ context) {\n\t// deezer didn't DMCA'd dzr so let's follow the same path here\n\tconf().get('cbc') || vscode.window.withProgress({ title: 'Extracting CBC key...', location }, async () => {\n\t\tconst html_url = 'https://www.deezer.com/en/channels/explore';\n\t\tconst html = (await fetch(html_url)).toString('utf-8');\n\t\tconst js_url = html.match(/src=\"(http[^\"]+app-web\\.[^\"]+\\.js)\"/)?.[1];\n\t\tif (!js_url) return await vscode.window.showErrorMessage('CBC Extract: No JS WebApp found');\n\t\tconst keys = (await fetch(js_url)).toString('utf-8').match(/%5B0x..%2C.{39}%2C0x..%5D/g);\n\t\tconst [a, b] = keys.map(part => part.slice(3, -3).split('%2C').map(i => String.fromCharCode(parseInt(i))).reverse());\n\t\tconst cbc = a.map((a, i) => `${a}${b[i]}`).join('');// zip a+b\n\t\tconst sha = crypto.createHash('sha1').update(cbc).digest('hex').slice(0, 8);\n\t\tif (sha != '3ad58d92') return await vscode.window.showErrorMessage('Bad extracted key');\n\t\tconf().update('cbc', cbc, vscode.ConfigurationTarget.Global);\n\t});\n\tconst dzr = new DzrWebView();\n\tconst htmlUri = vscode.Uri.joinPath(context.extensionUri, 'webview.html');\n\tconst iconUri = vscode.Uri.joinPath(context.extensionUri, 'logo.svg'); //same for light+dark\n\n\tcontext.subscriptions.push(...dzr.statuses, dzr.treeView,\n\t\t// catch vscode://yne.dzr/* urls\n\t\tvscode.window.registerUriHandler({ handleUri(uri) { (({ path, query }) => vscode.commands.executeCommand(`dzr.${path.slice(1)}`, ...(query ? JSON.parse(query) : [])))(uri); } }),\n\t\tvscode.commands.registerCommand('dzr.show', () => dzr.show(htmlUri, iconUri)),\n\t\tvscode.commands.registerCommand(\"dzr.play\", () => dzr.post('play')),\n\t\tvscode.commands.registerCommand(\"dzr.pause\", () => dzr.post('pause')),\n\t\tvscode.commands.registerCommand(\"dzr.href\", (track) => vscode.env.openExternal(vscode.Uri.parse(`https://deezer.com/track/${track.id}`))),\n\t\tvscode.commands.registerCommand(\"dzr.loopQueue\", () => dzr.state.looping = \"queue\"),\n\t\tvscode.commands.registerCommand(\"dzr.loopTrack\", () => dzr.state.looping = \"track\"),\n\t\tvscode.commands.registerCommand(\"dzr.loopOff\", () => dzr.state.looping = \"off\"),\n\t\tvscode.commands.registerCommand(\"dzr.add\", async (path, label) => with_url(await browse(path, label)).then(tracks => dzr.state.queue = [...dzr.state.queue, ...tracks])),\n\t\tvscode.commands.registerCommand(\"dzr.remove\", async (item, items) => (items || [item]).map(i => vscode.commands.executeCommand('dzr.removeAt', dzr.state.queue.indexOf(i)))),\n\t\tvscode.commands.registerCommand(\"dzr.removeAt\", async (index) => index >= 0 && (dzr.state.queue = [...dzr.state.queue.slice(0, index), ...dzr.state.queue.slice(index + 1)])),\n\t\tvscode.commands.registerCommand(\"dzr.clear\", async () => dzr.state.queue = []),\n\t\tvscode.commands.registerCommand(\"dzr.shareAll\", async () => vscode.commands.executeCommand(\"dzr.share\")),\n\t\tvscode.commands.registerCommand(\"dzr.share\", async (track, tracks) => {\n\t\t\tconst ids = JSON.stringify(track ? [(tracks || [track]).map(e => e.id || track.id)] : [dzr.state.queue.map(q => q.id)]);\n\t\t\tvscode.env.clipboard.writeText(vscode.Uri.from({ scheme: \"vscode\", authority: context.extension.id, path: '/add', query: ids }).toString())\n\t\t}),\n\t\tvscode.commands.registerCommand(\"dzr.shuffle\", async () => {\n\t\t\tconst shuffle = [...dzr.state.queue];\n\t\t\tfor (let i = shuffle.length - 1; i > 0; i--) {\n\t\t\t\tconst j = Math.floor(Math.random() * (i + 1));\n\t\t\t\t[shuffle[i], shuffle[j]] = [shuffle[j], shuffle[i]];\n\t\t\t}\n\t\t\tdzr.state.queue = shuffle;\n\t\t}),\n\t\tvscode.commands.registerCommand(\"dzr.load\", async (pos) => { //pos=null if player_end / pos=undefine if user click\n\t\t\tpos = pos ?? dzr.state.queue.indexOf(dzr.state.current) + (dzr.state.looping == 'track' ? 0 : 1);\n\t\t\tif (!dzr.state.queue[pos]) { // out of bound track\n\t\t\t\tif (dzr.state.looping == 'off') return; // don't loop if unwanted\n\t\t\t\tpos = 0; // loop position if looping\n\t\t\t}\n\t\t\tif (!dzr.state.ready) {\n\t\t\t\tvscode.commands.executeCommand('dzr.show');\n\t\t\t\twhile (!dzr.state.ready) await wait();\n\t\t\t}\n\t\t\tif ((dzr.state.queue[pos].expire || 0) < (+new Date() / 1000)) {\n\t\t\t\tdzr.state.queue = await with_url(dzr.state.queue);//TODO: hope item is now up to date\n\t\t\t}\n\t\t\tconst prev = dzr.state?.current;\n\t\t\tdzr.state.current = dzr.state.queue[pos];\n\t\t\tdzr._onDidChangeTreeData.fire(prev);\n\t\t\tdzr._onDidChangeTreeData.fire(this.state?.current);\n\t\t\tconst hex = (str) => str.split('').map(c => c.charCodeAt(0))\n\t\t\tconst md5 = hex(crypto.createHash('md5').update(`${dzr.state.current.id}`).digest('hex'));\n\t\t\tconst key = Buffer.from(hex(conf().get('cbc')).map((c, i) => c ^ md5[i] ^ md5[i + 16]));\n\t\t\tconst iv = Buffer.from([0, 1, 2, 3, 4, 5, 6, 7]);\n\t\t\tconst stripe = 2048;//TODO:use .pipe() API https://codereview.stackexchange.com/questions/57492/\n\t\t\tdzr.post('open', dzr.state.current, conf().get(\"volume\"));\n\t\t\tconst buf_enc = await fetch(dzr.state.current.url);\n\t\t\tfor (let pos = 0; pos < buf_enc.length; pos += stripe) {\n\t\t\t\tif ((pos >> 11) % 3) continue;\n\t\t\t\tconst ciph = crypto.createDecipheriv('bf-cbc', key, iv).setAutoPadding(false)\n\t\t\t\tconst deco = ciph.update(buf_enc.subarray(pos, pos + stripe));\n\t\t\t\tbuf_enc.set(deco, pos);\n\t\t\t}\n\t\t\tdzr.post('append', Uint8Array.from(buf_enc));\n\t\t\tawait dzr.postAck;\n\t\t\tdzr.post('close');\n\t\t}),\n\t)\n}"
  },
  {
    "path": "extension/package.json",
    "content": "{\n\t\"name\": \"dzr\",\n\t\"displayName\": \"DZR player\",\n\t\"description\": \"deezer.com player\",\n\t\"version\": \"0.3.0\",\n\t\"publisher\": \"yne\",\n\t\"engines\": {\n\t\t\"vscode\": \"^1.73.0\"\n\t},\n\t\"main\": \"./main.js\",\n\t\"extensionKind\": [\n\t\t\"ui\",\n\t\t\"workspace\"\n\t],\n\t\"activationEvents\": [\n\t\t\"onStartupFinished\"\n\t],\n\t\"capabilities\": {\n\t\t\"untrustedWorkspaces\": {\n\t\t\t\"supported\": true\n\t\t}\n\t},\n\t\"contributes\": {\n\t\t\"viewsWelcome\": [\n\t\t\t{\n\t\t\t\t\"view\": \"explorer.dzr\",\n\t\t\t\t\"contents\": \"not shown\"\n\t\t\t}\n\t\t],\n\t\t\"keybindings (displayed but dont work)\": [\n\t\t\t{\n\t\t\t\t\"command\": \"dzr.remove\",\n\t\t\t\t\"key\": \"delete\"\n\t\t\t}\n\t\t],\n\t\t\"commands\": [\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.show\",\n\t\t\t\t\"title\": \"Show Player Tab\",\n\t\t\t\t\"icon\": \"$(eye)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.play\",\n\t\t\t\t\"title\": \"Play\",\n\t\t\t\t\"icon\": \"$(debug-run)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.href\",\n\t\t\t\t\"title\": \"Open Web\",\n\t\t\t\t\"when\": \"false\",\n\t\t\t\t\"icon\": \"$(link-external)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.pause\",\n\t\t\t\t\"title\": \"Pause\",\n\t\t\t\t\"icon\": \"$(debug-pause)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.load\",\n\t\t\t\t\"title\": \"Play next\",\n\t\t\t\t\"icon\": \"$(chevron-right)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.loopQueue\",\n\t\t\t\t\"title\": \"Loop Queue\",\n\t\t\t\t\"icon\": \"$(sync)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.loopTrack\",\n\t\t\t\t\"title\": \"Loop Track\",\n\t\t\t\t\"icon\": \"$(redo)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.loopOff\",\n\t\t\t\t\"title\": \"Loop Disabled\",\n\t\t\t\t\"icon\": \"$(sync-ignored)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.add\",\n\t\t\t\t\"title\": \"Queue Add\",\n\t\t\t\t\"icon\": \"$(add)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.remove\",\n\t\t\t\t\"title\": \"Queue Remove Track\",\n\t\t\t\t\"when\": \"false\",\n\t\t\t\t\"icon\": \"$(close)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.clear\",\n\t\t\t\t\"title\": \"Queue Remove All\",\n\t\t\t\t\"icon\": \"$(clear-all)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.shuffle\",\n\t\t\t\t\"title\": \"Queue Shuffle\",\n\t\t\t\t\"icon\": \"$(arrow-swap)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.share\",\n\t\t\t\t\"title\": \"Copy vscode:// Link(s)\",\n\t\t\t\t\"icon\": \"$(link)\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"category\": \"dzr\",\n\t\t\t\t\"command\": \"dzr.shareAll\",\n\t\t\t\t\"title\": \"Copy all vscode:// Links\",\n\t\t\t\t\"icon\": \"$(link)\"\n\t\t\t}\n\t\t],\n\t\t\"menus\": {\n\t\t\t\"view/title\": [\n\t\t\t\t{\n\t\t\t\t\t\"when\": \"view == dzr.queue\",\n\t\t\t\t\t\"group\": \"navigation@9\",\n\t\t\t\t\t\"command\": \"dzr.add\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"when\": \"view == dzr.queue && dzr.queue!=''\",\n\t\t\t\t\t\"group\": \"navigation@8\",\n\t\t\t\t\t\"command\": \"dzr.clear\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"when\": \"view == dzr.queue && dzr.queue!=''\",\n\t\t\t\t\t\"group\": \"navigation@7\",\n\t\t\t\t\t\"command\": \"dzr.shareAll\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"when\": \"view == dzr.queue && dzr.queue!=''\",\n\t\t\t\t\t\"group\": \"navigation@6\",\n\t\t\t\t\t\"command\": \"dzr.shuffle\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"when\": \"view == dzr.queue && dzr.queue!='' && dzr.looping == 'off'\",\n\t\t\t\t\t\"group\": \"navigation@2\",\n\t\t\t\t\t\"command\": \"dzr.loopQueue\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"when\": \"view == dzr.queue && dzr.queue!='' && dzr.looping == 'queue'\",\n\t\t\t\t\t\"group\": \"navigation@2\",\n\t\t\t\t\t\"command\": \"dzr.loopTrack\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"when\": \"view == dzr.queue && dzr.queue!='' && dzr.looping == 'track'\",\n\t\t\t\t\t\"group\": \"navigation@2\",\n\t\t\t\t\t\"command\": \"dzr.loopOff\"\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"view/item/context\": [\n\t\t\t\t{\n\t\t\t\t\t\"group\": \"inline\",\n\t\t\t\t\t\"command\": \"dzr.remove\",\n\t\t\t\t\t\"when\": \"viewItem == dzr.track && !listMultiSelection\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"group\": \"navigation\",\n\t\t\t\t\t\"command\": \"dzr.remove\",\n\t\t\t\t\t\"when\": \"viewItem == dzr.track && listMultiSelection\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"group\": \"navigation\",\n\t\t\t\t\t\"command\": \"dzr.href\",\n\t\t\t\t\t\"when\": \"viewItem == dzr.track && !listMultiSelection\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"group\": \"navigation\",\n\t\t\t\t\t\"command\": \"dzr.share\",\n\t\t\t\t\t\"when\": \"viewItem == dzr.track\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"configuration\": {\n\t\t\t\"type\": \"object\",\n\t\t\t\"title\": \"dzr configuration\",\n\t\t\t\"properties\": {\n\t\t\t\t\"dzr.cbc\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"default\": \"\",\n\t\t\t\t\t\"description\": \"track decryption key\"\n\t\t\t\t},\n\t\t\t\t\"dzr.arl\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"default\": \"\",\n\t\t\t\t\t\"description\": \"Access Rights Locator\"\n\t\t\t\t},\n\t\t\t\t\"dzr.format\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"default\": \"MP3_128\",\n\t\t\t\t\t\"enum\": [\n\t\t\t\t\t\t\"AAC_64\",\n\t\t\t\t\t\t\"FLAC\",\n\t\t\t\t\t\t\"MP3_64\",\n\t\t\t\t\t\t\"MP3_128\",\n\t\t\t\t\t\t\"MP3_256\",\n\t\t\t\t\t\t\"MP3_320\",\n\t\t\t\t\t\t\"MP4_RA1\",\n\t\t\t\t\t\t\"MP4_RA2\",\n\t\t\t\t\t\t\"MP4_RA3\"\n\t\t\t\t\t],\t\n\t\t\t\t\t\"description\": \"Requested format\"\n\t\t\t\t},\n\t\t\t\t\"dzr.nextCount\": {\n\t\t\t\t\t\"type\": \"number\",\n\t\t\t\t\t\"default\": 5,\n\t\t\t\t\t\"markdownDescription\": \"Number of `.next` page to fetch before showing quickpick (`-1 for unlimited`)\"\n\t\t\t\t},\n\t\t\t\t\"dzr.secure\": {\n\t\t\t\t\t\"type\": \"boolean\",\n\t\t\t\t\t\"default\": false,\n\t\t\t\t\t\"description\": \"Disable if you are behind a corporate proxy\"\n\t\t\t\t},\n\t\t\t\t\"dzr.volume\": {\n\t\t\t\t\t\"type\": \"number\",\n\t\t\t\t\t\"default\": 1,\n\t\t\t\t\t\"minimum\": 0,\n\t\t\t\t\t\"maximum\": 1,\n\t\t\t\t\t\"description\": \"Player volume [0..1], Applied at next track play\"\n\t\t\t\t},\n\t\t\t\t\"dzr.queue\": {\n\t\t\t\t\t\"type\": \"array\",\n\t\t\t\t\t\"default\": [],\n\t\t\t\t\t\"description\": \"Persistent track queue\"\n\t\t\t\t},\n\t\t\t\t\"dzr.looping\": {\n\t\t\t\t\t\"type\": \"string\",\n\t\t\t\t\t\"enum\": [\n\t\t\t\t\t\t\"queue\",\n\t\t\t\t\t\t\"track\",\n\t\t\t\t\t\t\"off\"\n\t\t\t\t\t],\n\t\t\t\t\t\"default\": \"queue\",\n\t\t\t\t\t\"description\": \"Queue looping\"\n\t\t\t\t},\n\t\t\t\t\"dzr.menus\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"description\": \"API tree (since deezer endpoint are not RESTful)\",\n\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\"_\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"search/track?q=\",\n\t\t\t\t\t\t\t\t\"label\": \"$(play-circle) track search\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"search/artist?q=\",\n\t\t\t\t\t\t\t\t\"label\": \"$(person) artist search\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"search/album?q=\",\n\t\t\t\t\t\t\t\t\"label\": \"$(issues) album search\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"search/playlist?q=\",\n\t\t\t\t\t\t\t\t\"label\": \"$(list-unordered) playlist search\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"search/user?q=\",\n\t\t\t\t\t\t\t\t\"label\": \"$(account) user search\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"search/radio?q=\",\n\t\t\t\t\t\t\t\t\"label\": \"$(broadcast) radio search\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"genre\",\n\t\t\t\t\t\t\t\t\"label\": \"$(telescope) explore\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"radio\",\n\t\t\t\t\t\t\t\t\"label\": \"$(broadcast) radios list\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"user/0\",\n\t\t\t\t\t\t\t\t\"label\": \"$(account) user id\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"_artist_0\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/top?limit=50\",\n\t\t\t\t\t\t\t\t\"label\": \"$(star) Top Tracks\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/albums\",\n\t\t\t\t\t\t\t\t\"label\": \"$(issues) Albums\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/related\",\n\t\t\t\t\t\t\t\t\"label\": \"$(person) Similar Artists\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/radio\",\n\t\t\t\t\t\t\t\t\"label\": \"$(broadcast) Flow\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/playlists\",\n\t\t\t\t\t\t\t\t\"label\": \"$(list-unordered) Playlists\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"_user_0\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/playlists\",\n\t\t\t\t\t\t\t\t\"label\": \"$(list-unordered) Playlists\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/tracks\",\n\t\t\t\t\t\t\t\t\"label\": \"$(play-circle) Favorite Tracks\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/albums\",\n\t\t\t\t\t\t\t\t\"label\": \"$(issues) Favorite Albums\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/artists\",\n\t\t\t\t\t\t\t\t\"label\": \"$(person) Favorite Artists\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/flow\",\n\t\t\t\t\t\t\t\t\"label\": \"$(broadcast) Flow\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/charts\",\n\t\t\t\t\t\t\t\t\"label\": \"$(play-circle) Charts\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"_genre_0\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/radios\",\n\t\t\t\t\t\t\t\t\"label\": \"$(broadcast) radios\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/artists\",\n\t\t\t\t\t\t\t\t\"label\": \"$(person) Artists\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"_radio_0\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/tracks\",\n\t\t\t\t\t\t\t\t\"label\": \"$(list-unordered) tracks\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"_album_0\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"path\": \"/tracks\",\n\t\t\t\t\t\t\t\t\"label\": \"$(list-unordered) tracks\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t\"dzr.type2icon\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"description\": \"VSCode picker icon to display according to item type\",\n\t\t\t\t\t\"additionalProperties\": true,\n\t\t\t\t\t\"default\": {\n\t\t\t\t\t\t\"track\": \"$(play-circle) \",\n\t\t\t\t\t\t\"artist\": \"$(person) \",\n\t\t\t\t\t\t\"album\": \"$(issues) \",\n\t\t\t\t\t\t\"playlist\": \"$(list-unordered)\",\n\t\t\t\t\t\t\"radio\": \"$(broadcast) \",\n\t\t\t\t\t\t\"genre\": \"$(telescope) \",\n\t\t\t\t\t\t\"user\": \"$(account) \"\n\t\t\t\t\t},\n\t\t\t\t\t\"patternProperties\": {\n\t\t\t\t\t\t\".*\": {\n\t\t\t\t\t\t\t\"type\": \"string\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"viewsContainers\": {\n\t\t\t\"activitybar\": [\n\t\t\t\t{\n\t\t\t\t\t\"id\": \"dzr\",\n\t\t\t\t\t\"title\": \"dzr\",\n\t\t\t\t\t\"icon\": \"logo.svg\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"views\": {\n\t\t\t\"dzr\": [\n\t\t\t\t{\n\t\t\t\t\t\"id\": \"dzr.queue\",\n\t\t\t\t\t\"name\": \"Player Queue\",\n\t\t\t\t\t\"icon\": \"logo.svg\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t}\n}"
  },
  {
    "path": "extension/webview.html",
    "content": "<!--VSCode require at least 1 (one) user interaction for Audio playback: use this popup -->\n<dialog open style=\"border:none;border-radius:25px;box-shadow: 0 0 0 2000px rgb(20 20 20 / 95%);z-index: 9;\">\n    <form method=\"dialog\">\n        <h1>Disclamer</h1>\n        <ul>\n            <li>Use the <b>Player Queue side-panel</b> to add/remove track</li>\n            <li>Closing <b>this player tab</b> will stop the playback</li>\n        </ul>\n        <div><button style=\"width: 100%;\">OK</button></div>\n    </form>\n</dialog>\n<img style=\"display: block;border:0;margin: auto; width: 50vmin; height: 50vmin; margin-top: 10vmin;border-radius:27px\">\n<div id=\"title\" style=\"font-size:20px;margin: 20px 0 0 0;text-align: center;\"></div>\n<div id=\"artists\" style=\"font-size:20px;margin: 20px;display: flex;justify-content: center;gap: 0.5em;flex-flow: wrap;\"></div>\n<audio controls onloadstart=\"play()\" style=\"display: block;margin: auto;width: 50vmin;\"></audio>\n<script type=module>\nconst el = (tag, props={}, ch=[]) => ch.reduce((e,c) => (e.appendChild(c),e),Object.assign(document.createElement(tag),props));\nconst post = acquireVsCodeApi().postMessage;\nconst [img] = document.getElementsByTagName('img');\nconst [audio] = document.getElementsByTagName('audio');\nconst [dialog] = document.getElementsByTagName('dialog');\nconst title = document.getElementById('title');\nconst artists = document.getElementById('artists');\nconst cmd = (name, ...args) => `command:${name}?` + encodeURIComponent(JSON.stringify(args));\ndialog.onclose=()=>{audio.play();post(['user_interact']);}\n['ended', 'pause', 'playing', 'volumechange'].map(on => audio.addEventListener(on, () => post(['player_' + on, {volume:audio.volume}])));\nlet mediaSource, sourceBuffer;\nconst image = (type,md5,size=80) => `https://e-cdns-images.dzcdn.net/images/${type}/${md5}/${size}x${size}.jpg`\nconst on = {// event from VSCode\n    async open(item,volume) {\n        mediaSource = new window.MediaSource();\n        audio.volume = volume;\n        audio.src = window.URL.createObjectURL(mediaSource);\n        await new Promise(then => mediaSource.onsourceopen = () => then());\n        sourceBuffer = mediaSource.addSourceBuffer(\"audio/mpeg\");\n        sourceBuffer.addEventListener(\"updateend\", (ev) => post(['player_bufferized']));\n    },\n    append(buf) { sourceBuffer.appendBuffer(buf) },\n    close() { mediaSource.endOfStream() },\n    play() { audio.play().catch(e=>post(['error', `${e}`])) },\n    pause() { audio.pause() },\n    state(state, updates=[]) {\n        if (updates.includes('current')) {\n            title.innerText = state.current.title;\n            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)])));\n            img.src = image(\"cover\", state.current.md5_image, 1000);\n            navigator.mediaSession.metadata = new MediaMetadata({\n                title: state.current.title,\n                artist: state.current.artists.map(a=>a.name).join(),\n                album: 'dzr',\n                artwork: [{ src: img.src, sizes: \"1000x1000\", type: \"image/jpg\" }]\n            });\n        }\n    },\n};\nwindow.addEventListener('message', ({ data: [cmd,...args] }) => {\n    if (!on[cmd]) return console.log(\"bad message:\" + JSON.stringify([cmd,...args]));\n    on[cmd](...args);\n});\nnavigator.mediaSession.setActionHandler(\"play\", on.play);\nnavigator.mediaSession.setActionHandler(\"pause\", on.pause);\nnavigator.mediaSession.setActionHandler(\"nexttrack\", ()=>post(['user_next']))\n//navigator.mediaSession.setActionHandler(\"previoustrack\", ()=>{})\n</script>\n<style>\n    a:not(:hover){text-decoration: none;}\n    img:not([src]){opacity: 0;}\n    dialog::backdrop {background-color: rgba(0, 0, 0, .9);}\n    #artists>a {padding-left: 1.5em;background-repeat: no-repeat;background-size: contain;}\n</style>"
  },
  {
    "path": "index.html",
    "content": "<!doctype html>\n<html lang=en>\n<meta name=\"viewport\" content=\"width=device-width, user-scalable=no\">\n<script type=module>\nconst $$ = document.querySelectorAll.bind(document);\nconst el = (tag, props={}, ch=[]) => ch.reduce((e,c) => (e.appendChild(c),e),Object.assign(document.createElement(tag),props))\nconst api = (path, callback=`cb_${+new Date()}`) => new Promise(function(ok, ko) {\n\twindow[callback] = (data) => {delete window[callback]; data?.error?ko(data.error):ok(data)};\n\tconst src = new URL(`https://api.deezer.com/${path}`);\n\tsrc.searchParams.append('output','jsonp');\n\tsrc.searchParams.append('callback',callback);\n\tdocument.head.append(el('script', { src, onload:({target})=>target.parentNode.removeChild(target)}));\n});\nconst rel = (h) => h.startsWith('/') ? h : `${location.hash.slice(1)}/${h}`\nconst list = (tags) => document.forms.results.replaceChildren(...tags.map(([tag, props]) => el(tag, {innerText:props?.href, ...props, href:`#${rel(props.href)}`})))\nconst toLink = (...links) => links.map(href => ['a', {href}])\nconst routes = {\n\t'' :     (_) => list(toLink('/search/track?q=','/search/artist?q=','/search/album?q=','/search/playlist?q=','/search/radio?q=','/search/user?q=','/user/0','/genre','/radio')),\n\tgenre_0: (_) => list(toLink('radios','artists')),\n\tradio_0: (_) => list(toLink('tracks', 'fans')),\n\talbum_0: (_) => list(toLink('tracks', 'fans')),\n\tuser_0:  (_) => list(toLink('charts','albums','playlist','flow','tracks','artists')),\n\tartist_0:(_) => list(toLink('top?limit=50','albums','fans','related','radio','playlist')),\n\tdefault: async (h) => list( (await api(h)).data.map(d => ['a', {\n\t\thref:`/${d.type}/${d.id}`,\n\t\tinnerText: `${d.title||d.name} ${d.artist?.name||''}`\n\t}]) )\n};\nconst modes = {\n\tnone: (p) => {},\n\treplay: (p) => p.play(),\n\trandom: (p) => window.onhashchange({newURL:$$('a[href^=\"#/track/\"]')[0].href}),\n\tnext: (p) => window.onhashchange({newURL:$$(`a[href=\"${p.dataset.href}\"]`)[0].nextElementSibling.href}),\n}\nwindow.onhashchange = function(event) {\n\tconst newurl = new URL(event.newURL);\n\tconst path = newurl.hash.replace(/#?\\/?/,'');\n\tif (path.endsWith('=')||path.endsWith('/0'))\n\treturn location.hash += prompt(`query for ${path}`);\n\tif (path.startsWith('track')){\n\t\tif(!window.cgi.value) return alert('No dzr cgi url detected! (is dzr running as cgi ?)\\nplease manually set it up in options');\n\t\twindow.player.src = `${window.cgi.value}?`+newurl.hash.match(/\\d+/);\n\t\twindow.player.dataset.href = newurl.hash; // to find it back in next track\n\t\twindow.document.title = path;\n\t\treturn location.hash = (new URL(event.oldURL||location)).hash; // don't change URL\n\t}\n\tconst route = path.replace(/[,?].*/,'').replace(/[^a-zA-Z0-9]+/g,'_').replace(/[0-9,]+/g,'0').replace(/_+/g,'_');\n\t(routes[route]||routes.default)(path);\n}\nwindow.onload = function() {\n\twindow.player.onended = ()=>modes[window.mode.value](window.player);\n\twindow.https.hidden = location.protocol=='http:';\n\tif (localStorage.dzr_mode) window.mode.value = localStorage.dzr_mode;\n\tif (localStorage.dzr_cgi) window.cgi.value = localStorage.dzr_cgi;\n\telse ['/cgi-bin/dzr', '//0.0.0.0:8000/cgi-bin/dzr', '//127.0.0.1:8000/cgi-bin/dzr', '//localhost:8000/cgi-bin/dzr']\n\t     .forEach(url => fetch(url,{method:'HEAD'}).then(e=>e.ok ? window.cgi.value = localStorage.dzr_cgi = url:0))\n\twindow.onhashchange({newURL:`${location}`});\n}\n</script>\n<body style=\"margin-bottom:100px;font-size:2em;font-family:monospace\">\n\t<cite id=https>Warning: dzr don't normaly use https, please downgrade to http://</cite>\n\t<details style=\"display: grid\">\n\t\t<summary>Options</summary>\n\t\t<label>dzr cgi url: <input id=\"cgi\" placeholder=\"http://example:8000/cgi-bin/dzr\"></label>\n\t\t<label>next track: <select id=\"mode\" onchange=\"localStorage.dzr_mode=value\"><option>none<option>replay<option>random<option>next</select></label>\n\t</details>\n\t<form name=results style=\"display: grid\"></form>\n\t<audio id=\"player\" autoplay controls style=\"position:fixed;bottom:0;left:0;width:100%\"></audio>\n</body>"
  }
]