viddl-rb 0.95 → 0.96

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.
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.