From e7a3116fae3e035719c1f85e67928cd744c53d2c Mon Sep 17 00:00:00 2001 From: Kaimi Date: Fri, 28 Feb 2014 00:40:47 +0400 Subject: [PATCH] Init --- src/YaHash.pm | 311 ++++++++++++++++++++++++++++++++++++++ src/ya.pl | 402 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 713 insertions(+) create mode 100755 src/YaHash.pm create mode 100755 src/ya.pl diff --git a/src/YaHash.pm b/src/YaHash.pm new file mode 100755 index 0000000..7af88d6 --- /dev/null +++ b/src/YaHash.pm @@ -0,0 +1,311 @@ +package YaHash; + +use strict; +use warnings; + +require Exporter; +use base qw/Exporter/; + +our @EXPORT = qw/hash/; + +sub M +{ + my ($c, $b) = @_; + return ($c << $b) | ($c >> (32 - $b)); #>>> +} + +sub L +{ + my ($x, $c) = @_; + + my ($G, $b, $k, $F, $d); + + $k = ($x & 2147483648); + $F = ($c & 2147483648); + $G = ($x & 1073741824); + $b = ($c & 1073741824); + $d = ($x & 1073741823) + ($c & 1073741823); + + if ($G & $b) + { + return ($d ^ 2147483648 ^ $k ^ $F) + } + + if ($G | $b) + { + if ($d & 1073741824) + { + return ($d ^ 3221225472 ^ $k ^ $F) + } + else + { + return ($d ^ 1073741824 ^ $k ^ $F) + } + } + else + { + return ($d ^ $k ^ $F); + } +} + +sub r +{ + my ($b, $d, $c) = @_; + + return ($b & $d) | ((~$b) & $c); +} + +sub qz +{ + my ($b, $d, $c) = @_; + + return ($b & $c) | ($d & (~$c)); +} + +sub p +{ + my ($b, $d, $c) = @_; + + return ($b ^ $d ^ $c); +} + +sub n +{ + my ($b, $d, $c) = @_; + + return ($d ^ ($b | (~$c))) +} + +sub u +{ + my ($G, $F, $ab, $aa, $k, $H, $I) = @_; + + $G = L($G, L(L(r($F, $ab, $aa), $k), $I)); + return L(M($G, $H), $F); +} + +sub f +{ + my ($G, $F, $ab, $aa, $k, $H, $I) = @_; + + $G = L($G, L(L(qz($F, $ab, $aa), $k), $I)); + return L(M($G, $H), $F); +} + +sub E +{ + my ($G, $F, $ab, $aa, $k, $H, $I) = @_; + + $G = L($G, L(L(p($F, $ab, $aa), $k), $I)); + return L(M($G, $H), $F); +} + +sub t +{ + my ($G, $F, $ab, $aa, $k, $H, $I) = @_; + + $G = L($G, L(L(n($F, $ab, $aa), $k), $I)); + return L(M($G, $H), $F); +} + +sub e +{ + my $x = shift; + my $H; + my $k = length $x; + my $d = $k + 8; + my $c = ($d - ($d % 64)) / 64; + my $G = ($c + 1) * 16; + my @I = split //, (0 x ($G - 1)); + my $b = 0; + my $F = 0; + + + while ($F < $k) + { + $H = ($F - ($F % 4)) / 4; + $b = ($F % 4) * 8; + $I[$H] = ($I[$H] | (ord(substr $x, $F, 1) << $b)); + $F++ + } + + $H = ($F - ($F % 4)) / 4; + $b = ($F % 4) * 8; + $I[$H] = $I[$H] | (128 << $b); + $I[$G - 2] = $k << 3; + $I[$G - 1] = $k >> 29; #>>> + + return @I; +} + +sub C +{ + my $d = shift; + + my $c = ""; + my $k = ""; + my ($x, $b); + + for (my $b = 0; $b <= 3; $b++) + { + $x = ($d >> ($b * 8)) & 255; #>>> + $k = sprintf("%02X", $x); + $c = $c . substr($k, (length($k) - 2), 2); + } + + return $c; +} + +sub z +{ + return chr(shift); +} + +sub K +{ + my $d = shift; + + $d =~ s/\r\n/\n/g; + + $d = z(498608 / 5666) . z(39523855 / 556674) . z(47450778 / 578668) . z(82156899 / 760712) . z(5026300 / 76156) . z(26011178 / 298979) . z(28319886 / 496840) . z(23477867 / 335398) . z(21650560 / 246029) . z(22521465 / 208532) . z(16067393 / 159083) . z(94458862 / 882793) . z(67654429 / 656839) . z(82331283 / 840115) . z(11508494 / 143856) . z(30221073 / 265097) . z(18712908 / 228206) . z(21423113 / 297543) . z(65168784 / 556998) . z(48924535 / 589452) . z(61018985 / 581133) . z(10644616 / 163763) . $d; + + my $b = ""; + for (my $x = 0; $x < length $d; $x++) + { + my $k = ord(substr $d, $x, 1); + if ($k < 128) + { + $b .= z($k); + } + else + { + if (($k > 127) && ($k < 2048)) + { + $b += z(($k >> 6) | 192); + $b += z(($k & 63) | 128) + } + else + { + $b += z(($k >> 12) | 224); + $b += z((($k >> 6) & 63) | 128); + $b += z(($k & 63) | 128) + } + } + } + + return $b; +} + +sub hash +{ + my $s = shift; + + + my @D; + my ($Q, $h, $J, $v, $g, $Z, $Y, $X, $W); + + my $T = 7; + my $R = 12; + my $O = 17; + my $N = 22; + my $B = 5; + my $A = 9; + my $y = 14; + my $w = 20; + my $o = 4; + my $m = 11; + my $l = 16; + my $j = 23; + my $V = 6; + my $U = 10; + my $S = 15; + my $P = 21; + + $s = K($s); + @D = e($s); + $Z = 1732584193; + $Y = 4023233417; + $X = 2562383102; + $W = 271733878; + for ($Q = 0; $Q < scalar @D; $Q += 16) + { + $h = $Z; + $J = $Y; + $v = $X; + $g = $W; + $Z = u($Z, $Y, $X, $W, $D[$Q + 0], $T, 3614090360); + $W = u($W, $Z, $Y, $X, $D[$Q + 1], $R, 3905402710); + $X = u($X, $W, $Z, $Y, $D[$Q + 2], $O, 606105819); + $Y = u($Y, $X, $W, $Z, $D[$Q + 3], $N, 3250441966); + $Z = u($Z, $Y, $X, $W, $D[$Q + 4], $T, 4118548399); + $W = u($W, $Z, $Y, $X, $D[$Q + 5], $R, 1200080426); + $X = u($X, $W, $Z, $Y, $D[$Q + 6], $O, 2821735955); + $Y = u($Y, $X, $W, $Z, $D[$Q + 7], $N, 4249261313); + $Z = u($Z, $Y, $X, $W, $D[$Q + 8], $T, 1770035416); + $W = u($W, $Z, $Y, $X, $D[$Q + 9], $R, 2336552879); + $X = u($X, $W, $Z, $Y, $D[$Q + 10], $O, 4294925233); + $Y = u($Y, $X, $W, $Z, $D[$Q + 11], $N, 2304563134); + $Z = u($Z, $Y, $X, $W, $D[$Q + 12], $T, 1804603682); + $W = u($W, $Z, $Y, $X, $D[$Q + 13], $R, 4254626195); + $X = u($X, $W, $Z, $Y, $D[$Q + 14], $O, 2792965006); + $Y = u($Y, $X, $W, $Z, $D[$Q + 15], $N, 1236535329); + $Z = f($Z, $Y, $X, $W, $D[$Q + 1], $B, 4129170786); + $W = f($W, $Z, $Y, $X, $D[$Q + 6], $A, 3225465664); + $X = f($X, $W, $Z, $Y, $D[$Q + 11], $y, 643717713); + $Y = f($Y, $X, $W, $Z, $D[$Q + 0], $w, 3921069994); + $Z = f($Z, $Y, $X, $W, $D[$Q + 5], $B, 3593408605); + $W = f($W, $Z, $Y, $X, $D[$Q + 10], $A, 38016083); + $X = f($X, $W, $Z, $Y, $D[$Q + 15], $y, 3634488961); + $Y = f($Y, $X, $W, $Z, $D[$Q + 4], $w, 3889429448); + $Z = f($Z, $Y, $X, $W, $D[$Q + 9], $B, 568446438); + $W = f($W, $Z, $Y, $X, $D[$Q + 14], $A, 3275163606); + $X = f($X, $W, $Z, $Y, $D[$Q + 3], $y, 4107603335); + $Y = f($Y, $X, $W, $Z, $D[$Q + 8], $w, 1163531501); + $Z = f($Z, $Y, $X, $W, $D[$Q + 13], $B, 2850285829); + $W = f($W, $Z, $Y, $X, $D[$Q + 2], $A, 4243563512); + $X = f($X, $W, $Z, $Y, $D[$Q + 7], $y, 1735328473); + $Y = f($Y, $X, $W, $Z, $D[$Q + 12], $w, 2368359562); + $Z = E($Z, $Y, $X, $W, $D[$Q + 5], $o, 4294588738); + $W = E($W, $Z, $Y, $X, $D[$Q + 8], $m, 2272392833); + $X = E($X, $W, $Z, $Y, $D[$Q + 11], $l, 1839030562); + $Y = E($Y, $X, $W, $Z, $D[$Q + 14], $j, 4259657740); + $Z = E($Z, $Y, $X, $W, $D[$Q + 1], $o, 2763975236); + $W = E($W, $Z, $Y, $X, $D[$Q + 4], $m, 1272893353); + $X = E($X, $W, $Z, $Y, $D[$Q + 7], $l, 4139469664); + $Y = E($Y, $X, $W, $Z, $D[$Q + 10], $j, 3200236656); + $Z = E($Z, $Y, $X, $W, $D[$Q + 13], $o, 681279174); + $W = E($W, $Z, $Y, $X, $D[$Q + 0], $m, 3936430074); + $X = E($X, $W, $Z, $Y, $D[$Q + 3], $l, 3572445317); + $Y = E($Y, $X, $W, $Z, $D[$Q + 6], $j, 76029189); + $Z = E($Z, $Y, $X, $W, $D[$Q + 9], $o, 3654602809); + $W = E($W, $Z, $Y, $X, $D[$Q + 12], $m, 3873151461); + $X = E($X, $W, $Z, $Y, $D[$Q + 15], $l, 530742520); + $Y = E($Y, $X, $W, $Z, $D[$Q + 2], $j, 3299628645); + $Z = t($Z, $Y, $X, $W, $D[$Q + 0], $V, 4096336452); + $W = t($W, $Z, $Y, $X, $D[$Q + 7], $U, 1126891415); + $X = t($X, $W, $Z, $Y, $D[$Q + 14], $S, 2878612391); + $Y = t($Y, $X, $W, $Z, $D[$Q + 5], $P, 4237533241); + $Z = t($Z, $Y, $X, $W, $D[$Q + 12], $V, 1700485571); + $W = t($W, $Z, $Y, $X, $D[$Q + 3], $U, 2399980690); + $X = t($X, $W, $Z, $Y, $D[$Q + 10], $S, 4293915773); + $Y = t($Y, $X, $W, $Z, $D[$Q + 1], $P, 2240044497); + $Z = t($Z, $Y, $X, $W, $D[$Q + 8], $V, 1873313359); + $W = t($W, $Z, $Y, $X, $D[$Q + 15], $U, 4264355552); + $X = t($X, $W, $Z, $Y, $D[$Q + 6], $S, 2734768916); + $Y = t($Y, $X, $W, $Z, $D[$Q + 13], $P, 1309151649); + $Z = t($Z, $Y, $X, $W, $D[$Q + 4], $V, 4149444226); + $W = t($W, $Z, $Y, $X, $D[$Q + 11], $U, 3174756917); + $X = t($X, $W, $Z, $Y, $D[$Q + 2], $S, 718787259); + $Y = t($Y, $X, $W, $Z, $D[$Q + 9], $P, 3951481745); + $Z = L($Z, $h); + $Y = L($Y, $J); + $X = L($X, $v); + $W = L($W, $g) + } + + my $i = C($Z) . C($Y) . C($X) . C($W); + + return lc $i; +} + +1; diff --git a/src/ya.pl b/src/ya.pl new file mode 100755 index 0000000..cc99319 --- /dev/null +++ b/src/ya.pl @@ -0,0 +1,402 @@ +use strict; +use warnings; +use Encode qw/from_to/; +use File::Basename; +use POSIX qw/strftime/; +use YaHash; + +use constant IS_WIN => $^O eq 'MSWin32'; +use constant +{ + NL => IS_WIN ? "\015\012" : "\012", + TARGET_ENC => IS_WIN ? 'cp1251' : 'utf8', + TIMEOUT => 5, + AGENT => 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:25.0) Gecko/20100101 Firefox/25.0', + YANDEX_BASE => 'http://music.yandex.ru', + TRACK_URI_MASK => '/fragment/track/%d/album/%d?prefix=%s', + DOWNLOAD_INFO_MASK => '/xml/storage-proxy.xml?p=download-info/%s/2.mp3&nc=%d', + DOWNLOAD_PATH_MASK => 'http://%s/get-mp3/%s/%s?track-id=%s&from=service-10-track&similarities-experiment=default', + PLAYLIST_INFO_MASK => '/get/playlist2.xml?kinds=%d&owner=%s&r=%d', + PLAYLIST_TRACK_INFO_MASK => '/get/tracks.xml?tracks=%s', + ALBUM_INFO_MASK => '/fragment/album/%d?prefix=%s', + FILE_SAVE_EXT => '.mp3', + ARTIST_TITLE_DELIM => ' - ', + FACEGEN => POSIX::strftime('facegen-%Y-%m-%dT00-00-00', localtime) +}; +use constant +{ + DEBUG => 'DEBUG', + ERROR => 'ERROR', + INFO => 'INFO', + OK => 'OK' +}; + +my %log_colors = +( + &DEBUG => 'red on_white', + &ERROR => 'red', + &INFO => 'blue on_white', + &OK => 'green on_white' +); + +my %req_modules = +( + NIX => [], + WIN => [ qw/Win32::Console::ANSI/ ], + ALL => [ qw/JSON::PP Getopt::Long::Descriptive Term::ANSIColor LWP::UserAgent HTTP::Cookies HTML::Entities/ ] +); + +$\ = NL; + +my @missing_modules; +for(@{$req_modules{ALL}}, IS_WIN ? @{$req_modules{WIN}} : @{$req_modules{NIX}}) +{ + eval "require $_"; + if($@) + { + ($_) = $@ =~ /locate (.+?)(?:\.pm)? in \@INC/; + $_ =~ s/\//::/g; + push @missing_modules, $_; + } +} + +if(@missing_modules) +{ + print 'Please, install this modules: '.join ', ', @missing_modules; + exit; +} + +my ($opt, $usage) = Getopt::Long::Descriptive::describe_options +( + basename(__FILE__).' %o', + ['playlist|p:i', 'playlist id to download'], + ['kind|k:s', 'playlist kind (eg. ya-playlist, music-blog, music-partners, etc.)'], + ['album|a:i', 'album to download'], + ['track|t:i', 'track to download (album id must be specified)'], + ['dir|d:s', 'download path (current direcotry will be used by default)', {default => '.'}], + [], + ['debug', 'print debug info during work'], + ['help', 'print usage'], + [], + ['Example: '], + ["\t".basename(__FILE__).' -p 123 -k ya-playlist'], + ["\t".basename(__FILE__).' -a 123'], + ["\t".basename(__FILE__).' -a 123 -t 321'] +); + +if( $opt->help || ( !($opt->track && $opt->album) && !$opt->album && !($opt->playlist && $opt->kind) ) ) +{ + print $usage->text; + exit; +} + +if($opt->dir && !-d $opt->dir) +{ + info(ERROR, 'Please, specify an existing directory'); + exit; +} + +my $ua = LWP::UserAgent->new(agent => AGENT, cookie_jar => new HTTP::Cookies, timeout => TIMEOUT); +my $json_decoder = JSON::PP->new->utf8->pretty->allow_nonref; +$json_decoder->allow_singlequote(1); + + +if($opt->album || ($opt->playlist && $opt->kind)) +{ + my @track_list_info; + + if($opt->track && $opt->album) + { + info(INFO, 'Fetching track info: '.$opt->track.' ['.$opt->album.']'); + + @track_list_info = get_single_track_info($opt->album, $opt->track); + } + elsif($opt->album) + { + info(INFO, 'Fetching album info: '.$opt->album); + + @track_list_info = get_album_tracks_info($opt->album); + } + else + { + info(INFO, 'Fetching playlist info: '.$opt->playlist.' ['.$opt->kind.']'); + + @track_list_info = get_playlist_tracks_info($opt->playlist); + } + + + if(!@track_list_info) + { + info(ERROR, 'Can\'t get track list info'); + exit; + } + + for my $track_info_ref(@track_list_info) + { + fetch_track($track_info_ref); + } +} + +sub fetch_track +{ + my $track_info_ref = shift; + + fix_encoding(\$track_info_ref->{title}); + $track_info_ref->{title} =~ s/\s+$//; + $track_info_ref->{title} =~ s/[\\\/:"*?<>|]+/-/g; + + info(INFO, 'Trying to fetch track: '.$track_info_ref->{title}); + + my $track_url = get_track_url($track_info_ref->{dir}); + if(!$track_url) + { + info(ERROR, 'Can\'t get track url'); + return; + } + + my $file_path = download_track($track_url, $track_info_ref->{title}); + if(!$file_path) + { + info(ERROR, 'Failed to download track'); + return; + } + + info(OK, 'Saved track at '.$file_path); +} + + +sub download_track +{ + my ($url, $title) = @_; + + my $request = $ua->get($url); + if(!$request->is_success) + { + info(DEBUG, 'Request failed in download_track'); + return; + } + + my $web_data_size = $request->headers->{'content-length'}; + + my $file_path = $opt->dir.'/'.$title.FILE_SAVE_EXT; + if(open(F, '>', $file_path)) + { + # Awkward moment + undef $\; + + binmode F; + print F $request->content; + close F; + + $\ = NL; + + my $disk_data_size = -s $file_path; + + if($web_data_size && $disk_data_size != $web_data_size) + { + info(DEBUG, 'Actual file size differs from expected ('.$disk_data_size.'/'.$web_data_size.')'); + } + + return $file_path; + } + + info(DEBUG, 'Failed to open file '.$file_path); + return; +} + +sub get_track_url +{ + my $storage_dir = shift; + + my $request = $ua->get(YANDEX_BASE.sprintf(DOWNLOAD_INFO_MASK, $storage_dir, time)); + if(!$request->is_success) + { + info(DEBUG, 'Request failed in get_track_url'); + return; + } + + my %fields = (host => '', path => '', ts => '', region => '', s => ''); + + for my $key(keys %fields) + { + if($request->as_string =~ /<$key>(.+?)<\/$key>/) + { + $fields{$key} = $1; + } + else + { + info(DEBUG, 'Failed to parse '.$key); + return; + } + } + + my $hash = hash(substr($fields{path}, 1) . $fields{s}); + + my $url = sprintf(DOWNLOAD_PATH_MASK, $fields{host}, $hash, $fields{ts}.$fields{path}, (split /\./, $storage_dir)[1]); + + info(DEBUG, 'Track url: '.$url); + + return $url; +} + +sub get_single_track_info +{ + my ($album_id, $track_id) = @_; + + my $request = $ua->get(YANDEX_BASE.sprintf(TRACK_URI_MASK, $track_id, $album_id, FACEGEN)); + if(!$request->is_success) + { + info(DEBUG, 'Request failed in get_single_track_info'); + return; + } + + my ($json_data) = ($request->as_string =~ /data-from="track" onclick=["']return (.+?)["']>/); + if(!$json_data) + { + info(DEBUG, 'Can\'t parse JSON blob'); + return; + } + + HTML::Entities::decode_entities($json_data); + + my $json; + eval + { + $json = $json_decoder->decode($json_data); + }; + + if($@) + { + info(DEBUG, 'Error decoding json '.$@); + return; + } + + return {dir => $json->{storage_dir}, title => $json->{artist}.ARTIST_TITLE_DELIM.$json->{title}}; +} + +sub get_album_tracks_info +{ + my $album_id = shift; + + my $request = $ua->get(YANDEX_BASE.sprintf(ALBUM_INFO_MASK, $album_id, FACEGEN)); + if(!$request->is_success) + { + info(DEBUG, 'Request failed in get_album_tracks_info'); + return; + } + + my ($json_data) = ($request->as_string =~ /data-from="album-whole" onclick="return (.+?)">decode($json_data); + }; + + if($@) + { + info(DEBUG, 'Error decoding json '.$@); + return; + } + + + my $title = $json->{title}; + if(!$title) + { + info(DEBUG, 'Can\'t get album title'); + return; + } + + fix_encoding(\$title); + + info(INFO, 'Album title: '.$title); + info(INFO, 'Tracks total: '. $json->{track_count}); + + + return map { { dir => $_->{storage_dir}, title=> $_->{artist}.ARTIST_TITLE_DELIM.$_->{title} } } @{$json->{tracks}}; +} + +sub get_playlist_tracks_info +{ + my $playlist_id = shift; + + my $request = $ua->get(YANDEX_BASE.sprintf(PLAYLIST_INFO_MASK, $playlist_id, $opt->kind, time)); + if(!$request->is_success) + { + info(DEBUG, 'Request failed in get_playlist_tracks_info'); + return; + } + + my $json_data = $request->content; + + HTML::Entities::decode_entities($json_data); + + my $json; + eval + { + $json = $json_decoder->decode($json_data); + }; + + if($@) + { + info(DEBUG, 'Error decoding json '.$@); + return; + } + + + my $title = $json->{playlists}[0]->{title}; + if(!$title) + { + info(DEBUG, 'Can\'t get playlist title'); + return; + } + + fix_encoding(\$title); + + info(INFO, 'Playlist title: '.$title); + info(INFO, 'Tracks total: '. scalar @{$json->{playlists}[0]->{tracks}}); + + + $request = $ua->get(YANDEX_BASE.sprintf(PLAYLIST_TRACK_INFO_MASK, join(',', @{$json->{playlists}[0]->{tracks}}))); + if(!$request->is_success) + { + info(DEBUG, 'Request failed in get_playlist_tracks_info'); + return; + } + + eval + { + $json = $json_decoder->decode($request->content); + }; + + if($@) + { + info(DEBUG, 'Error decoding json '.$@); + return; + } + + + return map { { dir => $_->{storage_dir}, title=> $_->{artist}.ARTIST_TITLE_DELIM.$_->{title} } } @{$json->{tracks}}; +} + +sub fix_encoding +{ + my $ref = shift; + from_to($$ref, 'unicode', TARGET_ENC); +} + +sub info +{ + my ($type, $msg) = @_; + + return if !$opt->debug && $type eq DEBUG; + + print Term::ANSIColor::colored('['.$type.']', $log_colors{$type}), ' ', $msg; +}