viddl-rb 0.76 → 0.77

Sign up to get free protection for your applications and to get access to all the features.
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!