xiami_cloner 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Binary file
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>me.ljh.xiami_cloner</string>
7
+
8
+ <key>ProgramArguments</key>
9
+ <array>
10
+ <string>/Users/Sunday/Programming/Git/sunutils/xm_clone_script</string>
11
+ </array>
12
+
13
+ <key>WatchPaths</key>
14
+ <array>
15
+ <string>/Users/Sunday/Dropbox/Synced/Playlist</string>
16
+ </array>
17
+
18
+ <key>StandardOutPath</key>
19
+ <string>/Users/Sunday/Library/Logs/xiami_cloner.log</string>
20
+
21
+ <key>StandardErrorPath</key>
22
+ <string>/Users/Sunday/Library/Logs/xiami_cloner.log</string>
23
+ </dict>
24
+ </plist>
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env macruby
2
+ # encoding: utf-8
3
+
4
+ framework "Cocoa"
5
+ require 'thread'
6
+
7
+ class XiamiClonerMenubar
8
+
9
+ # We build the status bar item menu
10
+ def setup_menu
11
+ menu = NSMenu.new
12
+ menu.initWithTitle '虾米同步状态'
13
+
14
+ @log_menus = []
15
+
16
+ 5.times { @log_menus << NSMenuItem.new }
17
+ @log_menus.each { |m| menu.addItem m }
18
+
19
+ mi = NSMenuItem.new
20
+ mi.title = '退出'
21
+ mi.action = 'quit:'
22
+ mi.target = self
23
+ menu.addItem mi
24
+
25
+ menu
26
+ end
27
+
28
+ def icon_path(name)
29
+ File.join(File.expand_path(File.dirname(File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__))), 'assets', name)
30
+ end
31
+
32
+ # Init the status bar
33
+ def init_status_bar(menu)
34
+ status_bar = NSStatusBar.systemStatusBar
35
+ status_item = status_bar.statusItemWithLength(NSVariableStatusItemLength)
36
+ status_item.setMenu menu
37
+ status_item.setImage(NSImage.new.initWithContentsOfFile(icon_path('perok.png')))
38
+
39
+ @status_item = status_item
40
+ end
41
+
42
+ def quit(sender)
43
+ app = NSApplication.sharedApplication
44
+ app.terminate(self)
45
+ end
46
+
47
+ def initialize()
48
+ app = NSApplication.sharedApplication
49
+ init_status_bar(setup_menu)
50
+
51
+ Thread.new do |t|
52
+ while true
53
+ logs_a = `tail -n 5 ~/Library/Logs/xiami_cloner.log`.split("\n").map { |x| x.split("\r").last }
54
+
55
+ 0.upto(4) { |i| @log_menus[i].title = logs_a[i] }
56
+
57
+ last = logs_a[4]
58
+
59
+ if last == '同步完成'
60
+ @status_item.setImage(NSImage.new.initWithContentsOfFile(icon_path('icons/perok.png')))
61
+ elsif last == '开始同步'
62
+ @status_item.setImage(NSImage.new.initWithContentsOfFile(icon_path('icons/per00.png')))
63
+ elsif /正在下载.*?/.match(last)
64
+ @status_item.setImage(NSImage.new.initWithContentsOfFile(icon_path('icons/per00.png')))
65
+ elsif /#*?\w*?([0-9]*\.[0-9]*)%/.match(last)
66
+ progress = /#*?\w*?([0-9]*\.[0-9]*)%/.match(last)[1].to_f
67
+ i = (progress / 10).to_i
68
+ s = i.to_s
69
+ s = "0#{s}" if s.length == 1
70
+ @status_item.setImage(NSImage.new.initWithContentsOfFile(icon_path("icons/per#{s}.png")))
71
+ end
72
+
73
+ sleep 1
74
+ end
75
+ end
76
+
77
+ app.run
78
+ end
79
+
80
+ end
81
+
82
+ XiamiClonerMenubar.new
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'main'
5
+
6
+ require 'xiami_cloner'
7
+
8
+ Main {
9
+ mode 'single' do
10
+ mode 'download' do
11
+ argument('id') {
12
+ cast :int
13
+ description 'The ID of the song to be downloaded. '
14
+ }
15
+
16
+ option('output-dir', 'o') {
17
+ optional
18
+ argument_required
19
+ cast :string
20
+ description 'The output directory. Defaults to the current directory. '
21
+ default '.'
22
+ }
23
+
24
+ option('import-to-itunes', 'i') {
25
+ optional
26
+ cast :bool
27
+ description 'If this option is used, downloaded songs would be automatically imported into iTunes. '
28
+ default false
29
+ }
30
+
31
+ option('terse', 't') {
32
+ optional
33
+ cast :bool
34
+ description 'To output the progress in a terse form for easier parsing. '
35
+ default false
36
+ }
37
+
38
+ option('high-quality-song', 'q') {
39
+ optional
40
+ cast :bool
41
+ description 'Download the high quality format. '
42
+ default false
43
+ }
44
+
45
+ option('cookie', 'c') {
46
+ optional
47
+ argument_required
48
+ cast :string
49
+ description 'The CURL format cookie to be used when downloading HQ music. '
50
+ default ''
51
+ }
52
+
53
+ def run()
54
+ outdir = File.expand_path(params['output-dir'].value)
55
+ id = params['id'].value
56
+
57
+ options = {
58
+ import_to_itunes: params['import-to-itunes'].value,
59
+ terse: params['terse'].value,
60
+ high_quality_song: params['high-quality-song'].value,
61
+ cookie: params['cookie'].value.empty? ? nil : params['cookie'].value
62
+ }
63
+
64
+ XiamiCloner::Cloner.clone_song(id, outdir, options)
65
+ end
66
+ end
67
+ end
68
+
69
+ mode 'album' do
70
+ mode 'list' do
71
+ argument('album_id') {
72
+ cast :int
73
+ description 'The ID of the album to be listed. '
74
+ }
75
+
76
+ def run()
77
+ id = params['album_id'].value
78
+
79
+ XiamiCloner::Cloner.retrieve_album_list(id).each { |s| puts s }
80
+ end
81
+ end
82
+
83
+ mode 'download' do
84
+ argument('album_id') {
85
+ cast :int
86
+ description 'The ID of the album to be downloaded. '
87
+ }
88
+
89
+ option('output-dir', 'o') {
90
+ optional
91
+ argument_required
92
+ cast :string
93
+ description 'The output directory. Defaults to the current directory. '
94
+ default '.'
95
+ }
96
+
97
+ option('import-to-itunes', 'i') {
98
+ optional
99
+ cast :bool
100
+ description 'If this option is used, downloaded songs would be automatically imported into iTunes. '
101
+ default false
102
+ }
103
+
104
+ option('terse', 't') {
105
+ optional
106
+ cast :bool
107
+ description 'To output the progress in a terse form for easier parsing. '
108
+ default false
109
+ }
110
+
111
+ option('high-quality-song', 'q') {
112
+ optional
113
+ cast :bool
114
+ description 'Download the high quality format. '
115
+ default false
116
+ }
117
+
118
+ option('cookie', 'c') {
119
+ optional
120
+ argument_required
121
+ cast :string
122
+ description 'The CURL format cookie to be used when downloading HQ music. '
123
+ default ''
124
+ }
125
+
126
+ def run()
127
+ outdir = File.expand_path(params['output-dir'].value)
128
+ id = params['album_id'].value
129
+
130
+ options = {
131
+ import_to_itunes: params['import-to-itunes'].value,
132
+ terse: params['terse'].value,
133
+ high_quality_song: params['high-quality-song'].value,
134
+ cookie: params['cookie'].value.empty? ? nil : params['cookie'].value
135
+ }
136
+
137
+ list = XiamiCloner::Cloner.retrieve_album_list(id)
138
+
139
+ list.each do |song|
140
+ XiamiCloner::Cloner.clone_song(song, outdir, options)
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ mode 'sync' do
147
+ argument('dir') {
148
+ cast :string
149
+ description 'The directory to be synced. '
150
+ }
151
+
152
+ option('import-to-itunes', 'i') {
153
+ optional
154
+ cast :bool
155
+ description 'If this option is used, downloaded songs would be automatically imported into iTunes. '
156
+ default false
157
+ }
158
+
159
+ option('terse', 't') {
160
+ optional
161
+ cast :bool
162
+ description 'To output the progress in a terse form for easier parsing. '
163
+ default false
164
+ }
165
+
166
+ option('high-quality-song', 'q') {
167
+ optional
168
+ cast :bool
169
+ description 'Download the high quality format. '
170
+ default false
171
+ }
172
+
173
+ option('cookie', 'c') {
174
+ optional
175
+ argument_required
176
+ cast :string
177
+ description 'The CURL format cookie to be used when downloading HQ music. '
178
+ default ''
179
+ }
180
+
181
+ def run()
182
+ dir = File.expand_path(params['dir'].value)
183
+ playlist_file = File.join(dir, '.playlist')
184
+ cloned_file = File.join(dir, '.cloned')
185
+
186
+ options = {
187
+ cloned_file: cloned_file,
188
+ import_to_itunes: params['import-to-itunes'].value,
189
+ terse: params['terse'].value,
190
+ high_quality_song: params['high-quality-song'].value,
191
+ cookie: params['cookie'].value.empty? ? nil : params['cookie'].value
192
+ }
193
+
194
+ XiamiCloner::Cloner.clone(playlist_file, dir, options)
195
+ end
196
+ end
197
+
198
+ mode 'decode' do
199
+ argument('location') {
200
+ cast :string
201
+ description 'The encrypted location. '
202
+ }
203
+
204
+ def run()
205
+ location = params['location'].value
206
+
207
+ puts XiamiCloner::LocationDecoder.decode(location)
208
+ end
209
+ end
210
+ }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'xiami_cloner'
5
+
6
+ require 'fileutils'
7
+
8
+ `lock_acquire xiami_cloner`
9
+
10
+ exit unless `lock_check xiami_cloner`.strip == 'true'
11
+
12
+ trigger_file = '/tmp/xiami_cloner.trigger'
13
+ lock_file = '/tmp/xiami_cloner.lock'
14
+
15
+ while true
16
+
17
+ File.open(lock_file, 'w') do |lock|
18
+
19
+ FileUtils.rm_f(trigger_file)
20
+
21
+ unless lock.flock(File::LOCK_NB | File::LOCK_EX)
22
+ puts "已有同步进程执行中"
23
+ FileUtils.touch(trigger_file)
24
+ exit
25
+ end
26
+
27
+ puts "开始同步"
28
+
29
+ XiamiCloner::Cloner.clone(File.expand_path('~/Dropbox/Synced/Playlist'), File.expand_path('~/Music/Xiami'), cloned_file: File.expand_path('~/Dropbox/Synced/Cloned'), import_to_itunes: true)
30
+
31
+ puts "同步完成"
32
+
33
+ end
34
+
35
+ break unless File.exists?(trigger_file)
36
+
37
+ end
38
+
39
+ `lock_release xiami_cloner`
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'xiami_cloner'
5
+
6
+ XiamiCloner::Cloner.clone(File.expand_path('~/Dropbox/Synced/Playlist'), File.expand_path('~/Music/Xiami Local'), cloned_file: File.expand_path('~/Music/Xiami Local/.Cloned'), import_to_itunes: false)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'open-uri'
5
+ require 'nokogiri'
6
+
7
+ id = ARGV[0]
8
+ url = "http://www.xiami.com/album/#{id}"
9
+
10
+ doc = Nokogiri::HTML(open(url).read)
11
+
12
+ doc.css('.song_name').each do |song|
13
+ puts /\/song\/([0-9]*)/.match(song.css('a')[0]['href'])[1]
14
+ end
@@ -0,0 +1,430 @@
1
+ # encoding: utf-8
2
+
3
+ require "xiami_cloner/version"
4
+ require "xiami_cloner/location_decoder"
5
+
6
+ module XiamiCloner
7
+ class Cloner
8
+
9
+ require 'fileutils'
10
+ require 'digest/md5'
11
+ require 'nokogiri'
12
+ require 'net/http'
13
+ require 'ruby-pinyin'
14
+ require 'image_science'
15
+ require 'json'
16
+
17
+ INFO_URL = 'http://www.xiami.com/song/playlist/id/%d/object_name/default/object_id/0'
18
+ ALBUM_PAGE_URL = 'http://www.xiami.com/album/%d'
19
+ GET_HQ_URL = 'http://www.xiami.com/song/gethqsong/sid/%d'
20
+ CACHE_DIR = '~/Library/Caches/xiami_cloner'
21
+
22
+ def self.clone(playlist, outdir, options = {})
23
+ cloneds = options[:cloned_file] && File.exists?(options[:cloned_file])?
24
+ strip_invalid(File.read(options[:cloned_file]).lines) :
25
+ []
26
+ songs = strip_invalid(File.read(playlist).lines)
27
+ terse = options[:terse]
28
+
29
+ counter = 0
30
+
31
+ songs.each do |song|
32
+ counter += 1
33
+
34
+ print "正在下载第 #{counter} / #{songs.size} 首歌曲" unless terse
35
+
36
+ if cloneds.include?(song)
37
+ puts " ... 跳过" unless terse
38
+ next
39
+ end
40
+
41
+ puts
42
+
43
+ self.clone_song(song, outdir, options)
44
+
45
+ cloneds << song
46
+ File.write(options[:cloned_file], cloneds.join("\n")) if options[:cloned_file]
47
+ end
48
+ end
49
+
50
+ def self.clone_song(song, outdir, options = {})
51
+ options[:import_to_itunes] ||= false
52
+ terse = options[:terse]
53
+ hq = options[:high_quality_song]
54
+ cookie = options[:cookie]
55
+
56
+ FileUtils.mkdir_p outdir
57
+
58
+ print "正在下载 " unless terse
59
+
60
+ info = retrieve_info(song)
61
+
62
+ artist = info.search('artist').text
63
+ title = info.search('title').text
64
+
65
+ print "--- " if terse
66
+ print "#{artist} - #{title} "
67
+ puts
68
+
69
+ url = retrieve_url(song, hq, cookie)
70
+ song_path = hq ? "#{song}.hq.mp3" : "#{song}.mp3"
71
+
72
+ while true
73
+ break if check_song_integrity(song, hq)
74
+ FileUtils.rm(self.cache_path(song_path))
75
+ self.download_to_cache(url, song_path, false)
76
+ end
77
+
78
+ out_path = File.join(outdir, filename(song))
79
+ out_path = uniquefy(out_path, ".mp3")
80
+
81
+ FileUtils.cp(self.cache_path(song_path), out_path)
82
+
83
+ write_id3(song, out_path)
84
+
85
+ if options[:import_to_itunes]
86
+ import_to_itunes(out_path)
87
+ puts "已将 #{artist} - #{title} 导入 iTunes" unless terse
88
+ end
89
+ end
90
+
91
+ def self.check_integrity(playlist)
92
+ songs = strip_invalid(File.read(playlist).lines)
93
+
94
+ counter = 0
95
+
96
+ songs.each do |song|
97
+ counter += 1
98
+
99
+ puts "正在检查第 #{counter} / #{songs.size} 首歌曲的完整性"
100
+
101
+ puts " 歌曲 #{song} 没有下载完全" unless check_song_integrity(song)
102
+ end
103
+ end
104
+
105
+ def self.retrieve_album_list(id)
106
+ require 'open-uri'
107
+
108
+ url = ALBUM_PAGE_URL % id
109
+
110
+ doc = Nokogiri::HTML(open(url).read)
111
+
112
+ doc.css('.song_name').map do |song|
113
+ /\/song\/([0-9]*)/.match(song.css('a')[0]['href'])[1]
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def self.retrieve_url(song, hq = false, cookie = nil)
120
+ if hq
121
+ download_to_cache(GET_HQ_URL % song, "#{song}.gethq", true, cookie)
122
+ json = JSON.parse(File.open(cache_path("#{song}.gethq")) { |f| f.read })
123
+
124
+ if json['status'].to_i == 1
125
+ url = LocationDecoder.decode(json['location'])
126
+ if url =~ /auth_key/
127
+ # It's the low quality version
128
+ # Nasty hack
129
+ # TODO FIXME
130
+ puts " [信息] 高清地址获取失败,使用低音质版本"
131
+ return retrieve_url(song, false)
132
+ else
133
+ return url
134
+ end
135
+ else
136
+ puts " [信息] 无高音质版本可用,使用低音质版本"
137
+ return retrieve_url(song, false)
138
+ end
139
+ else
140
+ info = retrieve_info(song)
141
+ return LocationDecoder.decode(info.search('location').text)
142
+ end
143
+ end
144
+
145
+ def self.check_song_integrity(song, hq = false)
146
+ path = hq ? "#{song}.hq.complete" : "#{song}.complete"
147
+
148
+ return true if File.exists?(path)
149
+
150
+ url = retrieve_url(song, hq)
151
+ song_path = hq ? "#{song}.hq.mp3" : "#{song}.mp3"
152
+
153
+ size = get_content_size(url)
154
+ download_to_cache(url, song_path, false)
155
+
156
+ if File.size(cache_path(song_path)) == size
157
+ FileUtils.touch(cache_path(path))
158
+ return true
159
+ else
160
+ return false
161
+ end
162
+ end
163
+
164
+ def self.clear_cache(name)
165
+ FileUtils.rm_f(cache_path(name))
166
+ end
167
+
168
+ def self.retrieve_info(id)
169
+ info_url = INFO_URL % id
170
+ info_path = "#{id}.info"
171
+
172
+ if File.exists?(cache_path(info_path)) && (File.ctime(cache_path(info_path)) < (Time.now - 3600))
173
+ # Remove if cached more than an hour ago
174
+ FileUtils.rm(cache_path(info_path))
175
+ end
176
+
177
+ self.download_to_cache(info_url, info_path)
178
+
179
+ Nokogiri::XML(File.read(self.cache_path(info_path)))
180
+ end
181
+
182
+ def self.retrieve_order(page, id)
183
+ node = page.at_css('#track .chapter')
184
+
185
+ node.css('.track_list').each_with_index do |d, i|
186
+ disc = i + 1
187
+ d.css('tr td.song_name a').each_with_index do |s, i|
188
+ song = i + 1
189
+ return [disc.to_s, song.to_s] if s['href'].include?(id.to_s)
190
+ end
191
+ end
192
+
193
+ ['', '']
194
+ end
195
+
196
+ def self.retrieve_publish_year(page)
197
+ node = page.at_css('#album_block table')
198
+
199
+ node.css('tr').each do |r|
200
+ ds = r.css('td')
201
+ if ds[0].text == '发行时间:'
202
+ match = /([0-9]*)年/.match(ds[1].text)
203
+ if match
204
+ return match[1]
205
+ else
206
+ return ''
207
+ end
208
+ end
209
+ end
210
+
211
+ ''
212
+ end
213
+
214
+ def self.retrieve_album_page(id)
215
+ page_url = ALBUM_PAGE_URL % id
216
+ page_path = "album_#{id}.page"
217
+
218
+ self.download_to_cache(page_url, page_path)
219
+
220
+ Nokogiri::HTML(File.read(self.cache_path(page_path)))
221
+ end
222
+
223
+ def self.strip_invalid(list)
224
+ list.select { |x| x.to_i > 0 }.map { |x| x.strip }
225
+ end
226
+
227
+ def self.filename(song)
228
+ info = retrieve_info(song)
229
+ "#{info.search('artist').text} - #{info.search('title').text}"
230
+ end
231
+
232
+ def self.import_to_itunes(file)
233
+ itd = File.expand_path("~/Music/iTunes/iTunes Media/Automatically Add to iTunes.localized")
234
+
235
+ FileUtils.cp(file, itd)
236
+ end
237
+
238
+ def self.uniquefy(filename, extname)
239
+ return filename + extname unless File.exists?(filename + extname)
240
+
241
+ i = 2
242
+
243
+ while true
244
+ new_path = filename + " " + i.to_s + extname
245
+ return new_path unless File.exists?(new_path)
246
+ i += 1
247
+ end
248
+ end
249
+
250
+ def self.download_to_cache(url, filename, hidden = true, cookie = nil)
251
+ require 'fileutils'
252
+ # hidden = false
253
+
254
+ FileUtils.mkdir_p(File.expand_path(CACHE_DIR))
255
+
256
+ ccp = File.join(File.expand_path(CACHE_DIR), filename + ".tmp")
257
+ cfp = File.join(File.expand_path(CACHE_DIR), filename)
258
+
259
+ if !File.exists?(cfp)
260
+ FileUtils.rm_rf(ccp)
261
+ command = "curl --connect-timeout 15 --retry 999 --retry-max-time 0 -C - -# \"#{url}\" -o \"#{ccp}\""
262
+ # Changes User-Agent to avoid blocking HQ songs
263
+ command += " --cookie #{cookie}" if cookie
264
+ command += " > /dev/null 2>&1" if hidden
265
+ system(command)
266
+
267
+ if !File.exists?(ccp)
268
+ # TODO FIXME
269
+ # If curl goes to 404 or other errors, create stub file
270
+ FileUtils.touch(cfp)
271
+ else
272
+ FileUtils.mv(ccp, cfp)
273
+ end
274
+ end
275
+ end
276
+
277
+ def self.retrieve_album_artist(page)
278
+ page.css('table tr td a').each do |i|
279
+ return i.text if /\/artist\/([0-9]*)/.match(i['href'])
280
+ end
281
+ end
282
+
283
+ def self.get_text_frame(frame_id, text)
284
+ t = TagLib::ID3v2::TextIdentificationFrame.new(frame_id, TagLib::String::UTF8)
285
+ t.text = text.to_s
286
+ t
287
+ end
288
+
289
+ def self.write_id3(song, path)
290
+ require 'taglib'
291
+
292
+ info = retrieve_info(song)
293
+
294
+ TagLib::MPEG::File.open(path) do |f|
295
+ tag = f.id3v2_tag
296
+
297
+ # Basic infos
298
+ tag.artist = info.search('artist').text
299
+ tag.album = info.search('album_name').text
300
+ tag.title = info.search('title').text
301
+ tag.genre = "Xiami"
302
+
303
+ # Album artist
304
+ album_page = retrieve_album_page(info.search('album_id').text.to_i)
305
+ album_artist = retrieve_album_artist(album_page)
306
+ tag.remove_frames('TPE2')
307
+ tag.add_frame(get_text_frame('TPE2', album_artist))
308
+
309
+ # Sorting fields
310
+ tag.remove_frames('TSOT')
311
+ tag.add_frame(get_text_frame('TSOT', PinYin.sentence(tag.title)))
312
+
313
+ tag.remove_frames('TSOA')
314
+ tag.add_frame(get_text_frame('TSOA', PinYin.sentence(tag.album)))
315
+
316
+ tag.remove_frames('TSOP')
317
+ tag.add_frame(get_text_frame('TSOP', PinYin.sentence(tag.artist)))
318
+
319
+ tag.remove_frames('TSO2')
320
+ tag.add_frame(get_text_frame('TSO2', PinYin.sentence(album_artist)))
321
+
322
+ # Track order (returns strings that are empty if not retrieved successfully)
323
+ disc, track = retrieve_order(album_page, song)
324
+
325
+ tag.remove_frames('TRCK')
326
+ tag.add_frame(get_text_frame('TRCK', track))
327
+
328
+ tag.remove_frames('TPOS')
329
+ tag.add_frame(get_text_frame('TPOS', disc))
330
+
331
+ # Track year (returns string that is empty if not retrieved successfully)
332
+ year = retrieve_publish_year(album_page)
333
+
334
+ tag.remove_frames('TDRC')
335
+ tag.add_frame(get_text_frame('TDRC', year))
336
+
337
+ # Lyrics
338
+ lyrics = simplify_lyrics(retrieve_lyrics(song))
339
+ unless lyrics.strip.empty?
340
+ tag.remove_frames('USLT')
341
+ t = TagLib::ID3v2::UnsynchronizedLyricsFrame.new(TagLib::String::UTF8)
342
+ t.text = lyrics
343
+ tag.add_frame(t)
344
+ end
345
+
346
+ # Album cover
347
+ apic = TagLib::ID3v2::AttachedPictureFrame.new
348
+ apic.mime_type = 'image/png'
349
+ apic.description = 'Cover'
350
+ apic.type = TagLib::ID3v2::AttachedPictureFrame::FrontCover
351
+ apic.picture = retrieve_cover(song)
352
+ tag.add_frame(apic)
353
+
354
+ # Save
355
+ f.save
356
+ end
357
+ end
358
+
359
+ def self.cache_path(filename)
360
+ File.expand_path(File.join(CACHE_DIR, filename))
361
+ end
362
+
363
+ def self.simplify_lyrics(l)
364
+ ls = l.lines.to_a
365
+ nls = []
366
+
367
+ ls.each do |ll|
368
+ unless ll =~ /\[[^0-9][^0-9]:.*?\]/
369
+ ll.gsub! /\[.*?\]/, ''
370
+ nls += [ll.strip]
371
+ end
372
+ end
373
+
374
+ nls.join("\n")
375
+ end
376
+
377
+ def self.retrieve_lyrics(song)
378
+ info = retrieve_info(song)
379
+
380
+ if info.search('lyric') && !info.search('lyric').text.strip.empty?
381
+ self.download_to_cache(info.search('lyric').text, "#{song}.lrc")
382
+ return File.read(self.cache_path("#{song}.lrc"))
383
+ else
384
+ return ''
385
+ end
386
+ end
387
+
388
+ def self.retrieve_cover(song)
389
+ info = retrieve_info(song)
390
+
391
+ if info.search('pic') && !info.search('pic').text.strip.empty?
392
+ url = info.search('pic').text
393
+
394
+ re = /(\/[0-9]*)(_[0-9])(\.)/
395
+
396
+ if !re.match(url)
397
+ # Fallback low-res
398
+ self.download_to_cache(url, "#{song}.cover")
399
+ return File.open(self.cache_path("#{song}.cover"), 'rb').read
400
+ puts " [信息] 使用低清版本专辑封面"
401
+ else
402
+ # Retrieve and crop the high-res version
403
+ new_url = url.gsub(re, '\1\3')
404
+
405
+ return File.open(self.cache_path("#{song}.cover_hq_c"), 'rb').read if File.exists?(self.cache_path("#{song}.cover_hq_c"))
406
+
407
+ self.download_to_cache(new_url, "#{song}.cover_hq")
408
+ ImageScience.with_image(cache_path("#{song}.cover_hq")) do |img|
409
+ img.cropped_thumbnail(500) do |thumb|
410
+ thumb.save cache_path("#{song}.cover_hq_c")
411
+ end
412
+ end
413
+
414
+ return File.open(self.cache_path("#{song}.cover_hq_c"), 'rb').read
415
+ end
416
+ else
417
+ return nil
418
+ end
419
+ end
420
+
421
+ def self.get_content_size(url)
422
+ response = `curl -s -I \"#{url}\"`
423
+
424
+ regexp = /Content-Length: ([0-9]*)[^0-9]/
425
+
426
+ regexp.match(response)[1].to_i
427
+ end
428
+
429
+ end
430
+ end