Advertisement
Guest User

ya.pl

a guest
Apr 7th, 2014
146
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Perl 10.06 KB | None | 0 0
  1. use strict;
  2. use warnings;
  3. use Encode qw/from_to/;
  4. use File::Basename;
  5. use POSIX qw/strftime/;
  6. use YaHash;
  7.  
  8. use constant IS_WIN => $^O eq 'MSWin32';
  9. use constant
  10. {
  11.     NL => IS_WIN ? "\015\012" : "\012",
  12.     TARGET_ENC => IS_WIN ? 'cp1251' : 'utf8',
  13.     TIMEOUT => 5,
  14.     AGENT => 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:25.0) Gecko/20100101 Firefox/25.0',
  15.     YANDEX_BASE => 'http://music.yandex.ru',
  16.     TRACK_URI_MASK => '/fragment/track/%d/album/%d?prefix=%s',
  17.     DOWNLOAD_INFO_MASK => '/xml/storage-proxy.xml?p=download-info/%s/2.mp3&nc=%d',
  18.     DOWNLOAD_PATH_MASK => 'http://%s/get-mp3/%s/%s?track-id=%s&from=service-10-track&similarities-experiment=default',
  19.     PLAYLIST_INFO_MASK => '/get/playlist2.xml?kinds=%d&owner=%s&r=%d',
  20.     PLAYLIST_TRACK_INFO_MASK => '/get/tracks.xml?tracks=%s',
  21.     ALBUM_INFO_MASK => '/fragment/album/%d?prefix=%s',
  22.     FILE_SAVE_EXT => '.mp3',
  23.     ARTIST_TITLE_DELIM => ' - ',
  24.     FACEGEN => POSIX::strftime('facegen-%Y-%m-%dT00-00-00', localtime)
  25. };
  26. use constant
  27. {
  28.     DEBUG => 'DEBUG',
  29.     ERROR => 'ERROR',
  30.     INFO => 'INFO',
  31.     OK => 'OK'
  32. };
  33.  
  34. my %log_colors =
  35. (
  36.     &DEBUG => 'red on_white',
  37.     &ERROR => 'red',
  38.     &INFO => 'blue on_white',
  39.     &OK => 'green on_white'
  40. );
  41.  
  42. my %req_modules =
  43. (
  44.     NIX => [],
  45.     WIN => [ qw/Win32::Console::ANSI/ ],
  46.     ALL => [ qw/JSON::PP Getopt::Long::Descriptive Term::ANSIColor LWP::UserAgent HTTP::Cookies HTML::Entities/ ]
  47. );
  48.  
  49. $\ = NL;
  50.  
  51. my @missing_modules;
  52. for(@{$req_modules{ALL}}, IS_WIN ? @{$req_modules{WIN}} : @{$req_modules{NIX}})
  53. {
  54.     eval "require $_";
  55.     if($@)
  56.     {
  57.         ($_) = $@ =~ /locate (.+?)(?:\.pm)? in \@INC/;
  58.         $_ =~ s/\//::/g;
  59.         push @missing_modules, $_;
  60.     }
  61. }
  62.  
  63. if(@missing_modules)
  64. {
  65.     print 'Please, install this modules: '.join ', ', @missing_modules;
  66.     exit;
  67. }
  68.  
  69. my ($opt, $usage) = Getopt::Long::Descriptive::describe_options
  70. (
  71.     basename(__FILE__).' %o',
  72.     ['playlist|p:i',    'playlist id to download'],
  73.     ['kind|k:s',        'playlist kind (eg. ya-playlist, music-blog, music-partners, etc.)'],
  74.     ['album|a:i',       'album to download'],
  75.     ['track|t:i',       'track to download (album id must be specified)'],
  76.     ['dir|d:s',         'download path (current direcotry will be used by default)', {default => '.'}],
  77.     [],
  78.     ['debug',           'print debug info during work'],
  79.     ['help',            'print usage'],
  80.     [],
  81.     ['Example: '],
  82.     ["\t".basename(__FILE__).' -p 123 -k ya-playlist'],
  83.     ["\t".basename(__FILE__).' -a 123'],
  84.     ["\t".basename(__FILE__).' -a 123 -t 321']
  85. );
  86.  
  87. if( $opt->help || ( !($opt->track && $opt->album) && !$opt->album && !($opt->playlist && $opt->kind) )  )
  88. {
  89.     print $usage->text;
  90.     exit;
  91. }
  92.  
  93. if($opt->dir && !-d $opt->dir)
  94. {
  95.     info(ERROR, 'Please, specify an existing directory');
  96.     exit;
  97. }
  98.  
  99. my $ua = LWP::UserAgent->new(agent => AGENT, cookie_jar => new HTTP::Cookies, timeout => TIMEOUT);
  100. my $json_decoder = JSON::PP->new->utf8->pretty->allow_nonref;
  101. $json_decoder->allow_singlequote(1);
  102.  
  103.  
  104. if($opt->album || ($opt->playlist && $opt->kind))
  105. {
  106.     my @track_list_info;
  107.    
  108.     if($opt->track && $opt->album)
  109.     {
  110.         info(INFO, 'Fetching track info: '.$opt->track.' ['.$opt->album.']');
  111.        
  112.         @track_list_info = get_single_track_info($opt->album, $opt->track);
  113.     }
  114.     elsif($opt->album)
  115.     {
  116.         info(INFO, 'Fetching album info: '.$opt->album);
  117.        
  118.         @track_list_info = get_album_tracks_info($opt->album);
  119.     }
  120.     else
  121.     {
  122.         info(INFO, 'Fetching playlist info: '.$opt->playlist.' ['.$opt->kind.']');
  123.        
  124.         @track_list_info = get_playlist_tracks_info($opt->playlist);
  125.     }
  126.    
  127.    
  128.     if(!@track_list_info)
  129.     {
  130.         info(ERROR, 'Can\'t get track list info');
  131.         exit;
  132.     }
  133.    
  134.     for my $track_info_ref(@track_list_info)
  135.     {
  136.         fetch_track($track_info_ref);
  137.     }
  138. }
  139.  
  140. sub fetch_track
  141. {
  142.     my $track_info_ref = shift;
  143.    
  144.     fix_encoding(\$track_info_ref->{title});
  145.     $track_info_ref->{title} =~ s/\s+$//;
  146.     $track_info_ref->{title} =~ s/[\\\/:"*?<>|]+/-/g;
  147.    
  148.     info(INFO, 'Trying to fetch track: '.$track_info_ref->{title});
  149.    
  150.     my $track_url = get_track_url($track_info_ref->{dir});
  151.     if(!$track_url)
  152.     {
  153.         info(ERROR, 'Can\'t get track url');
  154.         return;
  155.     }
  156.    
  157.     my $file_path = download_track($track_url, $track_info_ref->{title});
  158.     if(!$file_path)
  159.     {
  160.         info(ERROR, 'Failed to download track');
  161.         return;
  162.     }
  163.    
  164.     info(OK, 'Saved track at '.$file_path);
  165. }
  166.  
  167.  
  168. sub download_track
  169. {
  170.     my ($url, $title) = @_;
  171.    
  172.     my $request = $ua->get($url);
  173.     if(!$request->is_success)
  174.     {
  175.         info(DEBUG, 'Request failed in download_track');
  176.         return;
  177.     }
  178.    
  179.     my $web_data_size = $request->headers->{'content-length'};
  180.    
  181.     my $file_path = $opt->dir.'/'.$title.FILE_SAVE_EXT;
  182.     if(open(F, '>', $file_path))
  183.     {
  184.         # Awkward moment
  185.         undef $\;
  186.        
  187.         binmode F;
  188.         print F $request->content;
  189.         close F;
  190.        
  191.         $\ = NL;
  192.        
  193.         my $disk_data_size = -s $file_path;
  194.        
  195.         if($web_data_size && $disk_data_size != $web_data_size)
  196.         {
  197.             info(DEBUG, 'Actual file size differs from expected ('.$disk_data_size.'/'.$web_data_size.')');
  198.         }
  199.    
  200.         return $file_path;
  201.     }
  202.    
  203.     info(DEBUG, 'Failed to open file '.$file_path);
  204.     return;
  205. }
  206.  
  207. sub get_track_url
  208. {
  209.     my $storage_dir = shift;
  210.    
  211.     my $request = $ua->get(YANDEX_BASE.sprintf(DOWNLOAD_INFO_MASK, $storage_dir, time));
  212.     if(!$request->is_success)
  213.     {
  214.         info(DEBUG, 'Request failed in get_track_url');
  215.         return;
  216.     }
  217.    
  218.     my %fields = (host => '', path => '', ts => '', region => '', s => '');
  219.    
  220.     for my $key(keys %fields)
  221.     {
  222.         if($request->as_string =~ /<$key>(.+?)<\/$key>/)
  223.         {
  224.             $fields{$key} = $1;
  225.         }
  226.         else
  227.         {
  228.             info(DEBUG, 'Failed to parse '.$key);
  229.             return;
  230.         }
  231.     }
  232.    
  233.     my $hash = hash(substr($fields{path}, 1) . $fields{s});
  234.    
  235.     my $url = sprintf(DOWNLOAD_PATH_MASK, $fields{host}, $hash, $fields{ts}.$fields{path}, (split /\./, $storage_dir)[1]);
  236.    
  237.     info(DEBUG, 'Track url: '.$url);
  238.    
  239.     return $url;
  240. }
  241.  
  242. sub get_single_track_info
  243. {
  244.     my ($album_id, $track_id) = @_;
  245.    
  246.     my $request = $ua->get(YANDEX_BASE.sprintf(TRACK_URI_MASK, $track_id, $album_id, FACEGEN));
  247.     if(!$request->is_success)
  248.     {
  249.         info(DEBUG, 'Request failed in get_single_track_info');
  250.         return;
  251.     }
  252.    
  253.     my ($json_data) = ($request->as_string =~ /data-from="track" onclick=["']return (.+?)["']>/);
  254.     if(!$json_data)
  255.     {
  256.         info(DEBUG, 'Can\'t parse JSON blob');
  257.         return;
  258.     }
  259.    
  260.     HTML::Entities::decode_entities($json_data);
  261.    
  262.     my $json;
  263.     eval
  264.     {
  265.         $json = $json_decoder->decode($json_data);
  266.     };
  267.    
  268.     if($@)
  269.     {
  270.         info(DEBUG, 'Error decoding json '.$@);
  271.         return;
  272.     }
  273.    
  274.     return {dir => $json->{storage_dir}, title => $json->{artist}.ARTIST_TITLE_DELIM.$json->{title}};
  275. }
  276.  
  277. sub get_album_tracks_info
  278. {
  279.     my $album_id = shift;
  280.    
  281.     my $request = $ua->get(YANDEX_BASE.sprintf(ALBUM_INFO_MASK, $album_id, FACEGEN));
  282.     if(!$request->is_success)
  283.     {
  284.         info(DEBUG, 'Request failed in get_album_tracks_info');
  285.         return;
  286.     }
  287.    
  288.     my ($json_data) = ($request->as_string =~ /data-from="album-whole" onclick="return (.+?)"><a/);
  289.     if(!$json_data)
  290.     {
  291.         info(DEBUG, 'Can\'t parse JSON blob');
  292.         return;
  293.     }
  294.    
  295.     HTML::Entities::decode_entities($json_data);
  296.    
  297.     my $json;
  298.     eval
  299.     {
  300.         $json = $json_decoder->decode($json_data);
  301.     };
  302.    
  303.     if($@)
  304.     {
  305.         info(DEBUG, 'Error decoding json '.$@);
  306.         return;
  307.     }
  308.    
  309.    
  310.     my $title = $json->{title};
  311.     if(!$title)
  312.     {
  313.         info(DEBUG, 'Can\'t get album title');
  314.         return;
  315.     }
  316.    
  317.     fix_encoding(\$title);
  318.    
  319.     info(INFO, 'Album title: '.$title);
  320.     info(INFO, 'Tracks total: '. $json->{track_count});
  321.    
  322.    
  323.     return map { { dir => $_->{storage_dir}, title=> $_->{artist}.ARTIST_TITLE_DELIM.$_->{title} } } @{$json->{tracks}};
  324. }
  325.  
  326. sub get_playlist_tracks_info
  327. {
  328.     my $playlist_id = shift;
  329.    
  330.     my $request = $ua->get(YANDEX_BASE.sprintf(PLAYLIST_INFO_MASK, $playlist_id, $opt->kind, time));
  331.     if(!$request->is_success)
  332.     {
  333.         info(DEBUG, 'Request failed in get_playlist_tracks_info');
  334.         return;
  335.     }
  336.    
  337.     my $json_data = $request->content;
  338.    
  339.     HTML::Entities::decode_entities($json_data);
  340.    
  341.     my $json;
  342.     eval
  343.     {
  344.         $json = $json_decoder->decode($json_data);
  345.     };
  346.    
  347.     if($@)
  348.     {
  349.         info(DEBUG, 'Error decoding json '.$@);
  350.         return;
  351.     }
  352.    
  353.    
  354.     my $title = $json->{playlists}[0]->{title};
  355.     if(!$title)
  356.     {
  357.         info(DEBUG, 'Can\'t get playlist title');
  358.         return;
  359.     }
  360.    
  361.     fix_encoding(\$title);
  362.    
  363.     info(INFO, 'Playlist title: '.$title);
  364.     info(INFO, 'Tracks total: '. scalar @{$json->{playlists}[0]->{tracks}});
  365.    
  366.    
  367.     $request = $ua->get(YANDEX_BASE.sprintf(PLAYLIST_TRACK_INFO_MASK, join(',', @{$json->{playlists}[0]->{tracks}})));
  368.     if(!$request->is_success)
  369.     {
  370.         info(DEBUG, 'Request failed in get_playlist_tracks_info');
  371.         return;
  372.     }
  373.    
  374.     eval
  375.     {
  376.         $json = $json_decoder->decode($request->content);
  377.     };
  378.    
  379.     if($@)
  380.     {
  381.         info(DEBUG, 'Error decoding json '.$@);
  382.         return;
  383.     }
  384.    
  385.    
  386.     return map { { dir => $_->{storage_dir}, title=> $_->{artist}.ARTIST_TITLE_DELIM.$_->{title} } } @{$json->{tracks}};
  387. }
  388.  
  389. sub fix_encoding
  390. {
  391.     my $ref = shift;
  392.     from_to($$ref, 'unicode', TARGET_ENC);
  393. }
  394.  
  395. sub info
  396. {
  397.     my ($type, $msg) = @_;
  398.    
  399.     return if !$opt->debug && $type eq DEBUG;
  400.    
  401.     print Term::ANSIColor::colored('['.$type.']', $log_colors{$type}), ' ', $msg;
  402. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement