zhangmen 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.project +17 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +37 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +42 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/bin/zhangmen +6 -0
- data/lib/zhangmen/cli.rb +120 -0
- data/lib/zhangmen/client.rb +154 -0
- data/lib/zhangmen.rb +7 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/zhangmen/client_spec.rb +108 -0
- data/spec/zhangmen_spec.rb +7 -0
- data/zhangmen.gemspec +78 -0
- metadata +156 -0
data/.document
ADDED
data/.project
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<projectDescription>
|
3
|
+
<name>zhangmen</name>
|
4
|
+
<comment></comment>
|
5
|
+
<projects>
|
6
|
+
</projects>
|
7
|
+
<buildSpec>
|
8
|
+
<buildCommand>
|
9
|
+
<name>com.aptana.ide.core.unifiedBuilder</name>
|
10
|
+
<arguments>
|
11
|
+
</arguments>
|
12
|
+
</buildCommand>
|
13
|
+
</buildSpec>
|
14
|
+
<natures>
|
15
|
+
<nature>com.aptana.ruby.core.rubynature</nature>
|
16
|
+
</natures>
|
17
|
+
</projectDescription>
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem 'activesupport', '>= 2.3.5'
|
5
|
+
gem 'json'
|
6
|
+
gem 'mechanize'
|
7
|
+
gem 'nokogiri'
|
8
|
+
|
9
|
+
# Add dependencies to develop your gem here.
|
10
|
+
# Include everything needed to run rake, tests, features, etc.
|
11
|
+
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'
|
17
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
diff-lcs (1.1.2)
|
5
|
+
git (1.2.5)
|
6
|
+
jeweler (1.6.2)
|
7
|
+
bundler (~> 1.0)
|
8
|
+
git (>= 1.2.5)
|
9
|
+
rake
|
10
|
+
json (1.5.2)
|
11
|
+
mechanize (1.0.0)
|
12
|
+
nokogiri (>= 1.2.1)
|
13
|
+
nokogiri (1.4.5)
|
14
|
+
rake (0.9.2)
|
15
|
+
rcov (0.9.9)
|
16
|
+
rdoc (3.6.1)
|
17
|
+
rspec (2.6.0)
|
18
|
+
rspec-core (~> 2.6.0)
|
19
|
+
rspec-expectations (~> 2.6.0)
|
20
|
+
rspec-mocks (~> 2.6.0)
|
21
|
+
rspec-core (2.6.4)
|
22
|
+
rspec-expectations (2.6.0)
|
23
|
+
diff-lcs (~> 1.1.2)
|
24
|
+
rspec-mocks (2.6.0)
|
25
|
+
|
26
|
+
PLATFORMS
|
27
|
+
ruby
|
28
|
+
|
29
|
+
DEPENDENCIES
|
30
|
+
bundler (~> 1.0.0)
|
31
|
+
jeweler (~> 1.6.2)
|
32
|
+
json
|
33
|
+
mechanize
|
34
|
+
nokogiri
|
35
|
+
rcov
|
36
|
+
rdoc (>= 3.6.0)
|
37
|
+
rspec (~> 2.6.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Victor Costan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
= zhangmen
|
2
|
+
|
3
|
+
Fetches music from the Baidu streaming music player.
|
4
|
+
|
5
|
+
== Usage
|
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.
|
8
|
+
|
9
|
+
rvm use 1.9.2
|
10
|
+
|
11
|
+
First get the directory containing all the categories and playlists.
|
12
|
+
|
13
|
+
zhangmen list
|
14
|
+
|
15
|
+
Then choose a playlist and download all its songs.
|
16
|
+
|
17
|
+
zhangmen fetch 602
|
18
|
+
|
19
|
+
Or, if you're feeling undecided, grab the entire library.
|
20
|
+
|
21
|
+
zhangmen all
|
22
|
+
|
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.
|
24
|
+
|
25
|
+
== Known Issues
|
26
|
+
|
27
|
+
Some songs might not download. The Flash player skips over those songs too, so it seems to be a server issue.
|
28
|
+
|
29
|
+
== Contributing to zhangmen
|
30
|
+
|
31
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
32
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
33
|
+
* Fork the project
|
34
|
+
* Start a feature/bugfix branch
|
35
|
+
* Commit and push until you are happy with your contribution
|
36
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
37
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
38
|
+
|
39
|
+
== Copyright
|
40
|
+
|
41
|
+
Copyright (c) 2011 Victor Costan. See LICENSE.txt for further details.
|
42
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "zhangmen"
|
18
|
+
gem.homepage = "http://github.com/pwnall/zhangmen"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{CLI and library for downloading music from Baidu}
|
21
|
+
gem.description = %Q{CLI and library for downloading music from Baidu}
|
22
|
+
gem.email = "victor@costan.us"
|
23
|
+
gem.authors = ["Victor Costan"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
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']
|
32
|
+
end
|
33
|
+
|
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
|
40
|
+
|
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
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/zhangmen
ADDED
data/lib/zhangmen/cli.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
# :nodoc: namespace
|
5
|
+
module Zhangmen
|
6
|
+
|
7
|
+
# Command-line interface.
|
8
|
+
class Cli
|
9
|
+
def scan_categories
|
10
|
+
category_id = 1
|
11
|
+
empty_categories = 0
|
12
|
+
|
13
|
+
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
|
29
|
+
end
|
30
|
+
end
|
31
|
+
category_id
|
32
|
+
end
|
33
|
+
|
34
|
+
def categories
|
35
|
+
scan_categories do |id, playlists|
|
36
|
+
puts "Category #{id}"
|
37
|
+
playlists.each do |list|
|
38
|
+
puts " #{list[:name]} - #{list[:id]} - #{list[:song_count]} songs"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def playlist(list_id)
|
44
|
+
songs = @client.playlist :id => list_id
|
45
|
+
# save_client_cache
|
46
|
+
|
47
|
+
songs.each do |song|
|
48
|
+
print "#{song[:author]} - #{song[:title]} ... "
|
49
|
+
bits = @client.song song
|
50
|
+
if bits
|
51
|
+
FileUtils.mkdir_p song[:author]
|
52
|
+
filename = File.join song[:author],
|
53
|
+
"#{song[:author]} - #{song[:title]}.mp3"
|
54
|
+
File.open filename, 'w' do |f|
|
55
|
+
f.write bits
|
56
|
+
end
|
57
|
+
print "ok\n"
|
58
|
+
else
|
59
|
+
print "FAIL\n"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def all
|
65
|
+
scan_categories do |id, playlists|
|
66
|
+
puts "Category #{id}"
|
67
|
+
playlists.each do |list|
|
68
|
+
puts " #{list[:name]} - #{list[:id]} - #{list[:song_count]} songs"
|
69
|
+
playlist list[:id]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Points to the client's metadata cache file.
|
75
|
+
def client_cache_path
|
76
|
+
File.expand_path '~/.zhangmen_cache'
|
77
|
+
end
|
78
|
+
|
79
|
+
# Last snapshot of the client's metadata cache.
|
80
|
+
def client_cache
|
81
|
+
if File.exist? client_cache_path
|
82
|
+
YAML.load(File.read(client_cache_path)) || {} rescue {}
|
83
|
+
else
|
84
|
+
{}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Saves a new snapshot of the client's metadata cache.
|
89
|
+
def client_cache=(new_contents)
|
90
|
+
begin
|
91
|
+
File.open(client_cache_path, 'w') do |f|
|
92
|
+
YAML.dump new_contents, f
|
93
|
+
end
|
94
|
+
rescue ArgumentError => e
|
95
|
+
# Encoding error; bummer, can't cache
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Saves a new snapshot of the client's metadata cache.
|
100
|
+
def save_client_cache
|
101
|
+
self.client_cache = @client.cache
|
102
|
+
end
|
103
|
+
|
104
|
+
# Runs a command.
|
105
|
+
def run(args)
|
106
|
+
@client = Zhangmen::Client.new
|
107
|
+
@client.cache = client_cache
|
108
|
+
|
109
|
+
case args[0]
|
110
|
+
when 'list'
|
111
|
+
categories
|
112
|
+
when 'fetch'
|
113
|
+
args[1..-1].each { |arg| playlist arg }
|
114
|
+
when 'all'
|
115
|
+
all
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end # class Zhangmen::Cli
|
119
|
+
|
120
|
+
end # namespace Zhangmen
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
require 'mechanize'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
# :nodoc: namespace
|
7
|
+
module Zhangmen
|
8
|
+
|
9
|
+
# Wraps a client session for accessing Baidu's streaming music service.
|
10
|
+
class Client
|
11
|
+
def initialize
|
12
|
+
@mech = mechanizer
|
13
|
+
@cache = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Cache of HTTP requests / responses.
|
17
|
+
#
|
18
|
+
# The cache covers all the song metadata, but not actual song data.
|
19
|
+
attr_accessor :cache
|
20
|
+
|
21
|
+
# Fetches a collection of playlists by the category ID.
|
22
|
+
#
|
23
|
+
# Args:
|
24
|
+
# category_id:: right now, categories seem to be numbers, so enumerating
|
25
|
+
# from 1 onwards should be good
|
26
|
+
#
|
27
|
+
# Returns an array of playlists.
|
28
|
+
def category(category_id)
|
29
|
+
result = op 3, :list_cat => category_id
|
30
|
+
result.css('data').map do |playlist_node|
|
31
|
+
{
|
32
|
+
:id => playlist_node.css('id').inner_text,
|
33
|
+
:name => playlist_node.css('name').inner_text.encode('UTF-8'),
|
34
|
+
:song_count => playlist_node.css('tcount').inner_text.to_i
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Fetches a playlist.
|
40
|
+
#
|
41
|
+
# Args:
|
42
|
+
# list:: a playlist obtained by calling category
|
43
|
+
#
|
44
|
+
# Returns an array of songs.
|
45
|
+
def playlist(list)
|
46
|
+
result = op 22, :listid => list[:id]
|
47
|
+
native_encoding = result.document.encoding
|
48
|
+
|
49
|
+
count = result.css('count').inner_text.to_i
|
50
|
+
result.css('data').map do |song_node|
|
51
|
+
raw_name = song_node.css('name').inner_text
|
52
|
+
if match = /^(.*)\$\$(.*)\$\$\$\$/.match(raw_name)
|
53
|
+
title = match[1].encode('UTF-8')
|
54
|
+
author = match[2].encode('UTF-8')
|
55
|
+
else
|
56
|
+
author = title = raw_name.encode('UTF-8')
|
57
|
+
end
|
58
|
+
|
59
|
+
{
|
60
|
+
:raw_name => raw_name.encode(native_encoding),
|
61
|
+
:title => title, :author => author,
|
62
|
+
:id => song_node.css('id').inner_text
|
63
|
+
}
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Fetches the MP3 contents of a song.
|
68
|
+
#
|
69
|
+
# Args:
|
70
|
+
# song:: a song obtained by calling playlist
|
71
|
+
#
|
72
|
+
# Returns the MP3 bits.
|
73
|
+
def song(entry)
|
74
|
+
song_sources(entry).each do |src|
|
75
|
+
3.times do
|
76
|
+
begin
|
77
|
+
result = @mech.get src[:url]
|
78
|
+
next unless result.kind_of?(Mechanize::File)
|
79
|
+
bits = result.body
|
80
|
+
if bits[-256, 3] == 'TAG' || bits[0, 3] == 'ID3'
|
81
|
+
return bits
|
82
|
+
else
|
83
|
+
break
|
84
|
+
end
|
85
|
+
rescue EOFError
|
86
|
+
# Server hung up on us. Try again in case the error is temporary.
|
87
|
+
rescue Timeout::Error
|
88
|
+
# Server hung up on us. Try again in case the error is temporary.
|
89
|
+
rescue Mechanize::ResponseCodeError
|
90
|
+
# 500-ish response. Try again in case the error is temporary.
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
# Fetches the MP3 download locations for a song.
|
98
|
+
#
|
99
|
+
# Args:
|
100
|
+
# song:: a song obtained by calling playlist
|
101
|
+
#
|
102
|
+
# Returns
|
103
|
+
def song_sources(entry)
|
104
|
+
result = op 12, :count => 1, :mtype => 1, :title => entry[:raw_name],
|
105
|
+
:url => '', :listenreelect => 0
|
106
|
+
result.css('url').map do |url_node|
|
107
|
+
filename = url_node.css('decode').inner_text
|
108
|
+
encoded_url = url_node.css('encode').inner_text
|
109
|
+
url = File.join File.dirname(encoded_url), filename
|
110
|
+
{
|
111
|
+
:url => url,
|
112
|
+
:type => url_node.css('type').inner_text.to_i,
|
113
|
+
:lyrics_id => url_node.css('lrid').inner_text.to_i,
|
114
|
+
:flag => url_node.css('flag').inner_text
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Performs a numbered operation.
|
120
|
+
#
|
121
|
+
# Args:
|
122
|
+
# opcode:: operation number (e.g. 22 for playlist fetch)
|
123
|
+
# args:: operation arguments (e.g. :listid => number for playlist ID)
|
124
|
+
#
|
125
|
+
# Returns a Nokogiri root node.
|
126
|
+
def op(opcode, args)
|
127
|
+
url = op_url(opcode, args)
|
128
|
+
cache_key = url.to_s
|
129
|
+
@cache[cache_key] ||= @mech.get(url).body
|
130
|
+
Nokogiri.XML(@cache[cache_key]).root
|
131
|
+
end
|
132
|
+
|
133
|
+
# The fetch URL for an XML opcode.
|
134
|
+
def op_url(opcode, args)
|
135
|
+
query = { :op => opcode }.merge(args).
|
136
|
+
map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.
|
137
|
+
join('&') + '&.r=' + ('%.16f' % Kernel.rand)
|
138
|
+
URI.parse "http://#{hostname}/x?#{query}"
|
139
|
+
end
|
140
|
+
|
141
|
+
# The service hostname.
|
142
|
+
def hostname
|
143
|
+
'box.zhangmen.baidu.com'
|
144
|
+
end
|
145
|
+
|
146
|
+
# Mechanize instance customized to maximize fetch success.
|
147
|
+
def mechanizer
|
148
|
+
mech = Mechanize.new
|
149
|
+
mech.user_agent_alias = 'Linux Firefox'
|
150
|
+
mech
|
151
|
+
end
|
152
|
+
end # class Zhangmen::Client
|
153
|
+
|
154
|
+
end # namespace Zhangmen
|
data/lib/zhangmen.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
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
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# :encoding: UTF-8
|
2
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
3
|
+
|
4
|
+
describe Zhangmen::Client do
|
5
|
+
let(:client) { Zhangmen::Client.new }
|
6
|
+
|
7
|
+
describe 'op_url' do
|
8
|
+
it 'encodes everything correctly' do
|
9
|
+
Kernel.should_receive(:rand).and_return(0.42)
|
10
|
+
client.op_url(22, :listid => 600).to_s.should ==
|
11
|
+
'http://box.zhangmen.baidu.com/x?op=22&listid=600&.r=0.42' + '0' * 14
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'op' do
|
16
|
+
describe 'with good known arguments' do
|
17
|
+
let(:result) { client.op 22, :listid => 600 }
|
18
|
+
|
19
|
+
it 'returns a Nokogiri root node' do
|
20
|
+
result.should be_kind_of(Nokogiri::XML::Element)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns a <result> root node' do
|
24
|
+
result.name.should == 'result'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'category' do
|
30
|
+
describe '1' do
|
31
|
+
let(:category) { client.category 1 }
|
32
|
+
|
33
|
+
it 'should have a non-trivial number of playlists' do
|
34
|
+
category.length.should > 10
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should have a non-empty name for each playlist' do
|
38
|
+
category.map { |pl| pl[:name] }.any?(&:empty?).should be_false
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should have a non-empty download id for each playlist' do
|
42
|
+
category.map { |pl| pl[:id] }.any?(&:empty?).should be_false
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should have a non-empty song count for each playlist' do
|
46
|
+
category.map { |pl| pl[:song_count] }.any? { |count| count <= 0 }.
|
47
|
+
should be_false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'playlist' do
|
53
|
+
describe 'first one in category 1' do
|
54
|
+
let(:playlist) { client.playlist client.category(1).first }
|
55
|
+
|
56
|
+
it 'should have a non-trivial number of songs' do
|
57
|
+
playlist.length.should > 10
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should have a non-empty raw name in each song' do
|
61
|
+
playlist.map { |song| song[:raw_name] }.any?(&:empty?).should be_false
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should have a non-empty author in each song' do
|
65
|
+
playlist.map { |song| song[:author] }.any?(&:empty?).should be_false
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should have a non-empty title in each song' do
|
69
|
+
playlist.map { |song| song[:title] }.any?(&:empty?).should be_false
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should have a non-empty download id in each song' do
|
73
|
+
playlist.map { |song| song[:id] }.any?(&:empty?).should be_false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe 'song_sources' do
|
79
|
+
describe 'first song in first playlits in category 1' do
|
80
|
+
let(:sources) do
|
81
|
+
client.song_sources client.playlist(client.category(1).first)[0]
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should have a positive number of sources' do
|
85
|
+
sources.length.should > 0
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should have a url key in each source' do
|
89
|
+
sources.map { |src| src[:url] }.any?(&:empty?).should be_false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe 'song' do
|
95
|
+
describe 'some popular song' do
|
96
|
+
let(:entry) { client.playlist(client.category(5).first).first }
|
97
|
+
let(:bits) { client.song entry }
|
98
|
+
|
99
|
+
it 'should be a string' do
|
100
|
+
bits.should respond_to(:to_str)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should be more than 1Mb in size' do
|
104
|
+
bits.length.should >= 2 ** 20
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/zhangmen.gemspec
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{zhangmen}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = [%q{Victor Costan}]
|
12
|
+
s.date = %q{2011-07-09}
|
13
|
+
s.description = %q{CLI and library for downloading music from Baidu}
|
14
|
+
s.email = %q{victor@costan.us}
|
15
|
+
s.executables = [%q{zhangmen}]
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"LICENSE.txt",
|
18
|
+
"README.rdoc"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".document",
|
22
|
+
".project",
|
23
|
+
".rspec",
|
24
|
+
"Gemfile",
|
25
|
+
"Gemfile.lock",
|
26
|
+
"LICENSE.txt",
|
27
|
+
"README.rdoc",
|
28
|
+
"Rakefile",
|
29
|
+
"VERSION",
|
30
|
+
"bin/zhangmen",
|
31
|
+
"lib/zhangmen.rb",
|
32
|
+
"lib/zhangmen/cli.rb",
|
33
|
+
"lib/zhangmen/client.rb",
|
34
|
+
"spec/spec_helper.rb",
|
35
|
+
"spec/zhangmen/client_spec.rb",
|
36
|
+
"spec/zhangmen_spec.rb",
|
37
|
+
"zhangmen.gemspec"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/pwnall/zhangmen}
|
40
|
+
s.licenses = [%q{MIT}]
|
41
|
+
s.require_paths = [%q{lib}]
|
42
|
+
s.rubygems_version = %q{1.8.5}
|
43
|
+
s.summary = %q{CLI and library for downloading music from Baidu}
|
44
|
+
|
45
|
+
if s.respond_to? :specification_version then
|
46
|
+
s.specification_version = 3
|
47
|
+
|
48
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
49
|
+
s.add_runtime_dependency(%q<json>, [">= 0"])
|
50
|
+
s.add_runtime_dependency(%q<mechanize>, [">= 0"])
|
51
|
+
s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
|
52
|
+
s.add_development_dependency(%q<rdoc>, [">= 3.6.0"])
|
53
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.6.0"])
|
54
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
55
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
|
56
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<json>, [">= 0"])
|
59
|
+
s.add_dependency(%q<mechanize>, [">= 0"])
|
60
|
+
s.add_dependency(%q<nokogiri>, [">= 0"])
|
61
|
+
s.add_dependency(%q<rdoc>, [">= 3.6.0"])
|
62
|
+
s.add_dependency(%q<rspec>, ["~> 2.6.0"])
|
63
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
64
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
|
65
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
66
|
+
end
|
67
|
+
else
|
68
|
+
s.add_dependency(%q<json>, [">= 0"])
|
69
|
+
s.add_dependency(%q<mechanize>, [">= 0"])
|
70
|
+
s.add_dependency(%q<nokogiri>, [">= 0"])
|
71
|
+
s.add_dependency(%q<rdoc>, [">= 3.6.0"])
|
72
|
+
s.add_dependency(%q<rspec>, ["~> 2.6.0"])
|
73
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
74
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
|
75
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
metadata
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zhangmen
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Victor Costan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-07-09 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: &26919700 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *26919700
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mechanize
|
27
|
+
requirement: &26919100 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *26919100
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: nokogiri
|
38
|
+
requirement: &26918520 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *26918520
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rdoc
|
49
|
+
requirement: &26917940 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.6.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *26917940
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rspec
|
60
|
+
requirement: &26917340 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 2.6.0
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *26917340
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: &26916760 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 1.0.0
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *26916760
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: jeweler
|
82
|
+
requirement: &26916200 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 1.6.2
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *26916200
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: rcov
|
93
|
+
requirement: &26915680 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *26915680
|
102
|
+
description: CLI and library for downloading music from Baidu
|
103
|
+
email: victor@costan.us
|
104
|
+
executables:
|
105
|
+
- zhangmen
|
106
|
+
extensions: []
|
107
|
+
extra_rdoc_files:
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.rdoc
|
110
|
+
files:
|
111
|
+
- .document
|
112
|
+
- .project
|
113
|
+
- .rspec
|
114
|
+
- Gemfile
|
115
|
+
- Gemfile.lock
|
116
|
+
- LICENSE.txt
|
117
|
+
- README.rdoc
|
118
|
+
- Rakefile
|
119
|
+
- VERSION
|
120
|
+
- bin/zhangmen
|
121
|
+
- lib/zhangmen.rb
|
122
|
+
- lib/zhangmen/cli.rb
|
123
|
+
- lib/zhangmen/client.rb
|
124
|
+
- spec/spec_helper.rb
|
125
|
+
- spec/zhangmen/client_spec.rb
|
126
|
+
- spec/zhangmen_spec.rb
|
127
|
+
- zhangmen.gemspec
|
128
|
+
homepage: http://github.com/pwnall/zhangmen
|
129
|
+
licenses:
|
130
|
+
- MIT
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options: []
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
none: false
|
137
|
+
requirements:
|
138
|
+
- - ! '>='
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
segments:
|
142
|
+
- 0
|
143
|
+
hash: 1290430065827447959
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
requirements: []
|
151
|
+
rubyforge_project:
|
152
|
+
rubygems_version: 1.8.5
|
153
|
+
signing_key:
|
154
|
+
specification_version: 3
|
155
|
+
summary: CLI and library for downloading music from Baidu
|
156
|
+
test_files: []
|