zhangmen 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -2,16 +2,20 @@ source 'http://rubygems.org'
2
2
  # Add dependencies required to use your gem here.
3
3
  # Example:
4
4
  # gem 'activesupport', '>= 2.3.5'
5
- gem 'json'
6
- gem 'mechanize'
7
- gem 'nokogiri'
5
+ gem 'curb', '>= 0.8.0'
6
+ gem 'hpricot', '>= 0.8.6'
7
+ gem 'json', '>= 1.0.0', :platform => :ruby_18
8
+ gem 'mechanize', '>= 2.3.0'
9
+ gem 'nokogiri', '>= 1.5.2'
8
10
 
9
11
  # Add dependencies to develop your gem here.
10
12
  # Include everything needed to run rake, tests, features, etc.
11
13
  group :development do
12
- gem 'rdoc', '>= 3.6.0'
13
- gem 'rspec', '~> 2.6.0'
14
- gem 'bundler', '~> 1.0.0'
15
- gem 'jeweler', '~> 1.6.2'
16
- gem 'rcov', '>= 0'
14
+ gem 'bundler', '>= 1.1.0'
15
+ gem 'jeweler', '>= 1.8.3'
16
+ gem 'minitest', '>= 2.11.2'
17
+ gem 'mocha', '>= 0.10.5', :require => false
18
+ gem 'rdoc', '>= 3.12'
19
+ gem 'simplecov', '>= 0.6.1'
20
+ gem 'yard', '>= 0.7.5'
17
21
  end
@@ -1,44 +1,61 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
- diff-lcs (1.1.2)
4
+ curb (0.8.0)
5
+ domain_name (0.5.3)
6
+ unf (~> 0.0.3)
5
7
  git (1.2.5)
6
- jeweler (1.6.4)
8
+ hpricot (0.8.6)
9
+ jeweler (1.8.3)
7
10
  bundler (~> 1.0)
8
11
  git (>= 1.2.5)
9
12
  rake
13
+ rdoc
10
14
  json (1.5.3)
11
- mechanize (2.0.1)
15
+ mechanize (2.3)
16
+ domain_name (~> 0.5, >= 0.5.1)
17
+ mime-types (~> 1.17, >= 1.17.2)
12
18
  net-http-digest_auth (~> 1.1, >= 1.1.1)
13
- net-http-persistent (~> 1.8)
19
+ net-http-persistent (~> 2.5, >= 2.5.2)
14
20
  nokogiri (~> 1.4)
21
+ ntlm-http (~> 0.1, >= 0.1.1)
15
22
  webrobots (~> 0.0, >= 0.0.9)
16
- net-http-digest_auth (1.1.1)
17
- net-http-persistent (1.8)
18
- nokogiri (1.5.0)
19
- rake (0.9.2)
20
- rcov (0.9.9)
21
- rdoc (3.8)
22
- rspec (2.6.0)
23
- rspec-core (~> 2.6.0)
24
- rspec-expectations (~> 2.6.0)
25
- rspec-mocks (~> 2.6.0)
26
- rspec-core (2.6.4)
27
- rspec-expectations (2.6.0)
28
- diff-lcs (~> 1.1.2)
29
- rspec-mocks (2.6.0)
30
- webrobots (0.0.10)
31
- nokogiri (>= 1.4.4)
23
+ metaclass (0.0.1)
24
+ mime-types (1.18)
25
+ minitest (2.12.0)
26
+ mocha (0.10.5)
27
+ metaclass (~> 0.0.1)
28
+ multi_json (1.2.0)
29
+ net-http-digest_auth (1.2)
30
+ net-http-persistent (2.6)
31
+ nokogiri (1.5.2)
32
+ ntlm-http (0.1.1)
33
+ rake (0.9.2.2)
34
+ rdoc (3.12)
35
+ json (~> 1.4)
36
+ simplecov (0.6.1)
37
+ multi_json (~> 1.0)
38
+ simplecov-html (~> 0.5.3)
39
+ simplecov-html (0.5.3)
40
+ unf (0.0.5)
41
+ unf_ext
42
+ unf_ext (0.0.4)
43
+ webrobots (0.0.13)
44
+ yard (0.7.5)
32
45
 
33
46
  PLATFORMS
34
47
  ruby
35
48
 
36
49
  DEPENDENCIES
37
- bundler (~> 1.0.0)
38
- jeweler (~> 1.6.2)
39
- json
40
- mechanize
41
- nokogiri
42
- rcov
43
- rdoc (>= 3.6.0)
44
- rspec (~> 2.6.0)
50
+ bundler (>= 1.1.0)
51
+ curb (>= 0.8.0)
52
+ hpricot (>= 0.8.6)
53
+ jeweler (>= 1.8.3)
54
+ json (>= 1.0.0)
55
+ mechanize (>= 2.3.0)
56
+ minitest (>= 2.11.2)
57
+ mocha (>= 0.10.5)
58
+ nokogiri (>= 1.5.2)
59
+ rdoc (>= 3.12)
60
+ simplecov (>= 0.6.1)
61
+ yard (>= 0.7.5)
@@ -4,7 +4,7 @@ Fetches music from the Baidu streaming music player.
4
4
 
5
5
  == Usage
6
6
 
7
- The code currently requires the string encoding support in Ruby 1.9. Use MRI 1.9.2 or Rubinius / Jruby in 1.9 mode.
7
+ The code currently requires the string encoding support in Ruby 1.9. Use MRI 1.9.3 or Rubinius / Jruby in 1.9 mode.
8
8
 
9
9
  rvm use 1.9.2
10
10
 
@@ -19,10 +19,6 @@ Then choose a playlist and download all its songs.
19
19
  Or, if you're feeling undecided, grab the entire library.
20
20
 
21
21
  zhangmen all
22
-
23
- Feeling lazy? Download the entire catalog.
24
-
25
- zhangmen all
26
22
 
27
23
  The songs will be downloaded in the current directory. One directory will be made for each artist, and all that artist's songs will be saved there.
28
24
 
@@ -30,16 +26,17 @@ If you're not in Mainland China, you can use a proxy to get your music fix.
30
26
 
31
27
  http_proxy=google.for.a.proxy:1234 zhangmen fetch 602
32
28
 
29
+ Or, if you're feeling lazy, let the code try to pick a proxy for you.
30
+
31
+ http_proxy=auto zhangmen fetch 602
32
+
33
+
33
34
  == Testing
34
35
 
35
36
  The tests are written in RSpec. Run them like this.
36
37
 
37
38
  rake spec
38
39
 
39
- If you're not in Mainland China, set http_proxy so the downloading tests pass.
40
-
41
- http_proxy=google.for.a.proxy:1234 rake spec
42
-
43
40
  == Known Issues
44
41
 
45
42
  Some songs might not download. The Flash player skips over those songs too, so it seems to be a server issue.
data/Rakefile CHANGED
@@ -25,25 +25,14 @@ Jeweler::Tasks.new do |gem|
25
25
  end
26
26
  Jeweler::RubygemsDotOrgTasks.new
27
27
 
28
- require 'rspec/core'
29
- require 'rspec/core/rake_task'
30
- RSpec::Core::RakeTask.new(:spec) do |spec|
31
- spec.pattern = FileList['spec/**/*_spec.rb']
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
32
33
  end
33
34
 
34
- RSpec::Core::RakeTask.new(:rcov) do |spec|
35
- spec.pattern = 'spec/**/*_spec.rb'
36
- spec.rcov = true
37
- end
38
-
39
- task :default => :spec
35
+ task :default => :test
40
36
 
41
- require 'rdoc/task'
42
- Rake::RDocTask.new do |rdoc|
43
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
-
45
- rdoc.rdoc_dir = 'rdoc'
46
- rdoc.title = "zhangmen #{version}"
47
- rdoc.rdoc_files.include('README*')
48
- rdoc.rdoc_files.include('lib/**/*.rb')
49
- end
37
+ require 'yard'
38
+ YARD::Rake::YardocTask.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -5,3 +5,4 @@ end # namespace Zhangmen
5
5
 
6
6
  require 'zhangmen/client.rb'
7
7
  require 'zhangmen/cli.rb'
8
+ require 'zhangmen/proxy.rb'
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'logger'
2
3
  require 'yaml'
3
4
 
4
5
  # :nodoc: namespace
@@ -11,22 +12,16 @@ class Cli
11
12
  empty_categories = 0
12
13
 
13
14
  loop do
14
- begin
15
- playlists = @client.category category_id
16
- save_client_cache
17
- if playlists.empty?
18
- empty_categories += 1
19
- break if empty_categories == 5
20
- else
21
- empty_categories = 0
22
- yield category_id, playlists
23
- end
24
- category_id += 1
25
- rescue Exception => e
26
- puts "#{e.class.name}: #{e}"
27
- puts e.backtrace.join("\n")
28
- break
15
+ playlists = @client.category category_id
16
+ save_client_cache
17
+ if playlists.empty?
18
+ empty_categories += 1
19
+ break if empty_categories == 5
20
+ else
21
+ empty_categories = 0
22
+ yield category_id, playlists
29
23
  end
24
+ category_id += 1
30
25
  end
31
26
  category_id
32
27
  end
@@ -93,6 +88,7 @@ class Cli
93
88
  end
94
89
  rescue ArgumentError => e
95
90
  # Encoding error; bummer, can't cache
91
+ # puts "#{e.class.name}: #{e}"
96
92
  end
97
93
  end
98
94
 
@@ -107,16 +103,22 @@ class Cli
107
103
  if proxy_server = ENV['http_proxy'] || ENV['all_proxy']
108
104
  options[:proxy] = proxy_server
109
105
  end
106
+ @logger = Logger.new STDERR
107
+ options[:logger] = @logger
110
108
  @client = Zhangmen::Client.new options
111
109
  @client.cache = client_cache
112
-
113
- case args[0]
114
- when 'list'
115
- categories
116
- when 'fetch'
117
- args[1..-1].each { |arg| playlist arg }
118
- when 'all'
119
- all
110
+
111
+ begin
112
+ case args[0]
113
+ when 'list'
114
+ categories
115
+ when 'fetch'
116
+ args[1..-1].each { |arg| playlist arg }
117
+ when 'all'
118
+ all
119
+ end
120
+ rescue Exception => e
121
+ @logger.error "#{e.class.name}: #{e}\n#{e.backtrace.join("\n")}\n"
120
122
  end
121
123
  end
122
124
  end # class Zhangmen::Cli
@@ -1,5 +1,8 @@
1
1
  require 'cgi'
2
+ require 'logger'
2
3
 
4
+ require 'curb'
5
+ require 'hpricot'
3
6
  require 'mechanize'
4
7
  require 'nokogiri'
5
8
 
@@ -10,11 +13,24 @@ module Zhangmen
10
13
  class Client
11
14
  # New client session.
12
15
  #
13
- # The options hash accepts the following keys:
14
- # :proxy:: "host:port" string
16
+ # @option options [String] proxy "host:port" string, "auto" to have a proxy
17
+ # discovered automatically, or nil / false to use direct requests
18
+ # @option options [Integer] cache_ttl validity of cached requests, in seconds
19
+ # @option options [Integer] log_level severity treshold (e.g., Logger::ERROR)
20
+ # @option options [Logger] logger receiver of logging info
15
21
  def initialize(options = {})
22
+ if options[:proxy] == 'auto'
23
+ options[:proxy] = Zhangmen::Proxy.fetch
24
+ end
25
+
16
26
  @mech = mechanizer options
27
+ @curb = curber options
17
28
  @cache = {}
29
+ @cache_ttl = options[:cache_ttl] || (24 * 60 * 60) # 1 day
30
+ log_level = options[:log_level] || Logger::WARN
31
+ @logger = options[:logger] || Logger.new(STDERR)
32
+ @logger.level = log_level
33
+ @parser = options[:use_hpricot] ? :hpricot : :nokogiri
18
34
  end
19
35
 
20
36
  # Cache of HTTP requests / responses.
@@ -31,11 +47,11 @@ class Client
31
47
  # Returns an array of playlists.
32
48
  def category(category_id)
33
49
  result = op 3, :list_cat => category_id
34
- result.css('data').map do |playlist_node|
50
+ result.search('data').map do |playlist_node|
35
51
  {
36
- :id => playlist_node.css('id').inner_text,
37
- :name => playlist_node.css('name').inner_text.encode('UTF-8'),
38
- :song_count => playlist_node.css('tcount').inner_text.to_i
52
+ :id => playlist_node.search('id').inner_text,
53
+ :name => playlist_node.search('name').inner_text.encode('UTF-8'),
54
+ :song_count => playlist_node.search('tcount').inner_text.to_i
39
55
  }
40
56
  end
41
57
  end
@@ -48,11 +64,10 @@ class Client
48
64
  # Returns an array of songs.
49
65
  def playlist(list)
50
66
  result = op 22, :listid => list[:id]
51
- native_encoding = result.document.encoding
52
67
 
53
- count = result.css('count').inner_text.to_i
54
- result.css('data').map do |song_node|
55
- raw_name = song_node.css('name').inner_text
68
+ count = result.search('count').inner_text.to_i
69
+ result.search('data').map do |song_node|
70
+ raw_name = song_node.search('name').inner_text
56
71
  if match = /^(.*)\$\$(.*)\$\$\$\$/.match(raw_name)
57
72
  title = match[1].encode('UTF-8')
58
73
  author = match[2].encode('UTF-8')
@@ -60,10 +75,16 @@ class Client
60
75
  author = title = raw_name.encode('UTF-8')
61
76
  end
62
77
 
78
+ if @parser == :nokogiri
79
+ native_encoding = result.document.encoding
80
+ else
81
+ native_encoding = raw_name.encoding
82
+ end
63
83
  {
64
- :raw_name => raw_name.encode(native_encoding),
84
+ :raw_name => raw_name.encode('UTF-8'),
85
+ :raw_encoding => native_encoding,
65
86
  :title => title, :author => author,
66
- :id => song_node.css('id').inner_text
87
+ :id => song_node.search('id').inner_text
67
88
  }
68
89
  end
69
90
  end
@@ -78,21 +99,49 @@ class Client
78
99
  song_sources(entry).each do |src|
79
100
  3.times do
80
101
  begin
81
- result = @mech.get src[:url]
82
- next unless result.kind_of?(Mechanize::File)
83
- bits = result.body
102
+ @curb.url = src[:url]
103
+ begin
104
+ @curb.perform
105
+ rescue Curl::Err::PartialFileError
106
+ got = @curb.body_str.length
107
+ expected = @curb.downloaded_content_length.to_i
108
+ if got < expected
109
+ @logger.warn do
110
+ "Server hangup fetching #{src[:url]}; got #{got} bytes, " +
111
+ "expected #{expected}"
112
+ end
113
+ # Server gave us fewer bytes than promised in Content-Length.
114
+ # Try again in case the error is temporary.
115
+ sleep 1
116
+ next
117
+ end
118
+ end
119
+ next unless @curb.response_code >= 200 && @curb.response_code < 300
120
+ bits = @curb.body_str
84
121
  if bits[-256, 3] == 'TAG' || bits[0, 3] == 'ID3'
85
122
  return bits
86
123
  else
87
124
  break
88
125
  end
89
- rescue EOFError
90
- # Server hung up on us. Try again in case the error is temporary.
126
+ rescue Curl::Err::GotNothingError
127
+ @logger.warn do
128
+ "Server hangup fetching #{src[:url]}; got no HTTP response"
129
+ end
130
+ # Try again in case the error is temporary.
131
+ sleep 1
132
+ rescue Curl::Err::RecvError
133
+ @logger.warn do
134
+ "TCP error fetching #{src[:url]}"
135
+ end
136
+ # Try again in case the error is temporary.
137
+ sleep 1
91
138
  rescue Timeout::Error
92
- # Server hung up on us. Try again in case the error is temporary.
93
- rescue Mechanize::ResponseCodeError
94
- # 500-ish response. Try again in case the error is temporary.
95
- end
139
+ @logger.warn do
140
+ "Timeout while downloading #{src[:url]}"
141
+ end
142
+ # Try again in case the error is temporary.
143
+ sleep 1
144
+ end
96
145
  end
97
146
  end
98
147
  nil
@@ -105,17 +154,18 @@ class Client
105
154
  #
106
155
  # Returns
107
156
  def song_sources(entry)
108
- result = op 12, :count => 1, :mtype => 1, :title => entry[:raw_name],
109
- :url => '', :listenreelect => 0
110
- result.css('url').map do |url_node|
111
- filename = url_node.css('decode').inner_text
112
- encoded_url = url_node.css('encode').inner_text
157
+ title = entry[:raw_name].encode(entry[:raw_encoding])
158
+ result = op 12, :count => 1, :mtype => 1, :title => title, :url => '',
159
+ :listenreelect => 0
160
+ result.search('url').map do |url_node|
161
+ filename = url_node.search('decode').inner_text
162
+ encoded_url = url_node.search('encode').inner_text
113
163
  url = File.join File.dirname(encoded_url), filename
114
164
  {
115
165
  :url => url,
116
- :type => url_node.css('type').inner_text.to_i,
117
- :lyrics_id => url_node.css('lrid').inner_text.to_i,
118
- :flag => url_node.css('flag').inner_text
166
+ :type => url_node.search('type').inner_text.to_i,
167
+ :lyrics_id => url_node.search('lrid').inner_text.to_i,
168
+ :flag => url_node.search('flag').inner_text
119
169
  }
120
170
  end
121
171
  end
@@ -128,13 +178,45 @@ class Client
128
178
  #
129
179
  # Returns a Nokogiri root node.
130
180
  def op(opcode, args)
131
- url = op_url(opcode, args)
132
- cache_key = url.to_s
133
- @cache[cache_key] ||= @mech.get(url).body
134
- Nokogiri.XML(@cache[cache_key]).root
181
+ @logger.debug { "XML op #{opcode} with #{args.inspect}" }
182
+ cache_key = op_cache_key opcode, args
183
+ if @cache[cache_key] && Time.now.to_f - @cache[cache_key][:at] < @cache_ttl
184
+ xml = @cache[cache_key][:xml]
185
+ @logger.debug { "Cached response\n#{xml}" }
186
+ else
187
+ xml = op_xml_without_cache opcode, args
188
+ @cache[cache_key] = { :at => Time.now.to_f, :xml => xml }
189
+ @logger.debug { "Live response\n#{xml}" }
190
+ end
191
+
192
+ if @parser == :nokogiri
193
+ Nokogiri.XML(xml).root
194
+ else
195
+ Hpricot(xml)
196
+ end
197
+ end
198
+
199
+ # Performs a numbered operation, returning the raw XML.
200
+ #
201
+ # Accepts the same arguments as Client#op.
202
+ #
203
+ # Does not perform any caching.
204
+ def op_xml_without_cache(opcode, args)
205
+ @mech.get(op_url(opcode, args)).body
206
+ end
207
+
208
+ # A string suitable as a key for caching a numbered operation's result.
209
+ #
210
+ # Accepts the same arguments as Client#op.
211
+ def op_cache_key(opcode, args)
212
+ { :op => opcode }.merge(args).
213
+ map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.
214
+ sort.join('&')
135
215
  end
136
216
 
137
217
  # The fetch URL for an XML opcode.
218
+ #
219
+ # Accepts the same arguments as Client#op.
138
220
  def op_url(opcode, args)
139
221
  query = { :op => opcode }.merge(args).
140
222
  map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.
@@ -150,7 +232,7 @@ class Client
150
232
  # Mechanize instance customized to maximize fetch success.
151
233
  def mechanizer(options = {})
152
234
  mech = Mechanize.new
153
- mech.user_agent_alias = 'Linux Firefox'
235
+ mech.user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.124 Safari/534.30'
154
236
  if options[:proxy]
155
237
  host, _, port_str = *options[:proxy].rpartition(':')
156
238
  port_str ||= 80
@@ -158,6 +240,20 @@ class Client
158
240
  end
159
241
  mech
160
242
  end
243
+
244
+ # Curl::Easy instance customized to maximize download success.
245
+ def curber(options = {})
246
+ curb = Curl::Easy.new
247
+ curb.enable_cookies = true
248
+ curb.follow_location = true
249
+ curb.useragent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.124 Safari/534.30'
250
+ if options[:proxy]
251
+ curb.proxy_url = options[:proxy]
252
+ else
253
+ curb.proxy_url = nil
254
+ end
255
+ curb
256
+ end
161
257
  end # class Zhangmen::Client
162
258
 
163
259
  end # namespace Zhangmen
@@ -0,0 +1,32 @@
1
+ # :nodoc: namespace
2
+ module Zhangmen
3
+
4
+ # Finds a HTTP proxy that will help bypass Baidu's region check.
5
+ module Proxy
6
+ # Fresh information for a HTTP proxy in China.
7
+ #
8
+ # @return [String] proxy information that can be passed into the :proxy option
9
+ # of Zhangmen::Client#initialize
10
+ def self.fetch
11
+ list_url = 'http://www.xroxy.com/proxylist.php?type=Anonymous&country=CN'
12
+ agent = Mechanize.new { |a| a.user_agent_alias = 'Linux Firefox' }
13
+ html = agent.get_file list_url
14
+ doc = Nokogiri.HTML html
15
+ doc.css('table tr').each do |row|
16
+ cells = row.css('td').map { |td| td.inner_text }
17
+ cells.each_with_index do |cell, index|
18
+ next unless match_data = /(\d{1,3}\.){3}\d{1,3}/.match(cell)
19
+ ip = match_data[0]
20
+ next unless match_data = /\d{2,5}/.match(cells[index + 1])
21
+ port = match_data[0].to_i
22
+ next unless match_data = /(false)|(true)/.match(cells[index + 3])
23
+ ssl = match_data[0] == 'true'
24
+ next if ssl
25
+
26
+ return "#{ip}:#{port}"
27
+ end
28
+ end
29
+ end
30
+ end # module Zhangmen::Proxy
31
+
32
+ end # namespace Zhangmen
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'minitest/unit'
11
+ require 'minitest/spec'
12
+ require 'mocha'
13
+
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
16
+ require 'zhangmen'
17
+
18
+ class MiniTest::Unit::TestCase
19
+ end
20
+
21
+ MiniTest::Unit.autorun
@@ -0,0 +1,163 @@
1
+ require File.expand_path('../helper.rb', File.dirname(__FILE__))
2
+
3
+ describe Zhangmen::Client do
4
+ describe 'mechanizer' do
5
+ let(:empty_client) { Zhangmen::Client.new }
6
+
7
+ it 'returns a Mechanize instance' do
8
+ empty_client.mechanizer.must_be_kind_of Mechanize
9
+ end
10
+
11
+ describe 'with a proxy option' do
12
+ let(:mech) do
13
+ empty_client.mechanizer :proxy => '127.0.0.1:3306'
14
+ end
15
+
16
+ it 'parses the address correctly' do
17
+ mech.proxy_addr.must_equal '127.0.0.1'
18
+ end
19
+
20
+ it 'parses the port correctly' do
21
+ mech.proxy_port.must_equal 3306
22
+ end
23
+ end
24
+ end
25
+
26
+ let(:client) { Zhangmen::Client.new :proxy => ENV['http_proxy'] || 'auto' }
27
+
28
+ describe 'op_url' do
29
+ it 'encodes everything correctly' do
30
+ Kernel.stubs(:rand).returns(0.42)
31
+ client.op_url(22, :listid => 600).to_s.must_equal(
32
+ 'http://box.zhangmen.baidu.com/x?op=22&listid=600&.r=0.42' + '0' * 14)
33
+ end
34
+ end
35
+
36
+ describe 'op_cache_key' do
37
+ it 'encodes arguments correctly' do
38
+ client.op_cache_key(22, :listid => 600).must_equal 'listid=600&op=22'
39
+ end
40
+ end
41
+
42
+ describe 'cache' do
43
+ it 'behaves like a Hash' do
44
+ client.cache.must_respond_to(:has_key?)
45
+ client.cache.must_respond_to(:[])
46
+ client.cache.must_respond_to(:[]=)
47
+ end
48
+ end
49
+
50
+ describe 'op' do
51
+ describe 'with good known arguments' do
52
+ let(:key) { client.op_cache_key(22, :listid => 600) }
53
+ let(:result) { client.op 22, :listid => 600 }
54
+
55
+ it 'returns a Nokogiri root node' do
56
+ result.must_be_kind_of Nokogiri::XML::Element
57
+ end
58
+
59
+ it 'returns a <result> root node' do
60
+ result.name.must_equal 'result'
61
+ end
62
+
63
+ it 'caches the request' do
64
+ result.wont_be_nil # Make sure the request is performed
65
+ client.cache.must_be :has_key?, key
66
+ client.cache[key].must_be :has_key?, :at
67
+ client.cache[key].must_be :has_key?, :xml
68
+ end
69
+
70
+ describe 'repeated with same arguments' do
71
+ it 'uses the cache' do
72
+ result.wont_be_nil # Make sure the request is performed.
73
+ class <<client
74
+ def op_xml_without_cache(*args)
75
+ raise 'Cacheable request invoking un-cached code path'
76
+ end
77
+ end
78
+ client.op(22, :listid => 600).wont_be_nil
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ describe 'category' do
85
+ describe '1' do
86
+ let(:category) { client.category 1 }
87
+
88
+ it 'has a non-trivial number of playlists' do
89
+ category.length.must_be :>, 10
90
+ end
91
+
92
+ it 'has a non-empty name for each playlist' do
93
+ category.map { |pl| pl[:name] }.any?(&:empty?).must_equal false
94
+ end
95
+
96
+ it 'has a non-empty download id for each playlist' do
97
+ category.map { |pl| pl[:id] }.any?(&:empty?).must_equal false
98
+ end
99
+
100
+ it 'has a non-empty song count for each playlist' do
101
+ category.map { |pl| pl[:song_count] }.any? { |count| count <= 0 }.
102
+ must_equal false
103
+ end
104
+ end
105
+ end
106
+
107
+ describe 'playlist' do
108
+ describe 'first one in category 1' do
109
+ let(:playlist) { client.playlist client.category(1).first }
110
+
111
+ it 'has a non-trivial number of songs' do
112
+ playlist.length.must_be :>, 10
113
+ end
114
+
115
+ it 'has a non-empty raw name in each song' do
116
+ playlist.map { |song| song[:raw_name] }.any?(&:empty?).must_equal false
117
+ end
118
+
119
+ it 'has a non-empty author in each song' do
120
+ playlist.map { |song| song[:author] }.any?(&:empty?).must_equal false
121
+ end
122
+
123
+ it 'has a non-empty title in each song' do
124
+ playlist.map { |song| song[:title] }.any?(&:empty?).must_equal false
125
+ end
126
+
127
+ it 'has a non-empty download id in each song' do
128
+ playlist.map { |song| song[:id] }.any?(&:empty?).must_equal false
129
+ end
130
+ end
131
+ end
132
+
133
+ describe 'song_sources' do
134
+ describe 'first song in first playlits in category 1' do
135
+ let(:sources) do
136
+ client.song_sources client.playlist(client.category(1).first)[0]
137
+ end
138
+
139
+ it 'has a positive number of sources' do
140
+ sources.length.must_be :>, 0
141
+ end
142
+
143
+ it 'has a url key in each source' do
144
+ sources.map { |src| src[:url] }.any?(&:empty?).must_equal false
145
+ end
146
+ end
147
+ end
148
+
149
+ describe 'song' do
150
+ describe 'some popular song' do
151
+ let(:entry) { client.playlist(client.category(5).first).first }
152
+ let(:bits) { client.song entry }
153
+
154
+ it 'is a string' do
155
+ bits.must_respond_to :to_str
156
+ end
157
+
158
+ it 'is more than 1Mb in size' do
159
+ bits.length.must_be :>=, 2 ** 20
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,12 @@
1
+ require File.expand_path('../helper.rb', File.dirname(__FILE__))
2
+
3
+ describe Zhangmen::Proxy do
4
+ describe 'fetch' do
5
+ let(:result) { Zhangmen::Proxy.fetch }
6
+
7
+ it 'matches the Zhangmen::Client :proxy format' do
8
+ proxy_regexp = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\:\d{2,4}/
9
+ result.must_match proxy_regexp
10
+ end
11
+ end
12
+ end
@@ -4,15 +4,14 @@
4
4
  # -*- encoding: utf-8 -*-
5
5
 
6
6
  Gem::Specification.new do |s|
7
- s.name = %q{zhangmen}
8
- s.version = "0.1.1"
7
+ s.name = "zhangmen"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Victor Costan"]
12
- s.date = %q{2011-07-17}
13
- s.default_executable = %q{zhangmen}
14
- s.description = %q{CLI and library for downloading music from Baidu}
15
- s.email = %q{victor@costan.us}
12
+ s.date = "2012-04-06"
13
+ s.description = "CLI and library for downloading music from Baidu"
14
+ s.email = "victor@costan.us"
16
15
  s.executables = ["zhangmen"]
17
16
  s.extra_rdoc_files = [
18
17
  "LICENSE.txt",
@@ -32,48 +31,61 @@ Gem::Specification.new do |s|
32
31
  "lib/zhangmen.rb",
33
32
  "lib/zhangmen/cli.rb",
34
33
  "lib/zhangmen/client.rb",
35
- "spec/spec_helper.rb",
36
- "spec/zhangmen/client_spec.rb",
37
- "spec/zhangmen_spec.rb",
34
+ "lib/zhangmen/proxy.rb",
35
+ "test/helper.rb",
36
+ "test/zhangmen/client_test.rb",
37
+ "test/zhangmen/proxy_test.rb",
38
38
  "zhangmen.gemspec"
39
39
  ]
40
- s.homepage = %q{http://github.com/pwnall/zhangmen}
40
+ s.homepage = "http://github.com/pwnall/zhangmen"
41
41
  s.licenses = ["MIT"]
42
42
  s.require_paths = ["lib"]
43
- s.rubygems_version = %q{1.6.2}
44
- s.summary = %q{CLI and library for downloading music from Baidu}
43
+ s.rubygems_version = "1.8.21"
44
+ s.summary = "CLI and library for downloading music from Baidu"
45
45
 
46
46
  if s.respond_to? :specification_version then
47
47
  s.specification_version = 3
48
48
 
49
49
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
50
- s.add_runtime_dependency(%q<json>, [">= 0"])
51
- s.add_runtime_dependency(%q<mechanize>, [">= 0"])
52
- s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
53
- s.add_development_dependency(%q<rdoc>, [">= 3.6.0"])
54
- s.add_development_dependency(%q<rspec>, ["~> 2.6.0"])
55
- s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
56
- s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
57
- s.add_development_dependency(%q<rcov>, [">= 0"])
50
+ s.add_runtime_dependency(%q<curb>, [">= 0.8.0"])
51
+ s.add_runtime_dependency(%q<hpricot>, [">= 0.8.6"])
52
+ s.add_runtime_dependency(%q<json>, [">= 1.0.0"])
53
+ s.add_runtime_dependency(%q<mechanize>, [">= 2.3.0"])
54
+ s.add_runtime_dependency(%q<nokogiri>, [">= 1.5.2"])
55
+ s.add_development_dependency(%q<bundler>, [">= 1.1.0"])
56
+ s.add_development_dependency(%q<jeweler>, [">= 1.8.3"])
57
+ s.add_development_dependency(%q<minitest>, [">= 2.11.2"])
58
+ s.add_development_dependency(%q<mocha>, [">= 0.10.5"])
59
+ s.add_development_dependency(%q<rdoc>, [">= 3.12"])
60
+ s.add_development_dependency(%q<simplecov>, [">= 0.6.1"])
61
+ s.add_development_dependency(%q<yard>, [">= 0.7.5"])
58
62
  else
59
- s.add_dependency(%q<json>, [">= 0"])
60
- s.add_dependency(%q<mechanize>, [">= 0"])
61
- s.add_dependency(%q<nokogiri>, [">= 0"])
62
- s.add_dependency(%q<rdoc>, [">= 3.6.0"])
63
- s.add_dependency(%q<rspec>, ["~> 2.6.0"])
64
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
65
- s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
66
- s.add_dependency(%q<rcov>, [">= 0"])
63
+ s.add_dependency(%q<curb>, [">= 0.8.0"])
64
+ s.add_dependency(%q<hpricot>, [">= 0.8.6"])
65
+ s.add_dependency(%q<json>, [">= 1.0.0"])
66
+ s.add_dependency(%q<mechanize>, [">= 2.3.0"])
67
+ s.add_dependency(%q<nokogiri>, [">= 1.5.2"])
68
+ s.add_dependency(%q<bundler>, [">= 1.1.0"])
69
+ s.add_dependency(%q<jeweler>, [">= 1.8.3"])
70
+ s.add_dependency(%q<minitest>, [">= 2.11.2"])
71
+ s.add_dependency(%q<mocha>, [">= 0.10.5"])
72
+ s.add_dependency(%q<rdoc>, [">= 3.12"])
73
+ s.add_dependency(%q<simplecov>, [">= 0.6.1"])
74
+ s.add_dependency(%q<yard>, [">= 0.7.5"])
67
75
  end
68
76
  else
69
- s.add_dependency(%q<json>, [">= 0"])
70
- s.add_dependency(%q<mechanize>, [">= 0"])
71
- s.add_dependency(%q<nokogiri>, [">= 0"])
72
- s.add_dependency(%q<rdoc>, [">= 3.6.0"])
73
- s.add_dependency(%q<rspec>, ["~> 2.6.0"])
74
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
75
- s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
76
- s.add_dependency(%q<rcov>, [">= 0"])
77
+ s.add_dependency(%q<curb>, [">= 0.8.0"])
78
+ s.add_dependency(%q<hpricot>, [">= 0.8.6"])
79
+ s.add_dependency(%q<json>, [">= 1.0.0"])
80
+ s.add_dependency(%q<mechanize>, [">= 2.3.0"])
81
+ s.add_dependency(%q<nokogiri>, [">= 1.5.2"])
82
+ s.add_dependency(%q<bundler>, [">= 1.1.0"])
83
+ s.add_dependency(%q<jeweler>, [">= 1.8.3"])
84
+ s.add_dependency(%q<minitest>, [">= 2.11.2"])
85
+ s.add_dependency(%q<mocha>, [">= 0.10.5"])
86
+ s.add_dependency(%q<rdoc>, [">= 3.12"])
87
+ s.add_dependency(%q<simplecov>, [">= 0.6.1"])
88
+ s.add_dependency(%q<yard>, [">= 0.7.5"])
77
89
  end
78
90
  end
79
91
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zhangmen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,97 +9,200 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-07-17 00:00:00.000000000 -04:00
13
- default_executable: zhangmen
12
+ date: 2012-04-06 00:00:00.000000000 Z
14
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: curb
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.8.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.8.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: hpricot
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 0.8.6
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.8.6
15
46
  - !ruby/object:Gem::Dependency
16
47
  name: json
17
- requirement: &23012660 !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
18
49
  none: false
19
50
  requirements:
20
51
  - - ! '>='
21
52
  - !ruby/object:Gem::Version
22
- version: '0'
53
+ version: 1.0.0
23
54
  type: :runtime
24
55
  prerelease: false
25
- version_requirements: *23012660
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.0.0
26
62
  - !ruby/object:Gem::Dependency
27
63
  name: mechanize
28
- requirement: &23012160 !ruby/object:Gem::Requirement
64
+ requirement: !ruby/object:Gem::Requirement
29
65
  none: false
30
66
  requirements:
31
67
  - - ! '>='
32
68
  - !ruby/object:Gem::Version
33
- version: '0'
69
+ version: 2.3.0
34
70
  type: :runtime
35
71
  prerelease: false
36
- version_requirements: *23012160
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: 2.3.0
37
78
  - !ruby/object:Gem::Dependency
38
79
  name: nokogiri
39
- requirement: &23011680 !ruby/object:Gem::Requirement
80
+ requirement: !ruby/object:Gem::Requirement
40
81
  none: false
41
82
  requirements:
42
83
  - - ! '>='
43
84
  - !ruby/object:Gem::Version
44
- version: '0'
85
+ version: 1.5.2
45
86
  type: :runtime
46
87
  prerelease: false
47
- version_requirements: *23011680
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: 1.5.2
48
94
  - !ruby/object:Gem::Dependency
49
- name: rdoc
50
- requirement: &23011200 !ruby/object:Gem::Requirement
95
+ name: bundler
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: 1.1.0
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
51
105
  none: false
52
106
  requirements:
53
107
  - - ! '>='
54
108
  - !ruby/object:Gem::Version
55
- version: 3.6.0
109
+ version: 1.1.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: jeweler
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: 1.8.3
56
118
  type: :development
57
119
  prerelease: false
58
- version_requirements: *23011200
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: 1.8.3
59
126
  - !ruby/object:Gem::Dependency
60
- name: rspec
61
- requirement: &23010720 !ruby/object:Gem::Requirement
127
+ name: minitest
128
+ requirement: !ruby/object:Gem::Requirement
62
129
  none: false
63
130
  requirements:
64
- - - ~>
131
+ - - ! '>='
65
132
  - !ruby/object:Gem::Version
66
- version: 2.6.0
133
+ version: 2.11.2
67
134
  type: :development
68
135
  prerelease: false
69
- version_requirements: *23010720
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: 2.11.2
70
142
  - !ruby/object:Gem::Dependency
71
- name: bundler
72
- requirement: &23010240 !ruby/object:Gem::Requirement
143
+ name: mocha
144
+ requirement: !ruby/object:Gem::Requirement
73
145
  none: false
74
146
  requirements:
75
- - - ~>
147
+ - - ! '>='
76
148
  - !ruby/object:Gem::Version
77
- version: 1.0.0
149
+ version: 0.10.5
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: 0.10.5
158
+ - !ruby/object:Gem::Dependency
159
+ name: rdoc
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '3.12'
78
166
  type: :development
79
167
  prerelease: false
80
- version_requirements: *23010240
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '3.12'
81
174
  - !ruby/object:Gem::Dependency
82
- name: jeweler
83
- requirement: &23009740 !ruby/object:Gem::Requirement
175
+ name: simplecov
176
+ requirement: !ruby/object:Gem::Requirement
84
177
  none: false
85
178
  requirements:
86
- - - ~>
179
+ - - ! '>='
87
180
  - !ruby/object:Gem::Version
88
- version: 1.6.2
181
+ version: 0.6.1
89
182
  type: :development
90
183
  prerelease: false
91
- version_requirements: *23009740
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: 0.6.1
92
190
  - !ruby/object:Gem::Dependency
93
- name: rcov
94
- requirement: &23009220 !ruby/object:Gem::Requirement
191
+ name: yard
192
+ requirement: !ruby/object:Gem::Requirement
95
193
  none: false
96
194
  requirements:
97
195
  - - ! '>='
98
196
  - !ruby/object:Gem::Version
99
- version: '0'
197
+ version: 0.7.5
100
198
  type: :development
101
199
  prerelease: false
102
- version_requirements: *23009220
200
+ version_requirements: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ! '>='
204
+ - !ruby/object:Gem::Version
205
+ version: 0.7.5
103
206
  description: CLI and library for downloading music from Baidu
104
207
  email: victor@costan.us
105
208
  executables:
@@ -122,11 +225,11 @@ files:
122
225
  - lib/zhangmen.rb
123
226
  - lib/zhangmen/cli.rb
124
227
  - lib/zhangmen/client.rb
125
- - spec/spec_helper.rb
126
- - spec/zhangmen/client_spec.rb
127
- - spec/zhangmen_spec.rb
228
+ - lib/zhangmen/proxy.rb
229
+ - test/helper.rb
230
+ - test/zhangmen/client_test.rb
231
+ - test/zhangmen/proxy_test.rb
128
232
  - zhangmen.gemspec
129
- has_rdoc: true
130
233
  homepage: http://github.com/pwnall/zhangmen
131
234
  licenses:
132
235
  - MIT
@@ -142,7 +245,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
245
  version: '0'
143
246
  segments:
144
247
  - 0
145
- hash: -1826109697642044863
248
+ hash: -4108129830385981857
146
249
  required_rubygems_version: !ruby/object:Gem::Requirement
147
250
  none: false
148
251
  requirements:
@@ -151,7 +254,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
254
  version: '0'
152
255
  requirements: []
153
256
  rubyforge_project:
154
- rubygems_version: 1.6.2
257
+ rubygems_version: 1.8.21
155
258
  signing_key:
156
259
  specification_version: 3
157
260
  summary: CLI and library for downloading music from Baidu
@@ -1,12 +0,0 @@
1
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
- $LOAD_PATH.unshift(File.dirname(__FILE__))
3
- require 'rspec'
4
- require 'zhangmen'
5
-
6
- # Requires supporting files with custom matchers and macros, etc,
7
- # in ./support/ and its subdirectories.
8
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
-
10
- RSpec.configure do |config|
11
-
12
- end
@@ -1,130 +0,0 @@
1
- # :encoding: UTF-8
2
- require File.expand_path('../../spec_helper', __FILE__)
3
-
4
- describe Zhangmen::Client do
5
- describe 'mechanizer' do
6
- let(:empty_client) { Zhangmen::Client.new }
7
-
8
- it 'returns a Mechanize instance' do
9
- empty_client.mechanizer.should be_kind_of(Mechanize)
10
- end
11
-
12
- describe 'with a proxy option' do
13
- let(:mech) do
14
- empty_client.mechanizer :proxy => '127.0.0.1:3306'
15
- end
16
-
17
- it 'parses the address correctly' do
18
- mech.proxy_addr.should == '127.0.0.1'
19
- end
20
-
21
- it 'parses the port correctly' do
22
- mech.proxy_port.should == 3306
23
- end
24
- end
25
- end
26
-
27
- let(:client) { Zhangmen::Client.new :proxy => ENV['http_proxy'] }
28
-
29
- describe 'op_url' do
30
- it 'encodes everything correctly' do
31
- Kernel.should_receive(:rand).and_return(0.42)
32
- client.op_url(22, :listid => 600).to_s.should ==
33
- 'http://box.zhangmen.baidu.com/x?op=22&listid=600&.r=0.42' + '0' * 14
34
- end
35
- end
36
-
37
- describe 'op' do
38
- describe 'with good known arguments' do
39
- let(:result) { client.op 22, :listid => 600 }
40
-
41
- it 'returns a Nokogiri root node' do
42
- result.should be_kind_of(Nokogiri::XML::Element)
43
- end
44
-
45
- it 'returns a <result> root node' do
46
- result.name.should == 'result'
47
- end
48
- end
49
- end
50
-
51
- describe 'category' do
52
- describe '1' do
53
- let(:category) { client.category 1 }
54
-
55
- it 'should have a non-trivial number of playlists' do
56
- category.length.should > 10
57
- end
58
-
59
- it 'should have a non-empty name for each playlist' do
60
- category.map { |pl| pl[:name] }.any?(&:empty?).should be_false
61
- end
62
-
63
- it 'should have a non-empty download id for each playlist' do
64
- category.map { |pl| pl[:id] }.any?(&:empty?).should be_false
65
- end
66
-
67
- it 'should have a non-empty song count for each playlist' do
68
- category.map { |pl| pl[:song_count] }.any? { |count| count <= 0 }.
69
- should be_false
70
- end
71
- end
72
- end
73
-
74
- describe 'playlist' do
75
- describe 'first one in category 1' do
76
- let(:playlist) { client.playlist client.category(1).first }
77
-
78
- it 'should have a non-trivial number of songs' do
79
- playlist.length.should > 10
80
- end
81
-
82
- it 'should have a non-empty raw name in each song' do
83
- playlist.map { |song| song[:raw_name] }.any?(&:empty?).should be_false
84
- end
85
-
86
- it 'should have a non-empty author in each song' do
87
- playlist.map { |song| song[:author] }.any?(&:empty?).should be_false
88
- end
89
-
90
- it 'should have a non-empty title in each song' do
91
- playlist.map { |song| song[:title] }.any?(&:empty?).should be_false
92
- end
93
-
94
- it 'should have a non-empty download id in each song' do
95
- playlist.map { |song| song[:id] }.any?(&:empty?).should be_false
96
- end
97
- end
98
- end
99
-
100
- describe 'song_sources' do
101
- describe 'first song in first playlits in category 1' do
102
- let(:sources) do
103
- client.song_sources client.playlist(client.category(1).first)[0]
104
- end
105
-
106
- it 'should have a positive number of sources' do
107
- sources.length.should > 0
108
- end
109
-
110
- it 'should have a url key in each source' do
111
- sources.map { |src| src[:url] }.any?(&:empty?).should be_false
112
- end
113
- end
114
- end
115
-
116
- describe 'song' do
117
- describe 'some popular song' do
118
- let(:entry) { client.playlist(client.category(5).first).first }
119
- let(:bits) { client.song entry }
120
-
121
- it 'should be a string' do
122
- bits.should respond_to(:to_str)
123
- end
124
-
125
- it 'should be more than 1Mb in size' do
126
- bits.length.should >= 2 ** 20
127
- end
128
- end
129
- end
130
- end
@@ -1,7 +0,0 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
-
3
- describe Zhangmen do
4
- pending "fails" do
5
- fail "hey buddy, you should probably rename this file and start specing for real"
6
- end
7
- end