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 +12 -8
- data/Gemfile.lock +45 -28
- data/README.rdoc +6 -9
- data/Rakefile +8 -19
- data/VERSION +1 -1
- data/lib/zhangmen.rb +1 -0
- data/lib/zhangmen/cli.rb +25 -23
- data/lib/zhangmen/client.rb +130 -34
- data/lib/zhangmen/proxy.rb +32 -0
- data/test/helper.rb +21 -0
- data/test/zhangmen/client_test.rb +163 -0
- data/test/zhangmen/proxy_test.rb +12 -0
- data/zhangmen.gemspec +48 -36
- metadata +144 -41
- data/spec/spec_helper.rb +0 -12
- data/spec/zhangmen/client_spec.rb +0 -130
- data/spec/zhangmen_spec.rb +0 -7
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 '
|
6
|
-
gem '
|
7
|
-
gem '
|
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 '
|
13
|
-
gem '
|
14
|
-
gem '
|
15
|
-
gem '
|
16
|
-
gem '
|
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
|
data/Gemfile.lock
CHANGED
@@ -1,44 +1,61 @@
|
|
1
1
|
GEM
|
2
2
|
remote: http://rubygems.org/
|
3
3
|
specs:
|
4
|
-
|
4
|
+
curb (0.8.0)
|
5
|
+
domain_name (0.5.3)
|
6
|
+
unf (~> 0.0.3)
|
5
7
|
git (1.2.5)
|
6
|
-
|
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.
|
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 (~>
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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 (
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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)
|
data/README.rdoc
CHANGED
@@ -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.
|
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 '
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
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 '
|
42
|
-
Rake::
|
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
|
+
0.2.0
|
data/lib/zhangmen.rb
CHANGED
data/lib/zhangmen/cli.rb
CHANGED
@@ -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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
data/lib/zhangmen/client.rb
CHANGED
@@ -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
|
-
#
|
14
|
-
#
|
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.
|
50
|
+
result.search('data').map do |playlist_node|
|
35
51
|
{
|
36
|
-
:id => playlist_node.
|
37
|
-
:name => playlist_node.
|
38
|
-
:song_count => playlist_node.
|
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.
|
54
|
-
result.
|
55
|
-
raw_name = song_node.
|
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(
|
84
|
+
:raw_name => raw_name.encode('UTF-8'),
|
85
|
+
:raw_encoding => native_encoding,
|
65
86
|
:title => title, :author => author,
|
66
|
-
:id => song_node.
|
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
|
-
|
82
|
-
|
83
|
-
|
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
|
90
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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.
|
117
|
-
:lyrics_id => url_node.
|
118
|
-
:flag => url_node.
|
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
|
-
|
132
|
-
cache_key =
|
133
|
-
@cache[cache_key]
|
134
|
-
|
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.
|
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
|
data/test/helper.rb
ADDED
@@ -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
|
data/zhangmen.gemspec
CHANGED
@@ -4,15 +4,14 @@
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
|
-
s.name =
|
8
|
-
s.version = "0.
|
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 =
|
13
|
-
s.
|
14
|
-
s.
|
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
|
-
"
|
36
|
-
"
|
37
|
-
"
|
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 =
|
40
|
+
s.homepage = "http://github.com/pwnall/zhangmen"
|
41
41
|
s.licenses = ["MIT"]
|
42
42
|
s.require_paths = ["lib"]
|
43
|
-
s.rubygems_version =
|
44
|
-
s.summary =
|
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<
|
51
|
-
s.add_runtime_dependency(%q<
|
52
|
-
s.add_runtime_dependency(%q<
|
53
|
-
s.
|
54
|
-
s.
|
55
|
-
s.add_development_dependency(%q<bundler>, ["
|
56
|
-
s.add_development_dependency(%q<jeweler>, ["
|
57
|
-
s.add_development_dependency(%q<
|
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<
|
60
|
-
s.add_dependency(%q<
|
61
|
-
s.add_dependency(%q<
|
62
|
-
s.add_dependency(%q<
|
63
|
-
s.add_dependency(%q<
|
64
|
-
s.add_dependency(%q<bundler>, ["
|
65
|
-
s.add_dependency(%q<jeweler>, ["
|
66
|
-
s.add_dependency(%q<
|
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<
|
70
|
-
s.add_dependency(%q<
|
71
|
-
s.add_dependency(%q<
|
72
|
-
s.add_dependency(%q<
|
73
|
-
s.add_dependency(%q<
|
74
|
-
s.add_dependency(%q<bundler>, ["
|
75
|
-
s.add_dependency(%q<jeweler>, ["
|
76
|
-
s.add_dependency(%q<
|
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.
|
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:
|
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:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
18
49
|
none: false
|
19
50
|
requirements:
|
20
51
|
- - ! '>='
|
21
52
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
53
|
+
version: 1.0.0
|
23
54
|
type: :runtime
|
24
55
|
prerelease: false
|
25
|
-
version_requirements:
|
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:
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
29
65
|
none: false
|
30
66
|
requirements:
|
31
67
|
- - ! '>='
|
32
68
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
69
|
+
version: 2.3.0
|
34
70
|
type: :runtime
|
35
71
|
prerelease: false
|
36
|
-
version_requirements:
|
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:
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
40
81
|
none: false
|
41
82
|
requirements:
|
42
83
|
- - ! '>='
|
43
84
|
- !ruby/object:Gem::Version
|
44
|
-
version:
|
85
|
+
version: 1.5.2
|
45
86
|
type: :runtime
|
46
87
|
prerelease: false
|
47
|
-
version_requirements:
|
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:
|
50
|
-
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:
|
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:
|
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:
|
61
|
-
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.
|
133
|
+
version: 2.11.2
|
67
134
|
type: :development
|
68
135
|
prerelease: false
|
69
|
-
version_requirements:
|
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:
|
72
|
-
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:
|
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:
|
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:
|
83
|
-
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:
|
181
|
+
version: 0.6.1
|
89
182
|
type: :development
|
90
183
|
prerelease: false
|
91
|
-
version_requirements:
|
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:
|
94
|
-
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:
|
197
|
+
version: 0.7.5
|
100
198
|
type: :development
|
101
199
|
prerelease: false
|
102
|
-
version_requirements:
|
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
|
-
-
|
126
|
-
-
|
127
|
-
-
|
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: -
|
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.
|
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
|
data/spec/spec_helper.rb
DELETED
@@ -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
|