├── .gitignore ├── .directory ├── img ├── it-works.png └── search.png ├── README.org └── getLyric.pl /.gitignore: -------------------------------------------------------------------------------- 1 | *~ -------------------------------------------------------------------------------- /.directory: -------------------------------------------------------------------------------- 1 | [Dolphin] 2 | PreviewsShown=true 3 | Timestamp=2016,11,29,10,45,9 4 | Version=3 5 | -------------------------------------------------------------------------------- /img/it-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxueyang/netease_music_lyric/HEAD/img/it-works.png -------------------------------------------------------------------------------- /img/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuxueyang/netease_music_lyric/HEAD/img/search.png -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * netease_music_lyric 2 | 根据歌曲名字下载网易云音乐歌词,生成`.lrc`文件,给 [[https://software.opensuse.org/package/osdlyrics][osdlyrics]] 这个软件 3 | 使用。 4 | 5 | ** 下载目录:~/.lyrics 6 | 7 | #+BEGIN_SRC bash 8 | mkdir ~/.lyrics 9 | #+END_SRC 10 | 11 | ** 安装依赖 12 | 13 | #+BEGIN_SRC bash 14 | sudo cpan install JSON::Parse 15 | sudo cpan install File::HomeDir 16 | sudo cpan install LWP::UserAgent 17 | #+END_SRC 18 | 19 | About how to configure cpan mirror: [[http://perltricks.com/article/44/2013/10/20/Find-CPAN-mirrors-and-configure-the-local-CPAN-mirror-list/][Find CPAN mirrors and configure the local CPAN mirror list]] 20 | 21 | CPAN mirror in China: [[http://mirrors.cpan.org/][Mirror List]] 22 | 23 | ** 运行方法 24 | 25 | #+BEGIN_SRC bash 26 | git clone https://github.com/liuxueyang/netease_music_lyric ~/Desktop/netease_music_lyric 27 | cd ~/Desktop/netease_music_lyric 28 | perl -SC ./getLyric.pl 29 | #+END_SRC 30 | 31 | 如果不能运行,根据错误信息,像上面那样安装对应的模块。如图: 32 | 33 | [[img/search.png]] 34 | 35 | 如果想对一个目录下的所有音乐下载歌词到某目录,假设音乐目录是: 36 | `/home/username/Music`,保存歌词的目录是:`/home/username/.lyrics`,那 37 | 么可以这样:(注意两个路径是绝对路径) 38 | 39 | #+BEGIN_SRC bash 40 | perl -SC ./getLyric.pl /home/username/Music /home/username/.lyrics 41 | #+END_SRC 42 | 43 | 如果脚本输出日志里面包含类似于:`wide character in print at ...`之类的 44 | 错误,可以不用管。 45 | 46 | ** 应用歌词 47 | 在osdlyric的设置里面`assign lyric...`就可以了。同一首歌最多有三个文件: 48 | 49 | 1) 歌曲名字-by-演唱者:歌词原文和翻译 50 | 2) 歌曲名字-another-by-演唱者:只有歌词翻译 51 | 3) 歌曲名字-ori-by-演唱者:只有歌词原文 52 | 53 | 最后效果如下: 54 | 55 | [[img/it-works.png]] 56 | ** 更新紀錄 57 | *** Version 0.1 58 | 二〇一六年十一月二十九日凌晨,初步完成。 59 | *** Version 0.2 60 | 二〇一六年十一月二十九日上午,下載歌詞原文和歌詞翻譯,並且合併兩個文件。 61 | *** Version 0.3 62 | 二〇一六年十一月三十日上午,發現顯示歌詞的軟件可以自動檢測歌詞目錄裏面 63 | 是不是包含和歌曲文件名同名的歌詞文件。所以增加一个功能:如果脚本运行的 64 | 时候带两个参数,一个是音乐文件夹的绝对路径,一个是要保存歌词的目录的绝 65 | 对路径,那么脚本会对音乐文件夹的所有音乐文件下载歌词,把包含歌詞原文和 66 | 歌詞翻譯(如果沒有翻譯,那麼就是歌詞原文)的歌詞保存成名爲和歌曲名稱相 67 | 同的文件。默认下载搜索结果的第一个的歌词文件,这样有一点不准确。但是大 68 | 部分时候是正确的,也可以字符串匹配。 69 | *** Version 0.4 70 | 二〇一七年三月十七日晚,发现带翻译的歌词处理逻辑有点问题,比如: 71 | 歌词原文应该在歌词翻译的_后面_,这样更合理。 -------------------------------------------------------------------------------- /getLyric.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # 2016/12/29 10:14:03 4 | 5 | use strict; 6 | use warnings; 7 | use LWP::UserAgent; 8 | use HTTP::Request::Common qw(GET POST); 9 | use HTTP::Cookies; 10 | use JSON::Parse ':all'; 11 | use JSON qw(decode_json); 12 | use Data::Dumper; 13 | use File::HomeDir; 14 | use Path::Class; 15 | 16 | # don't forget to use -SC command option 17 | 18 | my $ua = LWP::UserAgent->new; 19 | 20 | # Define user agent type 21 | $ua->agent('Mozilla/8.0'); 22 | 23 | # 提交給服務器的搜索的名字 24 | my $search_name; 25 | 26 | # 歌詞原文的歌詞文件名 27 | my $lyric_file_name; 28 | 29 | # 歌詞翻譯的歌詞文件名 30 | my $another_lyric_file_name; 31 | 32 | # 歌詞原文和翻譯的歌詞文件名 33 | my $all_in1_lyric_file_name; 34 | 35 | # 提交給服務器的歌曲id,通過搜索歌曲名字返回結果得到。(如果有返回結果 36 | # 的話:1.歌曲不存在,搜索不到。2.搜索失敗) 37 | 38 | my $id; 39 | 40 | my $search_action = 'http://music.163.com/api/search/get/web'; 41 | 42 | # 搜索,服務器返回值 43 | my $search_res; 44 | 45 | my ($musicD, $lyricD, $musicDir, $lyricDir); 46 | 47 | # music directory and lyric directory as command argument 48 | if ($ARGV[0] && $ARGV[1]) { 49 | $musicDir = $ARGV[0]; 50 | $lyricDir = $ARGV[1]; 51 | 52 | print "musicDir = $musicDir, lyricDir = $lyricDir\n"; 53 | 54 | $musicD = dir($musicDir); 55 | $lyricD = dir($lyricDir); 56 | 57 | chdir $musicD || die "can not change directory to $musicD"; 58 | 59 | my @files = glob("*.mp3"); 60 | for my $f (@files) { 61 | $f =~ s/\.mp3$//; 62 | $search_name = $f; 63 | $all_in1_lyric_file_name = $f . ".lrc"; 64 | $lyric_file_name = $f . ".lrc"; 65 | $another_lyric_file_name = $f . "-another-" . ".lrc"; 66 | 67 | print $f, "\n"; 68 | 69 | $search_res = search_song(); 70 | next unless ($search_res); 71 | 72 | #print $lyric_file_name, "\n"; 73 | 74 | my ($ids, $artist_names, $names, $cnt) = search_result(0); 75 | 76 | my $len=@$names; 77 | my $indice=0; 78 | # TODO regex unicode ? 79 | 80 | # for my $i (0..$len-1) { 81 | # if ($names->[$i] =~ /$f/) { $indice = $i; last; } 82 | # } 83 | 84 | if ($cnt) { 85 | # 参数中的0代表默认使用搜索结果列表中的第一个 86 | # generate_lyric_filename($ids, $artist_names, 87 | # $names, 0); 88 | $id = $ids->[$indice]; 89 | get_lyric($names, $indice); 90 | } 91 | else { 92 | print "$search_name DOES NOT HAVE LYRIC.\n"; 93 | } 94 | 95 | print "next\n"; 96 | } 97 | } 98 | else { 99 | # User input song name 100 | $lyricDir = File::HomeDir->my_home . "/.lyrics"; 101 | $lyricD = dir($lyricDir); 102 | 103 | print "Please enter the song name: "; 104 | chomp($search_name = <>); 105 | $search_res = search_song(); 106 | 107 | exit 0 unless ($search_res); 108 | 109 | my ($ids, $artist_names, $names, $cnt) = search_result(1); 110 | 111 | if ($cnt) 112 | { 113 | print "\nInput indice of the song: "; 114 | my $input_id; 115 | chomp($input_id = <>); 116 | 117 | die "id is invalid" unless ($input_id >=0 && $input_id < $cnt); 118 | 119 | generate_lyric_filename($ids, $artist_names, 120 | $names, $input_id); 121 | get_lyric($names, $input_id); 122 | } 123 | else { 124 | print "$search_name DOES NOT HAVE LYRIC.\n"; 125 | exit 0; 126 | } 127 | } 128 | 129 | sub get_lyric { 130 | my ($names, $input_id) = @_; 131 | # Request object 132 | my $lyri = "http://music.163.com/api/song/lyric?lv=1&kv=1&tv=-1&id="; 133 | $lyri .= $id; 134 | 135 | my $req = GET $lyri; 136 | 137 | # Make the request 138 | my $res = $ua->request($req); 139 | 140 | if ($res->is_success) { 141 | my ($another_lyric_content, $lyric_content); 142 | my $translated = parse_json($res->decoded_content)->{tlyric}; 143 | 144 | if ($translated) { 145 | # 有翻译 146 | $another_lyric_content = $translated->{lyric}; 147 | } 148 | 149 | $lyric_content = parse_json($res->decoded_content) 150 | ->{lrc}->{lyric}; 151 | 152 | unless ($lyric_content || $another_lyric_content) { 153 | print $names->[$input_id], " has no lyric online\n"; 154 | } 155 | else { 156 | write_lyric_file($lyric_content, $another_lyric_content, 0); 157 | } 158 | } 159 | else { 160 | print $res->status_line . "\n"; 161 | exit 0; 162 | } 163 | } 164 | 165 | sub generate_lyric_filename { 166 | my ($ids, $artist_names, $names, $input_id) = @_; 167 | 168 | $id = $ids->[$input_id]; 169 | my ($name, $artist) = ($names->[$input_id], 170 | $artist_names->[$input_id]); 171 | 172 | $lyric_file_name = $name . "-ori-by-" . $artist . ".lrc"; 173 | 174 | $another_lyric_file_name = $name . "-another-by-" . 175 | $artist . ".lrc"; 176 | 177 | $all_in1_lyric_file_name = $name . "-by-" . $artist . ".lrc"; 178 | } 179 | 180 | sub write_lyric_file { 181 | # mode 是一个标志,0表示是用户直接输入歌曲名字搜索,1是遍历目录自动生 182 | # 成歌词 183 | my ($lyric_content, $another_lyric_content) = @_; 184 | my ($fh, @lyric1, @lyric2, @head, @lyric); 185 | 186 | # 歌词原文 187 | if ($lyric_content) { 188 | $fh = $lyricD->file($lyric_file_name); 189 | $fh->openw()->print($lyric_content); 190 | @lyric1 = $fh->slurp(); 191 | } 192 | 193 | if ($another_lyric_content) { 194 | # 歌词翻译 195 | $fh = $lyricD->file($another_lyric_file_name); 196 | $fh->openw()->print($another_lyric_content); 197 | @lyric2 = $fh->slurp(); 198 | } 199 | 200 | if ($lyric_content && $another_lyric_content) { 201 | @head = (shift @lyric1, shift @lyric2); 202 | # @lyric = @lyric2; 203 | # push @lyric, @lyric1; 204 | 205 | # should not just sort!! 206 | # @lyric1 is original lyric 207 | # @lyric2 is the translated version 208 | # lyric2 should better appear before lyric1 209 | 210 | my %lyric1_hsh; 211 | for (@lyric1) { $lyric1_hsh{$1} = $_ if (/\[(.*)\]/); } 212 | my %lyric2_hsh; 213 | for (@lyric2) { $lyric2_hsh{$1} = $_ if (/\[(.*)\]/); } 214 | 215 | # @lyric = sort { $a cmp $b } @lyric; 216 | unshift @lyric, @head; 217 | for (sort keys %lyric2_hsh) { 218 | push @lyric, $lyric2_hsh{$_}; 219 | push @lyric, $lyric1_hsh{$_}; 220 | } 221 | 222 | my @del_indices = reverse(grep { $lyric[$_] =~ /^ *$/ } 223 | 0..$#lyric); 224 | for my $i (@del_indices) { splice @lyric, $i, 1; } 225 | 226 | $lyricD->file($all_in1_lyric_file_name)-> 227 | openw()->print(join '', @lyric); 228 | } 229 | } 230 | 231 | sub search_result { 232 | # 返回歌曲id、名字、演唱者列表,和搜索结果数目 233 | 234 | # 是否要输出列表 235 | my $mode = shift; 236 | 237 | my $response = $search_res->decoded_content; 238 | my $data = parse_json($response); 239 | my $result = $data->{result}; 240 | my $songs = $result->{songs}; 241 | my $cnt = 0; 242 | 243 | # 搜索结果中的歌曲id 244 | my @ids; 245 | 246 | # 搜索结果中的演唱者 247 | my @artist_names; 248 | 249 | # 搜索结果中的歌曲名字 250 | my @names; 251 | 252 | # 从搜索结果得到歌曲id、歌曲列表和演唱者这三个列表。 253 | for my $i (@$songs) { 254 | push @ids, $i->{id}; 255 | push @names, $i->{name}; 256 | 257 | print $cnt, ". ", $i->{id}, "\t\t\t\t", $i->{name}, "\t\t\t\t" 258 | if $mode; 259 | 260 | my $artists = $i->{artists}; 261 | my @artist_names_in_song; 262 | 263 | for my $name (@$artists) { # cant use @$i->{artists} 264 | print $name->{name} if $mode; 265 | push @artist_names_in_song, $name->{name}; 266 | } 267 | my $artist_name = join '_', @artist_names_in_song; 268 | push @artist_names, $artist_name; 269 | print "\n" if $mode; 270 | $cnt++; 271 | } 272 | 273 | return (\@ids, \@names, \@artist_names, $cnt); 274 | } 275 | 276 | sub search_song { 277 | return $ua->post($search_action, [s => $search_name, 278 | type => 1, 279 | offset => 0, 280 | limit => 10], 281 | 'Accept' => '*/*', 282 | 'Accept-Encoding' => 'gzip,deflate,sdch', 283 | 'Accept-Language' => 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 284 | 'Connection' => 'keep-alive', 285 | 'Content-Type' => 'application/x-www-form-urlencoded', 286 | 'Host' => 'music.163.com', 287 | 'Referer' => 'http://music.163.com/search/', 288 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36'); 289 | 290 | } 291 | --------------------------------------------------------------------------------