zhangmen 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|