[
  {
    "path": ".github/workflows/docker-image.yml",
    "content": "name: Docker Image CI\n\non:\n  workflow_dispatch: # Manual only\n\njobs:\n\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    -\n      name: Checkout\n      uses: actions/checkout@v3\n    -\n      name: Docker Setup Buildx\n      uses: docker/setup-buildx-action@v2.7.0\n    -\n      name: Login to Docker Hub\n      uses: docker/login-action@v3\n      with:\n        username: ${{ secrets.DOCKERHUB_USERNAME }}\n        password: ${{ secrets.DOCKERHUB_TOKEN }}\n    -\n      name: Build and push\n      uses: docker/build-push-action@v4\n      with:   \n        push: true\n        platforms: linux/arm/v7,linux/arm/v6,linux/arm64/v8,linux/amd64,linux/386\n        tags: ka1mi/yandex-music-downloader:latest,ka1mi/yandex-music-downloader:${{github.ref_name}}\n\n"
  },
  {
    "path": ".gitignore",
    "content": "# Swap\n[._]*.s[a-v][a-z]\n[._]*.sw[a-p]\n[._]s[a-rt-v][a-z]\n[._]ss[a-gi-z]\n[._]sw[a-p]\n\n# nix build\nresult\n\n# Session\nSession.vim\nSessionx.vim\n\n# Temporary\n.netrwhist\n*~\n# Auto-generated tag files\ntags\n# Persistent undo\n[._]*.un~\n\n#\n.omx/\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine:latest\nENV LANG=en_US.UTF-8 LC_ALL=C.UTF-8 LANGUAGE=en_US.UTF-8\nRUN apk --update add perl perl-app-cpanminus make unzip\nRUN apk add perl-libwww perl-lwp-protocol-https perl-http-cookies perl-html-parser perl-getopt-long-descriptive perl-archive-zip \\\n    --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/\nRUN [\"cpanm\", \"MP3::Tag\", \"File::Util\"]\nCOPY src /src\nENTRYPOINT [ \"/src/ya.pl\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License\n\nCopyright (c) 2014 Kaimi, https://kaimi.io\n\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "Yandex Music Downloader\n=====================\n\n[![Telegram](https://img.shields.io/badge/Telegram--lightgrey?logo=telegram&style=social)](https://t.me/kaimi_io)\n[![Twitter](https://img.shields.io/twitter/follow/kaimi_io?style=social)](https://twitter.com/kaimi_io)\n\n![Yandex Music Downloader usage](https://github.com/kaimi-io/yandex-music-download/blob/master/usage.gif?raw=true)\n\nSimple command line Perl script for downloading music from Yandex Music (http://music.yandex.ru).\nOrigin of the script is the following article: https://kaimi.io/2013/11/yandex-music-downloader/.\n\n## Requirements\n### Environment\n* Linux/Windows/MacOS (anything, that runs Perl)\n* Perl >= 5.12\n\n### Perl modules\n* General\n  * Digest::MD5\n  * File::Copy\n  * File::Spec\n  * File::Temp\n  * [File::Util](https://github.com/tommybutler/file-util)\n  * Getopt::Long::Descriptive\n  * HTML::Entities\n  * HTTP::Cookies\n  * JSON::PP\n  * LWP::Protocol::https\n  * LWP::UserAgent\n  * MP3::Tag\n  * Term::ANSIColor\n  * Mozilla::CA\n  \n* Windows-only modules\n  * Win32::API\n  * Win32::Console\n  * Win32API::File\n\n## Installation\n### Ubuntu / Debian\n```bash\n# Prerequisites\nsudo apt-get update\nsudo apt-get -y install perl cpanminus make git\nsudo apt-get -y install libwww-perl liblwp-protocol-https-perl libhttp-cookies-perl libhtml-parser-perl libmp3-tag-perl libgetopt-long-descriptive-perl libarchive-zip-perl\ncpanm Mozilla::CA\n\n# Get a copy and run\ngit clone https://github.com/kaimi-io/yandex-music-download.git\ncd yandex-music-download/src\nperl ya.pl -h\n```\n### Nix / NixOS\n```bash\nnix shell github:kaimi-io/yandex-music-download\nya-music -h\n```\n### MacOS\n1. Install brew (https://brew.sh/).\n2. Run:\n```bash\nbrew update\nbrew install perl cpanminus git\ncpanm Digest::MD5 File::Copy File::Spec File::Temp File::Util Getopt::Long::Descriptive HTML::Entities HTTP::Cookies JSON::PP LWP::Protocol::https LWP::UserAgent MP3::Tag Term::ANSIColor Mozilla::CA\n\ngit clone https://github.com/kaimi-io/yandex-music-download.git\ncd yandex-music-download/src\nperl ya.pl -h\n```\n### Windows\nWith WSL (Windows Subsystem for Linux) installation will be similar to [Ubuntu / Debian](#ubuntu--debian).\nOtherwise:\n1. Download and install ActiveState Perl (https://www.activestate.com/products/perl/downloads/) or Strawberry Perl (http://strawberryperl.com/).\n2. Ensure, that Perl was added to system `PATH` environment variable.\n3. From Windows command line run:\n```perl -v```. It should output Perl version. If not, refer to your Perl distribution documentation about adding Perl to your `PATH` environment variable.\n\n4. Install required modules (it can be done via PPM if you're using ActiveState Perl):\n```bash\ncpan install Digest::MD5 File::Copy File::Spec File::Temp File::Util Getopt::Long::Descriptive HTML::Entities HTTP::Cookies JSON::PP LWP::Protocol::https LWP::UserAgent MP3::Tag Term::ANSIColor Mozilla::CA Win32::API Win32::Console Win32API::File\n```\n5. Download and unpack Yandex Music Downloader (https://github.com/kaimi-io/yandex-music-download/archive/master.zip).\n6. Run:\n```bash\ncd yandex-music-download/src\nperl ya.pl -h\n```\n\n### Docker\n1. Install Docker (https://docs.docker.com/get-docker/).\n2. Pull image from Docker Hub (https://hub.docker.com/r/ka1mi/yandex-music-downloader):\n```bash\ndocker pull ka1mi/yandex-music-downloader:latest\n```\n3. Or build it:\n```bash\ngit clone https://github.com/kaimi-io/yandex-music-download.git\ncd yandex-music-download\ndocker build --tag yandex-music-downloader:1.0 .\n```\n4. Run:\n```bash\ndocker run --init --rm -v ${PWD}:/root/ --name yamusic yandex-music-downloader:1.0 -d /root --cookie \"Session_id=...\" -u https://music.yandex.ru/album/215688/track/1710808\n```\n\n## Usage\n```bat\nYandex Music Downloader v1.5\n\nya.pl [-adhklpstu] [long options...]\n        -p[=INT] --playlist[=INT]  playlist id to download\n        -k[=STR] --kind[=STR]      playlist kind (eg. ya-playlist,\n                                   music-blog, music-partners, etc.)\n        -a[=INT] --album[=INT]     album to download\n        -t[=INT] --track[=INT]     track to download (album id must be\n                                   specified)\n        -u[=STR] --url[=STR]       download by URL\n        -d[=STR] --dir[=STR]       download path (current direcotry will be\n                                   used by default)\n        --skip-existing            skip downloading tracks that already exist\n                                   on the specified path\n        --proxy STR                HTTP-proxy (format: 1.2.3.4:8888)\n        --exclude STR              skip tracks specified in file\n        --include STR              download only tracks specified in file\n        --delay INT                delay between downloads (in seconds)\n        --mobile INT               use mobile API\n        --auth STR                 authorization header for mobile version\n                                   (OAuth...)\n        --cookie STR               authorization cookie for web version\n                                   (Session_id=...)\n        --bitrate INT              bitrate (eg. 64, 128, 192, 320)\n        --pattern STR              track naming pattern\n        --path STR                 path saving pattern\n\n        Available placeholders: #number, #artist,\n        #title, #album, #year\n\n        Path pattern will be used in addition to\n        the download path directory\n\n        Example path pattern: #artist/#album-#year\n\n        -l --link                  do not fetch, only print links to the\n                                   tracks\n        -s --silent                do not print informational messages\n        --debug                    print debug info during work\n        -h --help                  print usage\n\n        --include and --exclude options use weak\n        match i.e. ~/$term/\n\n        Example:\n        ya.pl -p 123 -k ya-playlist\n        ya.pl -a 123\n        ya.pl -a 123 -t 321\n        ya.pl -u\n        https://music.yandex.ru/album/215690\n        --cookie ...\n        ya.pl -u\n        https://music.yandex.ru/album/215688/track/1710808 --auth ...\n        ya.pl -u\n        https://music.yandex.ru/users/ya.playlist/playlists/1257 --cookie ...\n\n        © 2013-2023 by Kaimi (https://kaimi.io)\n```\n\n## FAQ\n### What is the cause for \"[ERROR] Yandex.Music is not available\"?\nCurrently Yandex Music is available only for Russia and CIS countries. For other countries you should either acquire paid subscription or use it through proxy (```--proxy``` parameter) from one of those countries. Thus it is possible to download from any country if you have an active Yandex.Music service subscription (https://music.yandex.ru/pay).\n\n## Contribute\nIf you want to help make Yandex Music Downloader better the easiest thing you can do is to report issues and feature requests. Or you can help in development.\n\n## License\nYandex Music Downloader Copyright © 2013-2022 by Kaimi (Sergey Belov) - https://kaimi.io.\n\nYandex Music Downloader is free software: you can redistribute it and/or modify it under the terms of the Massachusetts Institute of Technology (MIT) License.\n\nYou should have received a copy of the MIT License along with Yandex Music Downloader. If not, see [MIT License](LICENSE).\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = {self, nixpkgs, flake-utils}: flake-utils.lib.eachDefaultSystem (system:\n    let\n      pkgs = import nixpkgs { inherit system; };\n      yaMusic = pkgs.stdenv.mkDerivation {\n        name = \"yandex-download-music\";\n        version = \"v1.5\";\n        src = ./.;\n  \n        nativeBuildInputs = [\n          pkgs.makeWrapper\n        ];\n  \n        buildInputs = [\n          pkgs.perl\n          (pkgs.buildEnv {\n            name = \"rt-perl-deps\";\n            paths = with pkgs.perlPackages; (requiredPerlModules [\n                FileUtil\n                MP3Tag\n                GetoptLongDescriptive LWPUserAgent\n                LWPProtocolHttps\n                HTTPCookies\n                MozillaCA\n            ]);\n          })\n        ];\n  \n        installPhase = ''\n          mkdir -p $out/bin\n          cp src/ya.pl $out/bin/ya-music\n          # cat src/ya.pl | perl -p -e \"s/basename\\(__FILE__\\)/'ya-music'/g\" > $out/bin/ya-music\n          # chmod +x $out/bin/ya-music\n        '';\n  \n        postFixup = ''\n          # wrapProgram will rename ya-music into .ya-music-wrapped\n          # so replace all __FILE__ calls\n          substituteInPlace $out/bin/ya-music \\\n            --replace \"basename(__FILE__)\" \"'ya-music'\"\n  \n          wrapProgram $out/bin/ya-music \\\n            --prefix PERL5LIB : $PERL5LIB\n        '';\n      };\n    in\n    {\n      packages.default = yaMusic;\n      apps.default = flake-utils.lib.mkApp { drv = yaMusic; };\n    }\n  );\n}\n"
  },
  {
    "path": "src/ya.pl",
    "content": "#!/usr/bin/env perl\n\nuse utf8;\nuse strict;\nuse warnings;\nuse Encode qw/from_to decode/;\nuse Encode::Guess;\nuse File::Basename;\nuse POSIX qw/strftime/;\n\nuse constant IS_WIN => $^O eq 'MSWin32';\nuse constant\n{\n\tNL => IS_WIN ? \"\\015\\012\" : \"\\012\",\n\tTIMEOUT => 10,\n\tAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36',\n\tMOBILE_AGENT => 'Mozilla/5.0 (Linux; Android 13; Pixel 7 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Mobile Safari/537.36',\n\tYANDEX_BASE => 'https://music.yandex.ru',\n\tMOBILE_YANDEX_BASE => 'https://api.music.yandex.net',\n\tMD5_SALT => 'XGRlBW9FXlekgbPrRHuSiA',\n\tDOWNLOAD_INFO_MASK => '/api/v2.1/handlers/track/%d:%d/web-album_track-track-track-main/download/m?external-domain=music.yandex.ru&overembed=no&__t=%d&hq=%d',\n\tMOBILE_DOWNLOAD_INFO_MASK => '/tracks/%d/download-info',\n\tDOWNLOAD_PATH_MASK => 'https://%s/get-mp3/%s/%s?track-id=%s&from=service-10-track&similarities-experiment=default',\n\tPLAYLIST_INFO_MASK => '/handlers/playlist.jsx?owner=%s&kinds=%d&light=true&madeFor=&withLikesCount=true&lang=ru&external-domain=music.yandex.ru&overembed=false&ncrnd=',\n\tMOBILE_PLAYLIST_INFO_MASK => '/users/%s/playlists/%d',\n\tPLAYLIST_REQ_PART => '{\"userFeed\":\"old\",\"similarities\":\"default\",\"genreRadio\":\"new-ichwill-matrixnet6\",\"recommendedArtists\":\"ichwill_similar_artists\",\"recommendedTracks\":\"recommended_tracks_by_artist_from_history\",\"recommendedAlbumsOfFavoriteGenre\":\"recent\",\"recommendedSimilarArtists\":\"default\",\"recommendedArtistsWithArtistsFromHistory\":\"force_recent\",\"adv\":\"a\",\"loserArtistsWithArtists\":\"off\",\"ny2015\":\"no\"}',\n\tPLAYLIST_FULL_INFO => '/handlers/track-entries.jsx',\n\tALBUM_INFO_MASK => '/api/v2.1/handlers/album/%d?external-domain=music.yandex.ru&overembed=no&__t=%d',\n\tMOBILE_ALBUM_INFO_MASK => '/albums/%d/with-tracks',\n\tLYRICS_MASK => '/handlers/track.jsx?track=%d:%d&lang=ru&external-domain=music.yandex.ru&overembed=false&ncrnd=%d',\n\tFILE_NAME_PATTERN => '#artist - #title',\n\tDEFAULT_PERMISSIONS => 755,\n\t# For more details refer to 'create_track_entry' function\n\tPATTERN_MP3TAGS_RELS =>\n\t{\n\t\t'number' => 'TRCK',\n\t\t'artist' => 'TPE1',\n\t\t'title' => 'TIT2',\n\t\t'album' => 'TALB',\n\t\t'year' => 'TYER',\n\t},\n\tFILE_SAVE_EXT => '.mp3',\n\tCOVER_RESOLUTION => '400x400',\n\tGENERIC_COLLECTION => \"\\x{441}\\x{431}\\x{43e}\\x{440}\\x{43d}\\x{438}\\x{43a}\",\n\tGENERIC_TITLE => 'Various Artists',\n\tURL_ALBUM_REGEX => qr{music\\.yandex\\.\\w+/album/(\\d+)}is,\n\tURL_TRACK_REGEX => qr{music\\.yandex\\.\\w+/album/(\\d+)/track/(\\d+)}is,\n\tURL_PLAYLIST_REGEX => qr{music\\.yandex\\.\\w+/users/(.+?)/playlists/(\\d+)}is,\n\tRESPONSE_LOG_PREFIX => 'log_',\n\tTEST_URL => 'https://api.music.yandex.net/users/ya.playlist/playlists/1',\n\tRENAME_ERRORS_MAX => 5,\n\tAUTH_TOKEN_PREFIX => 'OAuth ',\n\tCOOKIE_PREFIX => 'Session_id=',\n\tHQ_BITRATE => '320',\n\tDEFAULT_CODEC => 'mp3',\n\tPODCAST_TYPE => 'podcast',\n\tVERSION => '1.5',\n\tCOPYRIGHT => '© 2013-2023 by Kaimi (https://kaimi.io)',\n};\nuse constant\n{\n\tPLAYLIST_LIKE => 3,\n\tPLAYLIST_LIKE_TITLE => 'Мне нравится'\n};\nuse constant\n{\n\tDEBUG => 'DEBUG',\n\tERROR => 'ERROR',\n\tINFO => 'INFO',\n\tOK => 'OK'\n};\nuse constant\n{\n\tWIN_UTF8_CODEPAGE => 65001,\n\tSTD_OUTPUT_HANDLE => 0xFFFFFFF5,\n\tFG_BLUE => 1,\n\tFG_GREEN => 2,\n\tFG_RED => 4,\n\tBG_WHITE => 112,\n\tSZ_CONSOLE_FONT_INFOEX => 84,\n\tFF_DONTCARE => 0 << 4,\n\tFW_NORMAL => 400,\n\tCOORD => 0x000c0000,\n\tFONT_NAME => 'Lucida Console'\n};\n\nmy %log_colors = \n(\n\t&DEBUG => \n\t{\n\t\tnix => 'red on_white',\n\t\twin => FG_RED | BG_WHITE\n\t},\n\t&ERROR => \n\t{\n\t\tnix => 'red',\n\t\twin => FG_RED\n\t},\n\t&INFO => \n\t{\n\t\tnix => 'blue on_white',\n\t\twin => FG_BLUE | BG_WHITE\n\t},\n\t&OK =>\n\t{\n\t\tnix => 'green on_white',\n\t\twin => FG_GREEN | BG_WHITE\n\t}\n);\n\nmy %req_modules = \n(\n\tNIX => [],\n\tWIN => [ qw/Win32::API Win32API::File Win32::Console/ ],\n\tALL => [ qw/Mozilla::CA Digest::MD5 File::Copy File::Spec File::Temp File::Util MP3::Tag JSON::PP Getopt::Long::Descriptive Term::ANSIColor LWP::UserAgent LWP::Protocol::https HTTP::Cookies HTML::Entities/ ]\n);\n\n$\\ = NL;\n\nmy @missing_modules;\nfor my $module(@{$req_modules{ALL}}, IS_WIN ? @{$req_modules{WIN}} : @{$req_modules{NIX}})\n{\n\t# Suppress MP3::Tag deprecated regex and other warnings\n\teval \"local \\$SIG{'__WARN__'} = sub {}; require $module\";\n\tif($@)\n\t{\n\t\tpush @missing_modules, $module;\n\t}\n}\n\nif(@missing_modules)\n{\n\tprint 'Please, install this modules: ' . join ', ', @missing_modules;\n\texit(1);\n}\n\n# PAR issue workaround && different win* approach for Unicode output\nif(IS_WIN)\n{\n\tbinmode STDOUT, ':unix:utf8';\n\t# Unicode (UTF-8) codepage\n\tWin32::Console::OutputCP(WIN_UTF8_CODEPAGE);\n\t$main::console = Win32::Console->new(STD_OUTPUT_HANDLE);\n\n\t# Set console font with Unicode support (only for Vista+ OS)\n\tif((Win32::GetOSVersion())[1] eq 6)\n\t{\n\t\t# FaceName size = LF_FACESIZE\n\t\tWin32::API::Struct->typedef\n\t\t(\n\t\t\tCONSOLE_FONT_INFOEX =>\n\t\t\tqw\n\t\t\t{\n\t\t\t\tULONG cbSize; \n\t\t\t\tDWORD nFont; \n\t\t\t\tDWORD dwFontSize;\n\t\t\t\tUINT FontFamily;\n\t\t\t\tUINT FontWeight;\n\t\t\t\tWCHAR FaceName[32];\n\t\t\t}\n\t\t);\n\n\t\tWin32::API->Import\n\t\t(\n\t\t\t'kernel32',\n\t\t\t'HANDLE WINAPI GetStdHandle(DWORD nStdHandle)'\n\t\t);\n\t\tWin32::API->Import\n\t\t(\n\t\t\t'kernel32',\n\t\t\t'BOOL WINAPI SetCurrentConsoleFontEx(HANDLE hConsoleOutput, BOOL bMaximumWindow, LPCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx)'\n\t\t);\n\n\t\tmy $font = Win32::API::Struct->new('CONSOLE_FONT_INFOEX');\n\n\t\t$font->{cbSize} = SZ_CONSOLE_FONT_INFOEX;\n\t\t$font->{nFont} = 0;\n\t\t$font->{dwFontSize} = COORD; # COORD struct wrap\n\t\t$font->{FontFamily} = FF_DONTCARE;\n\t\t$font->{FontWeight} = FW_NORMAL;\n\t\t$font->{FaceName} = Encode::encode('UTF-16LE', FONT_NAME);\n\n\t\tSetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), 0, $font);\n\t}\n}\nelse\n{\n\tbinmode STDOUT, ':encoding(utf8)';\n}\n\nmy ($opt, $usage) = Getopt::Long::Descriptive::describe_options\n(\n\t'Yandex Music Downloader v' . VERSION . NL . NL .\n\tbasename(__FILE__).' %o',\n\t['playlist|p:i',    'playlist id to download'],\n\t['kind|k:s',        'playlist kind (eg. ya-playlist, music-blog, music-partners, etc.)'],\n\t['album|a:i',       'album to download'],\n\t['track|t:i',       'track to download (album id must be specified)'],\n\t['url|u:s',         'download by URL'],\n\t['dir|d:s',         'download path (current direcotry will be used by default)', {default => '.'}],\n\t['skip-existing',   'skip downloading tracks that already exist on the specified path'],\n\t['proxy=s',         'HTTP-proxy (format: 1.2.3.4:8888)'],\n\t['exclude=s',       'skip tracks specified in file'],\n\t['include=s',       'download only tracks specified in file'],\n\t['delay=i',         'delay between downloads (in seconds)', {default => 5}],\n\t['mobile=i',        'use mobile API', {default => 0}],\n\t['auth=s',          'authorization header for mobile version (OAuth...)'],\n\t['cookie=s',        'authorization cookie for web version (Session_id=...)'],\n\t['bitrate=i',       'bitrate (eg. 64, 128, 192, 320)'],\n\t['pattern=s',       'track naming pattern', {default => FILE_NAME_PATTERN}],\n\t['path=s',          'path saving pattern', {default => ''}],\n\t[],\n\t['Available placeholders: #number, #artist, #title, #album, #year'],\n\t[],\n\t['Path pattern will be used in addition to the download path directory'],\n\t[],\n\t['Example path pattern: #artist/#album-#year'],\n\t[],\n\t['link|l',          'do not fetch, only print links to the tracks'],\n\t['silent|s',        'do not print informational messages'],\n\t['debug',           'print debug info during work'],\n\t['help|h',          'print usage'],\n\t[],\n\t['--include and --exclude options use weak match i.e. ~/$term/'],\n\t[],\n\t['Example: '],\n\t[basename(__FILE__) . ' -p 123 -k ya-playlist'],\n\t[basename(__FILE__) . ' -a 123'],\n\t[basename(__FILE__) . ' -a 123 -t 321'],\n\t[basename(__FILE__) . ' -u https://music.yandex.ru/album/215690 --cookie ...'],\n\t[basename(__FILE__) . ' -u https://music.yandex.ru/album/215688/track/1710808 --auth ...'],\n\t[basename(__FILE__) . ' -u https://music.yandex.ru/users/ya.playlist/playlists/1257 --cookie ...'],\n\t[],\n\t[COPYRIGHT]\n);\n\n# Get a modifiable options copy\nmy %opt = %{$opt};\n\nif( $opt{help} || ( !$opt{url} && !($opt{track} && $opt{album}) && !$opt{album} && !($opt{playlist} && $opt{kind}) )  )\n{\n\tprint $usage->text;\n\texit(0);\n}\n\nif(!$opt{auth} && !$opt{cookie})\n{\n\tinfo(ERROR, 'Please, specify either mobile app auth header value (--auth) or web version auth cookie (--cookie)');\n\tinfo(ERROR, 'It is no longer possible to download full version of tracks without authentication');\n\texit(1);\n}\n\nif($opt{mobile} && !$opt{auth} && $opt{cookie})\n{\n\tinfo(ERROR, 'Please, provide --auth instead of --cookie for Mobile API');\n\texit(1);\n}\n\nif(!$opt{mobile} && $opt{auth} && !$opt{cookie})\n{\n\tinfo(ERROR, 'Please, provide --cookie instead of --auth for Web API');\n\texit(1);\n}\n\nif($opt{dir} && !-d $opt{dir})\n{\n\tinfo(ERROR, 'Please, specify an existing directory');\n\texit(1);\n}\n\n# Fix for \"Writing of ID3v2.4 is not fully supported (prohibited now via `write_v24')\"\nMP3::Tag->config(write_v24 => 1);\nMP3::Tag->config(id3v23_unsync => 0);\nMP3::Tag->config(decode_encoding_v2 => 'UTF-8');\n\nmy $ua = LWP::UserAgent->new\n(\n\tagent => $opt{mobile} ? MOBILE_AGENT : AGENT,\n\tdefault_headers => HTTP::Headers->new\n\t(\n\t\tX_Retpath_Y => 1\n\t),\n\tcookie_jar => HTTP::Cookies->new\n\t(\n\t\thide_cookie2 => 1\n\t),\n\ttimeout => TIMEOUT,\n\tssl_opts =>\n\t{\n\t\tverify_hostname => $opt{debug} ? 0 : 1,\n\t\tSSL_verify_mode => $opt{debug} ? IO::Socket::SSL->SSL_VERIFY_NONE : IO::Socket::SSL->SSL_VERIFY_PEER,\n\t},\n\tsend_te => 0\n);\n\nmy $download_ua = LWP::UserAgent->new\n(\n\tagent => $opt{mobile} ? MOBILE_AGENT : AGENT,\n\ttimeout => TIMEOUT,\n\tssl_opts =>\n\t{\n\t\tverify_hostname => $opt{debug} ? 0 : 1,\n\t\tSSL_verify_mode => $opt{debug} ? IO::Socket::SSL->SSL_VERIFY_NONE : IO::Socket::SSL->SSL_VERIFY_PEER,\n\t},\n\tsend_te => 0\n);\n\n# Fix auth token and cookie format if required\nmy $auth_token = '';\nif($opt{mobile} && $opt{auth})\n{\n\tif($opt{auth} !~ /${\\(AUTH_TOKEN_PREFIX)}/i)\n\t{\n\t\t$auth_token = AUTH_TOKEN_PREFIX;\n\t}\n\t$auth_token .= $opt{auth};\n\n\t$ua->default_header(Authorization => $auth_token);\n}\n\nmy $cookie = '';\nif(!$opt{mobile} && $opt{cookie})\n{\n\t$cookie = normalize_cookie($opt{cookie});\n\tif(!$cookie)\n\t{\n\t\texit(1);\n\t}\n\n\t$ua->default_header(Cookie => $cookie);\n}\n\nmy ($whole_file, $total_size);\n\nmy $json_decoder = JSON::PP->new->utf8->pretty->allow_nonref->allow_singlequote;\nmy @exclude = ();\nmy @include = ();\n\nif($opt{debug})\n{\n\tprint_debug_info();\n}\n\nif($opt{proxy})\n{\n\t$ua->proxy(['http', 'https'], 'http://' . $opt{proxy} . '/');\n\t$download_ua->proxy(['http', 'https'], 'http://' . $opt{proxy} . '/');\n}\n\nif($opt{exclude})\n{\n\t@exclude = read_file($opt{exclude});\n}\n\nif($opt{include})\n{\n\t@include = read_file($opt{include});\n}\n\nif($opt{url})\n{\n\tif($opt{url} =~ URL_TRACK_REGEX)\n\t{\n\t\t$opt{album} = $1;\n\t\t$opt{track} = $2;\n\t}\n\telsif($opt{url} =~ URL_ALBUM_REGEX)\n\t{\n\t\t$opt{album} = $1;\n\t}\n\telsif($opt{url} =~ URL_PLAYLIST_REGEX)\n\t{\n\t\t$opt{kind} = $1;\n\t\t$opt{playlist} = $2;\n\t}\n\telse\n\t{\n\t\tinfo(ERROR, 'Invalid URL format');\n\t}\n}\n\nif($opt{album} || ($opt{playlist} && $opt{kind}))\n{\n\tmy @track_list_info;\n=pod\n\tinfo(INFO, 'Checking Yandex.Music availability');\n\n\tmy $request = $ua->get(TEST_URL);\n\tif($request->code != 404)\n\t{\n\t\tinfo(ERROR, 'Yandex.Music is not available');\n\t\texit(1);\n\t}\n\telse\n\t{\n\t\tinfo(OK, 'Yandex.Music is available')\n\t}\n=cut\n\tif($opt{album})\n\t{\n\t\tinfo(INFO, 'Fetching album info: ' . $opt{album});\n\n\t\t@track_list_info = get_album_tracks_info($opt{album});\n\n\t\tif(scalar @track_list_info > 0 && $opt{track})\n\t\t{\n\t\t\tinfo(INFO, 'Filtering single track: ' . $opt{track} . ' [' . $opt{album} . ']');\n\t\t\t@track_list_info = grep\n\t\t\t(\n\t\t\t\t$_->{track_id} eq $opt{track}\n\t\t\t\t,\n\t\t\t\t@track_list_info\n\t\t\t);\n\t\t}\n\t}\n\telse\n\t{\n\t\tinfo(INFO, 'Fetching playlist info: ' . $opt{playlist} . ' [' . $opt{kind} . ']');\n\n\t\t@track_list_info = get_playlist_tracks_info($opt{playlist});\n\t}\n\n\n\tif(!@track_list_info)\n\t{\n\t\tinfo(ERROR, 'Can\\'t get track list info');\n\t\texit(1);\n\t}\n\n\tfor my $track_info_ref(@track_list_info)\n\t{\n\t\tmy $skip = 0;\n\t\tfor my $title(@exclude)\n\t\t{\n\t\t\tif($track_info_ref->{title} =~ /\\Q$title\\E/)\n\t\t\t{\n\t\t\t\t$skip = 1;\n\t\t\t\tlast;\n\t\t\t}\n\t\t}\n\n\t\tif($opt{skip_existing} && track_file_exists($track_info_ref))\n\t\t{\n\t\t\t$skip = 1;\n\t\t}\n\n\t\tif($skip)\n\t\t{\n\t\t\tinfo(INFO, 'Skipping: ' . $track_info_ref->{title});\n\t\t\tnext;\n\t\t}\n\n\t\t$skip = 1;\n\t\tfor my $title(@include)\n\t\t{\n\t\t\tif($track_info_ref->{title} =~ /\\Q$title\\E/)\n\t\t\t{\n\t\t\t\t$skip = 0;\n\t\t\t\tlast;\n\t\t\t}\n\t\t}\n\t\tif($skip && $opt{include})\n\t\t{\n\t\t\tinfo(INFO, 'Skipping: ' . $track_info_ref->{title});\n\t\t\tnext;\n\t\t}\n\n\t\tif(!$track_info_ref->{title})\n\t\t{\n\t\t\tinfo(ERROR, 'Track with non-existent title. Skipping...');\n\t\t\tnext;\n\t\t}\n\n\t\tif($opt{link})\n\t\t{\n\t\t\tprint(get_track_url($track_info_ref));\n\t\t}\n\t\telse\n\t\t{\n\t\t\tfetch_track($track_info_ref);\n\n\t\t\tif($opt{delay} && $track_info_ref != $track_list_info[-1])\n\t\t\t{\n\t\t\t\tinfo(INFO, 'Waiting for ' . $opt{delay} . ' seconds');\n\t\t\t\tsleep $opt{delay};\n\t\t\t}\n\t\t}\n\t}\n\n\tinfo(OK, 'Done!');\n}\n\nif(IS_WIN)\n{\n\t$main::console->Free();\n}\n\nsub fetch_track\n{\n\tmy $track_info_ref = shift;\n\n\t$track_info_ref->{title} =~ s/\\s+$//;\n\t$track_info_ref->{title} =~ s/[\\\\\\/:\"*?<>|]+/-/g;\n\n\tinfo(INFO, 'Trying to fetch track: '.$track_info_ref->{title});\n\n\tmy $track_url = get_track_url($track_info_ref);\n\tif(!$track_url)\n\t{\n\t\tinfo(ERROR, 'Can\\'t get track url');\n\t\treturn;\n\t}\n\n\tmy $file_path = download_track($track_url);\n\tif(!$file_path)\n\t{\n\t\tinfo(ERROR, 'Failed to download track');\n\t\treturn;\n\t}\n\n\tinfo(OK, 'Temporary saved track at '.$file_path);\n\n\tfetch_album_cover($track_info_ref->{mp3tags});\n\tfetch_track_lyrics($track_info_ref);\n\n\tif(write_mp3_tags($file_path, $track_info_ref->{mp3tags}))\n\t{\n\t\tinfo(INFO, 'MP3 tags added for ' . $file_path);\n\t}\n\telse\n\t{\n\t\tinfo(ERROR, 'Failed to add MP3 tags for ' . $file_path);\n\t}\n\n\tmy $target_path = create_storage_path($track_info_ref);\n\tif(!$target_path)\n\t{\n\t\tinfo(ERROR, 'Failed to create: ' . $target_path);\n\t\treturn;\n\t}\n\n\t$target_path = File::Spec->catfile($target_path,  $track_info_ref->{title} . FILE_SAVE_EXT);\n\n\tif(rename_track($file_path, $target_path))\n\t{\n\t\tinfo(INFO, $file_path . ' -> ' . $target_path);\n\t}\n\telse\n\t{\n\t\tinfo(ERROR, $file_path . ' -> ' . $target_path);\n\t}\n}\n\nsub create_storage_path\n{\n\tmy $track_info_ref = shift;\n\n\tmy $target_path = get_storage_path($track_info_ref);\n\n\tmy $file_util = File::Util->new();\n\tif(!-d $file_util->make_dir($target_path => oct DEFAULT_PERMISSIONS => {if_not_exists => 1}))\n\t{\n\t\treturn;\n\t}\n\n\treturn $target_path;\n}\n\nsub track_file_exists\n{\n\tmy $track_info_ref = shift;\n\n\tmy $target_path = get_storage_path($track_info_ref);\n\t$target_path = File::Spec->catfile($target_path,  $track_info_ref->{title} . FILE_SAVE_EXT);\n\n\treturn -e $target_path;\n}\n\nsub get_storage_path\n{\n\tmy $track_info_ref = shift;\n\n\tmy $target_path = $opt{dir};\n\tif($opt{path})\n\t{\n\t\t$target_path = File::Spec->catdir($target_path, $track_info_ref->{storage_path});\n\t}\n\n\treturn $target_path;\n}\n\nsub download_track\n{\n\tmy ($url) = @_;\n\n\tmy $request = $download_ua->head($url);\n\tif(!$request->is_success)\n\t{\n\t\tinfo(DEBUG, 'Request failed');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\t$whole_file = '';\n\t$total_size = $request->headers->content_length;\n\n\tinfo(DEBUG, 'File size from header: ' . $total_size);\n\n\t$request = $download_ua->get($url, ':content_cb' => \\&progress);\n\tif(!$request->is_success)\n\t{\n\t\tinfo(DEBUG, 'Request failed');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tmy ($file_handle, $file_path) = File::Temp::tempfile(DIR => $opt{dir});\n\treturn unless $file_handle;\n\n\tbinmode $file_handle;\n\t# Autoflush file contents\n\tselect((select($file_handle),$|=1)[0]);\n\t{\n\t\tlocal $\\ = undef;\n\t\tprint $file_handle $whole_file;\n\t}\n\n\tmy $disk_data_size = (stat($file_handle))[7];\n\tclose $file_handle;\n\n\tif($total_size && $disk_data_size != $total_size)\n\t{\n\t\tinfo(DEBUG, 'Actual file size differs from expected ('.$disk_data_size.'/'.$total_size.')');\n\t}\n\n\treturn $file_path;\n}\n\nsub get_track_url\n{\n\tmy $track_info_ref = shift;\n\n\tmy $album_id = $track_info_ref->{album_id};\n\tmy $track_id = $track_info_ref->{track_id};\n\tmy $is_hq = ($opt{bitrate} && ($opt{bitrate} eq HQ_BITRATE)) ? 1 : 0;\n\t# Get track path information\n\tmy $request = $ua->get\n\t(\n\t\t$opt{mobile} ?\n\t\t\tMOBILE_YANDEX_BASE.sprintf(MOBILE_DOWNLOAD_INFO_MASK, $track_id)\n\t\t\t:\n\t\t\tYANDEX_BASE.sprintf(DOWNLOAD_INFO_MASK, $track_id, $album_id, time, $is_hq)\n\t);\n\tif(!$request->is_success)\n\t{\n\t\tinfo(DEBUG, 'Request failed');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tmy ($json_data) = $request->content;\n\tif(!$json_data)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t parse JSON blob');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tmy $json = create_json($json_data);\n\tif(!$json)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t create json from data');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\t# Pick specified bitrate or highest available\n\tmy $url;\n\tif($opt{mobile})\n\t{\n\t\t# Sort by available bitrate (highest first)\n\t\t@{$json->{result}} = sort { $b->{bitrateInKbps} <=> $a->{bitrateInKbps} } @{$json->{result}};\n\n\t\tmy ($idx, $target_idx) = (0, -1);\n\t\tfor my $track_info(@{$json->{result}})\n\t\t{\n\t\t\tif($track_info->{codec} eq DEFAULT_CODEC)\n\t\t\t{\n\t\t\t\tif($opt{bitrate} && $track_info->{bitrateInKbps} == $opt{bitrate})\n\t\t\t\t{\n\t\t\t\t\t$target_idx = $idx;\n\t\t\t\t\tlast;\n\t\t\t\t}\n\t\t\t\telsif(!$opt{bitrate})\n\t\t\t\t{\n\t\t\t\t\t$target_idx = $idx;\n\t\t\t\t\tlast;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t$idx++;\n\t\t}\n\n\t\tif($target_idx < 0)\n\t\t{\n\t\t\tinfo(DEBUG, 'Can\\'t find track with proper format & bitrate');\n\t\t\tlog_response($request);\n\t\t\treturn;\n\t\t}\n\n\t\t$url = @{$json->{result}}[$target_idx]->{downloadInfoUrl};\n\t}\n\telse\n\t{\n\t\t$url = $json->{src};\n\t}\n\n\t$url = 'https:' . $url unless $url =~ /^https:/;\n\t$request = $download_ua->get($url);\n\tif(!$request->is_success)\n\t{\n\t\tinfo(DEBUG, 'Request failed');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\t# No proper XML parsing cause it will break soon\n\tmy %fields = ($request->content =~ /<(\\w+)>([^<]+?)<\\/\\w+>/g);\n\n\tmy $hash = Digest::MD5::md5_hex(MD5_SALT . substr($fields{path}, 1) . $fields{s});\n\t$url = sprintf(DOWNLOAD_PATH_MASK, $fields{host}, $hash, $fields{ts}.$fields{path}, $track_id);\n\n\tinfo(DEBUG, 'Track url: ' . $url);\n\n\treturn $url;\n}\n\nsub get_album_tracks_info\n{\n\tmy $album_id = shift;\n\n\tmy $request = $ua->get\n\t(\n\t\t$opt{mobile} ?\n\t\t\tMOBILE_YANDEX_BASE.sprintf(MOBILE_ALBUM_INFO_MASK, $album_id)\n\t\t\t:\n\t\t\tYANDEX_BASE.sprintf(ALBUM_INFO_MASK, $album_id, time)\n\t);\n\tif(!$request->is_success)\n\t{\n\t\tinfo(DEBUG, 'Request failed');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\n\tmy ($json_data) = $request->content;\n\tif(!$json_data)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t parse JSON blob');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tmy $json = create_json($json_data);\n\tif(!$json)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t create json from data: ' . $@);\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\t# \"Rebase\" JSON\n\t$json = $opt{mobile} ? $json->{'result'} : $json;\n\n\tmy $title = $json->{title};\n\tif(!$title)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t get album title');\n\t\treturn;\n\t}\n\n\tinfo(INFO, 'Album title: ' . $title);\n\tinfo(INFO, 'Tracks total: ' . $json->{trackCount});\n\n\tif($opt{mobile} && !$json->{availableForMobile})\n\t{\n\t\tinfo(ERROR, 'Album is not available via Mobile API');\n\t\treturn;\n\t}\n\n\tmy @tracks = ();\n\tfor my $vol(@{$json->{volumes}})\n\t{\n\t\tfor my $track(@{$vol})\n\t\t{\n\t\t\tif(!$track->{error})\n\t\t\t{\n\t\t\t\tpush @tracks, create_track_entry($track, 0);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn @tracks;\n}\n\nsub get_playlist_tracks_info\n{\n\tmy $playlist_id = shift;\n\n\tmy $request = $ua->get\n\t(\n\t\t$opt{mobile} ?\n\t\t\tMOBILE_YANDEX_BASE.sprintf(MOBILE_PLAYLIST_INFO_MASK, $opt{kind}, $playlist_id)\n\t\t\t:\n\t\t\tYANDEX_BASE.sprintf(PLAYLIST_INFO_MASK, $opt{kind}, $playlist_id)\n\t);\n\tif(!$request->is_success)\n\t{\n\t\tinfo(DEBUG, 'Request failed');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tmy ($json_data) = $request->content;\n\tif(!$json_data)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t parse JSON blob');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tmy $json = create_json($json_data);\n\tif(!$json)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t create json from data: ' . $@);\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tmy $title =  $opt{mobile}\n\t\t?\n\t\t( $opt{playlist} == PLAYLIST_LIKE ? PLAYLIST_LIKE_TITLE : $json->{result}->{title} )\n\t\t:\n\t\t$json->{playlist}->{title};\n\n\tif(!$title)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t get playlist title');\n\t\treturn;\n\t}\n\n\tinfo(INFO, 'Playlist title: ' . $title);\n\tinfo\n\t(\n\t\tINFO,\n\t\t'Tracks total: ' .\n\t\t(\n\t\t\t$opt{mobile} ?\n\t\t\t\t$json->{result}->{trackCount}\n\t\t\t\t:\n\t\t\t\t$json->{playlist}->{trackCount}\n\t\t)\n\t);\n\n\tmy @tracks_info;\n\tmy $track_number = 1;\n\n\tif(!$opt{mobile} && $json->{playlist}->{trackIds})\n\t{\n\t\tmy @playlist_chunks;\n\t\tmy $tracks_ref = $json->{playlist}->{trackIds};\n\t\tmy $sign = $json->{authData}->{user}->{sign};\n\n\t\tpush @playlist_chunks, [splice @{$tracks_ref}, 0, 150] while @{$tracks_ref};\n\n\t\tfor my $chunk(@playlist_chunks)\n\t\t{\n\t\t\t$request = $ua->post\n\t\t\t(\n\t\t\t\tYANDEX_BASE.PLAYLIST_FULL_INFO,\n\t\t\t\t{\n\t\t\t\t\tstrict => 'true',\n\t\t\t\t\tsign => $sign,\n\t\t\t\t\tlang => 'ru',\n\t\t\t\t\texperiments => PLAYLIST_REQ_PART,\n\t\t\t\t\tentries => join ',', @{$chunk}\n\t\t\t\t}\n\t\t\t);\n\n\t\t\tif(!$request->is_success)\n\t\t\t{\n\t\t\t\tinfo(DEBUG, 'Request failed');\n\t\t\t\tlog_response($request);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t$json = create_json($request->content);\n\t\t\tif(!$json)\n\t\t\t{\n\t\t\t\tinfo(DEBUG, 'Can\\'t create json from data');\n\t\t\t\tlog_response($request);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tpush @tracks_info,\n\t\t\t\tmap\n\t\t\t\t{\n\t\t\t\t\tcreate_track_entry($_, $track_number++)\n\t\t\t\t} grep { !$_->{error} } @{ $json };\n\t\t}\n\t}\n\telse\n\t{\n\t\t@tracks_info = map\n\t\t{\n\t\t\tcreate_track_entry\n\t\t\t(\n\t\t\t\t$opt{mobile} ?\n\t\t\t\t\t$_->{track}\n\t\t\t\t\t:\n\t\t\t\t\t$_\n\t\t\t\t, $track_number++\n\t\t\t)\n\t\t}\n\t\tgrep { !$_->{error} }\n\t\t@\n\t\t{ \n\t\t\t$opt{mobile} ?\n\t\t\t\t$json->{result}->{tracks}\n\t\t\t\t:\n\t\t\t\t$json->{playlist}->{tracks} \n\t\t};\n\t}\n\n\treturn @tracks_info;\n}\n\nsub create_track_entry\n{\n\tmy ($track_info, $track_number) = @_;\n\n\t# Better detection algo?\n\tmy $is_part_of_album = scalar @{$track_info->{albums}} != 0;\n\n\tmy $is_various;\n\tif\n\t(\n\t\texists $track_info->{albums}->[0]->{metaType}\n\t\t&&\n\t\t$track_info->{albums}->[0]->{metaType} ne PODCAST_TYPE\n\t)\n\t{\n\t\t$is_various =\n\t\t\tscalar @{$track_info->{artists}} > 1\n\t\t\t||\n\t\t\t($is_part_of_album && $track_info->{albums}->[0]->{artists}->[0]->{name} eq GENERIC_COLLECTION)\n\t\t;\n\t}\n\n\t# TALB - album title; TPE2 - album artist;\n\t# APIC - album picture; TYER - year;\n\t# TIT2 - song title; TPE1 - song artist;\n\t# TCON - track genre; TRCK - track number\n\t# USLT - unsychronised lyrics\n\tmy %mp3_tags = ();\n\t# Special case for podcasts\n\tif($track_info->{albums}->[0]->{metaType} eq PODCAST_TYPE)\n\t{\n\t\t$mp3_tags{TPE1} = $track_info->{albums}->[0]->{title};\n\t}\n\telse\n\t{\n\t\t$mp3_tags{TPE1} = join ', ', map { $_->{name} } @{$track_info->{artists}};\n\t}\n\t$mp3_tags{TIT2} = $track_info->{title};\n\t# No track number info in JSON if fetching from anything but album\n\tif($track_number)\n\t{\n\t\t$mp3_tags{TRCK} = $track_number;\n\t}\n\telse\n\t{\n\t\t$mp3_tags{TRCK} = $track_info->{albums}->[0]->{trackPosition}->{index};\n\t}\n\n\t# Append track postfix (like remix) if present\n\tif(exists $track_info->{version})\n\t{\n\t\t$mp3_tags{TIT2} .= \"\\x20\" . '(' . $track_info->{version} . ')';\n\t}\n\n\t# For deleted tracks\n\tif($is_part_of_album)\n\t{\n\t\t$mp3_tags{TALB} = $track_info->{albums}->[0]->{title};\n\t\tif($track_info->{albums}->[0]->{metaType} eq PODCAST_TYPE)\n\t\t{\n\t\t\t$mp3_tags{TPE2} = $mp3_tags{TALB};\n\t\t}\n\t\telse\n\t\t{\n\t\t\t$mp3_tags{TPE2} = $is_various ? GENERIC_TITLE : $track_info->{albums}->[0]->{artists}->[0]->{name};\n\t\t}\n\t\t# 'Dummy' cover for post-process\n\t\t$mp3_tags{APIC} = $track_info->{albums}->[0]->{coverUri};\n\t\t$mp3_tags{TYER} = $track_info->{albums}->[0]->{year};\n\t\t$mp3_tags{TCON} = $track_info->{albums}->[0]->{genre};\n\t}\n\n\t# Substitute placeholders within a track name and a path name\n\tmy $track_filename = $opt{pattern};\n\tmy $storage_path = $opt{path};\n\twhile (my ($pattern, $tag_id) = each %{&PATTERN_MP3TAGS_RELS})\n\t{\n\t\t$track_filename =~ s/\\#$pattern/$mp3_tags{$tag_id}/gi;\n\t\t$storage_path =~ s/\\#$pattern/$mp3_tags{$tag_id}/gi;\n\t}\n\n\treturn\n\t{\n\t\t# Album id\n\t\talbum_id => $track_info->{albums}->[0]->{id},\n\t\t# Track id\n\t\ttrack_id => $track_info->{id},\n\t\t# MP3 tags\n\t\tmp3tags => \\%mp3_tags,\n\t\t# 'Save As' file name\n\t\ttitle => $track_filename,\n\t\t# 'Save As' directory\n\t\tstorage_path => $storage_path,\n\t};\n}\n\nsub write_mp3_tags\n{\n\tmy ($file_path, $mp3tags) = @_;\n\n\tmy $mp3 = MP3::Tag->new($file_path);\n\tif(!$mp3)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t create MP3::Tag object: ' . $@);\n\t\treturn;\n\t}\n\n\t$mp3->new_tag('ID3v2');\n\n\twhile(my ($frame, $data) = each %{$mp3tags})\n\t{\n\t\t# Skip empty\n\t\tif($data)\n\t\t{\n\t\t\tinfo(DEBUG, 'add_frame: ' . $frame . '=' . substr $data, 0, 16);\n\n\t\t\t$mp3->{ID3v2}->add_frame\n\t\t\t(\n\t\t\t\t$frame,\n\t\t\t\tref $data eq ref [] ? @{$data} : $data\n\t\t\t);\n\t\t}\n\t}\n\n\t$mp3->{ID3v2}->write_tag;\n\t$mp3->close();\n\n\treturn 1;\n}\n\nsub fetch_album_cover\n{\n\tmy $mp3tags = shift;\n\n\tmy $cover_url = $mp3tags->{APIC};\n\tif(!$cover_url)\n\t{\n\t\tinfo(DEBUG, 'Empty cover URL');\n\t\treturn;\n\t}\n\n\t# Normalize url\n\t$cover_url =~ s/%%/${\\(COVER_RESOLUTION)}/;\n\t$cover_url = 'https://' . $cover_url;\n\n\tinfo(DEBUG, 'Cover URL: ' . $cover_url);\n\n\tmy $request = $ua->get($cover_url);\n\tif(!$request->is_success)\n\t{\n\t\tinfo(DEBUG, 'Request failed');\n\t\tlog_response($request);\n\t\tundef $mp3tags->{APIC};\n\t\treturn;\n\t}\n\n\t$mp3tags->{APIC} = [chr(0x0), 'image/jpg', chr(0x0), 'Cover (front)', $request->content];\n}\n\nsub fetch_track_lyrics\n{\n\tmy $track_info_ref = shift;\n\n\tmy $mp3tags = $track_info_ref->{mp3tags};\n\tmy $lyrics_url = YANDEX_BASE.sprintf(LYRICS_MASK, $track_info_ref->{track_id}, $track_info_ref->{album_id}, time);\n\n\tinfo(DEBUG, 'Lyrics URL: ' . $lyrics_url);\n\n\tmy $request = $ua->get($lyrics_url);\n\tif(!$request->is_success)\n\t{\n\t\tinfo(DEBUG, 'Request failed');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tmy ($json_data) = $request->content;\n\tif(!$json_data)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t parse JSON blob');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tmy $json = create_json($json_data);\n\tif(!$json)\n\t{\n\t\tinfo(DEBUG, 'Can\\'t create json from data');\n\t\tlog_response($request);\n\t\treturn;\n\t}\n\n\tif($json->{lyricsAvailable})\n\t{\n\t\tmy $lyrics = $json->{lyric}->[0]->{fullLyrics};\n\t\t# Encoding flag explanation: $03 UTF-8 [UTF-8]\n\t\t$mp3tags->{USLT} = [3, 'eng', undef, $lyrics];\n\t}\n}\n\nsub rename_track\n{\n\tmy ($src_path, $dst_path) = @_;\n\n\tmy ($src_fh, $dst_fh, $is_open_success, $errors) = (undef, undef, 1, 0);\n\n\tif(IS_WIN)\n\t{\n\t\t# Extend path limit to 32767\n\t\t$dst_path = '\\\\\\\\?\\\\' . File::Spec->rel2abs($dst_path);\n\t}\n\n\tfor(;;)\n\t{\n\t\tif($errors >= RENAME_ERRORS_MAX)\n\t\t{\n\t\t\tinfo(DEBUG, 'File manipulations failed');\n\t\t\tlast;\n\t\t}\n\n\t\tif(!$is_open_success)\n\t\t{\n\t\t\tclose $src_fh if $src_fh;\n\t\t\tclose $dst_fh if $dst_fh;\n\t\t\tunlink $src_path if -e $src_path;\n\n\t\t\tlast;\n\t\t}\n\n\t\t$is_open_success = open($src_fh, '<', $src_path);\n\t\tif(!$is_open_success)\n\t\t{\n\t\t\tinfo(DEBUG, 'Can\\'t open src_path: ' . $src_path);\n\t\t\t$errors++;\n\t\t\tredo;\n\t\t}\n\n\t\tif(IS_WIN)\n\t\t{\n\t\t\tmy $unicode_path = Encode::encode('UTF-16LE', $dst_path);\n\t\t\tEncode::_utf8_off($unicode_path);\n\t\t\t$unicode_path .= \"\\x00\\x00\";\n\t\t\t# GENERIC_WRITE, OPEN_ALWAYS\n\t\t\tmy $native_handle = Win32API::File::CreateFileW($unicode_path, 0x40000000, 0, [], 2, 0, 0);\n\t\t\t# ERROR_ALREADY_EXISTS\n\t\t\tif($^E && $^E != 183)\n\t\t\t{\n\t\t\t\tinfo(DEBUG, 'CreateFileW failed with: ' . $^E);\n\t\t\t\t$errors++;\n\t\t\t\tredo;\n\t\t\t}\n\n\t\t\t$is_open_success = Win32API::File::OsFHandleOpen($dst_fh = IO::Handle->new(), $native_handle, 'w');\n\t\t\tif(!$is_open_success)\n\t\t\t{\n\t\t\t\tinfo(DEBUG, 'OsFHandleOpen failed with: ' . $!);\n\t\t\t\t$errors++;\n\t\t\t\tredo;\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t$is_open_success = open($dst_fh, '>', $dst_path);\n\t\t\tif(!$is_open_success)\n\t\t\t{\n\t\t\t\tinfo(DEBUG, 'Can\\'t open dst_path: ' . $dst_path);\n\t\t\t\t$errors++;\n\t\t\t\tredo;\n\t\t\t}\n\t\t}\n\n\t\tif(!File::Copy::copy($src_fh, $dst_fh))\n\t\t{\n\t\t\t$is_open_success = 0;\n\t\t\tinfo(DEBUG, 'File::Copy::copy failed with: ' . $!);\n\t\t\t$errors++;\n\t\t\tredo;\n\t\t}\n\n\t\tclose $src_fh;\n\t\tclose $dst_fh;\n\n\t\tunlink $src_path;\n\n\t\treturn 1;\n\t}\n\n\treturn 0;\n}\n\nsub create_json\n{\n\tmy $json_data = shift;\n\n\tmy $json;\n\teval\n\t{\n\t\t$json = $json_decoder->decode($json_data);\n\t};\n\n\tif($@)\n\t{\n\t\tinfo(DEBUG, 'Error decoding json ' . $@);\n\t\treturn;\n\t}\n\n\tHTML::Entities::decode_entities($json_data);\n\n\treturn $json;\n}\n\nsub info\n{\n\tmy ($type, $msg) = @_;\n\n\tif($opt{silent} && $type ne ERROR)\n\t{\n\t\t return;\n\t}\n\n\tif($type eq DEBUG)\n\t{\n\t\treturn if !$opt{debug};\n\t\t# Func, line, msg\n\t\t$msg = (caller(1))[3] . \"(\" . (caller(0))[2] . \"): \" . $msg;\n\t}\n\n\tif(IS_WIN)\n\t{\n\t\tlocal $\\ = undef;\n\n\t\tmy $attr = $main::console->Attr();\n\t\t$main::console->Attr($log_colors{$type}->{win});\n\n\t\tprint '['.$type.']';\n\n\t\t$main::console->Attr($attr);\n\t\t$msg = ' ' . $msg;\n\t}\n\telse\n\t{\n\t\t$msg = Term::ANSIColor::colored('['.$type.']', $log_colors{$type}->{nix}) . ' ' . $msg;\n\t}\n\t# Actual terminal width detection?\n\t$msg = sprintf('%-80s', $msg);\n\n\tmy $out = $type eq ERROR ? *STDERR : *STDOUT;\n\tprint $out $msg;\n}\n\nsub progress\n{\n\tmy ($data, undef, undef) = @_;\n\n\t$whole_file .= $data;\n\tprint progress_bar(length($whole_file), $total_size);\n}\n\nsub progress_bar\n{\n\tmy ($got, $total, $width, $char) = @_;\n\n\t$width ||= 25; $char ||= '=';\n\tmy $num_width = length $total;\n\tsprintf \"|%-${width}s| Got %${num_width}s bytes of %s (%.2f%%)\\r\", \n\t\t$char x (($width-1) * $got / $total). '>', \n\t\t$got, $total, 100 * $got / +$total;\n}\n\nsub read_file\n{\n\tmy $filename = shift;\n\n\tif(open(my $fh, '<', $filename))\n\t{\n\t\tbinmode $fh;\n\t\tchomp(my @lines = <$fh>);\n\t\tclose $fh;\n\n\t\t# Should I just drop this stuff and demand only utf8?\n\t\tmy $blob = join '', @lines;\n\t\tmy $decoder = Encode::Guess->guess($blob, 'utf8');\n\t\t$decoder = Encode::Guess->guess($blob, 'cp1251') unless ref $decoder;\n\n\t\tif(!ref $decoder)\n\t\t{\n\t\t\tinfo(ERROR, 'Can\\'t detect ' . $filename . ' internal encoding');\n\t\t\treturn;\n\t\t}\n\n\t\t@lines = map($decoder->decode($_), @lines);\n\n\t\treturn @lines;\n\t}\n\n\tinfo(ERROR, 'Failed to open file ' . $opt{ignore});\n\n\treturn;\n}\n\nsub normalize_cookie\n{\n\tmy $cookie = shift;\n\tmy $cookie_prefix = COOKIE_PREFIX;\n\treturn if !defined $cookie;\n\n\t$cookie =~ s/^\\s+|\\s+$//g;\n\treturn if !$cookie;\n\n\tif($cookie =~ /^\\Q$cookie_prefix\\E[^;\\s]+$/i)\n\t{\n\t\treturn $cookie;\n\t}\n\n\tif($cookie !~ /[=;]/)\n\t{\n\t\treturn COOKIE_PREFIX . $cookie;\n\t}\n\n\tif($cookie =~ /(?:^|;\\s*)\\Q$cookie_prefix\\E([^;\\s]+)/i)\n\t{\n\t\treturn $cookie_prefix . $1;\n\t}\n\n\tinfo(ERROR, 'Failed to find Session_id in provided cookie string');\n\n\treturn;\n}\n\nsub log_response\n{\n\tmy $response = shift;\n\treturn if !$opt{debug};\n\n\tmy $log_filename = RESPONSE_LOG_PREFIX . time;\n\tif(open(my $fh, '>', $log_filename))\n\t{\n\t\tbinmode $fh;\n\t\tprint $fh $response->as_string;\n\t\tclose $fh;\n\n\t\tinfo(DEBUG, 'Response stored at ' . $log_filename);\n\t}\n\telse\n\t{\n\t\tinfo(DEBUG, 'Failed to store response stored at ' . $log_filename);\n\t}\n}\n\nsub print_debug_info\n{\n\tinfo(DEBUG, 'Yandex Music Downloader v' . VERSION . NL . NL);\n\tinfo(DEBUG, 'OS: ' . $^O . '; Path: ' . $^X . '; Version: ' . $^V);\n\t\n\tinfo(DEBUG, 'Cookie: <provided>') if $opt{cookie};\n\tinfo(DEBUG, 'Auth: <provided>') if $opt{auth};\n}\n"
  }
]