viddl-rb 0.95 → 0.96

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 6b2cb672ebfa65f7f7231fdd615a77287757a529
4
- data.tar.gz: 03441807280152b0f821e648bec6289862aa7a60
5
2
  SHA512:
6
- metadata.gz: 369b0e52e1d62e9616b31f1477a4fc9cd7c274cd994404e9ce104ebcad2ff82c9168cb37c531362f65462c801ddf0e84cf8205c6d908a447baa45949778d73db
7
- data.tar.gz: 2df7a2726f09cd7c74a5e39386e4fe22a411314e93f936a4b84873f75a008aa69c5fd0ba252352af66a230e28eca3625ff394ac072dd696d834597b5df11564a
3
+ data.tar.gz: ff67e34025fae7f2799db8f7fcde11485942e5a7971d8f915bff66ec9ed49d20a0d8013f14268765dade6adc24c6c80320306fef363b78c876076942071d11eb
4
+ metadata.gz: d77571b6da60ad4fdb6c235232c19c528ce7e3f03f290c580d89953ee4dc24d04d77fc02d9e5422007c4902a8263fa0c5fccf47e5329ca37c9970fa82ccbd462
5
+ SHA1:
6
+ data.tar.gz: fafe08cbf9074f38e92dcb632f397e45f86d2c00
7
+ metadata.gz: 2019cc2be56479dd8827a2979641b8b14bc6cd08
data/README.md CHANGED
@@ -23,7 +23,9 @@ Viddl-rb supports the following command line options:
23
23
  -f, --filter REGEX Filters a video playlist according to the regex (Youtube only right now)
24
24
  -s, --save-dir DIRECTORY Specifies the directory where videos should be saved
25
25
  -d, --downloader TOOL Specifies the tool to download with. Supports 'wget', 'curl' and 'net-http'
26
- -q, --quality QUALITY Specifies the video format and resolution in the following way => resolution:extension (e.g. 720:mp4). Currently only supported by the Youtube plugin.
26
+ -q, --quality QUALITY Specifies the video format and resolution in the following way: width:height:res (e.g. 1280:720:mp4)
27
+ The width, height and resolution may be omitted with a *.
28
+ For example, to match any quality with a width of 720 pixels in any format specify --quality *:720:*
27
29
  -h, --help Displays the help screen
28
30
  ```
29
31
 
data/Rakefile CHANGED
@@ -2,21 +2,31 @@ require 'rubygems'
2
2
  require 'bundler/setup'
3
3
  require 'rake/testtask'
4
4
 
5
- task :default => [:test]
5
+ ALL_INTEGRATION = FileList["spec/integration/*.rb"]
6
+ ALL_UNIT = FileList["spec/unit/*/*.rb"]
6
7
 
7
- Rake::TestTask.new(:test) do |t|
8
- #t.pattern = "spec/*_spec.rb"
9
- t.test_files = ["spec/lib_spec.rb", "spec/url_extraction_spec.rb", "spec/integration_spec.rb"]
8
+ task :default => [:all]
9
+
10
+ Rake::TestTask.new(:all) do |t|
11
+ t.test_files = ALL_INTEGRATION + ALL_UNIT
12
+ end
13
+
14
+ Rake::TestTask.new(:test_unit) do |t|
15
+ t.test_files = ALL_UNIT
16
+ end
17
+
18
+ Rake::TestTask.new(:test_integration) do |t|
19
+ t.test_files = ALL_INTEGRATION
10
20
  end
11
21
 
12
22
  Rake::TestTask.new(:test_lib) do |t|
13
- t.test_files = FileList["spec/lib_spec.rb"]
23
+ t.test_files = FileList["spec/integration/lib_spec.rb"]
14
24
  end
15
25
 
16
26
  Rake::TestTask.new(:test_extract) do |t|
17
- t.test_files = FileList["spec/url_extraction_spec.rb"]
27
+ t.test_files = FileList["spec/integration/url_extraction_spec.rb"]
18
28
  end
19
29
 
20
- Rake::TestTask.new(:test_integration) do |t|
21
- t.test_files = FileList["spec/integration_spec.rb"]
30
+ Rake::TestTask.new(:test_download) do |t|
31
+ t.test_files = FileList["spec/integration/download_spec.rb"]
22
32
  end
data/bin/helper/driver.rb CHANGED
@@ -36,7 +36,7 @@ class Driver
36
36
  plugin.get_urls_and_filenames(url, @params)
37
37
 
38
38
  rescue ViddlRb::PluginBase::CouldNotDownloadVideoError => e
39
- raise "ERROR: The video could not be downloaded.\n" +
39
+ raise "CouldNotDownloadVideoError.\n" +
40
40
  "Reason: #{e.message}"
41
41
  rescue StandardError => e
42
42
  raise "Error while running the #{plugin.name.inspect} plugin. Maybe it has to be updated?\n" +
@@ -34,6 +34,10 @@ class ParameterParser
34
34
  optparse = OptionParser.new do |opts|
35
35
  opts.banner = "Usage: viddl-rb URL [options]"
36
36
 
37
+ opts.on('-h', '--help', 'Display this screen') do
38
+ print_help_and_exit(opts)
39
+ end
40
+
37
41
  opts.on("-e", "--extract-audio", "Save video audio to file") do
38
42
  if ViddlRb::UtilityHelper.os_has?("ffmpeg")
39
43
  options[:extract_audio] = true
@@ -75,21 +79,18 @@ class ParameterParser
75
79
  end
76
80
 
77
81
  opts.on("-q", "--quality QUALITY",
78
- "Specifies the video format and resolution in the following way => resolution:extension (e.g. 720:mp4)") do |quality|
79
- if match = quality.match(/(\d+):(.*)/)
80
- res = match[1]
81
- ext = match[2]
82
- elsif match = quality.match(/\d+/)
83
- res = match[0]
84
- ext = nil
85
- else
86
- raise OptionParse.InvalidArgument.new("#{quality} is not a valid argument.")
87
- end
88
- options[:quality] = {:extension => ext, :resolution => res}
89
- end
90
-
91
- opts.on_tail('-h', '--help', 'Display this screen') do
92
- print_help_and_exit(opts)
82
+ "Specifies the video format and resolution in the following way: width:height:res (e.g. 1280:720:mp4). " +
83
+ "The width, height and resolution may be omitted with a *. For example, to match any quality with a " +
84
+ "width of 720 pixels in any format specify --quality *:720:*") do |quality|
85
+
86
+ tokens = quality.split(":")
87
+ raise OptionParser::InvalidArgument.new("#{quality} is not a valid argument.") unless tokens.size == 3
88
+ width, height, ext = tokens
89
+ validate_quality_options!(width, height, ext, quality)
90
+
91
+ options[:quality] = {width: width == "*" ? nil : width.to_i,
92
+ height: height == "*" ? nil : height.to_i,
93
+ extension: ext == "*" ? nil : ext}
93
94
  end
94
95
  end
95
96
 
@@ -109,7 +110,25 @@ class ParameterParser
109
110
  def self.validate_url!(url)
110
111
  unless url =~ /^http/
111
112
  raise OptionParser::InvalidArgument.new(
112
- "please include 'http' with your URL e.g. http://www.youtube.com/watch?v=QH2-TGUlwu4")
113
+ "please include 'http' with your URL e.g. http://www.youtube.com/watch?v=QH2-TGUlwu4")
114
+ end
115
+ end
116
+
117
+ def self.validate_quality_options!(width, height, extension, quality)
118
+ if !width =~ /(\*|\d+)/ || !height =~ /(\*|\d+)/ || !extension =~ /(\*|\w+)/
119
+ raise OptionParser::InvalidArgument.new("#{quality} is not a valid argument.")
113
120
  end
114
121
  end
115
122
  end
123
+
124
+
125
+
126
+
127
+
128
+
129
+
130
+
131
+
132
+
133
+
134
+
@@ -55,6 +55,4 @@ module ViddlRb
55
55
  nil
56
56
  end
57
57
  end
58
-
59
58
  end
60
-
@@ -3,11 +3,29 @@
3
3
  module ViddlRb
4
4
 
5
5
  class UtilityHelper
6
- #loads all plugins in the plugin directory.
7
- #the plugin classes are dynamically added to the ViddlRb module.
6
+
7
+ # Loads all plugins in the plugin directory.
8
+ # The plugin classes are dynamically added to the ViddlRb module.
9
+ # A plugin can have helper classes. These classes must exist in a in directory under the
10
+ # plugins directory that has the same name as the plugin filename wihouth the .rb extension.
11
+ # All classes found in such a directory will dynamically added as inner classes of the
12
+ # plugin class.
8
13
  def self.load_plugins
9
- Dir[File.join(File.dirname(__FILE__), "../plugins/*.rb")].each do |plugin|
10
- ViddlRb.class_eval(File.read(plugin))
14
+ plugins_dir = File.join(File.dirname(__FILE__), "../plugins")
15
+ plugin_paths = Dir[File.join(plugins_dir, "*.rb")]
16
+
17
+ plugin_paths.each do |path|
18
+ filename = File.basename(path, File.extname(path))
19
+ plugin_code = File.read(path)
20
+ class_name = plugin_code[/class (\w+) < PluginBase/, 1]
21
+ components = Dir[File.join(plugins_dir, filename, "*.rb")]
22
+
23
+ ViddlRb.class_eval(plugin_code)
24
+
25
+ components.each do |component|
26
+ code = File.read(component)
27
+ ViddlRb.const_get(class_name).class_eval(code)
28
+ end
11
29
  end
12
30
  end
13
31
 
data/plugins/youtube.rb CHANGED
@@ -1,231 +1,26 @@
1
- # -*- coding: utf-8 -*-
2
1
 
3
2
  class Youtube < PluginBase
4
3
 
5
- # see http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
6
- # TODO: we don't have all the formats from the wiki article here
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
- "46" => {:extension => "webm", :name => "WebM 1920x1080 (VP8, Vorbis)"},
12
- "45" => {:extension => "webm", :name => "WebM 1280x720 (VP8, Vorbis)"},
13
- "44" => {:extension => "webm", :name => "WebM 854x480 (VP8, Vorbis)"},
14
- "43" => {:extension => "webm", :name => "WebM 480x360 (VP8, Vorbis)"},
15
- "18" => {:extension => "mp4", :name => "MP4 640x360 (H.264, AAC)"},
16
- "35" => {:extension => "flv", :name => "FLV 854x480 (H.264, AAC)"},
17
- "34" => {:extension => "flv", :name => "FLV 640x360 (H.264, AAC)"},
18
- "6" => {:extension => "flv", :name => "FLV 640x360 (Soerenson H.263)"},
19
- "5" => {:extension => "flv", :name => "FLV 400x240 (Soerenson H.263)"},
20
- "36" => {:extension => "3gp", :name => "3gp Medium Quality - 320x240 (MPEG-4 Visual, AAC)"},
21
- "17" => {:extension => "3gp", :name => "3gp Medium Quality - 176x144 (MPEG-4 Visual, AAC)"},
22
- "13" => {:extension => "3gp", :name => "3gp Low Quality - 176x144 (MPEG-4 Visual, AAC)"},
23
- "82" => {:extension => "mp4", :name => "MP4 360p (H.264 AAC)"},
24
- "83" => {:extension => "mp4", :name => "MP4 240p (H.264 AAC)"},
25
- "84" => {:extension => "mp4", :name => "MP4 720p (H.264 AAC)"},
26
- "85" => {:extension => "mp4", :name => "MP4 520p (H.264 AAC)"},
27
- "100" => {:extension => "webm", :name => "WebM 360p (VP8 Vorbis)"},
28
- "101" => {:extension => "webm", :name => "WebM 360p (VP8 Vorbis)"},
29
- "102" => {:extension => "webm", :name => "WebM 720p (VP8 Vorbis)"},
30
- "120" => {:extension => "flv", :name => "FLV 720p (H.264 AAC)"},
31
- "133" => {:extension => "mp4", :name => "MP4 240p (H.264)"},
32
- "134" => {:extension => "mp4", :name => "MP4 360p (H.264)"},
33
- "135" => {:extension => "mp4", :name => "MP4 480p (H.264)"},
34
- "136" => {:extension => "mp4", :name => "MP4 720p (H.264)"},
35
- "137" => {:extension => "mp4", :name => "MP4 1080p (H.264)"},
36
- "139" => {:extension => "mp4", :name => "MP4 (AAC)"},
37
- "140" => {:extension => "mp4", :name => "MP4 (AAC"},
38
- "141" => {:extension => "mp4", :name => "MP4 (AAC)"},
39
- "160" => {:extension => "mp4", :name => "MP4 (H.264)"},
40
- "171" => {:extension => "webm", :name => "WebM (Vorbis)"},
41
- "172" => {:extension => "webm", :name => "WebM (Vorbis)"}
42
- }
43
-
44
- DEFAULT_FORMAT_ORDER = %w[38 37 22 46 45 44 43 18 35 34 6 5 36 17 13 82 83 84 85 100 101 102 120 133 134 135 136 137 139 140 141 160 171 172]
45
- VIDEO_INFO_URL = "http://www.youtube.com/get_video_info?video_id="
46
- VIDEO_INFO_PARMS = "&ps=default&eurl=&gl=US&hl=en"
47
-
48
4
  # this will be called by the main app to check whether this plugin is responsible for the url passed
49
5
  def self.matches_provider?(url)
50
6
  url.include?("youtube.com") || url.include?("youtu.be")
51
7
  end
52
8
 
53
9
  def self.get_urls_and_filenames(url, options = {})
54
- @quality = options[:quality]
55
- filter = options[:playlist_filter]
56
- parser = PlaylistParser.new
57
- return_vals = []
58
10
 
59
- if playlist_urls = parser.get_playlist_urls(url, filter)
60
- playlist_urls.each { |url| return_vals << grab_single_url_filename(url, options) }
61
- else
62
- return_vals << grab_single_url_filename(url, options)
63
- end
64
-
65
- clean_return_values(return_vals)
66
- end
11
+ @url_resolver = UrlResolver.new
12
+ @video_resolver = VideoResolver.new(Decipherer.new)
13
+ @format_picker = FormatPicker.new(options)
67
14
 
68
- def self.clean_return_values(return_values)
69
- cleaned = return_values.reject { |val| val == :no_embed }
15
+ urls = @url_resolver.get_all_urls(url, options[:filter])
16
+ videos = get_videos(urls)
70
17
 
71
- if cleaned.empty?
72
- download_error("No videos could be downloaded.")
73
- else
74
- cleaned
75
- end
76
- end
77
-
78
- def self.grab_single_url_filename(url, options)
79
- UrlGrabber.new(url, self, options).process
80
- end
81
-
82
- class UrlGrabber
83
- attr_accessor :url, :options, :plugin, :quality
84
-
85
- def initialize(url, plugin, options)
86
- @url = url
87
- @plugin = plugin
88
- @options = options
89
- @quality = options[:quality]
90
- end
91
-
92
- def process
93
- grab_url_embeddable(url) || grab_url_non_embeddable(url)
94
- end
95
-
96
- # VEVO video: http://www.youtube.com/watch?v=A_J7kEhY9sM
97
- # Non-VEVO video: http://www.youtube.com/watch?v=WkkC9cK8Hz0
98
-
99
- def grab_url_embeddable(url)
100
- video_info = get_video_info(url)
101
- video_params = extract_video_parameters(video_info)
102
-
103
- unless video_params[:embeddable]
104
- Youtube.notify("VIDEO IS NOT EMBEDDABLE")
105
- return false
106
- end
107
-
108
- urls_formats = extract_urls_formats(video_info)
109
- selected_format = choose_format(urls_formats)
110
- title = video_params[:title]
111
- file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
112
-
113
- {:url => urls_formats[selected_format], :name => file_name}
114
- end
115
-
116
- def grab_url_non_embeddable(url)
117
- video_info = open(url).read
118
- stream_map = video_info[/url_encoded_fmt_stream_map\" *: *\"([^\"]+)\"/,1]
119
-
120
- # Video has been deleted!
121
- if stream_map.nil? or !stream_map.index("been+removed").nil?
122
- Youtube.notify("VIDEO IS REMOVED")
123
- return false
124
- end
125
-
126
- urls_formats = parse_stream_map(url_decode(stream_map))
127
- selected_format = choose_format(urls_formats)
128
- title = video_info[/<meta name="title" content="([^"]*)">/, 1]
129
- file_name = PluginBase.make_filename_safe(title) + "." + VIDEO_FORMATS[selected_format][:extension]
130
-
131
- # cleaning
132
- clean_url = urls_formats[selected_format].gsub(/\\u0026[^&]*/, "").split(',type=video').first
133
- {:url => clean_url, :name => file_name}
134
- end
135
-
136
- def get_video_info(url)
137
- id = extract_video_id(url)
138
- request_url = VIDEO_INFO_URL + id + VIDEO_INFO_PARMS
139
- open(request_url).read
140
- end
141
-
142
- def extract_video_parameters(video_info)
143
- video_params = CGI.parse(url_decode(video_info))
144
-
145
- {
146
- :title => video_params["title"].first,
147
- :length_sec => video_params["length_seconds"].first,
148
- :author => video_params["author"].first,
149
- :embeddable => (video_params["status"].first != "fail")
150
- }
151
- end
152
-
153
- def extract_video_id(url)
154
- # the youtube video ID looks like this: [...]v=abc5a5_afe5agae6g&[...], we only want the ID (the \w in the brackets)
155
- # addition: might also look like this /v/abc5-a5afe5agae6g
156
- # alternative: video_id = url[/v[\/=]([\w-]*)&?/, 1]
157
- url = open(url).base_uri.to_s if url.include?("youtu.be")
158
- video_id = url[/(v|embed)[=\/]([^\/\?\&]*)/, 2]
159
-
160
- if video_id
161
- Youtube.notify("ID FOUND: #{video_id}")
162
- video_id
163
- else
164
- Youtube.download_error("No video id found.")
165
- end
166
- end
167
-
168
- def extract_urls_formats(video_info)
169
- stream_map = video_info[/url_encoded_fmt_stream_map=(.+?)(?:&|$)/, 1]
170
- parse_stream_map(stream_map)
171
- end
172
-
173
- def choose_format(urls_formats)
174
- available_formats = urls_formats.keys
175
-
176
- if @quality #if the user specified a format
177
- ext = @quality[:extension]
178
- res = @quality[:resolution] || ""
179
- #gets a nested array with all the formats of the same res as the user wanted
180
- requested = VIDEO_FORMATS.select { |id, format| available_formats.include?(id) && format[:name].include?(res) }.to_a
181
-
182
- if requested.empty?
183
- Youtube.notify "Requested format \"#{res}:#{ext}\" not found. Downloading default format."
184
- get_default_format(available_formats)
185
- else
186
- pick = requested.find { |format| format[1][:extension] == ext } # get requsted extension if possible
187
- pick ? pick.first : get_default_format(requested.map { |req| req.first }) # else return the default format
188
- end
189
- else
190
- get_default_format(available_formats)
191
- end
192
- end
193
-
194
- def parse_stream_map(stream_map)
195
- urls = extract_download_urls(stream_map)
196
- formats_urls = {}
197
-
198
- urls.each do |url|
199
- format = url[/itag=(\d+)/, 1]
200
- formats_urls[format] = url
201
- end
202
-
203
- formats_urls
204
- end
205
-
206
- def extract_download_urls(stream_map)
207
- entries = stream_map.split("%2C")
208
- decoded = entries.map { |entry| url_decode(entry) }
209
-
210
- decoded.map do |entry|
211
- url = entry[/url=(.*?itag=.+?)(?:itag=|;|$)/, 1]
212
- sig = entry[/sig=(.+?)(?:&|$)/, 1]
213
-
214
- url + "&signature=#{sig}"
215
- end
216
- end
217
-
218
- def get_default_format(available)
219
- DEFAULT_FORMAT_ORDER.find { |default| available.include?(default) }
220
- end
221
-
222
- def url_decode(text)
223
- while text != (decoded = CGI::unescape(text)) do
224
- text = decoded
225
- end
226
- text
18
+ return_value = videos.map do |video|
19
+ format = @format_picker.pick_format(video)
20
+ make_url_filname_hash(video, format)
227
21
  end
228
22
 
23
+ return_value.empty? ? download_error("No videos could be downloaded.") : return_value
229
24
  end
230
25
 
231
26
  def self.notify(message)
@@ -236,83 +31,23 @@ class Youtube < PluginBase
236
31
  raise CouldNotDownloadVideoError, message
237
32
  end
238
33
 
239
- #
240
- # class PlaylistParser
241
- #_____________________
242
-
243
- class PlaylistParser
244
-
245
- PLAYLIST_FEED = "http://gdata.youtube.com/feeds/api/playlists/%s?&max-results=50&v=2"
246
- USER_FEED = "http://gdata.youtube.com/feeds/api/users/%s/uploads?&max-results=50&v=2"
247
-
248
- def get_playlist_urls(url, filter = nil)
249
- @filter = filter
250
-
251
- if url.include?("view_play_list") || url.include?("playlist?list=") # if playlist URL
252
- parse_playlist(url)
253
- elsif username = url[/\/(?:user|channel)\/([\w\d]+)(?:\/|$)/, 1] # if user/channel URL
254
- parse_user(username)
255
- else # if neither return nil
256
- nil
257
- end
258
- end
259
-
260
- def parse_playlist(url)
261
- #http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
262
- #http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
263
- #http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
264
-
265
- playlist_ID = url[/(?:list=PL|p=)(.+?)(?:&|\/|$)/, 1]
266
- Youtube.notify "Playlist ID: #{playlist_ID}"
267
- feed_url = PLAYLIST_FEED % playlist_ID
268
- url_array = get_video_urls(feed_url)
269
- Youtube.notify "#{url_array.size} links found!"
270
- url_array
271
- end
272
-
273
- def parse_user(username)
274
- Youtube.notify "User: #{username}"
275
- feed_url = USER_FEED % username
276
- url_array = get_video_urls(feed_url)
277
- Youtube.notify "#{url_array.size} links found!"
278
- url_array
279
- end
280
-
281
- #get all videos and return their urls in an array
282
- def get_video_urls(feed_url)
283
- Youtube.notify "Retrieving videos..."
284
- urls_titles = {}
285
- result_feed = Nokogiri::XML(open(feed_url))
286
- urls_titles.merge!(grab_urls_and_titles(result_feed))
287
-
288
- #as long as the feed has a next link we follow it and add the resulting video urls
289
- loop do
290
- next_link = result_feed.search("//feed/link[@rel='next']").first
291
- break if next_link.nil?
292
- result_feed = Nokogiri::HTML(open(next_link["href"]))
293
- urls_titles.merge!(grab_urls_and_titles(result_feed))
34
+ def self.get_videos(urls)
35
+ videos = urls.map do |url|
36
+ begin
37
+ @video_resolver.get_video(url)
38
+ rescue VideoResolver::VideoRemovedError
39
+ notify "The video #{url} has been removed."
40
+ rescue => e
41
+ notify "Error getting the video: #{e.message}"
294
42
  end
295
-
296
- filter_urls(urls_titles)
297
43
  end
298
44
 
299
- #extract all video urls and their titles from a feed and return in a hash
300
- def grab_urls_and_titles(feed)
301
- feed.remove_namespaces! #so that we can get to the titles easily
302
- urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
303
- titles = feed.search("//entry/group/title").map { |title| title.text }
304
- Hash[urls.zip(titles)] #hash like this: url => title
305
- end
45
+ videos.reject(&:nil?)
46
+ end
306
47
 
307
- #returns only the urls that match the --filter argument regex (if present)
308
- def filter_urls(url_hash)
309
- if @filter
310
- Youtube.notify "Using filter: #{@filter}"
311
- filtered = url_hash.select { |url, title| title =~ @filter }
312
- filtered.keys
313
- else
314
- url_hash.keys
315
- end
316
- end
48
+ def self.make_url_filname_hash(video, format)
49
+ url = video.get_download_url(format.itag)
50
+ name = PluginBase.make_filename_safe(video.title) + ".#{format.extension}"
51
+ {url: url, name: name}
317
52
  end
318
53
  end
@@ -0,0 +1,149 @@
1
+
2
+ class Decipherer
3
+
4
+ class UnknownCipherVersionError < StandardError; end
5
+ class UnknownCipherOperationError < StandardError; end
6
+
7
+ CIPHERS = {
8
+ 'vflNzKG7n' => 's3 r s2 r s1 r w67', # 30 Jan 2013, untested
9
+ 'vfllMCQWM' => 's2 w46 r w27 s2 w43 s2 r', # 15 Feb 2013, untested
10
+ 'vflJv8FA8' => 's1 w51 w52 r', # 12 Mar 2013, untested
11
+ 'vflR_cX32' => 's2 w64 s3', # 11 Apr 2013, untested
12
+ 'vflveGye9' => 'w21 w3 s1 r w44 w36 r w41 s1', # 02 May 2013, untested
13
+ 'vflj7Fxxt' => 'r s3 w3 r w17 r w41 r s2', # 14 May 2013, untested
14
+ 'vfltM3odl' => 'w60 s1 w49 r s1 w7 r s2 r', # 23 May 2013
15
+ 'vflDG7-a-' => 'w52 r s3 w21 r s3 r', # 06 Jun 2013
16
+ 'vfl39KBj1' => 'w52 r s3 w21 r s3 r', # 12 Jun 2013
17
+ 'vflmOfVEX' => 'w52 r s3 w21 r s3 r', # 21 Jun 2013
18
+ 'vflJwJuHJ' => 'r s3 w19 r s2', # 25 Jun 2013
19
+ 'vfl_ymO4Z' => 'r s3 w19 r s2', # 26 Jun 2013
20
+ 'vfl26ng3K' => 'r s2 r', # 08 Jul 2013
21
+ 'vflcaqGO8' => 'w24 w53 s2 w31 w4', # 11 Jul 2013
22
+ 'vflQw-fB4' => 's2 r s3 w9 s3 w43 s3 r w23', # 16 Jul 2013
23
+ 'vflSAFCP9' => 'r s2 w17 w61 r s1 w7 s1', # 18 Jul 2013
24
+ 'vflART1Nf' => 's3 r w63 s2 r s1', # 22 Jul 2013
25
+ 'vflLC8JvQ' => 'w34 w29 w9 r w39 w24', # 25 Jul 2013
26
+ 'vflm_D8eE' => 's2 r w39 w55 w49 s3 w56 w2', # 30 Jul 2013
27
+ 'vflTWC9KW' => 'r s2 w65 r', # 31 Jul 2013
28
+ 'vflRFcHMl' => 's3 w24 r', # 04 Aug 2013
29
+ 'vflM2EmfJ' => 'w10 r s1 w45 s2 r s3 w50 r', # 06 Aug 2013
30
+ 'vflz8giW0' => 's2 w18 s3', # 07 Aug 2013
31
+ 'vfl_wGgYV' => 'w60 s1 r s1 w9 s3 r s3 r', # 08 Aug 2013
32
+ 'vfl1HXdPb' => 'w52 r w18 r s1 w44 w51 r s1', # 12 Aug 2013
33
+ 'vflkn6DAl' => 'w39 s2 w57 s2 w23 w35 s2', # 15 Aug 2013
34
+ 'vfl2LOvBh' => 'w34 w19 r s1 r s3 w24 r', # 16 Aug 2013
35
+ 'vfl-bxy_m' => 'w48 s3 w37 s2', # 20 Aug 2013
36
+ 'vflZK4ZYR' => 'w19 w68 s1', # 21 Aug 2013
37
+ 'vflh9ybst' => 'w48 s3 w37 s2', # 21 Aug 2013
38
+ 'vflapUV9V' => 's2 w53 r w59 r s2 w41 s3', # 27 Aug 2013
39
+ 'vflg0g8PQ' => 'w36 s3 r s2', # 28 Aug 2013
40
+ 'vflHOr_nV' => 'w58 r w50 s1 r s1 r w11 s3', # 30 Aug 2013
41
+ 'vfluy6kdb' => 'r w12 w32 r w34 s3 w35 w42 s2', # 05 Sep 2013
42
+ 'vflkuzxcs' => 'w22 w43 s3 r s1 w43', # 10 Sep 2013
43
+ 'vflGNjMhJ' => 'w43 w2 w54 r w8 s1', # 12 Sep 2013
44
+ 'vfldJ8xgI' => 'w11 r w29 s1 r s3', # 17 Sep 2013
45
+ 'vfl79wBKW' => 's3 r s1 r s3 r s3 w59 s2', # 19 Sep 2013
46
+ 'vflg3FZfr' => 'r s3 w66 w10 w43 s2', # 24 Sep 2013
47
+ 'vflUKrNpT' => 'r s2 r w63 r', # 25 Sep 2013
48
+ 'vfldWnjUz' => 'r s1 w68', # 30 Sep 2013
49
+ 'vflP7iCEe' => 'w7 w37 r s1', # 03 Oct 2013
50
+ 'vflzVne63' => 'w59 s2 r', # 07 Oct 2013
51
+ 'vflO-N-9M' => 'w9 s1 w67 r s3', # 09 Oct 2013
52
+ 'vflZ4JlpT' => 's3 r s1 r w28 s1', # 11 Oct 2013
53
+ 'vflDgXSDS' => 's3 r s1 r w28 s1', # 15 Oct 2013
54
+ 'vflW444Sr' => 'r w9 r s1 w51 w27 r s1 r', # 17 Oct 2013
55
+ 'vflK7RoTQ' => 'w44 r w36 r w45', # 21 Oct 2013
56
+ 'vflKOCFq2' => 's1 r w41 r w41 s1 w15', # 23 Oct 2013
57
+ 'vflcLL31E' => 's1 r w41 r w41 s1 w15', # 28 Oct 2013
58
+ 'vflz9bT3N' => 's1 r w41 r w41 s1 w15', # 31 Oct 2013
59
+ 'vfliZsE79' => 'r s3 w49 s3 r w58 s2 r s2', # 05 Nov 2013
60
+ 'vfljOFtAt' => 'r s3 r s1 r w69 r', # 07 Nov 2013
61
+ 'vflqSl9GX' => 'w32 r s2 w65 w26 w45 w24 w40 s2', # 14 Nov 2013
62
+ 'vflFrKymJ' => 'w32 r s2 w65 w26 w45 w24 w40 s2', # 15 Nov 2013
63
+ 'vflKz4WoM' => 'w50 w17 r w7 w65', # 19 Nov 2013
64
+ 'vflhdWW8S' => 's2 w55 w10 s3 w57 r w25 w41', # 21 Nov 2013
65
+ 'vfl66X2C5' => 'r s2 w34 s2 w39', # 26 Nov 2013
66
+ 'vflCXG8Sm' => 'r s2 w34 s2 w39', # 02 Dec 2013
67
+ 'vfl_3Uag6' => 'w3 w7 r s2 w27 s2 w42 r', # 04 Dec 2013
68
+ 'vflQdXVwM' => 's1 r w66 s2 r w12', # 10 Dec 2013
69
+ 'vflCtc3aO' => 's2 r w11 r s3 w28', # 12 Dec 2013
70
+ 'vflCt6YZX' => 's2 r w11 r s3 w28', # 17 Dec 2013
71
+ 'vflG49soT' => 'w32 r s3 r s1 r w19 w24 s3', # 18 Dec 2013
72
+ 'vfl4cHApe' => 'w25 s1 r s1 w27 w21 s1 w39', # 06 Jan 2014
73
+ 'vflwMrwdI' => 'w3 r w39 r w51 s1 w36 w14', # 06 Jan 2014
74
+ 'vfl4AMHqP' => 'r s1 w1 r w43 r s1 r', # 09 Jan 2014
75
+ 'vfln8xPyM' => 'w36 w14 s1 r s1 w54', # 10 Jan 2014
76
+ 'vflVSLmnY' => 's3 w56 w10 r s2 r w28 w35', # 13 Jan 2014
77
+ 'vflkLvpg7' => 'w4 s3 w53 s2', # 15 Jan 2014
78
+ 'vflbxes4n' => 'w4 s3 w53 s2', # 15 Jan 2014
79
+ 'vflmXMtFI' => 'w57 s3 w62 w41 s3 r w60 r', # 23 Jan 2014
80
+ 'vflYDqEW1' => 'w24 s1 r s2 w31 w4 w11 r', # 24 Jan 2014
81
+ 'vflapGX6Q' => 's3 w2 w59 s2 w68 r s3 r s1', # 28 Jan 2014
82
+ 'vflLCYwkM' => 's3 w2 w59 s2 w68 r s3 r s1', # 29 Jan 2014
83
+ 'vflcY_8N0' => 's2 w36 s1 r w18 r w19 r', # 30 Jan 2014
84
+ 'vfl9qWoOL' => 'w68 w64 w28 r', # 03 Feb 2014
85
+ 'vfle-mVwz' => 's3 w7 r s3 r w14 w59 s3 r', # 04 Feb 2014
86
+ 'vfltdb6U3' => 'w61 w5 r s2 w69 s2 r', # 05 Feb 2014
87
+ 'vflLjFx3B' => 'w40 w62 r s2 w21 s3 r w7 s3', # 10 Feb 2014
88
+ 'vfliqjKfF' => 'w40 w62 r s2 w21 s3 r w7 s3', # 13 Feb 2014
89
+ 'ima-vflxBu-5R' => 'w40 w62 r s2 w21 s3 r w7 s3', # 13 Feb 2014
90
+ 'ima-vflrGwWV9' => 'w36 w45 r s2 r' # 20 Feb 2014
91
+ }
92
+
93
+ def decipher_with_version(cipher, cipher_version)
94
+ operations = CIPHERS[cipher_version]
95
+ raise UnknownCipherVersionError.new("Unknown cipher version: #{cipher_version}") unless operations
96
+
97
+ decipher_with_operations(cipher, operations.split)
98
+ end
99
+
100
+ def decipher_with_operations(cipher, operations)
101
+ cipher = cipher.dup
102
+
103
+ operations.each do |op|
104
+ cipher = apply_operation(cipher, op)
105
+ end
106
+ cipher
107
+ end
108
+
109
+ private
110
+
111
+ def apply_operation(cipher, op)
112
+ op = check_operation(op)
113
+
114
+ case op[0].downcase
115
+ when "r"
116
+ cipher.reverse
117
+ when "w"
118
+ index = get_op_index(op)
119
+ swap_first_char(cipher, index)
120
+ when "s"
121
+ index = get_op_index(op)
122
+ cipher[index, cipher.length - 1] # slice from index to the end
123
+ else
124
+ raise_unknown_op_error(op)
125
+ end
126
+ end
127
+
128
+ def check_operation(op)
129
+ raise_unknown_op_error(op) if op.nil? || !op.respond_to?(:to_s)
130
+ op.to_s
131
+ end
132
+
133
+ def swap_first_char(string, index)
134
+ temp = string[0]
135
+ string[0] = string[index]
136
+ string[index] = temp
137
+ string
138
+ end
139
+
140
+ def get_op_index(op)
141
+ index = op[/.(\d+)/, 1]
142
+ raise_unknown_op_error(op) unless index
143
+ index.to_i
144
+ end
145
+
146
+ def raise_unknown_op_error(op)
147
+ raise UnknownCipherOperationError.new("Unkown operation: #{op}")
148
+ end
149
+ end
@@ -0,0 +1,116 @@
1
+
2
+ class FormatPicker
3
+
4
+ Format = Struct.new(:itag, :extension, :resolution, :name)
5
+ Resolution = Struct.new(:width, :height)
6
+
7
+ # see http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
8
+ # TODO: we don't have all the formats from the wiki article here
9
+ # :u means the resolution is unknown.
10
+ FORMATS = [
11
+ Format.new("38", "mp4", Resolution.new(4096, 3027), "MP4 Highest Quality 4096x3027 (H.264, AAC)"),
12
+ Format.new("37", "mp4", Resolution.new(1920, 1080), "MP4 Highest Quality 1920x1080 (H.264, AAC)"),
13
+ Format.new("22", "mp4", Resolution.new(1280, 720), "MP4 1280x720 (H.264, AAC)"),
14
+ Format.new("46", "webm", Resolution.new(1920, 1080), "WebM 1920x1080 (VP8, Vorbis)"),
15
+ Format.new("45", "webm", Resolution.new(1280, 720), "WebM 1280x720 (VP8, Vorbis)"),
16
+ Format.new("44", "webm", Resolution.new(854, 480), "WebM 854x480 (VP8, Vorbis)"),
17
+ Format.new("43", "webm", Resolution.new(480, 360), "WebM 480x360 (VP8, Vorbis)"),
18
+ Format.new("18", "mp4", Resolution.new(640, 360), "MP4 640x360 (H.264, AAC)"),
19
+ Format.new("35", "flv", Resolution.new(854, 480), "FLV 854x480 (H.264, AAC)"),
20
+ Format.new("34", "flv", Resolution.new(640, 360), "FLV 640x360 (H.264, AAC)"),
21
+ Format.new("6", "flv", Resolution.new(640, 360), "FLV 640x360 (Soerenson H.263)"),
22
+ Format.new("5", "flv", Resolution.new(400, 240), "FLV 400x240 (Soerenson H.263)"),
23
+ Format.new("36", "3gp", Resolution.new(320, 240), "3gp Medium Quality - 320x240 (MPEG-4 Visual, AAC)"),
24
+ Format.new("17", "3gp", Resolution.new(174, 144), "3gp Medium Quality - 176x144 (MPEG-4 Visual, AAC)"),
25
+ Format.new("13", "3gp", Resolution.new(176, 144), "3gp Low Quality - 176x144 (MPEG-4 Visual, AAC)"),
26
+ Format.new("82", "mp4", Resolution.new(480, 360), "MP4 360p (H.264 AAC)"),
27
+ Format.new("83", "mp4", Resolution.new(320, 240), "MP4 240p (H.264 AAC)"),
28
+ Format.new("84", "mp4", Resolution.new(1280, 720), "MP4 720p (H.264 AAC)"),
29
+ Format.new("85", "mp4", Resolution.new(960, 520), "MP4 520p (H.264 AAC)"),
30
+ Format.new("100", "webm", Resolution.new(480, 360), "WebM 360p (VP8 Vorbis)"),
31
+ Format.new("101", "webm", Resolution.new(480, 360), "WebM 360p (VP8 Vorbis)"),
32
+ Format.new("102", "webm", Resolution.new(1280, 720), "WebM 720p (VP8 Vorbis)"),
33
+ Format.new("120", "flv", Resolution.new(1280, 720), "FLV 720p (H.264 AAC)"),
34
+ Format.new("133", "mp4", Resolution.new(320, 240), "MP4 240p (H.264)"),
35
+ Format.new("134", "mp4", Resolution.new(480, 360), "MP4 360p (H.264)"),
36
+ Format.new("135", "mp4", Resolution.new(640, 480), "MP4 480p (H.264)"),
37
+ Format.new("136", "mp4", Resolution.new(1280, 720), "MP4 720p (H.264)"),
38
+ Format.new("137", "mp4", Resolution.new(1920, 1080), "MP4 1080p (H.264)"),
39
+ Format.new("139", "mp4", Resolution.new(:u, :u), "MP4 (AAC)"),
40
+ Format.new("140", "mp4", Resolution.new(:u, :u), "MP4 (AAC"),
41
+ Format.new("141", "mp4", Resolution.new(:u, :u), "MP4 (AAC)"),
42
+ Format.new("160", "mp4", Resolution.new(:u, :u), "MP4 (H.264)"),
43
+ Format.new("171", "webm", Resolution.new(:u, :u), "WebM (Vorbis)"),
44
+ Format.new("172", "webm", Resolution.new(:u, :u), "WebM (Vorbis)")
45
+ ]
46
+
47
+ DEFAULT_FORMAT_ORDER = %w[38 37 22 46 45 44 43 18 35 34 6 5 36 17 13 82 83 84 85 100 101 102 120 133 134 135 136 137 139 140 141 160 171 172]
48
+
49
+ def initialize(options)
50
+ @options = options
51
+ end
52
+
53
+ def pick_format(video)
54
+ if quality = @options[:quality]
55
+ get_quality_format(video, quality)
56
+ else
57
+ get_default_format_for_video(video)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def get_default_format_for_video(video)
64
+ available = get_available_formats_for_video(video)
65
+ get_default_format(available)
66
+ end
67
+
68
+ def get_available_formats_for_video(video)
69
+ video.available_itags.map { |itag| get_format_by_itag(itag) }
70
+ end
71
+
72
+ def get_format_by_itag(itag)
73
+ FORMATS.find { |format| format.itag == itag }
74
+ end
75
+
76
+ def get_default_format(formats)
77
+ DEFAULT_FORMAT_ORDER.each do |itag|
78
+ default_format = formats.find { |format| format.itag == itag }
79
+ return default_format if default_format
80
+ end
81
+ nil
82
+ end
83
+
84
+ def get_quality_format(video, quality)
85
+ available = get_available_formats_for_video(video)
86
+
87
+ matches = available.select do |format|
88
+ matches_extension?(format, quality) && matches_resolution?(format, quality)
89
+ end
90
+
91
+ select_format(video, matches)
92
+ end
93
+
94
+ def matches_extension?(format, quality)
95
+ return false if quality[:extension] && quality[:extension] != format.extension
96
+ true
97
+ end
98
+
99
+ def matches_resolution?(format, quality)
100
+ return false if quality[:width] && quality[:width] != format.resolution.width
101
+ return false if quality[:height] && quality[:height] != format.resolution.height
102
+ true
103
+ end
104
+
105
+ def select_format(video, formats)
106
+ case formats.length
107
+ when 0
108
+ Youtube.notify "Requested format not found. Downloading default format."
109
+ get_default_format_for_video(video)
110
+ when 1
111
+ formats.first
112
+ else
113
+ get_default_format(matches_resolution)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,77 @@
1
+ class UrlResolver
2
+
3
+ PLAYLIST_FEED = "http://gdata.youtube.com/feeds/api/playlists/%s?&max-results=50&v=2"
4
+ USER_FEED = "http://gdata.youtube.com/feeds/api/users/%s/uploads?&max-results=50&v=2"
5
+
6
+ def get_all_urls(url, filter = nil)
7
+ @filter = filter
8
+
9
+ if url.include?("view_play_list") || url.include?("playlist?list=") # if playlist URL
10
+ parse_playlist(url)
11
+ elsif username = url[/\/(?:user|channel)\/([\w\d]+)(?:\/|$)/, 1] # if user/channel URL
12
+ parse_user(username)
13
+ else # if neither return nil
14
+ [url]
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def parse_playlist(url)
21
+ #http://www.youtube.com/view_play_list?p=F96B063007B44E1E&search_query=welt+auf+schwäbisch
22
+ #http://www.youtube.com/watch?v=9WEP5nCxkEY&videos=jKY836_WMhE&playnext_from=TL&playnext=1
23
+ #http://www.youtube.com/watch?v=Tk78sr5JMIU&videos=jKY836_WMhE
24
+
25
+ playlist_ID = url[/(?:list=PL|p=)(.+?)(?:&|\/|$)/, 1]
26
+ Youtube.notify "Playlist ID: #{playlist_ID}"
27
+ feed_url = PLAYLIST_FEED % playlist_ID
28
+ url_array = get_video_urls(feed_url)
29
+ Youtube.notify "#{url_array.size} links found!"
30
+ url_array
31
+ end
32
+
33
+ def parse_user(username)
34
+ Youtube.notify "User: #{username}"
35
+ feed_url = USER_FEED % username
36
+ url_array = get_video_urls(feed_url)
37
+ Youtube.notify "#{url_array.size} links found!"
38
+ url_array
39
+ end
40
+
41
+ #get all videos and return their urls in an array
42
+ def get_video_urls(feed_url)
43
+ Youtube.notify "Retrieving videos..."
44
+ urls_titles = {}
45
+ result_feed = Nokogiri::XML(open(feed_url))
46
+ urls_titles.merge!(grab_urls_and_titles(result_feed))
47
+
48
+ #as long as the feed has a next link we follow it and add the resulting video urls
49
+ loop do
50
+ next_link = result_feed.search("//feed/link[@rel='next']").first
51
+ break if next_link.nil?
52
+ result_feed = Nokogiri::HTML(open(next_link["href"]))
53
+ urls_titles.merge!(grab_urls_and_titles(result_feed))
54
+ end
55
+
56
+ filter_urls(urls_titles)
57
+ end
58
+
59
+ #extract all video urls and their titles from a feed and return in a hash
60
+ def grab_urls_and_titles(feed)
61
+ feed.remove_namespaces! #so that we can get to the titles easily
62
+ urls = feed.search("//entry/link[@rel='alternate']").map { |link| link["href"] }
63
+ titles = feed.search("//entry/group/title").map { |title| title.text }
64
+ Hash[urls.zip(titles)] #hash like this: url => title
65
+ end
66
+
67
+ #returns only the urls that match the --filter argument regex (if present)
68
+ def filter_urls(url_hash)
69
+ if @filter
70
+ Youtube.notify "Using filter: #{@filter}"
71
+ filtered = url_hash.select { |url, title| title =~ @filter }
72
+ filtered.keys
73
+ else
74
+ url_hash.keys
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,111 @@
1
+
2
+ class VideoResolver
3
+
4
+ class VideoRemovedError < StandardError; end
5
+
6
+ CORRECT_SIGNATURE_LENGTH = 81
7
+ SIGNATURE_URL_PARAMETER = "signature"
8
+
9
+ def initialize(decipherer)
10
+ @decipherer = decipherer
11
+ end
12
+
13
+ def get_video(url)
14
+ @json = load_json(url)
15
+ Video.new(get_title, parse_stream_map(get_stream_map))
16
+ end
17
+
18
+ private
19
+
20
+ def load_json(url)
21
+ html = open(url).read
22
+ json_data = html[/ytplayer\.config\s*=\s*(\{.+?\});/m, 1]
23
+ MultiJson.load(json_data)
24
+ end
25
+
26
+ def get_stream_map
27
+ stream_map = @json["args"]["url_encoded_fmt_stream_map"]
28
+ raise VideoRemovedError.new if stream_map.nil? || stream_map.include?("been+removed")
29
+ stream_map
30
+ end
31
+
32
+ def get_html5player_version
33
+ @json["assets"]["js"][/html5player-(.+?)\.js/, 1]
34
+ end
35
+
36
+ def get_title
37
+ @json["args"]["title"]
38
+ end
39
+
40
+ #
41
+ # Returns a an array of hashes in the following format:
42
+ # [
43
+ # {format: format_id, url: download_url},
44
+ # {format: format_id, url: download_url}
45
+ # ...
46
+ # ]
47
+ #
48
+ def parse_stream_map(stream_map)
49
+ entries = stream_map.split(",")
50
+
51
+ parsed = entries.map { |entry| parse_stream_map_entry(entry) }
52
+ parsed.each { |entry| apply_signature!(entry) if entry[:sig] }
53
+ parsed
54
+ end
55
+
56
+ def parse_stream_map_entry(entry)
57
+ # Note: CGI.parse puts each value in an array.
58
+ params = CGI.parse((entry))
59
+
60
+ {
61
+ itag: params["itag"].first,
62
+ sig: fetch_signature(params),
63
+ url: url_decode(params["url"].first)
64
+ }
65
+ end
66
+
67
+ # The signature key can be either "sig" or "s".
68
+ # Very rarely there is no "s" or "sig" paramater. In this case the signature is already
69
+ # applied and the the video can be downloaded directly.
70
+ def fetch_signature(params)
71
+ sig = params.fetch("sig", nil) || params.fetch("s", nil)
72
+ sig && sig.first
73
+ end
74
+
75
+ def url_decode(text)
76
+ while text != (decoded = CGI::unescape(text)) do
77
+ text = decoded
78
+ end
79
+ text
80
+ end
81
+
82
+ def apply_signature!(entry)
83
+ sig = get_deciphered_sig(entry[:sig])
84
+ entry[:url] << "&#{SIGNATURE_URL_PARAMETER}=#{sig}"
85
+ entry.delete(:sig)
86
+ end
87
+
88
+ def get_deciphered_sig(sig)
89
+ return sig if sig.length == CORRECT_SIGNATURE_LENGTH
90
+ #crequire 'pry'; binding.pry; exit
91
+ @decipherer.decipher_with_version(sig, get_html5player_version)
92
+ end
93
+
94
+ class Video
95
+ attr_reader :title
96
+
97
+ def initialize(title, itags_urls)
98
+ @title = title
99
+ @itags_urls = itags_urls
100
+ end
101
+
102
+ def available_itags
103
+ @itags_urls.map { |iu| iu[:itag] }
104
+ end
105
+
106
+ def get_download_url(itag)
107
+ itag_url = @itags_urls.find { |iu| iu[:itag] == itag }
108
+ itag_url[:url] if itag_url
109
+ end
110
+ end
111
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: viddl-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.95"
4
+ version: "0.96"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc Seeger
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2014-02-16 00:00:00 Z
12
+ date: 2014-02-25 00:00:00 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mime-types
@@ -95,6 +95,10 @@ files:
95
95
  - plugins/soundcloud.rb
96
96
  - plugins/veoh.rb
97
97
  - plugins/vimeo.rb
98
+ - plugins/youtube/decipherer.rb
99
+ - plugins/youtube/format_picker.rb
100
+ - plugins/youtube/url_resolver.rb
101
+ - plugins/youtube/video_resolver.rb
98
102
  - plugins/youtube.rb
99
103
  - Gemfile
100
104
  - Gemfile.lock
@@ -123,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
127
  requirements: []
124
128
 
125
129
  rubyforge_project: viddl-rb
126
- rubygems_version: 2.0.14
130
+ rubygems_version: 2.1.11
127
131
  signing_key:
128
132
  specification_version: 4
129
133
  summary: An extendable commandline video downloader for flash video sites.