Class: Zhangmen::Client
- Inherits:
-
Object
- Object
- Zhangmen::Client
- Defined in:
- lib/zhangmen/client.rb
Overview
Wraps a client session for accessing Baidu’s streaming music service.
Instance Attribute Summary collapse
-
#cache ⇒ Object
Cache of HTTP requests / responses.
Instance Method Summary collapse
-
#category(category_id) ⇒ Object
Fetches a collection of playlists by the category ID.
-
#curber(options = {}) ⇒ Object
Curl::Easy instance customized to maximize download success.
-
#hostname ⇒ Object
The service hostname.
-
#initialize(options = {}) ⇒ Client
constructor
New client session.
-
#mechanizer(options = {}) ⇒ Object
Mechanize instance customized to maximize fetch success.
-
#op(opcode, args) ⇒ Object
Performs a numbered operation.
-
#op_cache_key(opcode, args) ⇒ Object
A string suitable as a key for caching a numbered operation’s result.
-
#op_url(opcode, args) ⇒ Object
The fetch URL for an XML opcode.
-
#op_xml_without_cache(opcode, args) ⇒ Object
Performs a numbered operation, returning the raw XML.
-
#playlist(list) ⇒ Object
Fetches a playlist.
-
#song(entry) ⇒ Object
Fetches the MP3 contents of a song.
-
#song_sources(entry) ⇒ Object
Fetches the MP3 download locations for a song.
Constructor Details
#initialize(options = {}) ⇒ Client
New client session.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/zhangmen/client.rb', line 21 def initialize( = {}) if [:proxy] == 'auto' [:proxy] = Zhangmen::Proxy.fetch end @mech = mechanizer @curb = curber @cache = {} @cache_ttl = [:cache_ttl] || (24 * 60 * 60) # 1 day log_level = [:log_level] || Logger::WARN @logger = [:logger] || Logger.new(STDERR) @logger.level = log_level @parser = [:use_hpricot] ? :hpricot : :nokogiri end |
Instance Attribute Details
#cache ⇒ Object
Cache of HTTP requests / responses.
The cache covers all the song metadata, but not actual song data.
39 40 41 |
# File 'lib/zhangmen/client.rb', line 39 def cache @cache end |
Instance Method Details
#category(category_id) ⇒ Object
Fetches a collection of playlists by the category ID.
Args:
category_id:: right now, categories seem to be numbers, so enumerating
from 1 onwards should be good
Returns an array of playlists.
48 49 50 51 52 53 54 55 56 57 |
# File 'lib/zhangmen/client.rb', line 48 def category(category_id) result = op 3, :list_cat => category_id result.search('data').map do |playlist_node| { :id => playlist_node.search('id').inner_text, :name => playlist_node.search('name').inner_text.encode('UTF-8'), :song_count => playlist_node.search('tcount').inner_text.to_i } end end |
#curber(options = {}) ⇒ Object
Curl::Easy instance customized to maximize download success.
245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/zhangmen/client.rb', line 245 def curber( = {}) curb = Curl::Easy.new curb. = true curb.follow_location = true curb.useragent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.124 Safari/534.30' if [:proxy] curb.proxy_url = [:proxy] else curb.proxy_url = nil end curb end |
#hostname ⇒ Object
The service hostname.
228 229 230 |
# File 'lib/zhangmen/client.rb', line 228 def hostname 'box.zhangmen.baidu.com' end |
#mechanizer(options = {}) ⇒ Object
Mechanize instance customized to maximize fetch success.
233 234 235 236 237 238 239 240 241 242 |
# File 'lib/zhangmen/client.rb', line 233 def mechanizer( = {}) mech = Mechanize.new mech.user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.124 Safari/534.30' if [:proxy] host, _, port_str = *[:proxy].rpartition(':') port_str ||= 80 mech.set_proxy host, port_str.to_i end mech end |
#op(opcode, args) ⇒ Object
Performs a numbered operation.
Args:
opcode:: operation number (e.g. 22 for playlist fetch)
args:: operation arguments (e.g. :listid => number for playlist ID)
Returns a Nokogiri root node.
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/zhangmen/client.rb', line 180 def op(opcode, args) @logger.debug { "XML op #{opcode} with #{args.inspect}" } cache_key = op_cache_key opcode, args if @cache[cache_key] && Time.now.to_f - @cache[cache_key][:at] < @cache_ttl xml = @cache[cache_key][:xml] @logger.debug { "Cached response\n#{xml}" } else xml = op_xml_without_cache opcode, args @cache[cache_key] = { :at => Time.now.to_f, :xml => xml } @logger.debug { "Live response\n#{xml}" } end if @parser == :nokogiri Nokogiri.XML(xml).root else Hpricot(xml) end end |
#op_cache_key(opcode, args) ⇒ Object
A string suitable as a key for caching a numbered operation’s result.
Accepts the same arguments as Client#op.
211 212 213 214 215 |
# File 'lib/zhangmen/client.rb', line 211 def op_cache_key(opcode, args) { :op => opcode }.merge(args). map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }. sort.join('&') end |
#op_url(opcode, args) ⇒ Object
The fetch URL for an XML opcode.
Accepts the same arguments as Client#op.
220 221 222 223 224 225 |
# File 'lib/zhangmen/client.rb', line 220 def op_url(opcode, args) query = { :op => opcode }.merge(args). map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }. join('&') + '&.r=' + ('%.16f' % Kernel.rand) URI.parse "http://#{hostname}/x?#{query}" end |
#op_xml_without_cache(opcode, args) ⇒ Object
Performs a numbered operation, returning the raw XML.
Accepts the same arguments as Client#op.
Does not perform any caching.
204 205 206 |
# File 'lib/zhangmen/client.rb', line 204 def op_xml_without_cache(opcode, args) @mech.get(op_url(opcode, args)).body end |
#playlist(list) ⇒ Object
Fetches a playlist.
Args:
list:: a playlist obtained by calling category
Returns an array of songs.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/zhangmen/client.rb', line 65 def playlist(list) result = op 22, :listid => list[:id] count = result.search('count').inner_text.to_i result.search('data').map do |song_node| raw_name = song_node.search('name').inner_text if match = /^(.*)\$\$(.*)\$\$\$\$/.match(raw_name) title = match[1].encode('UTF-8') = match[2].encode('UTF-8') else = title = raw_name.encode('UTF-8') end if @parser == :nokogiri native_encoding = result.document.encoding else native_encoding = raw_name.encoding end { :raw_name => raw_name.encode('UTF-8'), :raw_encoding => native_encoding, :title => title, :author => , :id => song_node.search('id').inner_text } end end |
#song(entry) ⇒ Object
Fetches the MP3 contents of a song.
Args:
song:: a song obtained by calling playlist
Returns the MP3 bits.
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/zhangmen/client.rb', line 98 def song(entry) song_sources(entry).each do |src| 3.times do begin @curb.url = src[:url] begin @curb.perform rescue Curl::Err::PartialFileError got = @curb.body_str.length expected = @curb.downloaded_content_length.to_i if got < expected @logger.warn do "Server hangup fetching #{src[:url]}; got #{got} bytes, " + "expected #{expected}" end # Server gave us fewer bytes than promised in Content-Length. # Try again in case the error is temporary. sleep 1 next end end next unless @curb.response_code >= 200 && @curb.response_code < 300 bits = @curb.body_str if bits[-256, 3] == 'TAG' || bits[0, 3] == 'ID3' return bits else break end rescue Curl::Err::GotNothingError @logger.warn do "Server hangup fetching #{src[:url]}; got no HTTP response" end # Try again in case the error is temporary. sleep 1 rescue Curl::Err::RecvError @logger.warn do "TCP error fetching #{src[:url]}" end # Try again in case the error is temporary. sleep 1 rescue Timeout::Error @logger.warn do "Timeout while downloading #{src[:url]}" end # Try again in case the error is temporary. sleep 1 end end end nil end |
#song_sources(entry) ⇒ Object
Fetches the MP3 download locations for a song.
Args:
song:: a song obtained by calling playlist
Returns
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/zhangmen/client.rb', line 156 def song_sources(entry) title = entry[:raw_name].encode(entry[:raw_encoding]) result = op 12, :count => 1, :mtype => 1, :title => title, :url => '', :listenreelect => 0 result.search('url').map do |url_node| filename = url_node.search('decode').inner_text encoded_url = url_node.search('encode').inner_text url = File.join File.dirname(encoded_url), filename { :url => url, :type => url_node.search('type').inner_text.to_i, :lyrics_id => url_node.search('lrid').inner_text.to_i, :flag => url_node.search('flag').inner_text } end end |