viddl-rb 0.76 → 0.77

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.
Files changed (3) hide show
  1. data/plugins/youtube.rb +254 -237
  2. metadata +5 -7
  3. data/Gemfile.lock +0 -47
data/plugins/youtube.rb CHANGED
@@ -1,237 +1,254 @@
1
- require 'open-uri'
2
-
3
- class Youtube < PluginBase
4
-
5
- VIDEO_INFO_URL = "http://www.youtube.com/get_video_info?video_id="
6
-
7
- VIDEO_FORMATS = {
8
- "38" => {:extension => "mp4", :name => "MP4 Highest Quality 4096x3027 (H.264, AAC)"},
9
- "37" => {:extension => "mp4", :name => "MP4 Highest Quality 1920x1080 (H.264, AAC)"},
10
- "22" => {:extension => "mp4", :name => "MP4 1280x720 (H.264, AAC)"},
11
- "45" => {:extension => "webm", :name => "WebM 1280x720 (VP8, Vorbis)"},
12
- "44" => {:extension => "webm", :name => "WebM 854x480 (VP8, Vorbis)"},
13
- "18" => {:extension => "mp4", :name => "MP4 640x360 (H.264, AAC)"},
14
- "35" => {:extension => "flv", :name => "FLV 854x480 (H.264, AAC)"},
15
- "34" => {:extension => "flv", :name => "FLV 640x360 (H.264, AAC)"},
16
- "5" => {:extension => "flv", :name => "FLV 400x240 (Soerenson H.263)"},
17
- "17" => {:extension => "3gp", :name => "3gp"}
18
- }
19
-
20
- DEFAULT_FORMAT_ORDER = %w[38 37 22 45 44 18 35 34 5 7]
21
-
22
- #this will be called by the main app to check whether this plugin is responsible for the url passed
23
- def self.matches_provider?(url)
24
- url.include?("youtube.com") || url.include?("youtu.be")
25
- end
26
-
27
- #get all videos and return their urls in an array
28
- def self.get_video_urls(feed_url)
29
- notify "Retrieving videos..."
30
- urls_titles = Hash.new
31
- result_feed = Nokogiri::XML(open(feed_url))
32
- urls_titles.merge!(grab_urls_and_titles(result_feed))
33
-
34
- #as long as the feed has a next link we follow it and add the resulting video urls
35
- loop do
36
- next_link = result_feed.search("//feed/link[@rel='next']").first
37
- break if next_link.nil?
38
- result_feed = Nokogiri::HTML(open(next_link["href"]))
39
- urls_titles.merge!(grab_urls_and_titles(result_feed))
40
- end
41
-
42
- self.filter_urls(urls_titles)
43
- end
44
-
45
- #returns only the urls that match the --filter argument regex (if present)
46
- def self.filter_urls(url_hash)
47
- if @filter
48
- notify "Using filter: #{@filter}"
49
- filtered = url_hash.select { |url, title| title =~ @filter }
50
- filtered.keys
51
- else
52
- url_hash.keys
53
- end
54
- end
55
-
56
- #extract all video urls and their titles from a feed and return in a hash
57
- def self.grab_urls_and_titles(feed)
58
- feed.remove_namespaces! #so that we can get to the titles easily
59
- urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
60
- titles = feed.search("//entry/group/title").map { |title| title.text }
61
- Hash[urls.zip(titles)] #hash like this: url => title
62
- end
63
-
64
- def self.parse_playlist(url)
65
- #http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
66
- #http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
67
- #http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
68
-
69
- playlist_ID = url[/(?:list=PL|p=)(\w{16})&?/,1]
70
- notify "Playlist ID: #{playlist_ID}"
71
- feed_url = "http://gdata.youtube.com/feeds/api/playlists/#{playlist_ID}?&max-results=50&v=2"
72
- url_array = self.get_video_urls(feed_url)
73
- notify "#{url_array.size} links found!"
74
- url_array
75
- end
76
-
77
- def self.parse_user(username)
78
- notify "User: #{username}"
79
- feed_url = "http://gdata.youtube.com/feeds/api/users/#{username}/uploads?&max-results=50&v=2"
80
- url_array = get_video_urls(feed_url)
81
- notify "#{url_array.size} links found!"
82
- url_array
83
- end
84
-
85
- def self.get_urls_and_filenames(url, options = {})
86
- @filter = options[:playlist_filter] #used to filter a playlist in self.filter_urls
87
- @quality = options[:quality]
88
-
89
- return_values = []
90
-
91
- if url.include?("view_play_list") || url.include?("playlist?list=") #if playlist
92
- notify "playlist found! analyzing..."
93
- files = parse_playlist(url)
94
- notify "Starting playlist download"
95
- files.each do |file|
96
- notify "Downloading next movie on the playlist (#{file})"
97
- return_values << grab_single_url_filename(file)
98
- end
99
- elsif match = url.match(/\/user\/([\w\d]+)$/) #if user url, e.g. youtube.com/user/woot
100
- username = match[1]
101
- video_urls = parse_user(username)
102
- notify "Starting user videos download"
103
- video_urls.each do |url|
104
- notify "Downloading next user video (#{url})"
105
- return_values << grab_single_url_filename(url)
106
- end
107
- else #if single video
108
- return_values << grab_single_url_filename(url)
109
- end
110
-
111
- return_values.reject! { |value| value == :no_embed } #remove results that can not be downloaded
112
-
113
- if return_values.empty?
114
- raise CouldNotDownloadVideoError, "No videos could be downloaded - embedding disabled."
115
- else
116
- return_values
117
- end
118
- end
119
-
120
- def self.grab_single_url_filename(url)
121
- #the youtube video ID looks like this: [...]v=abc5a5_afe5agae6g&[...], we only want the ID (the \w in the brackets)
122
- #addition: might also look like this /v/abc5-a5afe5agae6g
123
- # alternative: video_id = url[/v[\/=]([\w-]*)&?/, 1]
124
- # First get the redirect
125
-
126
- url = open(url).base_uri.to_s if url.include?("youtu.be")
127
- video_id = url[/(v|embed)[=\/]([^\/\?\&]*)/,2]
128
- video_id ? notify("ID FOUND: #{video_id}") : download_error("No video id found.")
129
-
130
- #let's get some infos about the video. data is urlencoded
131
- video_info = open(VIDEO_INFO_URL + video_id).read
132
-
133
- #converting the huge infostring into a hash. simply by splitting it at the & and then splitting it into key and value arround the =
134
- #[...]blabla=blubb&narf=poit&marc=awesome[...]
135
- video_info_hash = Hash[*video_info.split("&").collect { |v|
136
- key, encoded_value = v.split("=")
137
- if encoded_value.to_s.empty?
138
- value = ""
139
- else
140
- #decode until everything is "normal"
141
- while (encoded_value != CGI::unescape(encoded_value)) do
142
- #"decoding"
143
- encoded_value = CGI::unescape(encoded_value)
144
- end
145
- value = encoded_value
146
- end
147
-
148
- if key =~ /_map/
149
- orig_value = value
150
- value = value.split(",")
151
- if key == "url_encoded_fmt_stream_map"
152
- url_array = orig_value.split("url=").map{|url_string| url_string.chomp(",")}
153
- result_hash = {}
154
- url_array.each do |url|
155
- next if url.to_s.empty? || url.to_s.match(/^itag/)
156
- format_id = url[/\&itag=(\d+)/, 1]
157
- result_hash[format_id] = url
158
- end
159
- value = result_hash
160
- elsif key == "fmt_map"
161
- value = Hash[*value.collect { |v|
162
- k2, *v2 = v.split("/")
163
- [k2, v2]
164
- }.flatten(1)]
165
- elsif key == "fmt_url_map" || key == "fmt_stream_map"
166
- Hash[*value.collect { |v| v.split("|")}.flatten]
167
- end
168
- end
169
- [key, value]
170
- }.flatten]
171
-
172
- return :no_embed if video_info_hash["status"] == "fail"
173
-
174
- title = video_info_hash["title"]
175
- length_s = video_info_hash["length_seconds"]
176
- token = video_info_hash["token"]
177
-
178
- notify "Title: #{title}"
179
- notify "Length: #{length_s} s"
180
- notify "t-parameter: #{token}"
181
-
182
- #for the formats, see: http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
183
- fmt_list = video_info_hash["fmt_list"].split(",")
184
-
185
- selected_format = pick_video_format(fmt_list)
186
- puts "(downloading format #{selected_format} -> #{VIDEO_FORMATS[selected_format][:name]})"
187
-
188
- download_url = video_info_hash["url_encoded_fmt_stream_map"][selected_format]
189
-
190
- #if download url ends with a ';' followed by a codec string remove that part because it stops URI.parse from working
191
-
192
- if codec_part = download_url[/;\s*codec.+/m] #if we have the ; codec substring
193
- sig = codec_part[/&sig=(.+?)&/, 1] #extract the signature
194
-
195
- download_url.sub!(codec_part, "") #remove the ; codec substring from the download url
196
- download_url.concat("&signature=#{sig}") #concatenate the correct signature attribute
197
- else
198
- download_url.sub!("&sig=", "&signature=") #else we just have to change sig to signature
199
- end
200
-
201
- file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
202
- puts "downloading to " + file_name + "\n\n"
203
- {:url => download_url, :name => file_name}
204
- end
205
-
206
- #returns the format of the video the user picked or the first default format if it does not exist
207
- def self.pick_video_format(fmt_list)
208
- available_formats = fmt_list.map { |format| format.split("/").first }
209
- notify "formats available: #{available_formats.inspect}"
210
-
211
- if @quality #if the user specified a format
212
- ext = @quality[:extension]
213
- res = @quality[:resolution]
214
-
215
- #gets a nested array with all the formats of the same res as the user wanted
216
- requested = VIDEO_FORMATS.select { |id, format| format[:name].include?(res) }.to_a
217
-
218
- if requested.empty?
219
- notify "Requested format \"#{res}:#{ext}\" not found. Downloading default format."
220
- get_default_format(available_formats)
221
- else
222
- pick = requested.find { |format| format[1][:extension] == ext } #get requsted extension if possible
223
- pick ? pick.first : get_default_format(requested.map { |req| req.first }) #else return the default format
224
- end
225
- else
226
- get_default_format(available_formats)
227
- end
228
- end
229
-
230
- def self.get_default_format(available)
231
- DEFAULT_FORMAT_ORDER.find { |default| available.include?(default) }
232
- end
233
-
234
- def self.notify(message)
235
- puts "[YOUTUBE] #{message}"
236
- end
237
- end
1
+
2
+ class Youtube < PluginBase
3
+
4
+ # see http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
5
+ # TODO: we don't have all the formats from the wiki article here
6
+ VIDEO_FORMATS = {
7
+ "38" => {:extension => "mp4", :name => "MP4 Highest Quality 4096x3027 (H.264, AAC)"},
8
+ "37" => {:extension => "mp4", :name => "MP4 Highest Quality 1920x1080 (H.264, AAC)"},
9
+ "22" => {:extension => "mp4", :name => "MP4 1280x720 (H.264, AAC)"},
10
+ "46" => {:extension => "webm", :name => "WebM 1920x1080 (VP8, Vorbis)"},
11
+ "45" => {:extension => "webm", :name => "WebM 1280x720 (VP8, Vorbis)"},
12
+ "44" => {:extension => "webm", :name => "WebM 854x480 (VP8, Vorbis)"},
13
+ "43" => {:extension => "webm", :name => "WebM 480×360 (VP8, Vorbis)"},
14
+ "18" => {:extension => "mp4", :name => "MP4 640x360 (H.264, AAC)"},
15
+ "35" => {:extension => "flv", :name => "FLV 854x480 (H.264, AAC)"},
16
+ "34" => {:extension => "flv", :name => "FLV 640x360 (H.264, AAC)"},
17
+ "5" => {:extension => "flv", :name => "FLV 400x240 (Soerenson H.263)"},
18
+ "17" => {:extension => "3gp", :name => "3gp"}
19
+ }
20
+
21
+ DEFAULT_FORMAT_ORDER = %w[38 37 22 45 44 18 35 34 5 7]
22
+ VIDEO_INFO_URL = "http://www.youtube.com/get_video_info?video_id="
23
+ VIDEO_INFO_PARMS = "&ps=default&eurl=&gl=US&hl=en"
24
+
25
+ # this will be called by the main app to check whether this plugin is responsible for the url passed
26
+ def self.matches_provider?(url)
27
+ url.include?("youtube.com") || url.include?("youtu.be")
28
+ end
29
+
30
+ def self.get_urls_and_filenames(url, options = {})
31
+ @quality = options[:quality]
32
+ filter = options[:playlist_filter]
33
+ parser = PlaylistParser.new
34
+ return_vals = []
35
+
36
+ if playlist_urls = parser.get_playlist_urls(url, filter)
37
+ playlist_urls.each { |url| return_vals << grab_single_url_filename(url) }
38
+ else
39
+ return_vals << grab_single_url_filename(url)
40
+ end
41
+
42
+ clean_return_values(return_vals)
43
+ end
44
+
45
+ def self.clean_return_values(return_values)
46
+ cleaned = return_values.reject { |val| val == :no_embed }
47
+
48
+ if cleaned.empty?
49
+ download_error("No videos could be downloaded.")
50
+ else
51
+ cleaned
52
+ end
53
+ end
54
+
55
+ def self.grab_single_url_filename(url)
56
+ video_info = get_video_info(url)
57
+ video_params = extract_video_parameters(video_info)
58
+
59
+ if video_params[:embeddable]
60
+ urls_formats = extract_urls_formats(video_info)
61
+ selected_format = choose_format(urls_formats)
62
+ title = video_params[:title]
63
+ file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
64
+
65
+ {:url => urls_formats[selected_format], :name => file_name}
66
+ else
67
+ notify "Video is not embeddable and can't be downloaded."
68
+ :no_embed
69
+ end
70
+ end
71
+
72
+ def self.get_video_info(url)
73
+ id = extract_video_id(url)
74
+ request_url = VIDEO_INFO_URL + id + VIDEO_INFO_PARMS
75
+ open(request_url).read
76
+ end
77
+
78
+ def self.extract_video_id(url)
79
+ # the youtube video ID looks like this: [...]v=abc5a5_afe5agae6g&[...], we only want the ID (the \w in the brackets)
80
+ # addition: might also look like this /v/abc5-a5afe5agae6g
81
+ # alternative: video_id = url[/v[\/=]([\w-]*)&?/, 1]
82
+ url = open(url).base_uri.to_s if url.include?("youtu.be")
83
+ video_id = url[/(v|embed)[=\/]([^\/\?\&]*)/, 2]
84
+
85
+ if video_id
86
+ notify("ID FOUND: #{video_id}")
87
+ video_id
88
+ else
89
+ download_error("No video id found.")
90
+ end
91
+ end
92
+
93
+ def self.extract_video_parameters(video_info)
94
+ decoded = url_decode(video_info)
95
+
96
+ {:title => decoded[/title=(.+?)(?:&|$)/, 1],
97
+ :length_sec => decoded[/length_seconds=(.+?)(?:&|$)/, 1],
98
+ :author => decoded[/author=(.+?)(?:&|$)/, 1],
99
+ :embeddable => !decoded.include?("status=fail")}
100
+ end
101
+
102
+ def self.extract_urls_formats(video_info)
103
+ stream_map = video_info[/url_encoded_fmt_stream_map=(.+?)(?:&|$)/, 1]
104
+ parse_stream_map(stream_map)
105
+ end
106
+
107
+ def self.parse_stream_map(stream_map)
108
+ urls = extract_download_urls(stream_map)
109
+ formats_urls = {}
110
+
111
+ urls.each do |url|
112
+ format = url[/itag=(\d+)/, 1]
113
+ formats_urls[format] = url
114
+ end
115
+
116
+ formats_urls
117
+ end
118
+
119
+ def self.extract_download_urls(stream_map)
120
+ entries = stream_map.split("%2C")
121
+ decoded = entries.map { |entry| url_decode(entry) }
122
+
123
+ decoded.map do |entry|
124
+ url = entry[/url=(.*?itag=.+?)(?:itag=|;|$)/, 1]
125
+ sig = entry[/sig=(.+?)(?:&|$)/, 1]
126
+
127
+ url + "&signature=#{sig}"
128
+ end
129
+ end
130
+
131
+ def self.choose_format(urls_formats)
132
+ available_formats = urls_formats.keys
133
+
134
+ if @quality #if the user specified a format
135
+ ext = @quality[:extension]
136
+ res = @quality[:resolution]
137
+ #gets a nested array with all the formats of the same res as the user wanted
138
+ requested = VIDEO_FORMATS.select { |id, format| format[:name].include?(res) }.to_a
139
+
140
+ if requested.empty?
141
+ notify "Requested format \"#{res}:#{ext}\" not found. Downloading default format."
142
+ get_default_format(available_formats)
143
+ else
144
+ pick = requested.find { |format| format[1][:extension] == ext } # get requsted extension if possible
145
+ pick ? pick.first : get_default_format(requested.map { |req| req.first }) # else return the default format
146
+ end
147
+ else
148
+ get_default_format(available_formats)
149
+ end
150
+ end
151
+
152
+ def self.get_default_format(available)
153
+ DEFAULT_FORMAT_ORDER.find { |default| available.include?(default) }
154
+ end
155
+
156
+ def self.url_decode(text)
157
+ while text != (decoded = CGI::unescape(text)) do
158
+ text = decoded
159
+ end
160
+ text
161
+ end
162
+
163
+ def self.notify(message)
164
+ puts "[YOUTUBE] #{message}"
165
+ end
166
+
167
+ def self.download_error(message)
168
+ raise CouldNotDownloadVideoError, message
169
+ end
170
+
171
+ #
172
+ # class PlaylistParser
173
+ #_____________________
174
+
175
+ class PlaylistParser
176
+
177
+ PLAYLIST_FEED = "http://gdata.youtube.com/feeds/api/playlists/%s?&max-results=50&v=2"
178
+ USER_FEED = "http://gdata.youtube.com/feeds/api/users/%s/uploads?&max-results=50&v=2"
179
+
180
+ def get_playlist_urls(url, filter = nil)
181
+ @filter = filter
182
+
183
+ if url.include?("view_play_list") || url.include?("playlist?list=") # if playlist URL
184
+ parse_playlist(url)
185
+ elsif username = url[/\/user\/([\w\d]+)(?:\/|$)/, 1] # if user URL
186
+ parse_user(username)
187
+ else # if neither return nil
188
+ nil
189
+ end
190
+ end
191
+
192
+ def parse_playlist(url)
193
+ #http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
194
+ #http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
195
+ #http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
196
+
197
+ playlist_ID = url[/(?:list=PL|p=)(.+?)(?:&|\/|$)/, 1]
198
+ notify "Playlist ID: #{playlist_ID}"
199
+ feed_url = PLAYLIST_FEED % playlist_ID
200
+ url_array = get_video_urls(feed_url)
201
+ notify "#{url_array.size} links found!"
202
+ url_array
203
+ end
204
+
205
+ def parse_user(username)
206
+ notify "User: #{username}"
207
+ feed_url = USER_FEED % username
208
+ url_array = get_video_urls(feed_url)
209
+ notify "#{url_array.size} links found!"
210
+ url_array
211
+ end
212
+
213
+ #get all videos and return their urls in an array
214
+ def get_video_urls(feed_url)
215
+ notify "Retrieving videos..."
216
+ urls_titles = {}
217
+ result_feed = Nokogiri::XML(open(feed_url))
218
+ urls_titles.merge!(grab_urls_and_titles(result_feed))
219
+
220
+ #as long as the feed has a next link we follow it and add the resulting video urls
221
+ loop do
222
+ next_link = result_feed.search("//feed/link[@rel='next']").first
223
+ break if next_link.nil?
224
+ result_feed = Nokogiri::HTML(open(next_link["href"]))
225
+ urls_titles.merge!(grab_urls_and_titles(result_feed))
226
+ end
227
+
228
+ filter_urls(urls_titles)
229
+ end
230
+
231
+ #extract all video urls and their titles from a feed and return in a hash
232
+ def grab_urls_and_titles(feed)
233
+ feed.remove_namespaces! #so that we can get to the titles easily
234
+ urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
235
+ titles = feed.search("//entry/group/title").map { |title| title.text }
236
+ Hash[urls.zip(titles)] #hash like this: url => title
237
+ end
238
+
239
+ #returns only the urls that match the --filter argument regex (if present)
240
+ def filter_urls(url_hash)
241
+ if @filter
242
+ notify "Using filter: #{@filter}"
243
+ filtered = url_hash.select { |url, title| title =~ @filter }
244
+ filtered.keys
245
+ else
246
+ url_hash.keys
247
+ end
248
+ end
249
+
250
+ def notify(message)
251
+ Youtube.notify(message)
252
+ end
253
+ end
254
+ end
metadata CHANGED
@@ -1,12 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: viddl-rb
3
3
  version: !ruby/object:Gem::Version
4
- hash: 147
4
+ hash: 145
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 76
9
- version: "0.76"
8
+ - 77
9
+ version: "0.77"
10
10
  platform: ruby
11
11
  authors:
12
12
  - Marc Seeger
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2013-01-30 00:00:00 Z
17
+ date: 2013-02-24 00:00:00 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: nokogiri
@@ -126,7 +126,6 @@ files:
126
126
  - plugins/vimeo.rb
127
127
  - plugins/youtube.rb
128
128
  - Gemfile
129
- - Gemfile.lock
130
129
  - Rakefile
131
130
  - README.md
132
131
  - TODO.txt
@@ -161,10 +160,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
160
  requirements: []
162
161
 
163
162
  rubyforge_project: viddl-rb
164
- rubygems_version: 1.8.24
163
+ rubygems_version: 1.8.15
165
164
  signing_key:
166
165
  specification_version: 3
167
166
  summary: An extendable commandline video downloader for flash video sites.
168
167
  test_files: []
169
168
 
170
- has_rdoc: false
data/Gemfile.lock DELETED
@@ -1,47 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- viddl-rb (0.75)
5
- mechanize
6
- nokogiri
7
- progressbar
8
- rest-client
9
-
10
- GEM
11
- remote: http://rubygems.org/
12
- specs:
13
- domain_name (0.5.4)
14
- unf (~> 0.0.3)
15
- mechanize (2.5.1)
16
- domain_name (~> 0.5, >= 0.5.1)
17
- mime-types (~> 1.17, >= 1.17.2)
18
- net-http-digest_auth (~> 1.1, >= 1.1.1)
19
- net-http-persistent (~> 2.5, >= 2.5.2)
20
- nokogiri (~> 1.4)
21
- ntlm-http (~> 0.1, >= 0.1.1)
22
- webrobots (~> 0.0, >= 0.0.9)
23
- mime-types (1.19)
24
- minitest (4.1.0)
25
- net-http-digest_auth (1.2.1)
26
- net-http-persistent (2.8)
27
- nokogiri (1.5.5)
28
- nokogiri (1.5.5-java)
29
- ntlm-http (0.1.1)
30
- progressbar (0.11.0)
31
- rake (0.9.2.2)
32
- rest-client (1.6.7)
33
- mime-types (>= 1.16)
34
- unf (0.0.5)
35
- unf_ext
36
- unf (0.0.5-java)
37
- unf_ext (0.0.5)
38
- webrobots (0.0.13)
39
-
40
- PLATFORMS
41
- java
42
- ruby
43
-
44
- DEPENDENCIES
45
- minitest
46
- rake
47
- viddl-rb!